From 5a7c711a74f3f91c86ac2527af09f4c40f49b6aa Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Fri, 25 Nov 2016 21:07:24 -0800 Subject: [PATCH 001/274] bring dynamodb2 update expression handling closer to spec --- moto/dynamodb2/models.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index a70d6347d..e9980410b 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -3,6 +3,7 @@ from collections import defaultdict import datetime import decimal import json +import re from moto.compat import OrderedDict from moto.core import BaseBackend @@ -110,27 +111,24 @@ class Item(object): } def update(self, update_expression, expression_attribute_names, expression_attribute_values): - ACTION_VALUES = ['SET', 'set', 'REMOVE', 'remove'] - - action = None - for value in update_expression.split(): - if value in ACTION_VALUES: - # An action - action = value - continue - else: + parts = [p for p in re.split(r'\b(SET|REMOVE|ADD|DELETE)\b', update_expression) if p] + for action, valstr in zip(parts[:-1:1], parts[1::1]): + values = valstr.split(',') + for value in values: # A Real value value = value.lstrip(":").rstrip(",") - for k, v in expression_attribute_names.items(): - value = value.replace(k, v) - if action == "REMOVE" or action == 'remove': - self.attrs.pop(value, None) - elif action == 'SET' or action == 'set': - key, value = value.split("=") - if value in expression_attribute_values: - self.attrs[key] = DynamoType(expression_attribute_values[value]) - else: - self.attrs[key] = DynamoType({"S": value}) + for k, v in expression_attribute_names.items(): + value = value.replace(k, v) + if action == "REMOVE" or action == 'remove': + self.attrs.pop(value, None) + elif action == 'SET' or action == 'set': + key, value = value.split("=") + key = key.strip() + value = value.strip() + if value in expression_attribute_values: + self.attrs[key] = DynamoType(expression_attribute_values[value]) + else: + self.attrs[key] = DynamoType({"S": value}) def update_with_attribute_updates(self, attribute_updates): for attribute_name, update_action in attribute_updates.items(): From 2c505615631c2958e7b5f4a3c196b5386370a9e0 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Tue, 29 Nov 2016 14:04:23 -0800 Subject: [PATCH 002/274] fix decoding keys in query condition --- moto/dynamodb2/models.py | 7 ++++--- moto/dynamodb2/responses.py | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index e9980410b..2c6d8d60f 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -119,9 +119,9 @@ class Item(object): value = value.lstrip(":").rstrip(",") for k, v in expression_attribute_names.items(): value = value.replace(k, v) - if action == "REMOVE" or action == 'remove': + if action == "REMOVE": self.attrs.pop(value, None) - elif action == 'SET' or action == 'set': + elif action == 'SET': key, value = value.split("=") key = key.strip() value = value.strip() @@ -129,6 +129,8 @@ class Item(object): self.attrs[key] = DynamoType(expression_attribute_values[value]) else: self.attrs[key] = DynamoType({"S": value}) + else: + raise NotImplementedError('{} update action not yet supported'.format(action)) def update_with_attribute_updates(self, attribute_updates): for attribute_name, update_action in attribute_updates.items(): @@ -323,7 +325,6 @@ class Table(object): def query(self, hash_key, range_comparison, range_objs, limit, exclusive_start_key, scan_index_forward, index_name=None, **filter_kwargs): results = [] - if index_name: all_indexes = (self.global_indexes or []) + (self.indexes or []) indexes_by_name = dict((i['IndexName'], i) for i in all_indexes) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 081afc2c4..eea2bace5 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -279,19 +279,22 @@ class DynamoHandler(BaseResponse): else: index = table.schema - key_map = [column for _, column in sorted((k, v) for k, v in self.body['ExpressionAttributeNames'].items())] + reverse_attribute_lookup = {v: k for k, v in self.body['ExpressionAttributeNames'].iteritems()} if " AND " in key_condition_expression: expressions = key_condition_expression.split(" AND ", 1) index_hash_key = [key for key in index if key['KeyType'] == 'HASH'][0] - hash_key_index_in_key_map = key_map.index(index_hash_key['AttributeName']) + hash_key_var = reverse_attribute_lookup.get(index_hash_key['AttributeName'], index_hash_key['AttributeName']) + i, hash_key_expression = ((i, e) for i, e in enumerate(expressions) if re.search(r'[\s(]#n1\b'.format(hash_key_var), e)).next() + hash_key_expression = hash_key_expression.strip('()') + expressions.pop(i) - hash_key_expression = expressions.pop(hash_key_index_in_key_map).strip('()') # TODO implement more than one range expression and OR operators range_key_expression = expressions[0].strip('()') range_key_expression_components = range_key_expression.split() range_comparison = range_key_expression_components[1] + if 'AND' in range_key_expression: range_comparison = 'BETWEEN' range_values = [ From 98a39cf4b53ffeff99cfc31c990b6fafefe54089 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Tue, 6 Dec 2016 12:14:57 -0800 Subject: [PATCH 003/274] account for keys potentially being substrings of other keys (e.g. #c1 and #c10) --- moto/dynamodb2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 2c6d8d60f..5e7888842 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -118,7 +118,7 @@ class Item(object): # A Real value value = value.lstrip(":").rstrip(",") for k, v in expression_attribute_names.items(): - value = value.replace(k, v) + value = re.sub(r'{}\b'.format(k), v, value) if action == "REMOVE": self.attrs.pop(value, None) elif action == 'SET': From 390bef77521a683c3599198fc57a3375d6d79137 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Tue, 6 Dec 2016 16:57:36 -0800 Subject: [PATCH 004/274] fake change to force push because github was broken --- moto/dynamodb2/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 5e7888842..0e88d594b 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -119,6 +119,7 @@ class Item(object): value = value.lstrip(":").rstrip(",") for k, v in expression_attribute_names.items(): value = re.sub(r'{}\b'.format(k), v, value) + if action == "REMOVE": self.attrs.pop(value, None) elif action == 'SET': From 3c128fdb51d7cdb81f8128fe73a5adca6daf4c6a Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Wed, 7 Dec 2016 11:47:48 -0800 Subject: [PATCH 005/274] correct looping through update actions, value stripping, hash key regex --- moto/dynamodb2/models.py | 4 ++-- moto/dynamodb2/responses.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 0e88d594b..15a3e3ba3 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -112,11 +112,11 @@ class Item(object): def update(self, update_expression, expression_attribute_names, expression_attribute_values): parts = [p for p in re.split(r'\b(SET|REMOVE|ADD|DELETE)\b', update_expression) if p] - for action, valstr in zip(parts[:-1:1], parts[1::1]): + for action, valstr in zip(parts[:-1:2], parts[1::2]): values = valstr.split(',') for value in values: # A Real value - value = value.lstrip(":").rstrip(",") + value = value.lstrip(":").rstrip(",").strip() for k, v in expression_attribute_names.items(): value = re.sub(r'{}\b'.format(k), v, value) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index eea2bace5..39cdaae4e 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -286,7 +286,8 @@ class DynamoHandler(BaseResponse): index_hash_key = [key for key in index if key['KeyType'] == 'HASH'][0] hash_key_var = reverse_attribute_lookup.get(index_hash_key['AttributeName'], index_hash_key['AttributeName']) - i, hash_key_expression = ((i, e) for i, e in enumerate(expressions) if re.search(r'[\s(]#n1\b'.format(hash_key_var), e)).next() + hash_key_regex = r'(^|[\s(]){}\b'.format(hash_key_var) + i, hash_key_expression = ((i, e) for i, e in enumerate(expressions) if re.search(hash_key_regex, e)).next() hash_key_expression = hash_key_expression.strip('()') expressions.pop(i) From 0c875fd268d06d6b7fe8084537ae2bd3d653f8e6 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Wed, 7 Dec 2016 13:31:15 -0800 Subject: [PATCH 006/274] fixes for python 2.6 and 3 --- moto/dynamodb2/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 39cdaae4e..815bd9f57 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -279,7 +279,7 @@ class DynamoHandler(BaseResponse): else: index = table.schema - reverse_attribute_lookup = {v: k for k, v in self.body['ExpressionAttributeNames'].iteritems()} + reverse_attribute_lookup = dict((v, k) for k, v in six.iteritems(self.body['ExpressionAttributeNames'])) if " AND " in key_condition_expression: expressions = key_condition_expression.split(" AND ", 1) From 114de9ba0b8e745a0054408713dafdfb535d0ec3 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Wed, 7 Dec 2016 13:55:26 -0800 Subject: [PATCH 007/274] more fixes for 2.6 and 3 --- moto/dynamodb2/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 815bd9f57..636a0f9d3 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -286,8 +286,8 @@ class DynamoHandler(BaseResponse): index_hash_key = [key for key in index if key['KeyType'] == 'HASH'][0] hash_key_var = reverse_attribute_lookup.get(index_hash_key['AttributeName'], index_hash_key['AttributeName']) - hash_key_regex = r'(^|[\s(]){}\b'.format(hash_key_var) - i, hash_key_expression = ((i, e) for i, e in enumerate(expressions) if re.search(hash_key_regex, e)).next() + hash_key_regex = r'(^|[\s(]){0}\b'.format(hash_key_var) + i, hash_key_expression = next((i, e) for i, e in enumerate(expressions) if re.search(hash_key_regex, e)) hash_key_expression = hash_key_expression.strip('()') expressions.pop(i) From d4a31e5e50c1eca8fb6f885c60b56c15a50dce09 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Thu, 8 Dec 2016 14:34:21 -0800 Subject: [PATCH 008/274] unit tests did not catch this, but this will not work under python 2.6 --- moto/dynamodb2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 15a3e3ba3..4bca83582 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -118,7 +118,7 @@ class Item(object): # A Real value value = value.lstrip(":").rstrip(",").strip() for k, v in expression_attribute_names.items(): - value = re.sub(r'{}\b'.format(k), v, value) + value = re.sub(r'{0}\b'.format(k), v, value) if action == "REMOVE": self.attrs.pop(value, None) From ba7223f046c27be232bf0c5bd71925f74331e99d Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 8 Feb 2017 21:06:05 -0500 Subject: [PATCH 009/274] Fix issue for returning dynamodb floats. Closes #812. --- moto/dynamodb2/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index a70d6347d..0adbae946 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -56,7 +56,10 @@ class DynamoType(object): @property def cast_value(self): if self.type == 'N': - return int(self.value) + try: + return int(self.value) + except ValueError: + return float(self.value) else: return self.value From 1a01bae74ed6751f01237c1f998d246600af41b0 Mon Sep 17 00:00:00 2001 From: Stefan Nordhausen Date: Thu, 9 Feb 2017 03:21:43 +0100 Subject: [PATCH 010/274] Implement list_objects_v2() for S3 buckets (#814) This adds/fixes the following things: - Add missing KeyCount in result (fixes #734). - Do not hard code MaxKeys to 1000. - Truncate result if it has more than MaxKeys items. Set IsTruncated and NextContinuationToken accordingly. - Support the StartAfter parameter. - Return Owner information only when FetchOwner=True is given. - "Prefix" in response is now "" instead of None when omitted in request. - "Delimiter" is now omitted from response when not given in request. --- moto/s3/responses.py | 85 +++++++++++++++++++++++++++ tests/test_s3/test_s3.py | 121 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index ac1533eb0..d6855265e 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -215,6 +215,8 @@ class ResponseObject(_TemplateEnvironmentMixin): delimiter='', is_truncated='false', ) + elif querystring.get('list-type', [None])[0] == '2': + return 200, headers, self._handle_list_objects_v2(bucket_name, querystring) bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] @@ -229,6 +231,49 @@ class ResponseObject(_TemplateEnvironmentMixin): result_folders=result_folders ) + def _handle_list_objects_v2(self, bucket_name, querystring): + template = self.response_template(S3_BUCKET_GET_RESPONSE_V2) + bucket = self.backend.get_bucket(bucket_name) + + prefix = querystring.get('prefix', [None])[0] + delimiter = querystring.get('delimiter', [None])[0] + result_keys, result_folders = self.backend.prefix_query(bucket, prefix, delimiter) + + fetch_owner = querystring.get('fetch-owner', [False])[0] + max_keys = int(querystring.get('max-keys', [1000])[0]) + continuation_token = querystring.get('continuation-token', [None])[0] + start_after = querystring.get('start-after', [None])[0] + + if continuation_token or start_after: + limit = continuation_token or start_after + continuation_index = 0 + for key in result_keys: + if key.name > limit: + break + continuation_index += 1 + result_keys = result_keys[continuation_index:] + + if len(result_keys) > max_keys: + is_truncated = 'true' + result_keys = result_keys[:max_keys] + next_continuation_token = result_keys[-1].name + else: + is_truncated = 'false' + next_continuation_token = None + + return template.render( + bucket=bucket, + prefix=prefix or '', + delimiter=delimiter, + result_keys=result_keys, + result_folders=result_folders, + fetch_owner=fetch_owner, + max_keys=max_keys, + is_truncated=is_truncated, + next_continuation_token=next_continuation_token, + start_after=None if continuation_token else start_after + ) + def _bucket_response_put(self, request, body, region_name, bucket_name, querystring, headers): if 'versioning' in querystring: ver = re.search('([A-Za-z]+)', body) @@ -636,6 +681,46 @@ S3_BUCKET_GET_RESPONSE = """ {% endif %} """ +S3_BUCKET_GET_RESPONSE_V2 = """ + + {{ bucket.name }} + {{ prefix }} + {{ max_keys }} + {{ result_keys | length }} +{% if delimiter %} + {{ delimiter }} +{% endif %} + {{ is_truncated }} +{% if next_continuation_token %} + {{ next_continuation_token }} +{% endif %} +{% if start_after %} + {{ start_after }} +{% endif %} + {% for key in result_keys %} + + {{ key.name }} + {{ key.last_modified_ISO8601 }} + {{ key.etag }} + {{ key.size }} + {{ key.storage_class }} + {% if fetch_owner %} + + 75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a + webfile + + {% endif %} + + {% endfor %} + {% if delimiter %} + {% for folder in result_folders %} + + {{ folder }} + + {% endfor %} + {% endif %} + """ + S3_BUCKET_CREATE_RESPONSE = """ {{ bucket.name }} diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 0d8f7cb49..4990d7324 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -995,8 +995,129 @@ def test_boto3_list_keys_xml_escaped(): s3.create_bucket(Bucket='mybucket') key_name = 'Q&A.txt' s3.put_object(Bucket='mybucket', Key=key_name, Body=b'is awesome') + resp = s3.list_objects_v2(Bucket='mybucket', Prefix=key_name) + assert resp['Contents'][0]['Key'] == key_name + assert resp['KeyCount'] == 1 + assert resp['MaxKeys'] == 1000 + assert resp['Prefix'] == key_name + assert resp['IsTruncated'] == False + assert 'Delimiter' not in resp + assert 'StartAfter' not in resp + assert 'NextContinuationToken' not in resp + assert 'Owner' not in resp['Contents'][0] + + +@mock_s3 +def test_boto3_list_objects_v2_truncated_response(): + s3 = boto3.client('s3', region_name='us-east-1') + s3.create_bucket(Bucket='mybucket') + s3.put_object(Bucket='mybucket', Key='one', Body=b'1') + s3.put_object(Bucket='mybucket', Key='two', Body=b'22') + s3.put_object(Bucket='mybucket', Key='three', Body=b'333') + + # First list + resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1) + listed_object = resp['Contents'][0] + + assert listed_object['Key'] == 'one' + assert resp['MaxKeys'] == 1 + assert resp['Prefix'] == '' + assert resp['KeyCount'] == 1 + assert resp['IsTruncated'] == True + assert 'Delimiter' not in resp + assert 'StartAfter' not in resp + assert 'Owner' not in listed_object # owner info was not requested + + next_token = resp['NextContinuationToken'] + + + # Second list + resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, ContinuationToken=next_token) + listed_object = resp['Contents'][0] + + assert listed_object['Key'] == 'three' + assert resp['MaxKeys'] == 1 + assert resp['Prefix'] == '' + assert resp['KeyCount'] == 1 + assert resp['IsTruncated'] == True + assert 'Delimiter' not in resp + assert 'StartAfter' not in resp + assert 'Owner' not in listed_object + + next_token = resp['NextContinuationToken'] + + + # Third list + resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, ContinuationToken=next_token) + listed_object = resp['Contents'][0] + + assert listed_object['Key'] == 'two' + assert resp['MaxKeys'] == 1 + assert resp['Prefix'] == '' + assert resp['KeyCount'] == 1 + assert resp['IsTruncated'] == False + assert 'Delimiter' not in resp + assert 'Owner' not in listed_object + assert 'StartAfter' not in resp + assert 'NextContinuationToken' not in resp + + +@mock_s3 +def test_boto3_list_objects_v2_truncated_response_start_after(): + s3 = boto3.client('s3', region_name='us-east-1') + s3.create_bucket(Bucket='mybucket') + s3.put_object(Bucket='mybucket', Key='one', Body=b'1') + s3.put_object(Bucket='mybucket', Key='two', Body=b'22') + s3.put_object(Bucket='mybucket', Key='three', Body=b'333') + + # First list + resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, StartAfter='one') + listed_object = resp['Contents'][0] + + assert listed_object['Key'] == 'three' + assert resp['MaxKeys'] == 1 + assert resp['Prefix'] == '' + assert resp['KeyCount'] == 1 + assert resp['IsTruncated'] == True + assert resp['StartAfter'] == 'one' + assert 'Delimiter' not in resp + assert 'Owner' not in listed_object + + next_token = resp['NextContinuationToken'] + + # Second list + # The ContinuationToken must take precedence over StartAfter. + resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, StartAfter='one', + ContinuationToken=next_token) + listed_object = resp['Contents'][0] + + assert listed_object['Key'] == 'two' + assert resp['MaxKeys'] == 1 + assert resp['Prefix'] == '' + assert resp['KeyCount'] == 1 + assert resp['IsTruncated'] == False + # When ContinuationToken is given, StartAfter is ignored. This also means + # AWS does not return it in the response. + assert 'StartAfter' not in resp + assert 'Delimiter' not in resp + assert 'Owner' not in listed_object + + +@mock_s3 +def test_boto3_list_objects_v2_fetch_owner(): + s3 = boto3.client('s3', region_name='us-east-1') + s3.create_bucket(Bucket='mybucket') + s3.put_object(Bucket='mybucket', Key='one', Body=b'11') + + resp = s3.list_objects_v2(Bucket='mybucket', FetchOwner=True) + owner = resp['Contents'][0]['Owner'] + + assert 'ID' in owner + assert 'DisplayName' in owner + assert len(owner.keys()) == 2 + @mock_s3 def test_boto3_bucket_create(): From 8fc1ad03bd3e735c578ccc67effb5d0d0cff37f2 Mon Sep 17 00:00:00 2001 From: Jeffrey Gelens Date: Thu, 9 Feb 2017 03:22:14 +0100 Subject: [PATCH 011/274] Reload the server on a file change (#817) * Added simple server reload support * updated help text --- moto/server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/moto/server.py b/moto/server.py index 5ee12362e..1780083d8 100644 --- a/moto/server.py +++ b/moto/server.py @@ -137,6 +137,12 @@ def main(argv=sys.argv[1:]): '-p', '--port', type=int, help='Port number to use for connection', default=5000) + parser.add_argument( + '-r', '--reload', + action='store_true', + help='Reload server on a file change', + default=False + ) args = parser.parse_args(argv) @@ -144,7 +150,8 @@ def main(argv=sys.argv[1:]): main_app = DomainDispatcherApplication(create_backend_app, service=args.service) main_app.debug = True - run_simple(args.host, args.port, main_app, threaded=True) + run_simple(args.host, args.port, main_app, threaded=True, use_reloader=args.reload) + if __name__ == '__main__': main() From 1045dca7b225f879bbc3804297d0300725aece71 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 8 Feb 2017 21:23:49 -0500 Subject: [PATCH 012/274] make instanceTenancy configurable for VPCs (#819) * make instanceTenancy configurable for VPCs * fix issue with setting tenenancy --- moto/ec2/models.py | 10 +++++++--- moto/ec2/responses/vpcs.py | 7 ++++--- tests/test_ec2/test_vpcs.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 25befabc0..00e0f4960 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1812,12 +1812,13 @@ class EBSBackend(object): class VPC(TaggedEC2Resource): - def __init__(self, ec2_backend, vpc_id, cidr_block, is_default): + def __init__(self, ec2_backend, vpc_id, cidr_block, is_default, instance_tenancy='default'): self.ec2_backend = ec2_backend self.id = vpc_id self.cidr_block = cidr_block self.dhcp_options = None self.state = 'available' + self.instance_tenancy = instance_tenancy self.is_default = 'true' if is_default else 'false' self.enable_dns_support = 'true' # This attribute is set to 'true' only for default VPCs @@ -1831,6 +1832,7 @@ class VPC(TaggedEC2Resource): ec2_backend = ec2_backends[region_name] vpc = ec2_backend.create_vpc( cidr_block=properties['CidrBlock'], + instance_tenancy=properties.get('InstanceTenancy', 'default') ) return vpc @@ -1843,6 +1845,8 @@ class VPC(TaggedEC2Resource): return self.id elif filter_name in ('cidr', 'cidr-block', 'cidrBlock'): return self.cidr_block + elif filter_name in ('instance_tenancy', 'InstanceTenancy'): + return self.instance_tenancy elif filter_name in ('is-default', 'isDefault'): return self.is_default elif filter_name == 'state': @@ -1866,9 +1870,9 @@ class VPCBackend(object): self.vpcs = {} super(VPCBackend, self).__init__() - def create_vpc(self, cidr_block): + def create_vpc(self, cidr_block, instance_tenancy='default'): vpc_id = random_vpc_id() - vpc = VPC(self, vpc_id, cidr_block, len(self.vpcs) == 0) + vpc = VPC(self, vpc_id, cidr_block, len(self.vpcs) == 0, instance_tenancy) self.vpcs[vpc_id] = vpc # AWS creates a default main route table and security group. diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 58c5e80dd..3d2a99894 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -7,7 +7,8 @@ from moto.ec2.utils import filters_from_querystring, vpc_ids_from_querystring class VPCs(BaseResponse): def create_vpc(self): cidr_block = self.querystring.get('CidrBlock')[0] - vpc = self.ec2_backend.create_vpc(cidr_block) + instance_tenancy = self.querystring.get('InstanceTenancy', ['default'])[0] + vpc = self.ec2_backend.create_vpc(cidr_block, instance_tenancy) template = self.response_template(CREATE_VPC_RESPONSE) return template.render(vpc=vpc) @@ -51,7 +52,7 @@ CREATE_VPC_RESPONSE = """ pending {{ vpc.cidr_block }} {% if vpc.dhcp_options %}{{ vpc.dhcp_options.id }}{% else %}dopt-1a2b3c4d2{% endif %} - default + {{ vpc.instance_tenancy }} {% for tag in vpc.get_tags() %} @@ -75,7 +76,7 @@ DESCRIBE_VPCS_RESPONSE = """ {{ vpc.state }} {{ vpc.cidr_block }} {% if vpc.dhcp_options %}{{ vpc.dhcp_options.id }}{% else %}dopt-7a8b9c2d{% endif %} - default + {{ vpc.instance_tenancy }} {{ vpc.is_default }} {% for tag in vpc.get_tags() %} diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py index def2700e3..513238001 100644 --- a/tests/test_ec2/test_vpcs.py +++ b/tests/test_ec2/test_vpcs.py @@ -246,6 +246,7 @@ def test_default_vpc(): # Create the default VPC default_vpc = list(ec2.vpcs.all())[0] default_vpc.cidr_block.should.equal('172.31.0.0/16') + default_vpc.instance_tenancy.should.equal('default') default_vpc.reload() default_vpc.is_default.should.be.ok @@ -271,6 +272,9 @@ def test_non_default_vpc(): vpc.reload() vpc.is_default.shouldnt.be.ok + # Test default instance_tenancy + vpc.instance_tenancy.should.equal('default') + # Test default values for VPC attributes response = vpc.describe_attribute(Attribute='enableDnsSupport') attr = response.get('EnableDnsSupport') @@ -280,6 +284,19 @@ def test_non_default_vpc(): attr = response.get('EnableDnsHostnames') attr.get('Value').shouldnt.be.ok +@mock_ec2 +def test_vpc_dedicated_tenancy(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + + # Create the default VPC + ec2.create_vpc(CidrBlock='172.31.0.0/16') + + # Create the non default VPC + vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16', InstanceTenancy='dedicated') + vpc.reload() + vpc.is_default.shouldnt.be.ok + + vpc.instance_tenancy.should.equal('dedicated') @mock_ec2 def test_vpc_modify_enable_dns_support(): From 012dd497f2616cb20e04828ffd504ccaa2dd6268 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Thu, 9 Feb 2017 13:29:37 +1100 Subject: [PATCH 013/274] make get_all_security_groups filter AND match group ids, not OR them (#822) --- moto/ec2/models.py | 10 ++++++---- tests/test_ec2/test_security_groups.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 00e0f4960..30769fd7e 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1309,10 +1309,12 @@ class SecurityGroupBackend(object): if group_ids or groupnames or filters: for group in all_groups: - if ((group_ids and group.id in group_ids) or - (groupnames and group.name in groupnames) or - (filters and group.matches_filters(filters))): - groups.append(group) + if ((group_ids and not group.id in group_ids) or + (groupnames and not group.name in groupnames)): + continue + if filters and not group.matches_filters(filters): + continue + groups.append(group) else: groups = all_groups diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py index 83dad6f0c..19f43862d 100644 --- a/tests/test_ec2/test_security_groups.py +++ b/tests/test_ec2/test_security_groups.py @@ -610,3 +610,16 @@ def test_authorize_and_revoke_in_bulk(): sg01.ip_permissions_egress.should.have.length_of(1) for ip_permission in expected_ip_permissions: sg01.ip_permissions_egress.shouldnt.contain(ip_permission) + +@mock_ec2 +def test_get_all_security_groups_filter_with_same_vpc_id(): + conn = boto.connect_ec2('the_key', 'the_secret') + vpc_id = 'vpc-5300000c' + security_group = conn.create_security_group('test1', 'test1', vpc_id=vpc_id) + security_group2 = conn.create_security_group('test2', 'test2', vpc_id=vpc_id) + + security_group.vpc_id.should.equal(vpc_id) + security_group2.vpc_id.should.equal(vpc_id) + + security_groups = conn.get_all_security_groups(group_ids=[security_group.id], filters={'vpc-id': [vpc_id]}) + security_groups.should.have.length_of(1) From fd7b8e7c8893379db6b86f15ec9c37a8c5cb6679 Mon Sep 17 00:00:00 2001 From: Joseph Lawson Date: Wed, 8 Feb 2017 21:30:27 -0500 Subject: [PATCH 014/274] install server version of moto for Dockerfile (#824) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 16d9d7d91..72657903e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ADD . /moto/ ENV PYTHONUNBUFFERED 1 WORKDIR /moto/ -RUN python setup.py install +RUN pip install ".[server]" CMD ["moto_server"] From 53fbd7dca02f294afb0ffaeac92e793eb8021a95 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Thu, 9 Feb 2017 19:36:24 -0800 Subject: [PATCH 015/274] KMS encryption under Python 3 (#826) This upgrades the KMS encrypt and decrypt endpoints to work under both Python 2 and 3 --- moto/kms/responses.py | 8 ++++++-- tests/test_kms/test_kms.py | 32 ++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/moto/kms/responses.py b/moto/kms/responses.py index fb5c8590f..bc928f6f3 100644 --- a/moto/kms/responses.py +++ b/moto/kms/responses.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import base64 import json import re +import six from boto.exception import JSONResponseError from boto.kms.exceptions import AlreadyExistsException, NotFoundException @@ -220,11 +221,14 @@ class KmsResponse(BaseResponse): decode it in decrypt(). """ value = self.parameters.get("Plaintext") - return json.dumps({"CiphertextBlob": base64.b64encode(value).encode("utf-8")}) + if isinstance(value, six.text_type): + value = value.encode('utf-8') + return json.dumps({"CiphertextBlob": base64.b64encode(value).decode("utf-8")}) def decrypt(self): value = self.parameters.get("CiphertextBlob") - return json.dumps({"Plaintext": base64.b64decode(value).encode("utf-8")}) + print("value 3", value) + return json.dumps({"Plaintext": base64.b64decode(value).decode("utf-8")}) def _assert_valid_key_id(key_id): diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index 9ec4ffce4..04e6fbb4b 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals import re -import six import boto.kms from boto.exception import JSONResponseError @@ -137,25 +136,22 @@ def test_disable_key_rotation(): conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) -# Scoping encryption/decryption to only Python 2 because our test suite -# hardcodes a dependency on boto version 2.36.0 which is not compatible with -# Python 3 (2.40+, however, passes these tests). -if six.PY2: - @mock_kms - def test_encrypt(): - """ - Using base64 encoding to merely test that the endpoint was called - """ - conn = boto.kms.connect_to_region("us-west-2") - response = conn.encrypt('key_id', 'encryptme'.encode('utf-8')) - response['CiphertextBlob'].should.equal('ZW5jcnlwdG1l') +@mock_kms +def test_encrypt(): + """ + test_encrypt + Using base64 encoding to merely test that the endpoint was called + """ + conn = boto.kms.connect_to_region("us-west-2") + response = conn.encrypt('key_id', 'encryptme'.encode('utf-8')) + response['CiphertextBlob'].should.equal(b'ZW5jcnlwdG1l') - @mock_kms - def test_decrypt(): - conn = boto.kms.connect_to_region('us-west-2') - response = conn.decrypt('ZW5jcnlwdG1l'.encode('utf-8')) - response['Plaintext'].should.equal('encryptme') +@mock_kms +def test_decrypt(): + conn = boto.kms.connect_to_region('us-west-2') + response = conn.decrypt('ZW5jcnlwdG1l'.encode('utf-8')) + response['Plaintext'].should.equal(b'encryptme') @mock_kms From 2d03182ae2aa6d6f1e0204d133c48a58e343fb9f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Feb 2017 19:41:04 -0500 Subject: [PATCH 016/274] Migrate some sqs tests to boto3. --- moto/ec2/responses/ip_addresses.py | 4 +- tests/test_sqs/test_sqs.py | 183 +++++++++++++---------------- 2 files changed, 84 insertions(+), 103 deletions(-) diff --git a/moto/ec2/responses/ip_addresses.py b/moto/ec2/responses/ip_addresses.py index b57be64af..fd58741e2 100644 --- a/moto/ec2/responses/ip_addresses.py +++ b/moto/ec2/responses/ip_addresses.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals -from moto.core.responses import BaseResponse, JSONResponseError + +from boto.exception import JSONResponseError +from moto.core.responses import BaseResponse class IPAddresses(BaseResponse): diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 8972b7283..525f7bf89 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + import boto import boto3 import botocore.exceptions @@ -11,102 +12,130 @@ import time from moto import mock_sqs from tests.helpers import requires_boto_gte +import tests.backport_assert_raises # noqa +from nose.tools import assert_raises + +sqs = boto3.resource('sqs') @mock_sqs def test_create_queue(): - conn = boto.connect_sqs('the_key', 'the_secret') - conn.create_queue("test-queue", visibility_timeout=60) + sqs = boto3.resource('sqs', region_name='us-east-1') + new_queue = sqs.create_queue(QueueName='test-queue') + new_queue.should_not.be.none + new_queue.should.have.property('url').should.contain('test-queue') - all_queues = conn.get_all_queues() - all_queues[0].name.should.equal("test-queue") + queue = sqs.get_queue_by_name(QueueName='test-queue') + queue.attributes.get('QueueArn').should_not.be.none + queue.attributes.get('QueueArn').split(':')[-1].should.equal('test-queue') + queue.attributes.get('QueueArn').split(':')[3].should.equal('us-east-1') + queue.attributes.get('VisibilityTimeout').should_not.be.none + queue.attributes.get('VisibilityTimeout').should.equal('30') - all_queues[0].get_timeout().should.equal(60) + +@mock_sqs +def test_get_inexistent_queue(): + sqs = boto3.resource('sqs', region_name='us-east-1') + sqs.get_queue_by_name.when.called_with(QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) + + +@mock_sqs +def test_message_send(): + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + msg = queue.send_message(MessageBody="derp") + + msg.get('MD5OfMessageBody').should.equal('58fd9edd83341c29f1aebba81c31e257') + msg.get('ResponseMetadata', {}).get('RequestId').should.equal('27daac76-34dd-47df-bd01-1f6e873584a0') + msg.get('MessageId').should_not.contain(' \n') + + messages = queue.receive_messages() + messages.should.have.length_of(1) + + +@mock_sqs +def test_set_queue_attributes(): + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + + queue.attributes['VisibilityTimeout'].should.equal("30") + + queue.set_attributes(Attributes={"VisibilityTimeout": "45"}) + queue.attributes['VisibilityTimeout'].should.equal("45") @mock_sqs def test_create_queues_in_multiple_region(): - west1_conn = boto.sqs.connect_to_region("us-west-1") - west1_conn.create_queue("test-queue") + west1_conn = boto3.client('sqs', region_name='us-west-1') + west1_conn.create_queue(QueueName="blah") - west2_conn = boto.sqs.connect_to_region("us-west-2") - west2_conn.create_queue("test-queue") + west2_conn = boto3.client('sqs', region_name='us-west-2') + west2_conn.create_queue(QueueName="test-queue") - list(west1_conn.get_all_queues()).should.have.length_of(1) - list(west2_conn.get_all_queues()).should.have.length_of(1) + list(west1_conn.list_queues()['QueueUrls']).should.have.length_of(1) + list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) - west1_conn.get_all_queues()[0].url.should.equal('http://sqs.us-west-1.amazonaws.com/123456789012/test-queue') - - -@mock_sqs -def test_get_queue(): - conn = boto.connect_sqs('the_key', 'the_secret') - conn.create_queue("test-queue", visibility_timeout=60) - - queue = conn.get_queue("test-queue") - queue.name.should.equal("test-queue") - queue.get_timeout().should.equal(60) - - nonexisting_queue = conn.get_queue("nonexisting_queue") - nonexisting_queue.should.be.none + west1_conn.list_queues()['QueueUrls'][0].should.equal('http://sqs.us-west-1.amazonaws.com/123456789012/blah') @mock_sqs def test_get_queue_with_prefix(): - conn = boto.connect_sqs('the_key', 'the_secret') - conn.create_queue("prefixa-queue") - conn.create_queue("prefixb-queue") - conn.create_queue("test-queue") + conn = boto3.client("sqs", region_name='us-west-1') + conn.create_queue(QueueName="prefixa-queue") + conn.create_queue(QueueName="prefixb-queue") + conn.create_queue(QueueName="test-queue") - conn.get_all_queues().should.have.length_of(3) + conn.list_queues()['QueueUrls'].should.have.length_of(3) - queue = conn.get_all_queues("test-") + queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue.should.have.length_of(1) - queue[0].name.should.equal("test-queue") + queue[0].should.equal("http://sqs.us-west-1.amazonaws.com/123456789012/test-queue") @mock_sqs def test_delete_queue(): - conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + conn = boto3.client("sqs") + conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": "60"}) + queue = sqs.Queue('test-queue') - conn.get_all_queues().should.have.length_of(1) + conn.list_queues()['QueueUrls'].should.have.length_of(1) queue.delete() - conn.get_all_queues().should.have.length_of(0) + conn.list_queues().get('QueueUrls').should.equal(None) - queue.delete.when.called_with().should.throw(SQSError) + with assert_raises(botocore.exceptions.ClientError): + queue.delete() @mock_sqs def test_set_queue_attribute(): - conn = boto.connect_sqs('the_key', 'the_secret') - conn.create_queue("test-queue", visibility_timeout=60) + conn = boto3.client("sqs") + conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": '60'}) - queue = conn.get_all_queues()[0] - queue.get_timeout().should.equal(60) + queue = sqs.Queue("test-queue") + queue.attributes['VisibilityTimeout'].should.equal('60') - queue.set_attribute("VisibilityTimeout", 45) - queue = conn.get_all_queues()[0] - queue.get_timeout().should.equal(45) + queue.set_attributes(Attributes={"VisibilityTimeout": '45'}) + queue = sqs.Queue("test-queue") + queue.attributes['VisibilityTimeout'].should.equal('45') @mock_sqs def test_send_message(): - conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) - queue.set_message_class(RawMessage) + conn = boto3.client("sqs") + conn.create_queue(QueueName="test-queue") + queue = sqs.Queue("test-queue") body_one = 'this is a test message' body_two = 'this is another test message' - queue.write(queue.new_message(body_one)) - queue.write(queue.new_message(body_two)) + response = queue.send_message(MessageBody=body_one) + response = queue.send_message(MessageBody=body_two) - messages = conn.receive_message(queue, number_messages=2) + messages = conn.receive_message(QueueUrl=queue.url, MaxNumberOfMessages=2)['Messages'] - messages[0].get_body().should.equal(body_one) - messages[1].get_body().should.equal(body_two) + messages[0]['Body'].should.equal(body_one) + messages[1]['Body'].should.equal(body_two) @mock_sqs @@ -495,53 +524,3 @@ def test_delete_message_after_visibility_timeout(): m1_retrieved.delete() assert new_queue.count() == 0 - -""" -boto3 -""" - - -@mock_sqs -def test_boto3_get_queue(): - sqs = boto3.resource('sqs', region_name='us-east-1') - new_queue = sqs.create_queue(QueueName='test-queue') - new_queue.should_not.be.none - new_queue.should.have.property('url').should.contain('test-queue') - - queue = sqs.get_queue_by_name(QueueName='test-queue') - queue.attributes.get('QueueArn').should_not.be.none - queue.attributes.get('QueueArn').split(':')[-1].should.equal('test-queue') - queue.attributes.get('QueueArn').split(':')[3].should.equal('us-east-1') - queue.attributes.get('VisibilityTimeout').should_not.be.none - queue.attributes.get('VisibilityTimeout').should.equal('30') - - -@mock_sqs -def test_boto3_get_inexistent_queue(): - sqs = boto3.resource('sqs', region_name='us-east-1') - sqs.get_queue_by_name.when.called_with(QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) - - -@mock_sqs -def test_boto3_message_send(): - sqs = boto3.resource('sqs', region_name='us-east-1') - queue = sqs.create_queue(QueueName="blah") - msg = queue.send_message(MessageBody="derp") - - msg.get('MD5OfMessageBody').should.equal('58fd9edd83341c29f1aebba81c31e257') - msg.get('ResponseMetadata', {}).get('RequestId').should.equal('27daac76-34dd-47df-bd01-1f6e873584a0') - msg.get('MessageId').should_not.contain(' \n') - - messages = queue.receive_messages() - messages.should.have.length_of(1) - - -@mock_sqs -def test_boto3_set_queue_attributes(): - sqs = boto3.resource('sqs', region_name='us-east-1') - queue = sqs.create_queue(QueueName="blah") - - queue.attributes['VisibilityTimeout'].should.equal("30") - - queue.set_attributes(Attributes={"VisibilityTimeout": "45"}) - queue.attributes['VisibilityTimeout'].should.equal("45") From 9076e48fee95d8eb0fd2a9a4a3e4e47e4c24f1e4 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Feb 2017 19:50:26 -0500 Subject: [PATCH 017/274] Fix sqs tests region. --- tests/test_sqs/test_sqs.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 525f7bf89..32b026a46 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -15,12 +15,11 @@ from tests.helpers import requires_boto_gte import tests.backport_assert_raises # noqa from nose.tools import assert_raises -sqs = boto3.resource('sqs') +sqs = boto3.resource('sqs', region_name='us-east-1') @mock_sqs def test_create_queue(): - sqs = boto3.resource('sqs', region_name='us-east-1') new_queue = sqs.create_queue(QueueName='test-queue') new_queue.should_not.be.none new_queue.should.have.property('url').should.contain('test-queue') @@ -35,13 +34,11 @@ def test_create_queue(): @mock_sqs def test_get_inexistent_queue(): - sqs = boto3.resource('sqs', region_name='us-east-1') sqs.get_queue_by_name.when.called_with(QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) @mock_sqs def test_message_send(): - sqs = boto3.resource('sqs', region_name='us-east-1') queue = sqs.create_queue(QueueName="blah") msg = queue.send_message(MessageBody="derp") @@ -55,7 +52,6 @@ def test_message_send(): @mock_sqs def test_set_queue_attributes(): - sqs = boto3.resource('sqs', region_name='us-east-1') queue = sqs.create_queue(QueueName="blah") queue.attributes['VisibilityTimeout'].should.equal("30") @@ -94,7 +90,7 @@ def test_get_queue_with_prefix(): @mock_sqs def test_delete_queue(): - conn = boto3.client("sqs") + conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": "60"}) queue = sqs.Queue('test-queue') @@ -109,7 +105,7 @@ def test_delete_queue(): @mock_sqs def test_set_queue_attribute(): - conn = boto3.client("sqs") + conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": '60'}) queue = sqs.Queue("test-queue") @@ -122,7 +118,7 @@ def test_set_queue_attribute(): @mock_sqs def test_send_message(): - conn = boto3.client("sqs") + conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue") queue = sqs.Queue("test-queue") From d3df810065c9c453d40fcc971f9be6b7b2846061 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Feb 2017 00:22:29 -0500 Subject: [PATCH 018/274] Generalize decorator code. --- moto/apigateway/__init__.py | 10 ++-------- moto/autoscaling/__init__.py | 10 ++-------- moto/awslambda/__init__.py | 10 ++-------- moto/cloudformation/__init__.py | 10 ++-------- moto/cloudwatch/__init__.py | 10 ++-------- moto/core/models.py | 13 +++++++++++++ moto/datapipeline/__init__.py | 10 ++-------- moto/ec2/__init__.py | 10 ++-------- moto/ecs/__init__.py | 9 ++------- moto/elb/__init__.py | 10 ++-------- moto/emr/__init__.py | 10 ++-------- moto/glacier/__init__.py | 10 ++-------- moto/kinesis/__init__.py | 10 ++-------- moto/kms/__init__.py | 10 ++-------- moto/opsworks/__init__.py | 11 ++--------- moto/rds/__init__.py | 10 ++-------- moto/rds2/__init__.py | 10 ++-------- moto/redshift/__init__.py | 10 ++-------- moto/sns/__init__.py | 10 ++-------- moto/sqs/__init__.py | 10 ++-------- moto/swf/__init__.py | 10 ++-------- 21 files changed, 53 insertions(+), 160 deletions(-) diff --git a/moto/apigateway/__init__.py b/moto/apigateway/__init__.py index 47db4a703..f75a72add 100644 --- a/moto/apigateway/__init__.py +++ b/moto/apigateway/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import apigateway_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator apigateway_backend = apigateway_backends['us-east-1'] - - -def mock_apigateway(func=None): - if func: - return MockAWS(apigateway_backends)(func) - else: - return MockAWS(apigateway_backends) +mock_apigateway = base_decorator(apigateway_backends) diff --git a/moto/autoscaling/__init__.py b/moto/autoscaling/__init__.py index cefcc3cf7..1d3cc9b3e 100644 --- a/moto/autoscaling/__init__.py +++ b/moto/autoscaling/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import autoscaling_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator autoscaling_backend = autoscaling_backends['us-east-1'] - - -def mock_autoscaling(func=None): - if func: - return MockAWS(autoscaling_backends)(func) - else: - return MockAWS(autoscaling_backends) +mock_autoscaling = base_decorator(autoscaling_backends) diff --git a/moto/awslambda/__init__.py b/moto/awslambda/__init__.py index 0076f7f76..b27ff9a43 100644 --- a/moto/awslambda/__init__.py +++ b/moto/awslambda/__init__.py @@ -1,13 +1,7 @@ from __future__ import unicode_literals from .models import lambda_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator lambda_backend = lambda_backends['us-east-1'] - - -def mock_lambda(func=None): - if func: - return MockAWS(lambda_backends)(func) - else: - return MockAWS(lambda_backends) +mock_lambda = base_decorator(lambda_backends) diff --git a/moto/cloudformation/__init__.py b/moto/cloudformation/__init__.py index 98587fc41..0e4bd28dd 100644 --- a/moto/cloudformation/__init__.py +++ b/moto/cloudformation/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import cloudformation_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator cloudformation_backend = cloudformation_backends['us-east-1'] - - -def mock_cloudformation(func=None): - if func: - return MockAWS(cloudformation_backends)(func) - else: - return MockAWS(cloudformation_backends) +mock_cloudformation = base_decorator(cloudformation_backends) diff --git a/moto/cloudwatch/__init__.py b/moto/cloudwatch/__init__.py index b354b3be7..ea4bf7185 100644 --- a/moto/cloudwatch/__init__.py +++ b/moto/cloudwatch/__init__.py @@ -1,11 +1,5 @@ from .models import cloudwatch_backends -from ..core.models import MockAWS - +from ..core.models import MockAWS, base_decorator cloudwatch_backend = cloudwatch_backends['us-east-1'] - -def mock_cloudwatch(func=None): - if func: - return MockAWS(cloudwatch_backends)(func) - else: - return MockAWS(cloudwatch_backends) +mock_cloudwatch = base_decorator(cloudwatch_backends) diff --git a/moto/core/models.py b/moto/core/models.py index 60e744ff5..b6f4f541f 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -186,3 +186,16 @@ class BaseBackend(object): return MockAWS({'global': self})(func) else: return MockAWS({'global': self}) + + +class base_decorator(object): + mock_backend = MockAWS + + def __init__(self, backends): + self.backends = backends + + def __call__(self, func=None): + if func: + return self.mock_backend(self.backends)(func) + else: + return self.mock_backend(self.backends) diff --git a/moto/datapipeline/__init__.py b/moto/datapipeline/__init__.py index dcfe2f427..dd013526e 100644 --- a/moto/datapipeline/__init__.py +++ b/moto/datapipeline/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import datapipeline_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator datapipeline_backend = datapipeline_backends['us-east-1'] - - -def mock_datapipeline(func=None): - if func: - return MockAWS(datapipeline_backends)(func) - else: - return MockAWS(datapipeline_backends) +mock_datapipeline = base_decorator(datapipeline_backends) diff --git a/moto/ec2/__init__.py b/moto/ec2/__init__.py index 2b1cafd88..b269e933b 100644 --- a/moto/ec2/__init__.py +++ b/moto/ec2/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import ec2_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator ec2_backend = ec2_backends['us-east-1'] - - -def mock_ec2(func=None): - if func: - return MockAWS(ec2_backends)(func) - else: - return MockAWS(ec2_backends) +mock_ec2 = base_decorator(ec2_backends) diff --git a/moto/ecs/__init__.py b/moto/ecs/__init__.py index 0844b256c..9c07a0d55 100644 --- a/moto/ecs/__init__.py +++ b/moto/ecs/__init__.py @@ -1,11 +1,6 @@ from __future__ import unicode_literals from .models import ecs_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator ecs_backend = ecs_backends['us-east-1'] - -def mock_ecs(func=None): - if func: - return MockAWS(ecs_backends)(func) - else: - return MockAWS(ecs_backends) +mock_ecs = base_decorator(ecs_backends) diff --git a/moto/elb/__init__.py b/moto/elb/__init__.py index fd53c8587..376dfe0e1 100644 --- a/moto/elb/__init__.py +++ b/moto/elb/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import elb_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator elb_backend = elb_backends['us-east-1'] - - -def mock_elb(func=None): - if func: - return MockAWS(elb_backends)(func) - else: - return MockAWS(elb_backends) +mock_elb = base_decorator(elb_backends) diff --git a/moto/emr/__init__.py b/moto/emr/__init__.py index f0103319a..f79df39fa 100644 --- a/moto/emr/__init__.py +++ b/moto/emr/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import emr_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator emr_backend = emr_backends['us-east-1'] - - -def mock_emr(func=None): - if func: - return MockAWS(emr_backends)(func) - else: - return MockAWS(emr_backends) +mock_emr = base_decorator(emr_backends) diff --git a/moto/glacier/__init__.py b/moto/glacier/__init__.py index bc6fd1ff4..3256462a3 100644 --- a/moto/glacier/__init__.py +++ b/moto/glacier/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import glacier_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator glacier_backend = glacier_backends['us-east-1'] - - -def mock_glacier(func=None): - if func: - return MockAWS(glacier_backends)(func) - else: - return MockAWS(glacier_backends) +mock_glacier = base_decorator(glacier_backends) diff --git a/moto/kinesis/__init__.py b/moto/kinesis/__init__.py index 415b960e1..50bc07155 100644 --- a/moto/kinesis/__init__.py +++ b/moto/kinesis/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import kinesis_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator kinesis_backend = kinesis_backends['us-east-1'] - - -def mock_kinesis(func=None): - if func: - return MockAWS(kinesis_backends)(func) - else: - return MockAWS(kinesis_backends) +mock_kinesis = base_decorator(kinesis_backends) diff --git a/moto/kms/__init__.py b/moto/kms/__init__.py index d406cc913..4ee6dd2f4 100644 --- a/moto/kms/__init__.py +++ b/moto/kms/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import kms_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator kms_backend = kms_backends['us-east-1'] - - -def mock_kms(func=None): - if func: - return MockAWS(kms_backends)(func) - else: - return MockAWS(kms_backends) +mock_kms = base_decorator(kms_backends) diff --git a/moto/opsworks/__init__.py b/moto/opsworks/__init__.py index dfcd582e2..ef5190997 100644 --- a/moto/opsworks/__init__.py +++ b/moto/opsworks/__init__.py @@ -1,13 +1,6 @@ from __future__ import unicode_literals from .models import opsworks_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator opsworks_backend = opsworks_backends['us-east-1'] - - -def mock_opsworks(func=None): - if func: - return MockAWS(opsworks_backends)(func) - else: - return MockAWS(opsworks_backends) - +mock_opsworks = base_decorator(opsworks_backends) diff --git a/moto/rds/__init__.py b/moto/rds/__init__.py index 407f1680c..d3cafc066 100644 --- a/moto/rds/__init__.py +++ b/moto/rds/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import rds_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator rds_backend = rds_backends['us-east-1'] - - -def mock_rds(func=None): - if func: - return MockAWS(rds_backends)(func) - else: - return MockAWS(rds_backends) +mock_rds = base_decorator(rds_backends) diff --git a/moto/rds2/__init__.py b/moto/rds2/__init__.py index 602c21ede..b200f9b11 100644 --- a/moto/rds2/__init__.py +++ b/moto/rds2/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import rds2_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator rds2_backend = rds2_backends['us-west-1'] - - -def mock_rds2(func=None): - if func: - return MockAWS(rds2_backends)(func) - else: - return MockAWS(rds2_backends) +mock_rds2 = base_decorator(rds2_backends) diff --git a/moto/redshift/__init__.py b/moto/redshift/__init__.py index 7adf47865..821408493 100644 --- a/moto/redshift/__init__.py +++ b/moto/redshift/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import redshift_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator redshift_backend = redshift_backends['us-east-1'] - - -def mock_redshift(func=None): - if func: - return MockAWS(redshift_backends)(func) - else: - return MockAWS(redshift_backends) +mock_redshift = base_decorator(redshift_backends) diff --git a/moto/sns/__init__.py b/moto/sns/__init__.py index 1aa1a0e3e..0ed85e813 100644 --- a/moto/sns/__init__.py +++ b/moto/sns/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import sns_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator sns_backend = sns_backends['us-east-1'] - - -def mock_sns(func=None): - if func: - return MockAWS(sns_backends)(func) - else: - return MockAWS(sns_backends) +mock_sns = base_decorator(sns_backends) diff --git a/moto/sqs/__init__.py b/moto/sqs/__init__.py index 0a9de1a47..09b4ed9e9 100644 --- a/moto/sqs/__init__.py +++ b/moto/sqs/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import sqs_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator sqs_backend = sqs_backends['us-east-1'] - - -def mock_sqs(func=None): - if func: - return MockAWS(sqs_backends)(func) - else: - return MockAWS(sqs_backends) +mock_sqs = base_decorator(sqs_backends) diff --git a/moto/swf/__init__.py b/moto/swf/__init__.py index 7e43ca392..180919320 100644 --- a/moto/swf/__init__.py +++ b/moto/swf/__init__.py @@ -1,12 +1,6 @@ from __future__ import unicode_literals from .models import swf_backends -from ..core.models import MockAWS +from ..core.models import MockAWS, base_decorator swf_backend = swf_backends['us-east-1'] - - -def mock_swf(func=None): - if func: - return MockAWS(swf_backends)(func) - else: - return MockAWS(swf_backends) +mock_swf = base_decorator(swf_backends) From fde721bed7db62ad289c13667e428bbfb1399aa1 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Feb 2017 22:35:45 -0500 Subject: [PATCH 019/274] Testing new version of decorator. --- CHANGELOG.md | 4 + README.md | 4 - moto/__init__.py | 47 +- moto/apigateway/__init__.py | 3 +- moto/apigateway/models.py | 16 +- moto/autoscaling/__init__.py | 3 +- moto/awslambda/__init__.py | 4 +- moto/awslambda/responses.py | 14 +- moto/cloudformation/__init__.py | 3 +- moto/cloudwatch/__init__.py | 3 +- moto/core/models.py | 111 +- moto/core/responses.py | 8 +- moto/core/utils.py | 22 + moto/datapipeline/__init__.py | 3 +- moto/dynamodb/__init__.py | 1 + moto/dynamodb2/__init__.py | 1 + moto/ec2/__init__.py | 3 +- moto/ecs/__init__.py | 3 +- moto/elb/__init__.py | 3 +- moto/emr/__init__.py | 3 +- moto/glacier/__init__.py | 3 +- moto/iam/__init__.py | 1 + moto/kinesis/__init__.py | 3 +- moto/kinesis/responses.py | 5 +- moto/kms/__init__.py | 3 +- moto/opsworks/__init__.py | 2 +- moto/opsworks/urls.py | 2 +- moto/packages/__init__.py | 0 moto/packages/httpretty/__init__.py | 60 + moto/packages/httpretty/compat.py | 100 ++ moto/packages/httpretty/core.py | 1071 +++++++++++++++++ moto/packages/httpretty/errors.py | 39 + moto/packages/httpretty/http.py | 155 +++ moto/packages/httpretty/utils.py | 48 + moto/packages/responses | 1 + moto/rds/__init__.py | 3 +- moto/rds2/__init__.py | 3 +- moto/redshift/__init__.py | 3 +- moto/route53/__init__.py | 1 + moto/s3/__init__.py | 1 + moto/s3/models.py | 5 +- moto/s3/responses.py | 15 +- moto/s3bucket_path/__init__.py | 3 - moto/ses/__init__.py | 1 + moto/sns/__init__.py | 3 +- moto/sqs/__init__.py | 3 +- moto/sqs/responses.py | 1 + moto/sts/__init__.py | 1 + moto/swf/__init__.py | 3 +- setup.py | 1 - tests/__init__.py | 7 + tests/test_apigateway/test_apigateway.py | 7 +- tests/test_autoscaling/test_autoscaling.py | 30 +- .../test_launch_configurations.py | 20 +- tests/test_autoscaling/test_policies.py | 18 +- .../test_cloudformation_stack_crud.py | 42 +- .../test_cloudformation_stack_crud_boto3.py | 4 +- .../test_cloudformation_stack_integration.py | 186 +-- tests/test_cloudwatch/test_cloudwatch.py | 12 +- tests/test_core/test_decorator_calls.py | 16 +- tests/test_core/test_nested.py | 6 +- tests/test_datapipeline/test_datapipeline.py | 12 +- tests/test_dynamodb/test_dynamodb.py | 10 +- .../test_dynamodb_table_with_range_key.py | 38 +- .../test_dynamodb_table_without_range_key.py | 36 +- tests/test_dynamodb2/test_dynamodb.py | 8 +- .../test_dynamodb_table_with_range_key.py | 64 +- .../test_dynamodb_table_without_range_key.py | 46 +- tests/test_ec2/test_amis.py | 28 +- .../test_availability_zones_and_regions.py | 6 +- tests/test_ec2/test_customer_gateways.py | 10 +- tests/test_ec2/test_dhcp_options.py | 32 +- tests/test_ec2/test_elastic_block_store.py | 32 +- tests/test_ec2/test_elastic_ip_addresses.py | 32 +- .../test_elastic_network_interfaces.py | 18 +- tests/test_ec2/test_general.py | 6 +- tests/test_ec2/test_instances.py | 78 +- tests/test_ec2/test_internet_gateways.py | 34 +- tests/test_ec2/test_key_pairs.py | 20 +- tests/test_ec2/test_network_acls.py | 16 +- tests/test_ec2/test_regions.py | 10 +- tests/test_ec2/test_route_tables.py | 28 +- tests/test_ec2/test_security_groups.py | 38 +- tests/test_ec2/test_spot_instances.py | 15 +- tests/test_ec2/test_subnets.py | 18 +- tests/test_ec2/test_tags.py | 34 +- .../test_ec2/test_virtual_private_gateways.py | 14 +- tests/test_ec2/test_vpc_peering.py | 12 +- tests/test_ec2/test_vpcs.py | 32 +- tests/test_ec2/test_vpn_connections.py | 10 +- tests/test_elb/test_elb.py | 58 +- tests/test_emr/test_emr.py | 34 +- tests/test_glacier/test_glacier_archives.py | 4 +- tests/test_glacier/test_glacier_jobs.py | 10 +- tests/test_glacier/test_glacier_vaults.py | 6 +- tests/test_iam/test_iam.py | 50 +- tests/test_iam/test_iam_groups.py | 14 +- tests/test_kinesis/test_kinesis.py | 34 +- tests/test_kms/test_kms.py | 82 +- tests/test_opsworks/test_layers.py | 2 +- tests/test_rds/test_rds.py | 50 +- tests/test_rds2/test_rds2.py | 2 - tests/test_redshift/test_redshift.py | 52 +- tests/test_route53/test_route53.py | 24 +- tests/test_s3/test_s3.py | 164 +-- tests/test_s3/test_s3_lifecycle.py | 10 +- .../test_s3bucket_path/test_s3bucket_path.py | 48 +- .../test_s3bucket_path_combo.py | 6 +- tests/test_ses/test_ses.py | 14 +- tests/test_sns/test_application.py | 28 +- tests/test_sns/test_publishing.py | 21 +- tests/test_sns/test_publishing_boto3.py | 8 +- tests/test_sns/test_subscriptions.py | 8 +- tests/test_sns/test_topics.py | 14 +- tests/test_sqs/test_sqs.py | 50 +- tests/test_sts/test_sts.py | 8 +- .../test_swf/responses/test_activity_tasks.py | 28 +- .../test_swf/responses/test_activity_types.py | 22 +- .../test_swf/responses/test_decision_tasks.py | 38 +- tests/test_swf/responses/test_domains.py | 22 +- tests/test_swf/responses/test_timeouts.py | 8 +- .../responses/test_workflow_executions.py | 28 +- .../test_swf/responses/test_workflow_types.py | 23 +- 123 files changed, 2740 insertions(+), 1114 deletions(-) create mode 100644 moto/packages/__init__.py create mode 100644 moto/packages/httpretty/__init__.py create mode 100644 moto/packages/httpretty/compat.py create mode 100644 moto/packages/httpretty/core.py create mode 100644 moto/packages/httpretty/errors.py create mode 100644 moto/packages/httpretty/http.py create mode 100644 moto/packages/httpretty/utils.py create mode 160000 moto/packages/responses diff --git a/CHANGELOG.md b/CHANGELOG.md index 66610511f..790f6de95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Moto Changelog Latest ------ + BACKWARDS INCOMPATIBLE + * The normal @mock_ decorators will no longer work with boto. It is suggested that you upgrade to boto3 or use the standalone-server mode. If you would still like to use boto, you must use the @mock__deprecated decorators which will be removed in a future release. + * The @mock_s3bucket_path decorator is now deprecated. Use the @mock_s3 decorator instead. + 0.4.31 ------ diff --git a/README.md b/README.md index c05f1dff4..ae161dc5c 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,3 @@ boto3.resource( ```console $ pip install moto ``` - -## Thanks - -A huge thanks to [Gabriel Falcão](https://github.com/gabrielfalcao) and his [HTTPretty](https://github.com/gabrielfalcao/HTTPretty) library. Moto would not exist without it. diff --git a/moto/__init__.py b/moto/__init__.py index afc98d14a..4accf1d0c 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -6,33 +6,32 @@ __title__ = 'moto' __version__ = '0.4.31' from .apigateway import mock_apigateway # flake8: noqa -from .autoscaling import mock_autoscaling # flake8: noqa +from .autoscaling import mock_autoscaling, mock_autoscaling_deprecated # flake8: noqa from .awslambda import mock_lambda # flake8: noqa -from .cloudformation import mock_cloudformation # flake8: noqa -from .cloudwatch import mock_cloudwatch # flake8: noqa -from .datapipeline import mock_datapipeline # flake8: noqa -from .dynamodb import mock_dynamodb # flake8: noqa -from .dynamodb2 import mock_dynamodb2 # flake8: noqa -from .ec2 import mock_ec2 # flake8: noqa +from .cloudformation import mock_cloudformation, mock_cloudformation_deprecated # flake8: noqa +from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # flake8: noqa +from .datapipeline import mock_datapipeline, mock_datapipeline_deprecated # flake8: noqa +from .dynamodb import mock_dynamodb, mock_dynamodb_deprecated # flake8: noqa +from .dynamodb2 import mock_dynamodb2, mock_dynamodb2_deprecated # flake8: noqa +from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa from .ecs import mock_ecs # flake8: noqa -from .elb import mock_elb # flake8: noqa -from .emr import mock_emr # flake8: noqa -from .glacier import mock_glacier # flake8: noqa +from .elb import mock_elb, mock_elb_deprecated # flake8: noqa +from .emr import mock_emr, mock_emr_deprecated # flake8: noqa +from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa from .opsworks import mock_opsworks # flake8: noqa -from .iam import mock_iam # flake8: noqa -from .kinesis import mock_kinesis # flake8: noqa -from .kms import mock_kms # flake8: noqa -from .rds import mock_rds # flake8: noqa -from .rds2 import mock_rds2 # flake8: noqa -from .redshift import mock_redshift # flake8: noqa -from .s3 import mock_s3 # flake8: noqa -from .s3bucket_path import mock_s3bucket_path # flake8: noqa -from .ses import mock_ses # flake8: noqa -from .sns import mock_sns # flake8: noqa -from .sqs import mock_sqs # flake8: noqa -from .sts import mock_sts # flake8: noqa -from .route53 import mock_route53 # flake8: noqa -from .swf import mock_swf # flake8: noqa +from .iam import mock_iam, mock_iam_deprecated # flake8: noqa +from .kinesis import mock_kinesis, mock_kinesis_deprecated # flake8: noqa +from .kms import mock_kms, mock_kms_deprecated # flake8: noqa +from .rds import mock_rds, mock_rds_deprecated # flake8: noqa +from .rds2 import mock_rds2, mock_rds2_deprecated # flake8: noqa +from .redshift import mock_redshift, mock_redshift_deprecated # flake8: noqa +from .s3 import mock_s3, mock_s3_deprecated # flake8: noqa +from .ses import mock_ses, mock_ses_deprecated # flake8: noqa +from .sns import mock_sns, mock_sns_deprecated # flake8: noqa +from .sqs import mock_sqs, mock_sqs_deprecated # flake8: noqa +from .sts import mock_sts, mock_sts_deprecated # flake8: noqa +from .route53 import mock_route53, mock_route53_deprecated # flake8: noqa +from .swf import mock_swf, mock_swf_deprecated # flake8: noqa try: diff --git a/moto/apigateway/__init__.py b/moto/apigateway/__init__.py index f75a72add..c6ea9a3bc 100644 --- a/moto/apigateway/__init__.py +++ b/moto/apigateway/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import apigateway_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator apigateway_backend = apigateway_backends['us-east-1'] mock_apigateway = base_decorator(apigateway_backends) +mock_apigateway_deprecated = deprecated_base_decorator(apigateway_backends) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 11d650e05..be0bfa434 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -1,9 +1,10 @@ +from __future__ import absolute_import from __future__ import unicode_literals import datetime -import httpretty import requests +from moto.packages.responses import responses from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime_with_milliseconds from .utils import create_id @@ -315,8 +316,12 @@ class RestAPI(object): return resource # TODO deal with no matching resource - def resource_callback(self, request, full_url, headers): - path_after_stage_name = '/'.join(request.path.split("/")[2:]) + def resource_callback(self, request, full_url=None, headers=None): + if not headers: + headers = request.headers + + path = request.path if hasattr(request, 'path') else request.path_url + path_after_stage_name = '/'.join(path.split("/")[2:]) if not path_after_stage_name: path_after_stage_name = '/' @@ -325,11 +330,8 @@ class RestAPI(object): return status_code, headers, response def update_integration_mocks(self, stage_name): - httpretty.enable() - stage_url = STAGE_URL.format(api_id=self.id, region_name=self.region_name, stage_name=stage_name) - for method in httpretty.httpretty.METHODS: - httpretty.register_uri(method, stage_url, body=self.resource_callback) + responses.add_callback(responses.GET, stage_url, callback=self.resource_callback) def create_stage(self, name, deployment_id,variables=None,description='',cacheClusterEnabled=None,cacheClusterSize=None): if variables is None: diff --git a/moto/autoscaling/__init__.py b/moto/autoscaling/__init__.py index 1d3cc9b3e..9b5842788 100644 --- a/moto/autoscaling/__init__.py +++ b/moto/autoscaling/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import autoscaling_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator autoscaling_backend = autoscaling_backends['us-east-1'] mock_autoscaling = base_decorator(autoscaling_backends) +mock_autoscaling_deprecated = deprecated_base_decorator(autoscaling_backends) diff --git a/moto/awslambda/__init__.py b/moto/awslambda/__init__.py index b27ff9a43..46bc90fbd 100644 --- a/moto/awslambda/__init__.py +++ b/moto/awslambda/__init__.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from .models import lambda_backends -from ..core.models import MockAWS, base_decorator - +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator lambda_backend = lambda_backends['us-east-1'] mock_lambda = base_decorator(lambda_backends) +mock_lambda_deprecated = deprecated_base_decorator(lambda_backends) diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 468a95766..708a8796e 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -9,7 +9,7 @@ from .models import lambda_backends class LambdaResponse(BaseResponse): - + @classmethod def root(cls, request, full_url, headers): if request.method == 'GET': @@ -38,11 +38,13 @@ class LambdaResponse(BaseResponse): def _invoke(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - function_name = request.path.split('/')[-2] + path = request.path if hasattr(request, 'path') else request.path_url + function_name = path.split('/')[-2] if lambda_backend.has_function(function_name): fn = lambda_backend.get_function(function_name) payload = fn.invoke(request, headers) + headers['Content-Length'] = str(len(payload)) return 202, headers, payload else: return 404, headers, "{}" @@ -68,7 +70,8 @@ class LambdaResponse(BaseResponse): def _delete_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - function_name = request.path.split('/')[-1] + path = request.path if hasattr(request, 'path') else request.path_url + function_name = path.split('/')[-1] if lambda_backend.has_function(function_name): lambda_backend.delete_function(function_name) @@ -79,7 +82,8 @@ class LambdaResponse(BaseResponse): def _get_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - function_name = request.path.split('/')[-1] + path = request.path if hasattr(request, 'path') else request.path_url + function_name = path.split('/')[-1] if lambda_backend.has_function(function_name): fn = lambda_backend.get_function(function_name) @@ -87,7 +91,7 @@ class LambdaResponse(BaseResponse): return 200, headers, json.dumps(code) else: return 404, headers, "{}" - + def get_lambda_backend(self, full_url): from moto.awslambda.models import lambda_backends region = self._get_aws_region(full_url) diff --git a/moto/cloudformation/__init__.py b/moto/cloudformation/__init__.py index 0e4bd28dd..47e840ec6 100644 --- a/moto/cloudformation/__init__.py +++ b/moto/cloudformation/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import cloudformation_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator cloudformation_backend = cloudformation_backends['us-east-1'] mock_cloudformation = base_decorator(cloudformation_backends) +mock_cloudformation_deprecated = deprecated_base_decorator(cloudformation_backends) diff --git a/moto/cloudwatch/__init__.py b/moto/cloudwatch/__init__.py index ea4bf7185..17d1c0c50 100644 --- a/moto/cloudwatch/__init__.py +++ b/moto/cloudwatch/__init__.py @@ -1,5 +1,6 @@ from .models import cloudwatch_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator cloudwatch_backend = cloudwatch_backends['us-east-1'] mock_cloudwatch = base_decorator(cloudwatch_backends) +mock_cloudwatch_deprecated = deprecated_base_decorator(cloudwatch_backends) diff --git a/moto/core/models.py b/moto/core/models.py index b6f4f541f..fa6b74834 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -1,22 +1,23 @@ from __future__ import unicode_literals +from __future__ import absolute_import import functools import inspect import re -from httpretty import HTTPretty +from moto.packages.responses import responses +from moto.packages.httpretty import HTTPretty from .responses import metadata_response -from .utils import convert_regex_to_flask_path +from .utils import convert_regex_to_flask_path, convert_flask_to_responses_response - -class MockAWS(object): +class BaseMockAWS(object): nested_count = 0 def __init__(self, backends): self.backends = backends if self.__class__.nested_count == 0: - HTTPretty.reset() + self.reset() def __call__(self, func, reset=True): if inspect.isclass(func): @@ -35,24 +36,7 @@ class MockAWS(object): for backend in self.backends.values(): backend.reset() - if not HTTPretty.is_enabled(): - HTTPretty.enable() - - for method in HTTPretty.METHODS: - backend = list(self.backends.values())[0] - for key, value in backend.urls.items(): - HTTPretty.register_uri( - method=method, - uri=re.compile(key), - body=value, - ) - - # Mock out localhost instance metadata - HTTPretty.register_uri( - method=method, - uri=re.compile('http://169.254.169.254/latest/meta-data/.*'), - body=metadata_response - ) + self.enable_patching() def stop(self): self.__class__.nested_count -= 1 @@ -60,9 +44,7 @@ class MockAWS(object): if self.__class__.nested_count < 0: raise RuntimeError('Called stop() before start().') - if self.__class__.nested_count == 0: - HTTPretty.disable() - HTTPretty.reset() + self.disable_patching() def decorate_callable(self, func, reset): def wrapper(*args, **kwargs): @@ -97,6 +79,73 @@ class MockAWS(object): return klass +class HttprettyMockAWS(BaseMockAWS): + def reset(self): + HTTPretty.reset() + + def enable_patching(self): + if not HTTPretty.is_enabled(): + HTTPretty.enable() + + for method in HTTPretty.METHODS: + backend = list(self.backends.values())[0] + for key, value in backend.urls.items(): + HTTPretty.register_uri( + method=method, + uri=re.compile(key), + body=value, + ) + + # Mock out localhost instance metadata + HTTPretty.register_uri( + method=method, + uri=re.compile('http://169.254.169.254/latest/meta-data/.*'), + body=metadata_response + ) + + def disable_patching(self): + if self.__class__.nested_count == 0: + HTTPretty.disable() + HTTPretty.reset() + + +RESPONSES_METHODS = [responses.GET, responses.DELETE, responses.HEAD, + responses.OPTIONS, responses.PATCH, responses.POST, responses.PUT] + + +class ResponsesMockAWS(BaseMockAWS): + def reset(self): + responses.reset() + + def enable_patching(self): + responses.start() + for method in RESPONSES_METHODS: + backend = list(self.backends.values())[0] + for key, value in backend.urls.items(): + responses.add_callback( + method=method, + url=re.compile(key), + callback=convert_flask_to_responses_response(value), + ) + + # Mock out localhost instance metadata + responses.add_callback( + method=method, + url=re.compile('http://169.254.169.254/latest/meta-data/.*'), + callback=convert_flask_to_responses_response(metadata_response), + ) + for pattern in responses.mock._urls: + pattern['stream'] = True + + def disable_patching(self): + if self.__class__.nested_count == 0: + try: + responses.stop() + except AttributeError: + pass + responses.reset() +MockAWS = ResponsesMockAWS + class Model(type): def __new__(self, clsname, bases, namespace): cls = super(Model, self).__new__(self, clsname, bases, namespace) @@ -187,6 +236,12 @@ class BaseBackend(object): else: return MockAWS({'global': self}) + def deprecated_decorator(self, func=None): + if func: + return HttprettyMockAWS({'global': self})(func) + else: + return HttprettyMockAWS({'global': self}) + class base_decorator(object): mock_backend = MockAWS @@ -199,3 +254,7 @@ class base_decorator(object): return self.mock_backend(self.backends)(func) else: return self.mock_backend(self.backends) + + +class deprecated_base_decorator(base_decorator): + mock_backend = HttprettyMockAWS diff --git a/moto/core/responses.py b/moto/core/responses.py index af4217245..337227d3c 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -138,7 +138,7 @@ class BaseResponse(_TemplateEnvironmentMixin): flat = flatten_json_request_body('', decoded, input_spec) for key, value in flat.items(): querystring[key] = [value] - else: + elif self.body: querystring.update(parse_qs(self.body, keep_blank_values=True)) if not querystring: querystring.update(headers) @@ -152,6 +152,8 @@ class BaseResponse(_TemplateEnvironmentMixin): self.region = self.get_region_from_url(full_url) self.headers = request.headers + if 'host' not in self.headers: + self.headers['host'] = urlparse(full_url).netloc self.response_headers = headers def get_region_from_url(self, full_url): @@ -189,6 +191,9 @@ class BaseResponse(_TemplateEnvironmentMixin): body, new_headers = response status = new_headers.get('status', 200) headers.update(new_headers) + # Cast status to string + if "status" in headers: + headers['status'] = str(headers['status']) return status, headers, body raise NotImplementedError("The {0} action has not been implemented".format(action)) @@ -327,6 +332,7 @@ def metadata_response(request, full_url, headers): http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html """ + parsed_url = urlparse(full_url) tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1) credentials = dict( diff --git a/moto/core/utils.py b/moto/core/utils.py index 0b30556ac..0f4b20b6d 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -103,6 +103,28 @@ class convert_flask_to_httpretty_response(object): return response, status, headers +class convert_flask_to_responses_response(object): + + def __init__(self, callback): + self.callback = callback + + @property + def __name__(self): + # For instance methods, use class and method names. Otherwise + # use module and method name + if inspect.ismethod(self.callback): + outer = self.callback.__self__.__class__.__name__ + else: + outer = self.callback.__module__ + return "{0}.{1}".format(outer, self.callback.__name__) + + def __call__(self, request, *args, **kwargs): + result = self.callback(request, request.url, request.headers) + # result is a status, headers, response tuple + status, headers, response = result + return status, headers, response + + def iso_8601_datetime_with_milliseconds(datetime): return datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z' diff --git a/moto/datapipeline/__init__.py b/moto/datapipeline/__init__.py index dd013526e..cebcf22bf 100644 --- a/moto/datapipeline/__init__.py +++ b/moto/datapipeline/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import datapipeline_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator datapipeline_backend = datapipeline_backends['us-east-1'] mock_datapipeline = base_decorator(datapipeline_backends) +mock_datapipeline_deprecated = deprecated_base_decorator(datapipeline_backends) diff --git a/moto/dynamodb/__init__.py b/moto/dynamodb/__init__.py index 6f2509f79..008050317 100644 --- a/moto/dynamodb/__init__.py +++ b/moto/dynamodb/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import dynamodb_backend mock_dynamodb = dynamodb_backend.decorator +mock_dynamodb_deprecated = dynamodb_backend.deprecated_decorator diff --git a/moto/dynamodb2/__init__.py b/moto/dynamodb2/__init__.py index 8579c48df..f0892d13f 100644 --- a/moto/dynamodb2/__init__.py +++ b/moto/dynamodb2/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import dynamodb_backend2 mock_dynamodb2 = dynamodb_backend2.decorator +mock_dynamodb2_deprecated = dynamodb_backend2.deprecated_decorator \ No newline at end of file diff --git a/moto/ec2/__init__.py b/moto/ec2/__init__.py index b269e933b..608173577 100644 --- a/moto/ec2/__init__.py +++ b/moto/ec2/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import ec2_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator ec2_backend = ec2_backends['us-east-1'] mock_ec2 = base_decorator(ec2_backends) +mock_ec2_deprecated = deprecated_base_decorator(ec2_backends) diff --git a/moto/ecs/__init__.py b/moto/ecs/__init__.py index 9c07a0d55..6864355ad 100644 --- a/moto/ecs/__init__.py +++ b/moto/ecs/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import ecs_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator ecs_backend = ecs_backends['us-east-1'] mock_ecs = base_decorator(ecs_backends) +mock_ecs_deprecated = deprecated_base_decorator(ecs_backends) diff --git a/moto/elb/__init__.py b/moto/elb/__init__.py index 376dfe0e1..a8e8dab8d 100644 --- a/moto/elb/__init__.py +++ b/moto/elb/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import elb_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator elb_backend = elb_backends['us-east-1'] mock_elb = base_decorator(elb_backends) +mock_elb_deprecated = deprecated_base_decorator(elb_backends) diff --git a/moto/emr/__init__.py b/moto/emr/__init__.py index f79df39fa..fc6b4d4ab 100644 --- a/moto/emr/__init__.py +++ b/moto/emr/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import emr_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator emr_backend = emr_backends['us-east-1'] mock_emr = base_decorator(emr_backends) +mock_emr_deprecated = deprecated_base_decorator(emr_backends) diff --git a/moto/glacier/__init__.py b/moto/glacier/__init__.py index 3256462a3..49b3375e1 100644 --- a/moto/glacier/__init__.py +++ b/moto/glacier/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import glacier_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator glacier_backend = glacier_backends['us-east-1'] mock_glacier = base_decorator(glacier_backends) +mock_glacier_deprecated = deprecated_base_decorator(glacier_backends) diff --git a/moto/iam/__init__.py b/moto/iam/__init__.py index 483969e19..02519cbc9 100644 --- a/moto/iam/__init__.py +++ b/moto/iam/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import iam_backend mock_iam = iam_backend.decorator +mock_iam_deprecated = iam_backend.deprecated_decorator \ No newline at end of file diff --git a/moto/kinesis/__init__.py b/moto/kinesis/__init__.py index 50bc07155..c3f06d5b1 100644 --- a/moto/kinesis/__init__.py +++ b/moto/kinesis/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import kinesis_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator kinesis_backend = kinesis_backends['us-east-1'] mock_kinesis = base_decorator(kinesis_backends) +mock_kinesis_deprecated = deprecated_base_decorator(kinesis_backends) diff --git a/moto/kinesis/responses.py b/moto/kinesis/responses.py index 264f53a2c..d0a90a61e 100644 --- a/moto/kinesis/responses.py +++ b/moto/kinesis/responses.py @@ -19,7 +19,10 @@ class KinesisResponse(BaseResponse): @property def is_firehose(self): - host = self.headers.get('host') or self.headers['Host'] + try: + host = self.headers.get('host') or self.headers['Host'] + except KeyError: + import pdb;pdb.set_trace() return host.startswith('firehose') def create_stream(self): diff --git a/moto/kms/__init__.py b/moto/kms/__init__.py index 4ee6dd2f4..b6bffa804 100644 --- a/moto/kms/__init__.py +++ b/moto/kms/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import kms_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator kms_backend = kms_backends['us-east-1'] mock_kms = base_decorator(kms_backends) +mock_kms_deprecated = deprecated_base_decorator(kms_backends) diff --git a/moto/opsworks/__init__.py b/moto/opsworks/__init__.py index ef5190997..75f49eba5 100644 --- a/moto/opsworks/__init__.py +++ b/moto/opsworks/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import opsworks_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator opsworks_backend = opsworks_backends['us-east-1'] mock_opsworks = base_decorator(opsworks_backends) diff --git a/moto/opsworks/urls.py b/moto/opsworks/urls.py index 6913de6bb..3d72bb0dd 100644 --- a/moto/opsworks/urls.py +++ b/moto/opsworks/urls.py @@ -4,7 +4,7 @@ from .responses import OpsWorksResponse # AWS OpsWorks has a single endpoint: opsworks.us-east-1.amazonaws.com # and only supports HTTPS requests. url_bases = [ - "opsworks.us-east-1.amazonaws.com" + "https?://opsworks.us-east-1.amazonaws.com" ] url_paths = { diff --git a/moto/packages/__init__.py b/moto/packages/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/moto/packages/httpretty/__init__.py b/moto/packages/httpretty/__init__.py new file mode 100644 index 000000000..a752b452a --- /dev/null +++ b/moto/packages/httpretty/__init__.py @@ -0,0 +1,60 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011-2013> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +__version__ = version = '0.8.10' + +from .core import httpretty, httprettified, EmptyRequestHeaders +from .errors import HTTPrettyError, UnmockedError +from .core import URIInfo + +HTTPretty = httpretty +activate = httprettified + +enable = httpretty.enable +register_uri = httpretty.register_uri +disable = httpretty.disable +is_enabled = httpretty.is_enabled +reset = httpretty.reset +Response = httpretty.Response + +GET = httpretty.GET +PUT = httpretty.PUT +POST = httpretty.POST +DELETE = httpretty.DELETE +HEAD = httpretty.HEAD +PATCH = httpretty.PATCH +OPTIONS = httpretty.OPTIONS +CONNECT = httpretty.CONNECT + + +def last_request(): + """returns the last request""" + return httpretty.last_request + +def has_request(): + """returns a boolean indicating whether any request has been made""" + return not isinstance(httpretty.last_request.headers, EmptyRequestHeaders) diff --git a/moto/packages/httpretty/compat.py b/moto/packages/httpretty/compat.py new file mode 100644 index 000000000..6805cf638 --- /dev/null +++ b/moto/packages/httpretty/compat.py @@ -0,0 +1,100 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# +# Copyright (C) <2011-2013> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import sys +import types + +PY3 = sys.version_info[0] == 3 +if PY3: # pragma: no cover + text_type = str + byte_type = bytes + import io + StringIO = io.BytesIO + basestring = (str, bytes) + + class BaseClass(object): + def __repr__(self): + return self.__str__() +else: # pragma: no cover + text_type = unicode + byte_type = str + import StringIO + StringIO = StringIO.StringIO + basestring = basestring + + +class BaseClass(object): + def __repr__(self): + ret = self.__str__() + if PY3: # pragma: no cover + return ret + else: + return ret.encode('utf-8') + + +try: # pragma: no cover + from urllib.parse import urlsplit, urlunsplit, parse_qs, quote, quote_plus, unquote + unquote_utf8 = unquote +except ImportError: # pragma: no cover + from urlparse import urlsplit, urlunsplit, parse_qs, unquote + from urllib import quote, quote_plus + def unquote_utf8(qs): + if isinstance(qs, text_type): + qs = qs.encode('utf-8') + s = unquote(qs) + if isinstance(s, byte_type): + return s.decode("utf-8") + else: + return s + + +try: # pragma: no cover + from http.server import BaseHTTPRequestHandler +except ImportError: # pragma: no cover + from BaseHTTPServer import BaseHTTPRequestHandler + + +ClassTypes = (type,) +if not PY3: # pragma: no cover + ClassTypes = (type, types.ClassType) + + +__all__ = [ + 'PY3', + 'StringIO', + 'text_type', + 'byte_type', + 'BaseClass', + 'BaseHTTPRequestHandler', + 'quote', + 'quote_plus', + 'urlunsplit', + 'urlsplit', + 'parse_qs', + 'ClassTypes', +] diff --git a/moto/packages/httpretty/core.py b/moto/packages/httpretty/core.py new file mode 100644 index 000000000..4764cbba9 --- /dev/null +++ b/moto/packages/httpretty/core.py @@ -0,0 +1,1071 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011-2013> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import re +import codecs +import inspect +import socket +import functools +import itertools +import warnings +import logging +import traceback +import json +import contextlib + + +from .compat import ( + PY3, + StringIO, + text_type, + BaseClass, + BaseHTTPRequestHandler, + quote, + quote_plus, + urlunsplit, + urlsplit, + parse_qs, + unquote, + unquote_utf8, + ClassTypes, + basestring +) +from .http import ( + STATUSES, + HttpBaseClass, + parse_requestline, + last_requestline, +) + +from .utils import ( + utf8, + decode_utf8, +) + +from .errors import HTTPrettyError, UnmockedError + +from datetime import datetime +from datetime import timedelta +from errno import EAGAIN + +old_socket = socket.socket +old_create_connection = socket.create_connection +old_gethostbyname = socket.gethostbyname +old_gethostname = socket.gethostname +old_getaddrinfo = socket.getaddrinfo +old_socksocket = None +old_ssl_wrap_socket = None +old_sslwrap_simple = None +old_sslsocket = None + +if PY3: # pragma: no cover + basestring = (bytes, str) +try: # pragma: no cover + import socks + old_socksocket = socks.socksocket +except ImportError: + socks = None + +try: # pragma: no cover + import ssl + old_ssl_wrap_socket = ssl.wrap_socket + if not PY3: + old_sslwrap_simple = ssl.sslwrap_simple + old_sslsocket = ssl.SSLSocket +except ImportError: # pragma: no cover + ssl = None + + +DEFAULT_HTTP_PORTS = frozenset([80]) +POTENTIAL_HTTP_PORTS = set(DEFAULT_HTTP_PORTS) +DEFAULT_HTTPS_PORTS = frozenset([443]) +POTENTIAL_HTTPS_PORTS = set(DEFAULT_HTTPS_PORTS) + + +class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): + """Represents a HTTP request. It takes a valid multi-line, `\r\n` + separated string with HTTP headers and parse them out using the + internal `parse_request` method. + + It also replaces the `rfile` and `wfile` attributes with StringIO + instances so that we garantee that it won't make any I/O, neighter + for writing nor reading. + + It has some convenience attributes: + + `headers` -> a mimetype object that can be cast into a dictionary, + contains all the request headers + + `method` -> the HTTP method used in this request + + `querystring` -> a dictionary containing lists with the + attributes. Please notice that if you need a single value from a + query string you will need to get it manually like: + + ```python + >>> request.querystring + {'name': ['Gabriel Falcao']} + >>> print request.querystring['name'][0] + ``` + + `parsed_body` -> a dictionary containing parsed request body or + None if HTTPrettyRequest doesn't know how to parse it. It + currently supports parsing body data that was sent under the + `content-type` headers values: 'application/json' or + 'application/x-www-form-urlencoded' + """ + def __init__(self, headers, body=''): + # first of all, lets make sure that if headers or body are + # unicode strings, it must be converted into a utf-8 encoded + # byte string + self.raw_headers = utf8(headers.strip()) + self.body = utf8(body) + + # Now let's concatenate the headers with the body, and create + # `rfile` based on it + self.rfile = StringIO(b'\r\n\r\n'.join([self.raw_headers, self.body])) + self.wfile = StringIO() # Creating `wfile` as an empty + # StringIO, just to avoid any real + # I/O calls + + # parsing the request line preemptively + self.raw_requestline = self.rfile.readline() + + # initiating the error attributes with None + self.error_code = None + self.error_message = None + + # Parse the request based on the attributes above + if not self.parse_request(): + return + + # making the HTTP method string available as the command + self.method = self.command + + # Now 2 convenient attributes for the HTTPretty API: + + # `querystring` holds a dictionary with the parsed query string + try: + self.path = self.path.encode('iso-8859-1') + except UnicodeDecodeError: + pass + + self.path = decode_utf8(self.path) + + qstring = self.path.split("?", 1)[-1] + self.querystring = self.parse_querystring(qstring) + + # And the body will be attempted to be parsed as + # `application/json` or `application/x-www-form-urlencoded` + self.parsed_body = self.parse_request_body(self.body) + + def __str__(self): + return ''.format( + self.headers.get('content-type', ''), + len(self.headers), + len(self.body), + ) + + def parse_querystring(self, qs): + expanded = unquote_utf8(qs) + parsed = parse_qs(expanded) + result = {} + for k in parsed: + result[k] = list(map(decode_utf8, parsed[k])) + + return result + + def parse_request_body(self, body): + """ Attempt to parse the post based on the content-type passed. Return the regular body if not """ + + PARSING_FUNCTIONS = { + 'application/json': json.loads, + 'text/json': json.loads, + 'application/x-www-form-urlencoded': self.parse_querystring, + } + FALLBACK_FUNCTION = lambda x: x + + content_type = self.headers.get('content-type', '') + + do_parse = PARSING_FUNCTIONS.get(content_type, FALLBACK_FUNCTION) + try: + body = decode_utf8(body) + return do_parse(body) + except: + return body + + +class EmptyRequestHeaders(dict): + pass + + +class HTTPrettyRequestEmpty(object): + body = '' + headers = EmptyRequestHeaders() + + +class FakeSockFile(StringIO): + def close(self): + self.socket.close() + StringIO.close(self) + + +class FakeSSLSocket(object): + def __init__(self, sock, *args, **kw): + self._httpretty_sock = sock + + def __getattr__(self, attr): + return getattr(self._httpretty_sock, attr) + + +class fakesock(object): + class socket(object): + _entry = None + debuglevel = 0 + _sent_data = [] + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, + protocol=0): + self.truesock = (old_socket(family, type, protocol) + if httpretty.allow_net_connect + else None) + self._closed = True + self.fd = FakeSockFile() + self.fd.socket = self + self.timeout = socket._GLOBAL_DEFAULT_TIMEOUT + self._sock = self + self.is_http = False + self._bufsize = 1024 + + def getpeercert(self, *a, **kw): + now = datetime.now() + shift = now + timedelta(days=30 * 12) + return { + 'notAfter': shift.strftime('%b %d %H:%M:%S GMT'), + 'subjectAltName': ( + ('DNS', '*%s' % self._host), + ('DNS', self._host), + ('DNS', '*'), + ), + 'subject': ( + ( + ('organizationName', '*.%s' % self._host), + ), + ( + ('organizationalUnitName', + 'Domain Control Validated'), + ), + ( + ('commonName', '*.%s' % self._host), + ), + ), + } + + def ssl(self, sock, *args, **kw): + return sock + + def setsockopt(self, level, optname, value): + if self.truesock: + self.truesock.setsockopt(level, optname, value) + + def connect(self, address): + self._closed = False + + try: + self._address = (self._host, self._port) = address + except ValueError: + # We get here when the address is just a string pointing to a + # unix socket path/file + # + # See issue #206 + self.is_http = False + else: + self.is_http = self._port in POTENTIAL_HTTP_PORTS | POTENTIAL_HTTPS_PORTS + + if not self.is_http: + if self.truesock: + self.truesock.connect(self._address) + else: + raise UnmockedError() + + def close(self): + if not (self.is_http and self._closed): + if self.truesock: + self.truesock.close() + self._closed = True + + def makefile(self, mode='r', bufsize=-1): + """Returns this fake socket's own StringIO buffer. + + If there is an entry associated with the socket, the file + descriptor gets filled in with the entry data before being + returned. + """ + self._mode = mode + self._bufsize = bufsize + + if self._entry: + self._entry.fill_filekind(self.fd) + + return self.fd + + def real_sendall(self, data, *args, **kw): + """Sends data to the remote server. This method is called + when HTTPretty identifies that someone is trying to send + non-http data. + + The received bytes are written in this socket's StringIO + buffer so that HTTPretty can return it accordingly when + necessary. + """ + + if not self.truesock: + raise UnmockedError() + + if not self.is_http: + return self.truesock.sendall(data, *args, **kw) + + self.truesock.connect(self._address) + + self.truesock.setblocking(1) + self.truesock.sendall(data, *args, **kw) + + should_continue = True + while should_continue: + try: + received = self.truesock.recv(self._bufsize) + self.fd.write(received) + should_continue = len(received) == self._bufsize + + except socket.error as e: + if e.errno == EAGAIN: + continue + break + + self.fd.seek(0) + + def sendall(self, data, *args, **kw): + self._sent_data.append(data) + self.fd = FakeSockFile() + self.fd.socket = self + try: + requestline, _ = data.split(b'\r\n', 1) + method, path, version = parse_requestline(decode_utf8(requestline)) + is_parsing_headers = True + except ValueError: + is_parsing_headers = False + + if not self._entry: + # If the previous request wasn't mocked, don't mock the subsequent sending of data + return self.real_sendall(data, *args, **kw) + + self.fd.seek(0) + + if not is_parsing_headers: + if len(self._sent_data) > 1: + headers = utf8(last_requestline(self._sent_data)) + meta = self._entry.request.headers + body = utf8(self._sent_data[-1]) + if meta.get('transfer-encoding', '') == 'chunked': + if not body.isdigit() and body != b'\r\n' and body != b'0\r\n\r\n': + self._entry.request.body += body + else: + self._entry.request.body += body + + httpretty.historify_request(headers, body, False) + return + + # path might come with + s = urlsplit(path) + POTENTIAL_HTTP_PORTS.add(int(s.port or 80)) + headers, body = list(map(utf8, data.split(b'\r\n\r\n', 1))) + + request = httpretty.historify_request(headers, body) + + info = URIInfo(hostname=self._host, port=self._port, + path=s.path, + query=s.query, + last_request=request) + + matcher, entries = httpretty.match_uriinfo(info) + + if not entries: + self._entry = None + self.real_sendall(data) + return + + self._entry = matcher.get_next_entry(method, info, request) + + def debug(self, truesock_func, *a, **kw): + if self.is_http: + frame = inspect.stack()[0][0] + lines = list(map(utf8, traceback.format_stack(frame))) + + message = [ + "HTTPretty intercepted and unexpected socket method call.", + ("Please open an issue at " + "'https://github.com/gabrielfalcao/HTTPretty/issues'"), + "And paste the following traceback:\n", + "".join(decode_utf8(lines)), + ] + raise RuntimeError("\n".join(message)) + if not self.truesock: + raise UnmockedError() + return getattr(self.truesock, truesock_func)(*a, **kw) + + def settimeout(self, new_timeout): + self.timeout = new_timeout + + def send(self, *args, **kwargs): + return self.debug('send', *args, **kwargs) + + def sendto(self, *args, **kwargs): + return self.debug('sendto', *args, **kwargs) + + def recvfrom_into(self, *args, **kwargs): + return self.debug('recvfrom_into', *args, **kwargs) + + def recv_into(self, *args, **kwargs): + return self.debug('recv_into', *args, **kwargs) + + def recvfrom(self, *args, **kwargs): + return self.debug('recvfrom', *args, **kwargs) + + def recv(self, *args, **kwargs): + return self.debug('recv', *args, **kwargs) + + def __getattr__(self, name): + if not self.truesock: + raise UnmockedError() + return getattr(self.truesock, name) + + +def fake_wrap_socket(s, *args, **kw): + return s + + +def create_fake_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None): + s = fakesock.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + s.settimeout(timeout) + if source_address: + s.bind(source_address) + s.connect(address) + return s + + +def fake_gethostbyname(host): + return '127.0.0.1' + + +def fake_gethostname(): + return 'localhost' + + +def fake_getaddrinfo( + host, port, family=None, socktype=None, proto=None, flags=None): + return [(2, 1, 6, '', (host, port))] + + +class Entry(BaseClass): + def __init__(self, method, uri, body, + adding_headers=None, + forcing_headers=None, + status=200, + streaming=False, + **headers): + + self.method = method + self.uri = uri + self.info = None + self.request = None + + self.body_is_callable = False + if hasattr(body, "__call__"): + self.callable_body = body + self.body = None + self.body_is_callable = True + elif isinstance(body, text_type): + self.body = utf8(body) + else: + self.body = body + + self.streaming = streaming + if not streaming and not self.body_is_callable: + self.body_length = len(self.body or '') + else: + self.body_length = 0 + + self.adding_headers = adding_headers or {} + self.forcing_headers = forcing_headers or {} + self.status = int(status) + + for k, v in headers.items(): + name = "-".join(k.split("_")).title() + self.adding_headers[name] = v + + self.validate() + + def validate(self): + content_length_keys = 'Content-Length', 'content-length' + for key in content_length_keys: + got = self.adding_headers.get( + key, self.forcing_headers.get(key, None)) + + if got is None: + continue + + try: + igot = int(got) + except ValueError: + warnings.warn( + 'HTTPretty got to register the Content-Length header ' \ + 'with "%r" which is not a number' % got, + ) + + if igot > self.body_length: + raise HTTPrettyError( + 'HTTPretty got inconsistent parameters. The header ' \ + 'Content-Length you registered expects size "%d" but ' \ + 'the body you registered for that has actually length ' \ + '"%d".' % ( + igot, self.body_length, + ) + ) + + def __str__(self): + return r'' % ( + self.method, self.uri, self.status) + + def normalize_headers(self, headers): + new = {} + for k in headers: + new_k = '-'.join([s.lower() for s in k.split('-')]) + new[new_k] = headers[k] + + return new + + def fill_filekind(self, fk): + now = datetime.utcnow() + + headers = { + 'status': self.status, + 'date': now.strftime('%a, %d %b %Y %H:%M:%S GMT'), + 'server': 'Python/HTTPretty', + 'connection': 'close', + } + + if self.forcing_headers: + headers = self.forcing_headers + + if self.adding_headers: + headers.update(self.normalize_headers(self.adding_headers)) + + headers = self.normalize_headers(headers) + status = headers.get('status', self.status) + if self.body_is_callable: + status, headers, self.body = self.callable_body(self.request, self.info.full_url(), headers) + if self.request.method != "HEAD": + headers.update({ + 'content-length': len(self.body) + }) + + string_list = [ + 'HTTP/1.1 %d %s' % (status, STATUSES[status]), + ] + + if 'date' in headers: + string_list.append('date: %s' % headers.pop('date')) + + if not self.forcing_headers: + content_type = headers.pop('content-type', + 'text/plain; charset=utf-8') + + content_length = headers.pop('content-length', self.body_length) + + string_list.append('content-type: %s' % content_type) + if not self.streaming: + string_list.append('content-length: %s' % content_length) + + string_list.append('server: %s' % headers.pop('server')) + + for k, v in headers.items(): + string_list.append( + '{0}: {1}'.format(k, v), + ) + + for item in string_list: + fk.write(utf8(item) + b'\n') + + fk.write(b'\r\n') + + if self.streaming: + self.body, body = itertools.tee(self.body) + for chunk in body: + fk.write(utf8(chunk)) + else: + fk.write(utf8(self.body)) + + fk.seek(0) + + +def url_fix(s, charset='utf-8'): + scheme, netloc, path, querystring, fragment = urlsplit(s) + path = quote(path, b'/%') + querystring = quote_plus(querystring, b':&=') + return urlunsplit((scheme, netloc, path, querystring, fragment)) + + +class URIInfo(BaseClass): + def __init__(self, + username='', + password='', + hostname='', + port=80, + path='/', + query='', + fragment='', + scheme='', + last_request=None): + + self.username = username or '' + self.password = password or '' + self.hostname = hostname or '' + + if port: + port = int(port) + + elif scheme == 'https': + port = 443 + + self.port = port or 80 + self.path = path or '' + self.query = query or '' + if scheme: + self.scheme = scheme + elif self.port in POTENTIAL_HTTPS_PORTS: + self.scheme = 'https' + else: + self.scheme = 'http' + self.fragment = fragment or '' + self.last_request = last_request + + def __str__(self): + attrs = ( + 'username', + 'password', + 'hostname', + 'port', + 'path', + ) + fmt = ", ".join(['%s="%s"' % (k, getattr(self, k, '')) for k in attrs]) + return r'' % fmt + + def __hash__(self): + return hash(text_type(self)) + + def __eq__(self, other): + self_tuple = ( + self.port, + decode_utf8(self.hostname.lower()), + url_fix(decode_utf8(self.path)), + ) + other_tuple = ( + other.port, + decode_utf8(other.hostname.lower()), + url_fix(decode_utf8(other.path)), + ) + return self_tuple == other_tuple + + def full_url(self, use_querystring=True): + credentials = "" + if self.password: + credentials = "{0}:{1}@".format( + self.username, self.password) + + query = "" + if use_querystring and self.query: + query = "?{0}".format(decode_utf8(self.query)) + + result = "{scheme}://{credentials}{domain}{path}{query}".format( + scheme=self.scheme, + credentials=credentials, + domain=self.get_full_domain(), + path=decode_utf8(self.path), + query=query + ) + return result + + def get_full_domain(self): + hostname = decode_utf8(self.hostname) + # Port 80/443 should not be appended to the url + if self.port not in DEFAULT_HTTP_PORTS | DEFAULT_HTTPS_PORTS: + return ":".join([hostname, str(self.port)]) + + return hostname + + @classmethod + def from_uri(cls, uri, entry): + result = urlsplit(uri) + if result.scheme == 'https': + POTENTIAL_HTTPS_PORTS.add(int(result.port or 443)) + else: + POTENTIAL_HTTP_PORTS.add(int(result.port or 80)) + return cls(result.username, + result.password, + result.hostname, + result.port, + result.path, + result.query, + result.fragment, + result.scheme, + entry) + + +class URIMatcher(object): + regex = None + info = None + + def __init__(self, uri, entries, match_querystring=False): + self._match_querystring = match_querystring + if type(uri).__name__ == 'SRE_Pattern': + self.regex = uri + result = urlsplit(uri.pattern) + if result.scheme == 'https': + POTENTIAL_HTTPS_PORTS.add(int(result.port or 443)) + else: + POTENTIAL_HTTP_PORTS.add(int(result.port or 80)) + else: + self.info = URIInfo.from_uri(uri, entries) + + self.entries = entries + + #hash of current_entry pointers, per method. + self.current_entries = {} + + def matches(self, info): + if self.info: + return self.info == info + else: + return self.regex.search(info.full_url( + use_querystring=self._match_querystring)) + + def __str__(self): + wrap = 'URLMatcher({0})' + if self.info: + return wrap.format(text_type(self.info)) + else: + return wrap.format(self.regex.pattern) + + def get_next_entry(self, method, info, request): + """Cycle through available responses, but only once. + Any subsequent requests will receive the last response""" + + if method not in self.current_entries: + self.current_entries[method] = 0 + + #restrict selection to entries that match the requested method + entries_for_method = [e for e in self.entries if e.method == method] + + if self.current_entries[method] >= len(entries_for_method): + self.current_entries[method] = -1 + + if not self.entries or not entries_for_method: + raise ValueError('I have no entries for method %s: %s' + % (method, self)) + + entry = entries_for_method[self.current_entries[method]] + if self.current_entries[method] != -1: + self.current_entries[method] += 1 + + # Attach more info to the entry + # So the callback can be more clever about what to do + # This does also fix the case where the callback + # would be handed a compiled regex as uri instead of the + # real uri + entry.info = info + entry.request = request + return entry + + def __hash__(self): + return hash(text_type(self)) + + def __eq__(self, other): + return text_type(self) == text_type(other) + + +class httpretty(HttpBaseClass): + """The URI registration class""" + _entries = {} + latest_requests = [] + + last_request = HTTPrettyRequestEmpty() + _is_enabled = False + allow_net_connect = True + + @classmethod + def match_uriinfo(cls, info): + for matcher, value in cls._entries.items(): + if matcher.matches(info): + return (matcher, info) + + return (None, []) + + @classmethod + @contextlib.contextmanager + def record(cls, filename, indentation=4, encoding='utf-8'): + try: + import urllib3 + except ImportError: + raise RuntimeError('HTTPretty requires urllib3 installed for recording actual requests.') + + + http = urllib3.PoolManager() + + cls.enable() + calls = [] + def record_request(request, uri, headers): + cls.disable() + + response = http.request(request.method, uri) + calls.append({ + 'request': { + 'uri': uri, + 'method': request.method, + 'headers': dict(request.headers), + 'body': decode_utf8(request.body), + 'querystring': request.querystring + }, + 'response': { + 'status': response.status, + 'body': decode_utf8(response.data), + 'headers': dict(response.headers) + } + }) + cls.enable() + return response.status, response.headers, response.data + + for method in cls.METHODS: + cls.register_uri(method, re.compile(r'.*', re.M), body=record_request) + + yield + cls.disable() + with codecs.open(filename, 'w', encoding) as f: + f.write(json.dumps(calls, indent=indentation)) + + @classmethod + @contextlib.contextmanager + def playback(cls, origin): + cls.enable() + + data = json.loads(open(origin).read()) + for item in data: + uri = item['request']['uri'] + method = item['request']['method'] + cls.register_uri(method, uri, body=item['response']['body'], forcing_headers=item['response']['headers']) + + yield + cls.disable() + + @classmethod + def reset(cls): + POTENTIAL_HTTP_PORTS.intersection_update(DEFAULT_HTTP_PORTS) + POTENTIAL_HTTPS_PORTS.intersection_update(DEFAULT_HTTPS_PORTS) + cls._entries.clear() + cls.latest_requests = [] + cls.last_request = HTTPrettyRequestEmpty() + + @classmethod + def historify_request(cls, headers, body='', append=True): + request = HTTPrettyRequest(headers, body) + cls.last_request = request + if append or not cls.latest_requests: + cls.latest_requests.append(request) + else: + cls.latest_requests[-1] = request + return request + + @classmethod + def register_uri(cls, method, uri, body='HTTPretty :)', + adding_headers=None, + forcing_headers=None, + status=200, + responses=None, match_querystring=False, + **headers): + + uri_is_string = isinstance(uri, basestring) + + if uri_is_string and re.search(r'^\w+://[^/]+[.]\w{2,}$', uri): + uri += '/' + + if isinstance(responses, list) and len(responses) > 0: + for response in responses: + response.uri = uri + response.method = method + entries_for_this_uri = responses + else: + headers[str('body')] = body + headers[str('adding_headers')] = adding_headers + headers[str('forcing_headers')] = forcing_headers + headers[str('status')] = status + + entries_for_this_uri = [ + cls.Response(method=method, uri=uri, **headers), + ] + + matcher = URIMatcher(uri, entries_for_this_uri, + match_querystring) + if matcher in cls._entries: + matcher.entries.extend(cls._entries[matcher]) + del cls._entries[matcher] + + cls._entries[matcher] = entries_for_this_uri + + def __str__(self): + return '' % len(self._entries) + + @classmethod + def Response(cls, body, method=None, uri=None, adding_headers=None, forcing_headers=None, + status=200, streaming=False, **headers): + + headers[str('body')] = body + headers[str('adding_headers')] = adding_headers + headers[str('forcing_headers')] = forcing_headers + headers[str('status')] = int(status) + headers[str('streaming')] = streaming + return Entry(method, uri, **headers) + + @classmethod + def disable(cls): + cls._is_enabled = False + socket.socket = old_socket + socket.SocketType = old_socket + socket._socketobject = old_socket + + socket.create_connection = old_create_connection + socket.gethostname = old_gethostname + socket.gethostbyname = old_gethostbyname + socket.getaddrinfo = old_getaddrinfo + + socket.__dict__['socket'] = old_socket + socket.__dict__['_socketobject'] = old_socket + socket.__dict__['SocketType'] = old_socket + + socket.__dict__['create_connection'] = old_create_connection + socket.__dict__['gethostname'] = old_gethostname + socket.__dict__['gethostbyname'] = old_gethostbyname + socket.__dict__['getaddrinfo'] = old_getaddrinfo + + if socks: + socks.socksocket = old_socksocket + socks.__dict__['socksocket'] = old_socksocket + + if ssl: + ssl.wrap_socket = old_ssl_wrap_socket + ssl.SSLSocket = old_sslsocket + ssl.__dict__['wrap_socket'] = old_ssl_wrap_socket + ssl.__dict__['SSLSocket'] = old_sslsocket + + if not PY3: + ssl.sslwrap_simple = old_sslwrap_simple + ssl.__dict__['sslwrap_simple'] = old_sslwrap_simple + + @classmethod + def is_enabled(cls): + return cls._is_enabled + + @classmethod + def enable(cls): + cls._is_enabled = True + # Some versions of python internally shadowed the + # SocketType variable incorrectly https://bugs.python.org/issue20386 + bad_socket_shadow = (socket.socket != socket.SocketType) + + socket.socket = fakesock.socket + socket._socketobject = fakesock.socket + if not bad_socket_shadow: + socket.SocketType = fakesock.socket + + socket.create_connection = create_fake_connection + socket.gethostname = fake_gethostname + socket.gethostbyname = fake_gethostbyname + socket.getaddrinfo = fake_getaddrinfo + + socket.__dict__['socket'] = fakesock.socket + socket.__dict__['_socketobject'] = fakesock.socket + if not bad_socket_shadow: + socket.__dict__['SocketType'] = fakesock.socket + + socket.__dict__['create_connection'] = create_fake_connection + socket.__dict__['gethostname'] = fake_gethostname + socket.__dict__['gethostbyname'] = fake_gethostbyname + socket.__dict__['getaddrinfo'] = fake_getaddrinfo + + if socks: + socks.socksocket = fakesock.socket + socks.__dict__['socksocket'] = fakesock.socket + + if ssl: + ssl.wrap_socket = fake_wrap_socket + ssl.SSLSocket = FakeSSLSocket + + ssl.__dict__['wrap_socket'] = fake_wrap_socket + ssl.__dict__['SSLSocket'] = FakeSSLSocket + + if not PY3: + ssl.sslwrap_simple = fake_wrap_socket + ssl.__dict__['sslwrap_simple'] = fake_wrap_socket + + +def httprettified(test): + "A decorator tests that use HTTPretty" + def decorate_class(klass): + for attr in dir(klass): + if not attr.startswith('test_'): + continue + + attr_value = getattr(klass, attr) + if not hasattr(attr_value, "__call__"): + continue + + setattr(klass, attr, decorate_callable(attr_value)) + return klass + + def decorate_callable(test): + @functools.wraps(test) + def wrapper(*args, **kw): + httpretty.reset() + httpretty.enable() + try: + return test(*args, **kw) + finally: + httpretty.disable() + return wrapper + + if isinstance(test, ClassTypes): + return decorate_class(test) + return decorate_callable(test) diff --git a/moto/packages/httpretty/errors.py b/moto/packages/httpretty/errors.py new file mode 100644 index 000000000..cb6479bf5 --- /dev/null +++ b/moto/packages/httpretty/errors.py @@ -0,0 +1,39 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# +# Copyright (C) <2011-2013> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + + +class HTTPrettyError(Exception): + pass + + +class UnmockedError(HTTPrettyError): + def __init__(self): + super(UnmockedError, self).__init__( + 'No mocking was registered, and real connections are ' + 'not allowed (httpretty.allow_net_connect = False).' + ) diff --git a/moto/packages/httpretty/http.py b/moto/packages/httpretty/http.py new file mode 100644 index 000000000..7e9a56885 --- /dev/null +++ b/moto/packages/httpretty/http.py @@ -0,0 +1,155 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011-2013> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +import re +from .compat import BaseClass +from .utils import decode_utf8 + + +STATUSES = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request a Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 420: "Enhance Your Calm", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 424: "Method Failure", + 425: "Unordered Collection", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "No Response", + 449: "Retry With", + 450: "Blocked by Windows Parental Controls", + 451: "Unavailable For Legal Reasons", + 451: "Redirect", + 494: "Request Header Too Large", + 495: "Cert Error", + 496: "No Cert", + 497: "HTTP to HTTPS", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 509: "Bandwidth Limit Exceeded", + 510: "Not Extended", + 511: "Network Authentication Required", + 598: "Network read timeout error", + 599: "Network connect timeout error", +} + + +class HttpBaseClass(BaseClass): + GET = 'GET' + PUT = 'PUT' + POST = 'POST' + DELETE = 'DELETE' + HEAD = 'HEAD' + PATCH = 'PATCH' + OPTIONS = 'OPTIONS' + CONNECT = 'CONNECT' + METHODS = (GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS, CONNECT) + + +def parse_requestline(s): + """ + http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5 + + >>> parse_requestline('GET / HTTP/1.0') + ('GET', '/', '1.0') + >>> parse_requestline('post /testurl htTP/1.1') + ('POST', '/testurl', '1.1') + >>> parse_requestline('Im not a RequestLine') + Traceback (most recent call last): + ... + ValueError: Not a Request-Line + """ + methods = '|'.join(HttpBaseClass.METHODS) + m = re.match(r'(' + methods + ')\s+(.*)\s+HTTP/(1.[0|1])', s, re.I) + if m: + return m.group(1).upper(), m.group(2), m.group(3) + else: + raise ValueError('Not a Request-Line') + + +def last_requestline(sent_data): + """ + Find the last line in sent_data that can be parsed with parse_requestline + """ + for line in reversed(sent_data): + try: + parse_requestline(decode_utf8(line)) + except ValueError: + pass + else: + return line diff --git a/moto/packages/httpretty/utils.py b/moto/packages/httpretty/utils.py new file mode 100644 index 000000000..caa8fa13b --- /dev/null +++ b/moto/packages/httpretty/utils.py @@ -0,0 +1,48 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011-2013> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +from __future__ import unicode_literals + +from .compat import ( + byte_type, text_type +) + + +def utf8(s): + if isinstance(s, text_type): + s = s.encode('utf-8') + elif s is None: + return byte_type() + + return byte_type(s) + + +def decode_utf8(s): + if isinstance(s, byte_type): + s = s.decode("utf-8") + elif s is None: + return text_type() + + return text_type(s) diff --git a/moto/packages/responses b/moto/packages/responses new file mode 160000 index 000000000..8d500447e --- /dev/null +++ b/moto/packages/responses @@ -0,0 +1 @@ +Subproject commit 8d500447e3d5c2b96ace2eb7ab0f60158e921ed8 diff --git a/moto/rds/__init__.py b/moto/rds/__init__.py index d3cafc066..2c8c0ba97 100644 --- a/moto/rds/__init__.py +++ b/moto/rds/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import rds_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator rds_backend = rds_backends['us-east-1'] mock_rds = base_decorator(rds_backends) +mock_rds_deprecated = deprecated_base_decorator(rds_backends) diff --git a/moto/rds2/__init__.py b/moto/rds2/__init__.py index b200f9b11..0feecfac4 100644 --- a/moto/rds2/__init__.py +++ b/moto/rds2/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import rds2_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, deprecated_base_decorator rds2_backend = rds2_backends['us-west-1'] mock_rds2 = base_decorator(rds2_backends) +mock_rds2_deprecated = deprecated_base_decorator(rds2_backends) diff --git a/moto/redshift/__init__.py b/moto/redshift/__init__.py index 821408493..58be5fc70 100644 --- a/moto/redshift/__init__.py +++ b/moto/redshift/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import redshift_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator redshift_backend = redshift_backends['us-east-1'] mock_redshift = base_decorator(redshift_backends) +mock_redshift_deprecated = deprecated_base_decorator(redshift_backends) diff --git a/moto/route53/__init__.py b/moto/route53/__init__.py index 2c6bd223f..df629880f 100644 --- a/moto/route53/__init__.py +++ b/moto/route53/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import route53_backend mock_route53 = route53_backend.decorator +mock_route53_deprecated = route53_backend.deprecated_decorator diff --git a/moto/s3/__init__.py b/moto/s3/__init__.py index 6590d4324..7d0df53bd 100644 --- a/moto/s3/__init__.py +++ b/moto/s3/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import s3_backend mock_s3 = s3_backend.decorator +mock_s3_deprecated = s3_backend.deprecated_decorator \ No newline at end of file diff --git a/moto/s3/models.py b/moto/s3/models.py index c41ff3901..40370b5dd 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -25,7 +25,7 @@ class FakeKey(object): self.value = value self.last_modified = datetime.datetime.utcnow() self.acl = get_canned_acl('private') - self._storage_class = storage + self._storage_class = storage if storage else "STANDARD" self._metadata = {} self._expiry = None self._etag = etag @@ -92,6 +92,7 @@ class FakeKey(object): r = { 'etag': self.etag, 'last-modified': self.last_modified_RFC1123, + 'content-length': str(len(self.value)), } if self._storage_class != 'STANDARD': r['x-amz-storage-class'] = self._storage_class @@ -100,7 +101,7 @@ class FakeKey(object): r['x-amz-restore'] = rhdr.format(self.expiry_date) if self._is_versioned: - r['x-amz-version-id'] = self._version_id + r['x-amz-version-id'] = str(self._version_id) return r diff --git a/moto/s3/responses.py b/moto/s3/responses.py index d6855265e..3fbd058f2 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -49,6 +49,8 @@ class ResponseObject(_TemplateEnvironmentMixin): def subdomain_based_buckets(self, request): host = request.headers.get('host', request.headers.get('Host')) + if not host: + host = urlparse(request.url).netloc if not host or host.startswith("localhost"): # For localhost, default to path-based buckets @@ -130,6 +132,8 @@ class ResponseObject(_TemplateEnvironmentMixin): else: # Flask server body = request.data + if body is None: + body = '' body = body.decode('utf-8') if method == 'HEAD': @@ -334,7 +338,8 @@ class ResponseObject(_TemplateEnvironmentMixin): return 409, headers, template.render(bucket=removed_bucket) def _bucket_response_post(self, request, body, bucket_name, headers): - if self.is_delete_keys(request, request.path, bucket_name): + path = request.path if hasattr(request, 'path') else request.path_url + if self.is_delete_keys(request, path, bucket_name): return self._bucket_response_delete_keys(request, body, bucket_name, headers) # POST to bucket-url should create file from form @@ -344,7 +349,7 @@ class ResponseObject(_TemplateEnvironmentMixin): else: # HTTPretty, build new form object form = {} - for kv in request.body.decode('utf-8').split('&'): + for kv in body.decode('utf-8').split('&'): k, v = kv.split('=') form[k] = v @@ -428,9 +433,13 @@ class ResponseObject(_TemplateEnvironmentMixin): if hasattr(request, 'body'): # Boto body = request.body + if hasattr(body, 'read'): + body = body.read() else: # Flask server body = request.data + if body is None: + body = b'' if method == 'GET': return self._key_response_get(bucket_name, query, key_name, headers) @@ -546,7 +555,7 @@ class ResponseObject(_TemplateEnvironmentMixin): if key: headers.update(key.metadata) headers.update(key.response_dict) - return 200, headers, key.value + return 200, headers, "" else: return 404, headers, "" diff --git a/moto/s3bucket_path/__init__.py b/moto/s3bucket_path/__init__.py index 85031a06e..baffc4882 100644 --- a/moto/s3bucket_path/__init__.py +++ b/moto/s3bucket_path/__init__.py @@ -1,4 +1 @@ from __future__ import unicode_literals - -from moto import mock_s3 -mock_s3bucket_path = mock_s3 diff --git a/moto/ses/__init__.py b/moto/ses/__init__.py index 3b0e93c14..e1ec4b41a 100644 --- a/moto/ses/__init__.py +++ b/moto/ses/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import ses_backend mock_ses = ses_backend.decorator +mock_ses_deprecated = ses_backend.deprecated_decorator \ No newline at end of file diff --git a/moto/sns/__init__.py b/moto/sns/__init__.py index 0ed85e813..a50911e3b 100644 --- a/moto/sns/__init__.py +++ b/moto/sns/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import sns_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator sns_backend = sns_backends['us-east-1'] mock_sns = base_decorator(sns_backends) +mock_sns_deprecated = deprecated_base_decorator(sns_backends) diff --git a/moto/sqs/__init__.py b/moto/sqs/__init__.py index 09b4ed9e9..946ba8f47 100644 --- a/moto/sqs/__init__.py +++ b/moto/sqs/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import sqs_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator sqs_backend = sqs_backends['us-east-1'] mock_sqs = base_decorator(sqs_backends) +mock_sqs_deprecated = deprecated_base_decorator(sqs_backends) diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 15c067613..d57ec3430 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -122,6 +122,7 @@ class SQSResponse(BaseResponse): queue = self.sqs_backend.delete_queue(queue_name) if not queue: return "A queue with name {0} does not exist".format(queue_name), dict(status=404) + template = self.response_template(DELETE_QUEUE_RESPONSE) return template.render(queue=queue) diff --git a/moto/sts/__init__.py b/moto/sts/__init__.py index 04e93e2e7..57456c1b3 100644 --- a/moto/sts/__init__.py +++ b/moto/sts/__init__.py @@ -1,3 +1,4 @@ from __future__ import unicode_literals from .models import sts_backend mock_sts = sts_backend.decorator +mock_sts_deprecated = sts_backend.deprecated_decorator diff --git a/moto/swf/__init__.py b/moto/swf/__init__.py index 180919320..5ac59fbb6 100644 --- a/moto/swf/__init__.py +++ b/moto/swf/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from .models import swf_backends -from ..core.models import MockAWS, base_decorator +from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator swf_backend = swf_backends['us-east-1'] mock_swf = base_decorator(swf_backends) +mock_swf_deprecated = deprecated_base_decorator(swf_backends) diff --git a/setup.py b/setup.py index bfd8bbb87..52635d00b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ from setuptools import setup, find_packages install_requires = [ "Jinja2>=2.8", "boto>=2.36.0", - "httpretty==0.8.10", "requests", "xmltodict", "six", diff --git a/tests/__init__.py b/tests/__init__.py index baffc4882..bf582e0b3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,8 @@ from __future__ import unicode_literals + +import logging +# Disable extra logging for tests +logging.getLogger('boto').setLevel(logging.CRITICAL) +logging.getLogger('boto3').setLevel(logging.CRITICAL) +logging.getLogger('botocore').setLevel(logging.CRITICAL) +logging.getLogger('nose').setLevel(logging.CRITICAL) diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index fc41d3bc0..6bd6eb5e5 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -5,11 +5,11 @@ from datetime import datetime from dateutil.tz import tzutc import boto3 from freezegun import freeze_time -import httpretty import requests import sure # noqa from botocore.exceptions import ClientError +from moto.packages.responses import responses from moto import mock_apigateway @@ -883,11 +883,10 @@ def test_deployment(): stage['description'].should.equal('_new_description_') -@httpretty.activate @mock_apigateway def test_http_proxying_integration(): - httpretty.register_uri( - httpretty.GET, "http://httpbin.org/robots.txt", body='a fake response' + responses.add( + responses.GET, "http://httpbin.org/robots.txt", body='a fake response' ) region_name = 'us-west-2' diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index a048e81f5..4d0905196 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -8,12 +8,12 @@ from boto.ec2.autoscale import Tag import boto.ec2.elb import sure # noqa -from moto import mock_autoscaling, mock_ec2, mock_elb +from moto import mock_autoscaling, mock_ec2_deprecated, mock_elb_deprecated, mock_autoscaling_deprecated from tests.helpers import requires_boto_gte -@mock_autoscaling -@mock_elb +@mock_autoscaling_deprecated +@mock_elb_deprecated def test_create_autoscaling_group(): elb_conn = boto.ec2.elb.connect_to_region('us-east-1') elb_conn.create_load_balancer('test_lb', zones=[], listeners=[(80, 8080, 'http')]) @@ -73,7 +73,7 @@ def test_create_autoscaling_group(): tag.propagate_at_launch.should.equal(True) -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_autoscaling_groups_defaults(): """ Test with the minimum inputs and check that all of the proper defaults are assigned for the other attributes """ @@ -112,7 +112,7 @@ def test_create_autoscaling_groups_defaults(): list(group.tags).should.equal([]) -@mock_autoscaling +@mock_autoscaling_deprecated def test_autoscaling_group_describe_filter(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -138,7 +138,7 @@ def test_autoscaling_group_describe_filter(): conn.get_all_groups().should.have.length_of(3) -@mock_autoscaling +@mock_autoscaling_deprecated def test_autoscaling_update(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -169,7 +169,7 @@ def test_autoscaling_update(): group.vpc_zone_identifier.should.equal('subnet-5678efgh') -@mock_autoscaling +@mock_autoscaling_deprecated def test_autoscaling_tags_update(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -211,7 +211,7 @@ def test_autoscaling_tags_update(): group.tags.should.have.length_of(2) -@mock_autoscaling +@mock_autoscaling_deprecated def test_autoscaling_group_delete(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -235,8 +235,8 @@ def test_autoscaling_group_delete(): conn.get_all_groups().should.have.length_of(0) -@mock_ec2 -@mock_autoscaling +@mock_ec2_deprecated +@mock_autoscaling_deprecated def test_autoscaling_group_describe_instances(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -269,7 +269,7 @@ def test_autoscaling_group_describe_instances(): @requires_boto_gte("2.8") -@mock_autoscaling +@mock_autoscaling_deprecated def test_set_desired_capacity_up(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -304,7 +304,7 @@ def test_set_desired_capacity_up(): @requires_boto_gte("2.8") -@mock_autoscaling +@mock_autoscaling_deprecated def test_set_desired_capacity_down(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -339,7 +339,7 @@ def test_set_desired_capacity_down(): @requires_boto_gte("2.8") -@mock_autoscaling +@mock_autoscaling_deprecated def test_set_desired_capacity_the_same(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -372,8 +372,8 @@ def test_set_desired_capacity_the_same(): instances = list(conn.get_all_autoscaling_instances()) instances.should.have.length_of(2) -@mock_autoscaling -@mock_elb +@mock_autoscaling_deprecated +@mock_elb_deprecated def test_autoscaling_group_with_elb(): elb_conn = boto.connect_elb() zones = ['us-east-1a', 'us-east-1b'] diff --git a/tests/test_autoscaling/test_launch_configurations.py b/tests/test_autoscaling/test_launch_configurations.py index 8020e46f6..b2e21b03e 100644 --- a/tests/test_autoscaling/test_launch_configurations.py +++ b/tests/test_autoscaling/test_launch_configurations.py @@ -5,11 +5,11 @@ from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping import sure # noqa -from moto import mock_autoscaling +from moto import mock_autoscaling_deprecated from tests.helpers import requires_boto_gte -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -38,7 +38,7 @@ def test_create_launch_configuration(): @requires_boto_gte("2.27.0") -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration_with_block_device_mappings(): block_device_mapping = BlockDeviceMapping() @@ -101,7 +101,7 @@ def test_create_launch_configuration_with_block_device_mappings(): @requires_boto_gte("2.12") -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration_for_2_12(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -116,7 +116,7 @@ def test_create_launch_configuration_for_2_12(): @requires_boto_gte("2.25.0") -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration_using_ip_association(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -131,7 +131,7 @@ def test_create_launch_configuration_using_ip_association(): @requires_boto_gte("2.25.0") -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration_using_ip_association_should_default_to_false(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -144,7 +144,7 @@ def test_create_launch_configuration_using_ip_association_should_default_to_fals launch_config.associate_public_ip_address.should.equal(False) -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration_defaults(): """ Test with the minimum inputs and check that all of the proper defaults are assigned for the other attributes """ @@ -171,7 +171,7 @@ def test_create_launch_configuration_defaults(): @requires_boto_gte("2.12") -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_launch_configuration_defaults_for_2_12(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -184,7 +184,7 @@ def test_create_launch_configuration_defaults_for_2_12(): launch_config.ebs_optimized.should.equal(False) -@mock_autoscaling +@mock_autoscaling_deprecated def test_launch_configuration_describe_filter(): conn = boto.connect_autoscale() config = LaunchConfiguration( @@ -202,7 +202,7 @@ def test_launch_configuration_describe_filter(): conn.get_all_launch_configurations().should.have.length_of(3) -@mock_autoscaling +@mock_autoscaling_deprecated def test_launch_configuration_delete(): conn = boto.connect_autoscale() config = LaunchConfiguration( diff --git a/tests/test_autoscaling/test_policies.py b/tests/test_autoscaling/test_policies.py index 8ca585e89..54c64b749 100644 --- a/tests/test_autoscaling/test_policies.py +++ b/tests/test_autoscaling/test_policies.py @@ -5,7 +5,7 @@ from boto.ec2.autoscale.group import AutoScalingGroup from boto.ec2.autoscale.policy import ScalingPolicy import sure # noqa -from moto import mock_autoscaling +from moto import mock_autoscaling_deprecated def setup_autoscale_group(): @@ -27,7 +27,7 @@ def setup_autoscale_group(): return group -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_policy(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -48,7 +48,7 @@ def test_create_policy(): policy.cooldown.should.equal(60) -@mock_autoscaling +@mock_autoscaling_deprecated def test_create_policy_default_values(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -67,7 +67,7 @@ def test_create_policy_default_values(): policy.cooldown.should.equal(300) -@mock_autoscaling +@mock_autoscaling_deprecated def test_update_policy(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -94,7 +94,7 @@ def test_update_policy(): policy.scaling_adjustment.should.equal(2) -@mock_autoscaling +@mock_autoscaling_deprecated def test_delete_policy(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -112,7 +112,7 @@ def test_delete_policy(): conn.get_all_policies().should.have.length_of(0) -@mock_autoscaling +@mock_autoscaling_deprecated def test_execute_policy_exact_capacity(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -130,7 +130,7 @@ def test_execute_policy_exact_capacity(): instances.should.have.length_of(3) -@mock_autoscaling +@mock_autoscaling_deprecated def test_execute_policy_positive_change_in_capacity(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -148,7 +148,7 @@ def test_execute_policy_positive_change_in_capacity(): instances.should.have.length_of(5) -@mock_autoscaling +@mock_autoscaling_deprecated def test_execute_policy_percent_change_in_capacity(): setup_autoscale_group() conn = boto.connect_autoscale() @@ -166,7 +166,7 @@ def test_execute_policy_percent_change_in_capacity(): instances.should.have.length_of(3) -@mock_autoscaling +@mock_autoscaling_deprecated def test_execute_policy_small_percent_change_in_capacity(): """ http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html If PercentChangeInCapacity returns a value between 0 and 1, diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index e45dafbfa..3d41c9d91 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -12,7 +12,7 @@ import sure # noqa import tests.backport_assert_raises # noqa from nose.tools import assert_raises -from moto import mock_cloudformation, mock_s3 +from moto import mock_cloudformation_deprecated, mock_s3_deprecated from moto.cloudformation import cloudformation_backends dummy_template = { @@ -46,7 +46,7 @@ dummy_template_json2 = json.dumps(dummy_template2) dummy_template_json3 = json.dumps(dummy_template3) -@mock_cloudformation +@mock_cloudformation_deprecated def test_create_stack(): conn = boto.connect_cloudformation() conn.create_stack( @@ -69,7 +69,7 @@ def test_create_stack(): }) -@mock_cloudformation +@mock_cloudformation_deprecated def test_creating_stacks_across_regions(): west1_conn = boto.cloudformation.connect_to_region("us-west-1") west1_conn.create_stack("test_stack", template_body=dummy_template_json) @@ -81,7 +81,7 @@ def test_creating_stacks_across_regions(): list(west2_conn.describe_stacks()).should.have.length_of(1) -@mock_cloudformation +@mock_cloudformation_deprecated def test_create_stack_with_notification_arn(): conn = boto.connect_cloudformation() conn.create_stack( @@ -94,8 +94,8 @@ def test_create_stack_with_notification_arn(): [n.value for n in stack.notification_arns].should.contain('arn:aws:sns:us-east-1:123456789012:fake-queue') -@mock_cloudformation -@mock_s3 +@mock_cloudformation_deprecated +@mock_s3_deprecated def test_create_stack_from_s3_url(): s3_conn = boto.s3.connect_to_region('us-west-1') bucket = s3_conn.create_bucket("foobar") @@ -123,7 +123,7 @@ def test_create_stack_from_s3_url(): }) -@mock_cloudformation +@mock_cloudformation_deprecated def test_describe_stack_by_name(): conn = boto.connect_cloudformation() conn.create_stack( @@ -135,7 +135,7 @@ def test_describe_stack_by_name(): stack.stack_name.should.equal('test_stack') -@mock_cloudformation +@mock_cloudformation_deprecated def test_describe_stack_by_stack_id(): conn = boto.connect_cloudformation() conn.create_stack( @@ -149,7 +149,7 @@ def test_describe_stack_by_stack_id(): stack_by_id.stack_name.should.equal("test_stack") -@mock_cloudformation +@mock_cloudformation_deprecated def test_describe_deleted_stack(): conn = boto.connect_cloudformation() conn.create_stack( @@ -166,7 +166,7 @@ def test_describe_deleted_stack(): stack_by_id.stack_status.should.equal("DELETE_COMPLETE") -@mock_cloudformation +@mock_cloudformation_deprecated def test_get_template_by_name(): conn = boto.connect_cloudformation() conn.create_stack( @@ -188,7 +188,7 @@ def test_get_template_by_name(): }) -@mock_cloudformation +@mock_cloudformation_deprecated def test_list_stacks(): conn = boto.connect_cloudformation() conn.create_stack( @@ -205,7 +205,7 @@ def test_list_stacks(): stacks[0].template_description.should.equal("Stack 1") -@mock_cloudformation +@mock_cloudformation_deprecated def test_delete_stack_by_name(): conn = boto.connect_cloudformation() conn.create_stack( @@ -218,7 +218,7 @@ def test_delete_stack_by_name(): conn.list_stacks().should.have.length_of(0) -@mock_cloudformation +@mock_cloudformation_deprecated def test_delete_stack_by_id(): conn = boto.connect_cloudformation() stack_id = conn.create_stack( @@ -235,7 +235,7 @@ def test_delete_stack_by_id(): conn.describe_stacks(stack_id).should.have.length_of(1) -@mock_cloudformation +@mock_cloudformation_deprecated def test_delete_stack_with_resource_missing_delete_attr(): conn = boto.connect_cloudformation() conn.create_stack( @@ -248,14 +248,14 @@ def test_delete_stack_with_resource_missing_delete_attr(): conn.list_stacks().should.have.length_of(0) -@mock_cloudformation +@mock_cloudformation_deprecated def test_bad_describe_stack(): conn = boto.connect_cloudformation() with assert_raises(BotoServerError): conn.describe_stacks("bad_stack") -@mock_cloudformation() +@mock_cloudformation_deprecated() def test_cloudformation_params(): dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -279,7 +279,7 @@ def test_cloudformation_params(): param.value.should.equal('testing123') -@mock_cloudformation +@mock_cloudformation_deprecated def test_stack_tags(): conn = boto.connect_cloudformation() conn.create_stack( @@ -292,7 +292,7 @@ def test_stack_tags(): dict(stack.tags).should.equal({"foo": "bar", "baz": "bleh"}) -@mock_cloudformation +@mock_cloudformation_deprecated def test_update_stack(): conn = boto.connect_cloudformation() conn.create_stack( @@ -316,7 +316,7 @@ def test_update_stack(): }) -@mock_cloudformation +@mock_cloudformation_deprecated def test_update_stack(): conn = boto.connect_cloudformation() conn.create_stack( @@ -339,7 +339,7 @@ def test_update_stack(): }) -@mock_cloudformation +@mock_cloudformation_deprecated def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() stack_id = conn.create_stack("test_stack", template_body=dummy_template_json) @@ -355,7 +355,7 @@ def test_update_stack_when_rolled_back(): ex.reason.should.equal('Bad Request') ex.status.should.equal(400) -@mock_cloudformation +@mock_cloudformation_deprecated def test_describe_stack_events_shows_create_update_and_delete(): conn = boto.connect_cloudformation() stack_id = conn.create_stack("test_stack", template_body=dummy_template_json) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 97c3e864a..95ac6ede4 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -5,7 +5,7 @@ import boto import boto.s3 import boto.s3.key from botocore.exceptions import ClientError -from moto import mock_cloudformation, mock_s3 +from moto import mock_cloudformation, mock_s3_deprecated import json import sure # noqa @@ -118,7 +118,7 @@ def test_create_stack_with_role_arn(): @mock_cloudformation -@mock_s3 +@mock_s3_deprecated def test_create_stack_from_s3_url(): s3_conn = boto.s3.connect_to_region('us-west-1') bucket = s3_conn.create_bucket("foobar") diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 4237bee19..1b9330a9f 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -18,20 +18,26 @@ import boto3 import sure # noqa from moto import ( - mock_autoscaling, + mock_autoscaling_deprecated, mock_cloudformation, - mock_datapipeline, + mock_cloudformation_deprecated, + mock_datapipeline_deprecated, mock_ec2, + mock_ec2_deprecated, mock_elb, - mock_iam, + mock_elb_deprecated, + mock_iam_deprecated, mock_kms, mock_lambda, - mock_rds, + mock_rds_deprecated, mock_rds2, + mock_rds2_deprecated, mock_redshift, - mock_route53, - mock_sns, + mock_redshift_deprecated, + mock_route53_deprecated, + mock_sns_deprecated, mock_sqs, + mock_sqs_deprecated, ) from .fixtures import ( @@ -49,7 +55,7 @@ from .fixtures import ( ) -@mock_cloudformation() +@mock_cloudformation_deprecated() def test_stack_sqs_integration(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -79,7 +85,7 @@ def test_stack_sqs_integration(): queue.physical_resource_id.should.equal("my-queue") -@mock_cloudformation() +@mock_cloudformation_deprecated() def test_stack_list_resources(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -110,8 +116,8 @@ def test_stack_list_resources(): queue.physical_resource_id.should.equal("my-queue") -@mock_cloudformation() -@mock_sqs() +@mock_cloudformation_deprecated() +@mock_sqs_deprecated() def test_update_stack(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -148,8 +154,8 @@ def test_update_stack(): queues[0].get_attributes('VisibilityTimeout')['VisibilityTimeout'].should.equal('100') -@mock_cloudformation() -@mock_sqs() +@mock_cloudformation_deprecated() +@mock_sqs_deprecated() def test_update_stack_and_remove_resource(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -184,8 +190,8 @@ def test_update_stack_and_remove_resource(): queues.should.have.length_of(0) -@mock_cloudformation() -@mock_sqs() +@mock_cloudformation_deprecated() +@mock_sqs_deprecated() def test_update_stack_and_add_resource(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -223,8 +229,8 @@ def test_update_stack_and_add_resource(): queues.should.have.length_of(1) -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_stack_ec2_integration(): ec2_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -257,9 +263,9 @@ def test_stack_ec2_integration(): instance.physical_resource_id.should.equal(ec2_instance.id) -@mock_ec2() -@mock_elb() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_elb_deprecated() +@mock_cloudformation_deprecated() def test_stack_elb_integration_with_attached_ec2_instances(): elb_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -307,8 +313,8 @@ def test_stack_elb_integration_with_attached_ec2_instances(): list(load_balancer.availability_zones).should.equal(['us-east-1']) -@mock_elb() -@mock_cloudformation() +@mock_elb_deprecated() +@mock_cloudformation_deprecated() def test_stack_elb_integration_with_health_check(): elb_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -355,8 +361,8 @@ def test_stack_elb_integration_with_health_check(): health_check.unhealthy_threshold.should.equal(2) -@mock_elb() -@mock_cloudformation() +@mock_elb_deprecated() +@mock_cloudformation_deprecated() def test_stack_elb_integration_with_update(): elb_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -399,9 +405,9 @@ def test_stack_elb_integration_with_update(): load_balancer.availability_zones[0].should.equal('us-west-1b') -@mock_ec2() -@mock_redshift() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_redshift_deprecated() +@mock_cloudformation_deprecated() def test_redshift_stack(): redshift_template_json = json.dumps(redshift.template) @@ -443,8 +449,8 @@ def test_redshift_stack(): group.rules[0].grants[0].cidr_ip.should.equal("10.0.0.1/16") -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_stack_security_groups(): security_group_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -519,9 +525,9 @@ def test_stack_security_groups(): rule2.grants[0].group_id.should.equal(other_group.id) -@mock_autoscaling() -@mock_elb() -@mock_cloudformation() +@mock_autoscaling_deprecated() +@mock_elb_deprecated() +@mock_cloudformation_deprecated() def test_autoscaling_group_with_elb(): web_setup_template = { @@ -601,8 +607,8 @@ def test_autoscaling_group_with_elb(): elb_resource.physical_resource_id.should.contain("my-elb") -@mock_autoscaling() -@mock_cloudformation() +@mock_autoscaling_deprecated() +@mock_cloudformation_deprecated() def test_autoscaling_group_update(): asg_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -650,8 +656,8 @@ def test_autoscaling_group_update(): asg.max_size.should.equal(3) -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_vpc_single_instance_in_subnet(): template_json = json.dumps(vpc_single_instance_in_subnet.template) @@ -695,8 +701,8 @@ def test_vpc_single_instance_in_subnet(): eip_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] eip_resource.physical_resource_id.should.equal(eip.allocation_id) -@mock_cloudformation() -@mock_ec2() +@mock_cloudformation_deprecated() +@mock_ec2_deprecated() @mock_rds2() def test_rds_db_parameter_groups(): ec2_conn = boto.ec2.connect_to_region("us-west-1") @@ -734,9 +740,9 @@ def test_rds_db_parameter_groups(): -@mock_cloudformation() -@mock_ec2() -@mock_rds() +@mock_cloudformation_deprecated() +@mock_ec2_deprecated() +@mock_rds_deprecated() def test_rds_mysql_with_read_replica(): ec2_conn = boto.ec2.connect_to_region("us-west-1") ec2_conn.create_security_group('application', 'Our Application Group') @@ -776,9 +782,9 @@ def test_rds_mysql_with_read_replica(): security_group.ec2_groups[0].name.should.equal("application") -@mock_cloudformation() -@mock_ec2() -@mock_rds() +@mock_cloudformation_deprecated() +@mock_ec2_deprecated() +@mock_rds_deprecated() def test_rds_mysql_with_read_replica_in_vpc(): template_json = json.dumps(rds_mysql_with_read_replica.template) conn = boto.cloudformation.connect_to_region("eu-central-1") @@ -804,9 +810,9 @@ def test_rds_mysql_with_read_replica_in_vpc(): subnet_group.description.should.equal("my db subnet group") -@mock_autoscaling() -@mock_iam() -@mock_cloudformation() +@mock_autoscaling_deprecated() +@mock_iam_deprecated() +@mock_cloudformation_deprecated() def test_iam_roles(): iam_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -923,8 +929,8 @@ def test_iam_roles(): role_resource.physical_resource_id.should.equal(role.role_id) -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_single_instance_with_ebs_volume(): template_json = json.dumps(single_instance_with_ebs_volume.template) @@ -951,7 +957,7 @@ def test_single_instance_with_ebs_volume(): ebs_volumes[0].physical_resource_id.should.equal(volume.id) -@mock_cloudformation() +@mock_cloudformation_deprecated() def test_create_template_without_required_param(): template_json = json.dumps(single_instance_with_ebs_volume.template) conn = boto.cloudformation.connect_to_region("us-west-1") @@ -961,8 +967,8 @@ def test_create_template_without_required_param(): ).should.throw(BotoServerError) -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_classic_eip(): template_json = json.dumps(ec2_classic_eip.template) @@ -977,8 +983,8 @@ def test_classic_eip(): cfn_eip.physical_resource_id.should.equal(eip.public_ip) -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_vpc_eip(): template_json = json.dumps(vpc_eip.template) @@ -993,8 +999,8 @@ def test_vpc_eip(): cfn_eip.physical_resource_id.should.equal(eip.allocation_id) -@mock_ec2() -@mock_cloudformation() +@mock_ec2_deprecated() +@mock_cloudformation_deprecated() def test_fn_join(): template_json = json.dumps(fn_join.template) @@ -1008,8 +1014,8 @@ def test_fn_join(): fn_join_output.value.should.equal('test eip:{0}'.format(eip.public_ip)) -@mock_cloudformation() -@mock_sqs() +@mock_cloudformation_deprecated() +@mock_sqs_deprecated() def test_conditional_resources(): sqs_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1054,8 +1060,8 @@ def test_conditional_resources(): list(sqs_conn.get_all_queues()).should.have.length_of(1) -@mock_cloudformation() -@mock_ec2() +@mock_cloudformation_deprecated() +@mock_ec2_deprecated() def test_conditional_if_handling(): dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1110,8 +1116,8 @@ def test_conditional_if_handling(): ec2_instance.image_id.should.equal("ami-00000000") -@mock_cloudformation() -@mock_ec2() +@mock_cloudformation_deprecated() +@mock_ec2_deprecated() def test_cloudformation_mapping(): dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1155,8 +1161,8 @@ def test_cloudformation_mapping(): ec2_instance.image_id.should.equal("ami-c9c7978c") -@mock_cloudformation() -@mock_route53() +@mock_cloudformation_deprecated() +@mock_route53_deprecated() def test_route53_roundrobin(): route53_conn = boto.connect_route53() @@ -1198,9 +1204,9 @@ def test_route53_roundrobin(): output.value.should.equal('arn:aws:route53:::hostedzone/{0}'.format(zone_id)) -@mock_cloudformation() -@mock_ec2() -@mock_route53() +@mock_cloudformation_deprecated() +@mock_ec2_deprecated() +@mock_route53_deprecated() def test_route53_ec2_instance_with_public_ip(): route53_conn = boto.connect_route53() ec2_conn = boto.ec2.connect_to_region("us-west-1") @@ -1233,8 +1239,8 @@ def test_route53_ec2_instance_with_public_ip(): record_set1.resource_records[0].should.equal("10.0.0.25") -@mock_cloudformation() -@mock_route53() +@mock_cloudformation_deprecated() +@mock_route53_deprecated() def test_route53_associate_health_check(): route53_conn = boto.connect_route53() @@ -1270,8 +1276,8 @@ def test_route53_associate_health_check(): record_set.health_check.should.equal(health_check_id) -@mock_cloudformation() -@mock_route53() +@mock_cloudformation_deprecated() +@mock_route53_deprecated() def test_route53_with_update(): route53_conn = boto.connect_route53() @@ -1314,8 +1320,8 @@ def test_route53_with_update(): record_set.resource_records.should.equal(["my_other.example.com"]) -@mock_cloudformation() -@mock_sns() +@mock_cloudformation_deprecated() +@mock_sns_deprecated() def test_sns_topic(): dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1367,8 +1373,8 @@ def test_sns_topic(): topic_arn_output.value.should.equal(topic_arn) -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_vpc_gateway_attachment_creation_should_attach_itself_to_vpc(): template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1415,8 +1421,8 @@ def test_vpc_gateway_attachment_creation_should_attach_itself_to_vpc(): igws.should.have.length_of(1) -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_vpc_peering_creation(): vpc_conn = boto.vpc.connect_to_region("us-west-1") vpc_source = vpc_conn.create_vpc("10.0.0.0/16") @@ -1445,8 +1451,8 @@ def test_vpc_peering_creation(): peering_connections.should.have.length_of(1) -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_multiple_security_group_ingress_separate_from_security_group_by_id(): template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1507,8 +1513,8 @@ def test_multiple_security_group_ingress_separate_from_security_group_by_id(): security_group1.rules[0].to_port.should.equal('8080') -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_security_group_ingress_separate_from_security_group_by_id(): ec2_conn = boto.ec2.connect_to_region("us-west-1") ec2_conn.create_security_group("test-security-group1", "test security group") @@ -1558,8 +1564,8 @@ def test_security_group_ingress_separate_from_security_group_by_id(): security_group1.rules[0].to_port.should.equal('8080') -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): vpc_conn = boto.vpc.connect_to_region("us-west-1") vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -1624,8 +1630,8 @@ def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): security_group1.rules[0].to_port.should.equal('8080') -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_security_group_with_update(): vpc_conn = boto.vpc.connect_to_region("us-west-1") vpc1 = vpc_conn.create_vpc("10.0.0.0/16") @@ -1669,8 +1675,8 @@ def test_security_group_with_update(): security_group.vpc_id.should.equal(vpc2.id) -@mock_cloudformation -@mock_ec2 +@mock_cloudformation_deprecated +@mock_ec2_deprecated def test_subnets_should_be_created_with_availability_zone(): vpc_conn = boto.vpc.connect_to_region('us-west-1') vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -1698,8 +1704,8 @@ def test_subnets_should_be_created_with_availability_zone(): subnet.availability_zone.should.equal('us-west-1b') -@mock_cloudformation -@mock_datapipeline +@mock_cloudformation_deprecated +@mock_datapipeline_deprecated def test_datapipeline(): dp_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1796,11 +1802,10 @@ def lambda_handler(event, context): return _process_lamda(pfunc) -@mock_cloudformation +@mock_cloudformation_deprecated @mock_lambda def test_lambda_function(): # switch this to python as backend lambda only supports python execution. - conn = boto3.client('lambda', 'us-east-1') template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { @@ -1827,6 +1832,7 @@ def test_lambda_function(): template_body=template_json, ) + conn = boto3.client('lambda', 'us-east-1') result = conn.list_functions() result['Functions'].should.have.length_of(1) result['Functions'][0]['Description'].should.equal('Test function') diff --git a/tests/test_cloudwatch/test_cloudwatch.py b/tests/test_cloudwatch/test_cloudwatch.py index 7354241f0..88a3190c6 100644 --- a/tests/test_cloudwatch/test_cloudwatch.py +++ b/tests/test_cloudwatch/test_cloudwatch.py @@ -2,7 +2,7 @@ import boto from boto.ec2.cloudwatch.alarm import MetricAlarm import sure # noqa -from moto import mock_cloudwatch +from moto import mock_cloudwatch_deprecated def alarm_fixture(name="tester", action=None): action = action or ['arn:alarm'] @@ -23,7 +23,7 @@ def alarm_fixture(name="tester", action=None): unit='Seconds', ) -@mock_cloudwatch +@mock_cloudwatch_deprecated def test_create_alarm(): conn = boto.connect_cloudwatch() @@ -49,7 +49,7 @@ def test_create_alarm(): alarm.unit.should.equal('Seconds') -@mock_cloudwatch +@mock_cloudwatch_deprecated def test_delete_alarm(): conn = boto.connect_cloudwatch() @@ -68,7 +68,7 @@ def test_delete_alarm(): alarms.should.have.length_of(0) -@mock_cloudwatch +@mock_cloudwatch_deprecated def test_put_metric_data(): conn = boto.connect_cloudwatch() @@ -87,7 +87,7 @@ def test_put_metric_data(): dict(metric.dimensions).should.equal({'InstanceId': ['i-0123456,i-0123457']}) -@mock_cloudwatch +@mock_cloudwatch_deprecated def test_describe_alarms(): conn = boto.connect_cloudwatch() @@ -114,7 +114,7 @@ def test_describe_alarms(): alarms = conn.describe_alarms() alarms.should.have.length_of(0) -@mock_cloudwatch +@mock_cloudwatch_deprecated def test_describe_state_value_unimplemented(): conn = boto.connect_cloudwatch() diff --git a/tests/test_core/test_decorator_calls.py b/tests/test_core/test_decorator_calls.py index 7d32bc8b3..81dc0639a 100644 --- a/tests/test_core/test_decorator_calls.py +++ b/tests/test_core/test_decorator_calls.py @@ -7,19 +7,19 @@ import unittest import tests.backport_assert_raises # noqa from nose.tools import assert_raises -from moto import mock_ec2, mock_s3 +from moto import mock_ec2_deprecated, mock_s3_deprecated ''' Test the different ways that the decorator can be used ''' -@mock_ec2 +@mock_ec2_deprecated def test_basic_connect(): boto.connect_ec2() -@mock_ec2 +@mock_ec2_deprecated def test_basic_decorator(): conn = boto.connect_ec2('the_key', 'the_secret') list(conn.get_all_instances()).should.equal([]) @@ -30,7 +30,7 @@ def test_context_manager(): with assert_raises(EC2ResponseError): conn.get_all_instances() - with mock_ec2(): + with mock_ec2_deprecated(): conn = boto.connect_ec2('the_key', 'the_secret') list(conn.get_all_instances()).should.equal([]) @@ -44,7 +44,7 @@ def test_decorator_start_and_stop(): with assert_raises(EC2ResponseError): conn.get_all_instances() - mock = mock_ec2() + mock = mock_ec2_deprecated() mock.start() conn = boto.connect_ec2('the_key', 'the_secret') list(conn.get_all_instances()).should.equal([]) @@ -54,7 +54,7 @@ def test_decorator_start_and_stop(): conn.get_all_instances() -@mock_ec2 +@mock_ec2_deprecated def test_decorater_wrapped_gets_set(): """ Moto decorator's __wrapped__ should get set to the tests function @@ -62,7 +62,7 @@ def test_decorater_wrapped_gets_set(): test_decorater_wrapped_gets_set.__wrapped__.__name__.should.equal('test_decorater_wrapped_gets_set') -@mock_ec2 +@mock_ec2_deprecated class Tester(object): def test_the_class(self): conn = boto.connect_ec2() @@ -73,7 +73,7 @@ class Tester(object): list(conn.get_all_instances()).should.have.length_of(0) -@mock_s3 +@mock_s3_deprecated class TesterWithSetup(unittest.TestCase): def setUp(self): self.conn = boto.connect_s3() diff --git a/tests/test_core/test_nested.py b/tests/test_core/test_nested.py index 09967d743..7c0b8f687 100644 --- a/tests/test_core/test_nested.py +++ b/tests/test_core/test_nested.py @@ -5,12 +5,12 @@ from boto.sqs.connection import SQSConnection from boto.sqs.message import Message from boto.ec2 import EC2Connection -from moto import mock_sqs, mock_ec2 +from moto import mock_sqs_deprecated, mock_ec2_deprecated class TestNestedDecorators(unittest.TestCase): - @mock_sqs + @mock_sqs_deprecated def setup_sqs_queue(self): conn = SQSConnection() q = conn.create_queue('some-queue') @@ -21,7 +21,7 @@ class TestNestedDecorators(unittest.TestCase): self.assertEqual(q.count(), 1) - @mock_ec2 + @mock_ec2_deprecated def test_nested(self): self.setup_sqs_queue() diff --git a/tests/test_datapipeline/test_datapipeline.py b/tests/test_datapipeline/test_datapipeline.py index 5a958492f..aaa9f7f77 100644 --- a/tests/test_datapipeline/test_datapipeline.py +++ b/tests/test_datapipeline/test_datapipeline.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import boto.datapipeline import sure # noqa -from moto import mock_datapipeline +from moto import mock_datapipeline_deprecated from moto.datapipeline.utils import remove_capitalization_of_dict_keys @@ -13,7 +13,7 @@ def get_value_from_fields(key, fields): return field['stringValue'] -@mock_datapipeline +@mock_datapipeline_deprecated def test_create_pipeline(): conn = boto.datapipeline.connect_to_region("us-west-2") @@ -78,7 +78,7 @@ PIPELINE_OBJECTS = [ ] -@mock_datapipeline +@mock_datapipeline_deprecated def test_creating_pipeline_definition(): conn = boto.datapipeline.connect_to_region("us-west-2") res = conn.create_pipeline("mypipeline", "some-unique-id") @@ -97,7 +97,7 @@ def test_creating_pipeline_definition(): }]) -@mock_datapipeline +@mock_datapipeline_deprecated def test_describing_pipeline_objects(): conn = boto.datapipeline.connect_to_region("us-west-2") res = conn.create_pipeline("mypipeline", "some-unique-id") @@ -116,7 +116,7 @@ def test_describing_pipeline_objects(): }]) -@mock_datapipeline +@mock_datapipeline_deprecated def test_activate_pipeline(): conn = boto.datapipeline.connect_to_region("us-west-2") @@ -133,7 +133,7 @@ def test_activate_pipeline(): get_value_from_fields('@pipelineState', fields).should.equal("SCHEDULED") -@mock_datapipeline +@mock_datapipeline_deprecated def test_listing_pipelines(): conn = boto.datapipeline.connect_to_region("us-west-2") res1 = conn.create_pipeline("mypipeline1", "some-unique-id1") diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 1f85ce4d8..7ea56faa9 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -7,13 +7,13 @@ import requests import tests.backport_assert_raises from nose.tools import assert_raises -from moto import mock_dynamodb +from moto import mock_dynamodb, mock_dynamodb_deprecated from moto.dynamodb import dynamodb_backend from boto.exception import DynamoDBResponseError -@mock_dynamodb +@mock_dynamodb_deprecated def test_list_tables(): name = 'TestTable' dynamodb_backend.create_table(name, hash_key_attr="name", hash_key_type="S") @@ -21,7 +21,7 @@ def test_list_tables(): assert conn.list_tables() == ['TestTable'] -@mock_dynamodb +@mock_dynamodb_deprecated def test_list_tables_layer_1(): dynamodb_backend.create_table("test_1", hash_key_attr="name", hash_key_type="S") dynamodb_backend.create_table("test_2", hash_key_attr="name", hash_key_type="S") @@ -35,7 +35,7 @@ def test_list_tables_layer_1(): res.should.equal(expected) -@mock_dynamodb +@mock_dynamodb_deprecated def test_describe_missing_table(): conn = boto.connect_dynamodb('the_key', 'the_secret') with assert_raises(DynamoDBResponseError): @@ -49,7 +49,7 @@ def test_sts_handler(): res.text.should.contain("SecretAccessKey") -@mock_dynamodb +@mock_dynamodb_deprecated def test_dynamodb_with_connect_to_region(): # this will work if connected with boto.connect_dynamodb() dynamodb = boto.dynamodb.connect_to_region('us-west-2') diff --git a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py index f6ba9b307..c7832b08f 100644 --- a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py @@ -4,7 +4,7 @@ import boto import sure # noqa from freezegun import freeze_time -from moto import mock_dynamodb +from moto import mock_dynamodb_deprecated from boto.dynamodb import condition from boto.dynamodb.exceptions import DynamoDBKeyNotFoundError, DynamoDBValidationError @@ -29,7 +29,7 @@ def create_table(conn): @freeze_time("2012-01-14") -@mock_dynamodb +@mock_dynamodb_deprecated def test_create_table(): conn = boto.connect_dynamodb() create_table(conn) @@ -60,7 +60,7 @@ def test_create_table(): conn.describe_table('messages').should.equal(expected) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_table(): conn = boto.connect_dynamodb() create_table(conn) @@ -72,7 +72,7 @@ def test_delete_table(): conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_update_table_throughput(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -86,7 +86,7 @@ def test_update_table_throughput(): table.write_units.should.equal(6) -@mock_dynamodb +@mock_dynamodb_deprecated def test_item_add_and_describe_and_update(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -133,7 +133,7 @@ def test_item_add_and_describe_and_update(): }) -@mock_dynamodb +@mock_dynamodb_deprecated def test_item_put_without_table(): conn = boto.connect_dynamodb() @@ -146,7 +146,7 @@ def test_item_put_without_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_get_missing_item(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -158,7 +158,7 @@ def test_get_missing_item(): table.has_item("foobar", "more").should.equal(False) -@mock_dynamodb +@mock_dynamodb_deprecated def test_get_item_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -171,7 +171,7 @@ def test_get_item_with_undeclared_table(): ).should.throw(DynamoDBKeyNotFoundError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_get_item_without_range_key(): conn = boto.connect_dynamodb() message_table_schema = conn.create_schema( @@ -195,7 +195,7 @@ def test_get_item_without_range_key(): table.get_item.when.called_with(hash_key=hash_key).should.throw(DynamoDBValidationError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_item(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -223,7 +223,7 @@ def test_delete_item(): item.delete.when.called_with().should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_item_with_attribute_response(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -260,7 +260,7 @@ def test_delete_item_with_attribute_response(): item.delete.when.called_with().should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_item_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -273,7 +273,7 @@ def test_delete_item_with_undeclared_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_query(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -323,7 +323,7 @@ def test_query(): results.response['Items'].should.have.length_of(1) -@mock_dynamodb +@mock_dynamodb_deprecated def test_query_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -339,7 +339,7 @@ def test_query_with_undeclared_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_scan(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -402,7 +402,7 @@ def test_scan(): results.response['Items'].should.have.length_of(1) -@mock_dynamodb +@mock_dynamodb_deprecated def test_scan_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -419,7 +419,7 @@ def test_scan_with_undeclared_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_scan_after_has_item(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -430,7 +430,7 @@ def test_scan_after_has_item(): list(table.scan()).should.equal([]) -@mock_dynamodb +@mock_dynamodb_deprecated def test_write_batch(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -474,7 +474,7 @@ def test_write_batch(): table.item_count.should.equal(1) -@mock_dynamodb +@mock_dynamodb_deprecated def test_batch_read(): conn = boto.connect_dynamodb() table = create_table(conn) diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py index fa8492620..18d353928 100644 --- a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py @@ -4,7 +4,7 @@ import boto import sure # noqa from freezegun import freeze_time -from moto import mock_dynamodb +from moto import mock_dynamodb_deprecated from boto.dynamodb import condition from boto.dynamodb.exceptions import DynamoDBKeyNotFoundError @@ -27,7 +27,7 @@ def create_table(conn): @freeze_time("2012-01-14") -@mock_dynamodb +@mock_dynamodb_deprecated def test_create_table(): conn = boto.connect_dynamodb() create_table(conn) @@ -54,7 +54,7 @@ def test_create_table(): conn.describe_table('messages').should.equal(expected) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_table(): conn = boto.connect_dynamodb() create_table(conn) @@ -66,7 +66,7 @@ def test_delete_table(): conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_update_table_throughput(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -80,7 +80,7 @@ def test_update_table_throughput(): table.write_units.should.equal(6) -@mock_dynamodb +@mock_dynamodb_deprecated def test_item_add_and_describe_and_update(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -120,7 +120,7 @@ def test_item_add_and_describe_and_update(): }) -@mock_dynamodb +@mock_dynamodb_deprecated def test_item_put_without_table(): conn = boto.connect_dynamodb() @@ -132,7 +132,7 @@ def test_item_put_without_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_get_missing_item(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -142,7 +142,7 @@ def test_get_missing_item(): ).should.throw(DynamoDBKeyNotFoundError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_get_item_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -154,7 +154,7 @@ def test_get_item_with_undeclared_table(): ).should.throw(DynamoDBKeyNotFoundError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_item(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -181,7 +181,7 @@ def test_delete_item(): item.delete.when.called_with().should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_item_with_attribute_response(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -216,7 +216,7 @@ def test_delete_item_with_attribute_response(): item.delete.when.called_with().should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_delete_item_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -228,7 +228,7 @@ def test_delete_item_with_undeclared_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_query(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -248,7 +248,7 @@ def test_query(): results.response['Items'].should.have.length_of(1) -@mock_dynamodb +@mock_dynamodb_deprecated def test_query_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -258,7 +258,7 @@ def test_query_with_undeclared_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_scan(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -318,7 +318,7 @@ def test_scan(): results.response['Items'].should.have.length_of(1) -@mock_dynamodb +@mock_dynamodb_deprecated def test_scan_with_undeclared_table(): conn = boto.connect_dynamodb() @@ -335,7 +335,7 @@ def test_scan_with_undeclared_table(): ).should.throw(DynamoDBResponseError) -@mock_dynamodb +@mock_dynamodb_deprecated def test_scan_after_has_item(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -346,7 +346,7 @@ def test_scan_after_has_item(): list(table.scan()).should.equal([]) -@mock_dynamodb +@mock_dynamodb_deprecated def test_write_batch(): conn = boto.connect_dynamodb() table = create_table(conn) @@ -388,7 +388,7 @@ def test_write_batch(): table.item_count.should.equal(1) -@mock_dynamodb +@mock_dynamodb_deprecated def test_batch_read(): conn = boto.connect_dynamodb() table = create_table(conn) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 552611fa6..d66d36d9f 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -4,7 +4,7 @@ import six import boto import sure # noqa import requests -from moto import mock_dynamodb2 +from moto import mock_dynamodb2, mock_dynamodb2_deprecated from moto.dynamodb2 import dynamodb_backend2 from boto.exception import JSONResponseError from tests.helpers import requires_boto_gte @@ -16,7 +16,7 @@ except ImportError: print("This boto version is not supported") @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_list_tables(): name = 'TestTable' #{'schema': } @@ -32,7 +32,7 @@ def test_list_tables(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_list_tables_layer_1(): dynamodb_backend2.create_table("test_1",schema=[ {u'KeyType': u'HASH', u'AttributeName': u'name'} @@ -55,7 +55,7 @@ def test_list_tables_layer_1(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_describe_missing_table(): conn = boto.dynamodb2.connect_to_region( 'us-west-2', diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index 7e4403daa..029506378 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -7,7 +7,7 @@ import boto3 from boto3.dynamodb.conditions import Key import sure # noqa from freezegun import freeze_time -from moto import mock_dynamodb2 +from moto import mock_dynamodb2, mock_dynamodb2_deprecated from boto.exception import JSONResponseError from tests.helpers import requires_boto_gte try: @@ -61,7 +61,7 @@ def iterate_results(res): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated @freeze_time("2012-01-14") def test_create_table(): table = create_table() @@ -90,7 +90,7 @@ def test_create_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated @freeze_time("2012-01-14") def test_create_table_with_local_index(): table = create_table_with_local_indexes() @@ -132,7 +132,7 @@ def test_create_table_with_local_index(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_delete_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() table = create_table() @@ -144,7 +144,7 @@ def test_delete_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_update_table_throughput(): table = create_table() table.throughput["read"].should.equal(10) @@ -169,7 +169,7 @@ def test_update_table_throughput(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_item_add_and_describe_and_update(): table = create_table() ok = table.put_item(data={ @@ -212,7 +212,7 @@ def test_item_add_and_describe_and_update(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_item_partial_save(): table = create_table() @@ -242,7 +242,7 @@ def test_item_partial_save(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_item_put_without_table(): table = Table('undeclared-table') item_data = { @@ -256,7 +256,7 @@ def test_item_put_without_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_missing_item(): table = create_table() @@ -267,14 +267,14 @@ def test_get_missing_item(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_item_with_undeclared_table(): table = Table('undeclared-table') table.get_item.when.called_with(test_hash=3241526475).should.throw(JSONResponseError) @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_item_without_range_key(): table = Table.create('messages', schema=[ HashKey('test_hash'), @@ -291,7 +291,7 @@ def test_get_item_without_range_key(): @requires_boto_gte("2.30.0") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_delete_item(): table = create_table() item_data = { @@ -313,7 +313,7 @@ def test_delete_item(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_delete_item_with_undeclared_table(): table = Table("undeclared-table") item_data = { @@ -327,7 +327,7 @@ def test_delete_item_with_undeclared_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query(): table = create_table() @@ -384,7 +384,7 @@ def test_query(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_with_undeclared_table(): table = Table('undeclared') results = table.query( @@ -396,7 +396,7 @@ def test_query_with_undeclared_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_scan(): table = create_table() item_data = { @@ -451,7 +451,7 @@ def test_scan(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_scan_with_undeclared_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() conn.scan.when.called_with( @@ -468,7 +468,7 @@ def test_scan_with_undeclared_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_write_batch(): table = create_table() with table.batch_write() as batch: @@ -498,7 +498,7 @@ def test_write_batch(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_batch_read(): table = create_table() item_data = { @@ -542,14 +542,14 @@ def test_batch_read(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_key_fields(): table = create_table() kf = table.get_key_fields() kf.should.equal(['forum_name', 'subject']) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_create_with_global_indexes(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -594,7 +594,7 @@ def test_create_with_global_indexes(): ]) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_with_global_indexes(): table = Table.create('messages', schema=[ HashKey('subject'), @@ -638,7 +638,7 @@ def test_query_with_global_indexes(): list(results).should.have.length_of(0) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_with_local_indexes(): table = create_table_with_local_indexes() item_data = { @@ -658,7 +658,7 @@ def test_query_with_local_indexes(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_filter_eq(): table = create_table_with_local_indexes() item_data = [ @@ -691,7 +691,7 @@ def test_query_filter_eq(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_filter_lt(): table = create_table_with_local_indexes() item_data = [ @@ -726,7 +726,7 @@ def test_query_filter_lt(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_filter_gt(): table = create_table_with_local_indexes() item_data = [ @@ -760,7 +760,7 @@ def test_query_filter_gt(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_filter_lte(): table = create_table_with_local_indexes() item_data = [ @@ -794,7 +794,7 @@ def test_query_filter_lte(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_filter_gte(): table = create_table_with_local_indexes() item_data = [ @@ -827,7 +827,7 @@ def test_query_filter_gte(): list(results).should.have.length_of(2) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_reverse_query(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -851,7 +851,7 @@ def test_reverse_query(): [r['created_at'] for r in results].should.equal(expected) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_lookup(): from decimal import Decimal table = Table.create('messages', schema=[ @@ -871,7 +871,7 @@ def test_lookup(): message.get('test_range').should.equal(Decimal(range_key)) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_failed_overwrite(): table = Table.create('messages', schema=[ HashKey('id'), @@ -900,7 +900,7 @@ def test_failed_overwrite(): dict(returned_item).should.equal(data4) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_conflicting_writes(): table = Table.create('messages', schema=[ HashKey('id'), diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 691e14818..83eff6519 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -6,7 +6,7 @@ from boto3.dynamodb.conditions import Key import sure # noqa from freezegun import freeze_time from boto.exception import JSONResponseError -from moto import mock_dynamodb2 +from moto import mock_dynamodb2, mock_dynamodb2_deprecated from tests.helpers import requires_boto_gte import botocore try: @@ -29,7 +29,7 @@ def create_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated @freeze_time("2012-01-14") def test_create_table(): create_table() @@ -62,7 +62,7 @@ def test_create_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_delete_table(): create_table() conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -75,7 +75,7 @@ def test_delete_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_update_table_throughput(): table = create_table() table.throughput["read"].should.equal(10) @@ -91,7 +91,7 @@ def test_update_table_throughput(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_item_add_and_describe_and_update(): table = create_table() @@ -125,7 +125,7 @@ def test_item_add_and_describe_and_update(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_item_partial_save(): table = create_table() @@ -152,7 +152,7 @@ def test_item_partial_save(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_item_put_without_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -167,7 +167,7 @@ def test_item_put_without_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_item_with_undeclared_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -178,7 +178,7 @@ def test_get_item_with_undeclared_table(): @requires_boto_gte("2.30.0") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_delete_item(): table = create_table() @@ -202,7 +202,7 @@ def test_delete_item(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_delete_item_with_undeclared_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -213,7 +213,7 @@ def test_delete_item_with_undeclared_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query(): table = create_table() @@ -233,7 +233,7 @@ def test_query(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_query_with_undeclared_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -244,7 +244,7 @@ def test_query_with_undeclared_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_scan(): table = create_table() @@ -295,7 +295,7 @@ def test_scan(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_scan_with_undeclared_table(): conn = boto.dynamodb2.layer1.DynamoDBConnection() @@ -313,7 +313,7 @@ def test_scan_with_undeclared_table(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_write_batch(): table = create_table() @@ -344,7 +344,7 @@ def test_write_batch(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_batch_read(): table = create_table() @@ -385,7 +385,7 @@ def test_batch_read(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_key_fields(): table = create_table() kf = table.get_key_fields() @@ -393,14 +393,14 @@ def test_get_key_fields(): @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_missing_item(): table = create_table() table.get_item.when.called_with(forum_name='missing').should.throw(ItemNotFound) @requires_boto_gte("2.9") -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_get_special_item(): table = Table.create('messages', schema=[ HashKey('date-joined') @@ -418,7 +418,7 @@ def test_get_special_item(): dict(returned_item).should.equal(data) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_update_item_remove(): conn = boto.dynamodb2.connect_to_region("us-west-2") table = Table.create('messages', schema=[ @@ -444,7 +444,7 @@ def test_update_item_remove(): }) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_update_item_set(): conn = boto.dynamodb2.connect_to_region("us-west-2") table = Table.create('messages', schema=[ @@ -471,7 +471,7 @@ def test_update_item_set(): -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_failed_overwrite(): table = Table.create('messages', schema=[ HashKey('id'), @@ -499,7 +499,7 @@ def test_failed_overwrite(): dict(returned_item).should.equal(data4) -@mock_dynamodb2 +@mock_dynamodb2_deprecated def test_conflicting_writes(): table = Table.create('messages', schema=[ HashKey('id'), diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 095979f74..9c3fbd40d 100755 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -9,11 +9,11 @@ from boto.exception import EC2ResponseError, JSONResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_emr_deprecated from tests.helpers import requires_boto_gte -@mock_ec2 +@mock_emr_deprecated def test_ami_create_and_delete(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -69,7 +69,7 @@ def test_ami_create_and_delete(): @requires_boto_gte("2.14.0") -@mock_ec2 +@mock_emr_deprecated def test_ami_copy(): conn = boto.ec2.connect_to_region("us-west-1") reservation = conn.run_instances('ami-1234abcd') @@ -119,7 +119,7 @@ def test_ami_copy(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_emr_deprecated def test_ami_tagging(): conn = boto.connect_vpc('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -145,7 +145,7 @@ def test_ami_tagging(): image.tags["a key"].should.equal("some value") -@mock_ec2 +@mock_emr_deprecated def test_ami_create_from_missing_instance(): conn = boto.connect_ec2('the_key', 'the_secret') args = ["i-abcdefg", "test-ami", "this is a test ami"] @@ -157,7 +157,7 @@ def test_ami_create_from_missing_instance(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_emr_deprecated def test_ami_pulls_attributes_from_instance(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -169,7 +169,7 @@ def test_ami_pulls_attributes_from_instance(): image.kernel_id.should.equal('test-kernel') -@mock_ec2 +@mock_emr_deprecated def test_ami_filters(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -220,7 +220,7 @@ def test_ami_filters(): set([ami.id for ami in amis_by_nonpublic]).should.equal(set([imageA.id])) -@mock_ec2 +@mock_emr_deprecated def test_ami_filtering_via_tag(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -243,7 +243,7 @@ def test_ami_filtering_via_tag(): set([ami.id for ami in amis_by_tagB]).should.equal(set([imageB.id])) -@mock_ec2 +@mock_emr_deprecated def test_getting_missing_ami(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -254,7 +254,7 @@ def test_getting_missing_ami(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_emr_deprecated def test_getting_malformed_ami(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -265,7 +265,7 @@ def test_getting_malformed_ami(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_emr_deprecated def test_ami_attribute_group_permissions(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -318,7 +318,7 @@ def test_ami_attribute_group_permissions(): conn.modify_image_attribute.when.called_with(**REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError) -@mock_ec2 +@mock_emr_deprecated def test_ami_attribute_user_permissions(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -383,7 +383,7 @@ def test_ami_attribute_user_permissions(): conn.modify_image_attribute.when.called_with(**REMOVE_USERS_ARGS).should_not.throw(EC2ResponseError) -@mock_ec2 +@mock_emr_deprecated def test_ami_attribute_user_and_group_permissions(): """ Boto supports adding/removing both users and groups at the same time. @@ -435,7 +435,7 @@ def test_ami_attribute_user_and_group_permissions(): image.is_public.should.equal(False) -@mock_ec2 +@mock_emr_deprecated def test_ami_attribute_error_cases(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') diff --git a/tests/test_ec2/test_availability_zones_and_regions.py b/tests/test_ec2/test_availability_zones_and_regions.py index 88453e10b..7226cacaf 100644 --- a/tests/test_ec2/test_availability_zones_and_regions.py +++ b/tests/test_ec2/test_availability_zones_and_regions.py @@ -4,10 +4,10 @@ import boto.ec2 import boto3 import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_describe_regions(): conn = boto.connect_ec2('the_key', 'the_secret') regions = conn.get_all_regions() @@ -16,7 +16,7 @@ def test_describe_regions(): region.endpoint.should.contain(region.name) -@mock_ec2 +@mock_ec2_deprecated def test_availability_zones(): conn = boto.connect_ec2('the_key', 'the_secret') regions = conn.get_all_regions() diff --git a/tests/test_ec2/test_customer_gateways.py b/tests/test_ec2/test_customer_gateways.py index efd6ce993..93e35dc6a 100644 --- a/tests/test_ec2/test_customer_gateways.py +++ b/tests/test_ec2/test_customer_gateways.py @@ -5,10 +5,10 @@ from nose.tools import assert_raises from nose.tools import assert_false from boto.exception import EC2ResponseError -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_create_customer_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -19,7 +19,7 @@ def test_create_customer_gateways(): customer_gateway.bgp_asn.should.equal(65534) customer_gateway.ip_address.should.equal('205.251.242.54') -@mock_ec2 +@mock_ec2_deprecated def test_describe_customer_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) @@ -27,7 +27,7 @@ def test_describe_customer_gateways(): cgws.should.have.length_of(1) cgws[0].id.should.match(customer_gateway.id) -@mock_ec2 +@mock_ec2_deprecated def test_delete_customer_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -39,7 +39,7 @@ def test_delete_customer_gateways(): cgws = conn.get_all_customer_gateways() cgws.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_delete_customer_gateways_bad_id(): conn = boto.connect_vpc('the_key', 'the_secret') with assert_raises(EC2ResponseError) as cm: diff --git a/tests/test_ec2/test_dhcp_options.py b/tests/test_ec2/test_dhcp_options.py index ef671ec11..0279a3d54 100644 --- a/tests/test_ec2/test_dhcp_options.py +++ b/tests/test_ec2/test_dhcp_options.py @@ -9,13 +9,13 @@ from boto.exception import EC2ResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated SAMPLE_DOMAIN_NAME = u'example.com' SAMPLE_NAME_SERVERS = [u'10.0.0.6', u'10.0.0.7'] -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_associate(): """ associate dhcp option """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -26,7 +26,7 @@ def test_dhcp_options_associate(): rval.should.be.equal(True) -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_associate_invalid_dhcp_id(): """ associate dhcp option bad dhcp options id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -39,7 +39,7 @@ def test_dhcp_options_associate_invalid_dhcp_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_associate_invalid_vpc_id(): """ associate dhcp option invalid vpc id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -52,7 +52,7 @@ def test_dhcp_options_associate_invalid_vpc_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_delete_with_vpc(): """Test deletion of dhcp options with vpc""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -78,7 +78,7 @@ def test_dhcp_options_delete_with_vpc(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_create_dhcp_options(): """Create most basic dhcp option""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -89,7 +89,7 @@ def test_create_dhcp_options(): dhcp_option.options[u'domain-name-servers'][1].should.be.equal(SAMPLE_NAME_SERVERS[1]) -@mock_ec2 +@mock_ec2_deprecated def test_create_dhcp_options_invalid_options(): """Create invalid dhcp options""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -108,7 +108,7 @@ def test_create_dhcp_options_invalid_options(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_describe_dhcp_options(): """Test dhcp options lookup by id""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -121,7 +121,7 @@ def test_describe_dhcp_options(): dhcp_options.should.be.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_describe_dhcp_options_invalid_id(): """get error on invalid dhcp_option_id lookup""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -133,7 +133,7 @@ def test_describe_dhcp_options_invalid_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_delete_dhcp_options(): """delete dhcp option""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -151,7 +151,7 @@ def test_delete_dhcp_options(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_delete_dhcp_options_invalid_id(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -164,7 +164,7 @@ def test_delete_dhcp_options_invalid_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_delete_dhcp_options_malformed_id(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -177,7 +177,7 @@ def test_delete_dhcp_options_malformed_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_tagging(): conn = boto.connect_vpc('the_key', 'the_secret') dhcp_option = conn.create_dhcp_options() @@ -194,7 +194,7 @@ def test_dhcp_tagging(): dhcp_option.tags["a key"].should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_get_by_tag(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -230,7 +230,7 @@ def test_dhcp_options_get_by_tag(): dhcp_options_sets.should.have.length_of(2) -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_get_by_id(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -308,7 +308,7 @@ def test_dhcp_options_get_by_key_filter(): dhcp_options_sets.should.have.length_of(3) -@mock_ec2 +@mock_ec2_deprecated def test_dhcp_options_get_by_invalid_filter(): conn = boto.connect_vpc('the_key', 'the_secret') diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index f99cef5e4..c4794b1c8 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -8,10 +8,10 @@ import boto from boto.exception import EC2ResponseError, JSONResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_create_and_delete_volume(): conn = boto.connect_ec2('the_key', 'the_secret') volume = conn.create_volume(80, "us-east-1a") @@ -43,7 +43,7 @@ def test_create_and_delete_volume(): -@mock_ec2 +@mock_ec2_deprecated def test_create_encrypted_volume_dryrun(): conn = boto.connect_ec2('the_key', 'the_secret') with assert_raises(JSONResponseError) as ex: @@ -53,7 +53,7 @@ def test_create_encrypted_volume_dryrun(): ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') -@mock_ec2 +@mock_ec2_deprecated def test_create_encrypted_volume(): conn = boto.connect_ec2('the_key', 'the_secret') conn.create_volume(80, "us-east-1a", encrypted=True) @@ -68,7 +68,7 @@ def test_create_encrypted_volume(): all_volumes[0].encrypted.should.be(True) -@mock_ec2 +@mock_ec2_deprecated def test_filter_volume_by_id(): conn = boto.connect_ec2('the_key', 'the_secret') volume1 = conn.create_volume(80, "us-east-1a") @@ -82,7 +82,7 @@ def test_filter_volume_by_id(): vol2.should.have.length_of(2) -@mock_ec2 +@mock_ec2_deprecated def test_volume_filters(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -155,7 +155,7 @@ def test_volume_filters(): ) -@mock_ec2 +@mock_ec2_deprecated def test_volume_attach_and_detach(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -209,7 +209,7 @@ def test_volume_attach_and_detach(): cm3.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_create_snapshot(): conn = boto.connect_ec2('the_key', 'the_secret') volume = conn.create_volume(80, "us-east-1a") @@ -245,7 +245,7 @@ def test_create_snapshot(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_create_encrypted_snapshot(): conn = boto.connect_ec2('the_key', 'the_secret') volume = conn.create_volume(80, "us-east-1a", encrypted=True) @@ -260,7 +260,7 @@ def test_create_encrypted_snapshot(): snapshots[0].encrypted.should.be(True) -@mock_ec2 +@mock_ec2_deprecated def test_filter_snapshot_by_id(): conn = boto.connect_ec2('the_key', 'the_secret') volume1 = conn.create_volume(36, "us-east-1a") @@ -281,7 +281,7 @@ def test_filter_snapshot_by_id(): s.region.name.should.equal(conn.region.name) -@mock_ec2 +@mock_ec2_deprecated def test_snapshot_filters(): conn = boto.connect_ec2('the_key', 'the_secret') volume1 = conn.create_volume(20, "us-east-1a", encrypted=False) @@ -322,7 +322,7 @@ def test_snapshot_filters(): set([snap.id for snap in snapshots_by_encrypted]).should.equal(set([snapshot3.id])) -@mock_ec2 +@mock_ec2_deprecated def test_snapshot_attribute(): import copy @@ -418,7 +418,7 @@ def test_snapshot_attribute(): user_ids=['user']).should.throw(NotImplementedError) -@mock_ec2 +@mock_ec2_deprecated def test_create_volume_from_snapshot(): conn = boto.connect_ec2('the_key', 'the_secret') volume = conn.create_volume(80, "us-east-1a") @@ -439,7 +439,7 @@ def test_create_volume_from_snapshot(): new_volume.snapshot_id.should.equal(snapshot.id) -@mock_ec2 +@mock_ec2_deprecated def test_create_volume_from_encrypted_snapshot(): conn = boto.connect_ec2('the_key', 'the_secret') volume = conn.create_volume(80, "us-east-1a", encrypted=True) @@ -454,7 +454,7 @@ def test_create_volume_from_encrypted_snapshot(): new_volume.encrypted.should.be(True) -@mock_ec2 +@mock_ec2_deprecated def test_modify_attribute_blockDeviceMapping(): """ Reproduces the missing feature explained at [0], where we want to mock a @@ -481,7 +481,7 @@ def test_modify_attribute_blockDeviceMapping(): instance.block_device_mapping['/dev/sda1'].delete_on_termination.should.be(True) -@mock_ec2 +@mock_ec2_deprecated def test_volume_tag_escaping(): conn = boto.connect_ec2('the_key', 'the_secret') vol = conn.create_volume(10, 'us-east-1a') diff --git a/tests/test_ec2/test_elastic_ip_addresses.py b/tests/test_ec2/test_elastic_ip_addresses.py index df939a313..dc7910379 100644 --- a/tests/test_ec2/test_elastic_ip_addresses.py +++ b/tests/test_ec2/test_elastic_ip_addresses.py @@ -10,12 +10,12 @@ import six import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated import logging -@mock_ec2 +@mock_ec2_deprecated def test_eip_allocate_classic(): """Allocate/release Classic EIP""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -42,7 +42,7 @@ def test_eip_allocate_classic(): standard.should_not.be.within(conn.get_all_addresses()) -@mock_ec2 +@mock_ec2_deprecated def test_eip_allocate_vpc(): """Allocate/release VPC EIP""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -60,7 +60,7 @@ def test_eip_allocate_vpc(): vpc.release() -@mock_ec2 +@mock_ec2_deprecated def test_eip_allocate_invalid_domain(): """Allocate EIP invalid domain""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -72,7 +72,7 @@ def test_eip_allocate_invalid_domain(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_eip_associate_classic(): """Associate/Disassociate EIP to classic instance""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -114,7 +114,7 @@ def test_eip_associate_classic(): instance.terminate() -@mock_ec2 +@mock_ec2_deprecated def test_eip_associate_vpc(): """Associate/Disassociate EIP to VPC instance""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -176,7 +176,7 @@ def test_eip_boto3_vpc_association(): instance.public_dns_name.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_eip_associate_network_interface(): """Associate/Disassociate EIP to NIC""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -204,7 +204,7 @@ def test_eip_associate_network_interface(): eip.release() eip = None -@mock_ec2 +@mock_ec2_deprecated def test_eip_reassociate(): """reassociate EIP""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -233,7 +233,7 @@ def test_eip_reassociate(): instance1.terminate() instance2.terminate() -@mock_ec2 +@mock_ec2_deprecated def test_eip_reassociate_nic(): """reassociate EIP""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -261,7 +261,7 @@ def test_eip_reassociate_nic(): eip.release() eip = None -@mock_ec2 +@mock_ec2_deprecated def test_eip_associate_invalid_args(): """Associate EIP, invalid args """ conn = boto.connect_ec2('the_key', 'the_secret') @@ -280,7 +280,7 @@ def test_eip_associate_invalid_args(): instance.terminate() -@mock_ec2 +@mock_ec2_deprecated def test_eip_disassociate_bogus_association(): """Disassociate bogus EIP""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -291,7 +291,7 @@ def test_eip_disassociate_bogus_association(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_eip_release_bogus_eip(): """Release bogus EIP""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -303,7 +303,7 @@ def test_eip_release_bogus_eip(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_eip_disassociate_arg_error(): """Invalid arguments disassociate address""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -315,7 +315,7 @@ def test_eip_disassociate_arg_error(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_eip_release_arg_error(): """Invalid arguments release address""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -327,7 +327,7 @@ def test_eip_release_arg_error(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_eip_describe(): """Listing of allocated Elastic IP Addresses.""" conn = boto.connect_ec2('the_key', 'the_secret') @@ -363,7 +363,7 @@ def test_eip_describe(): len(conn.get_all_addresses()).should.be.equal(0) -@mock_ec2 +@mock_ec2_deprecated def test_eip_describe_none(): """Error when search for bogus IP""" conn = boto.connect_ec2('the_key', 'the_secret') diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py index 9b3f88a45..6f60c85a8 100644 --- a/tests/test_ec2/test_elastic_network_interfaces.py +++ b/tests/test_ec2/test_elastic_network_interfaces.py @@ -10,13 +10,13 @@ import boto.ec2 from boto.exception import EC2ResponseError, JSONResponseError import sure # noqa -from moto import mock_ec2, mock_cloudformation +from moto import mock_ec2, mock_cloudformation_deprecated, mock_ec2_deprecated from tests.helpers import requires_boto_gte from tests.test_cloudformation.fixtures import vpc_eni import json -@mock_ec2 +@mock_ec2_deprecated def test_elastic_network_interfaces(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -54,7 +54,7 @@ def test_elastic_network_interfaces(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_elastic_network_interfaces_subnet_validation(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -65,7 +65,7 @@ def test_elastic_network_interfaces_subnet_validation(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_elastic_network_interfaces_with_private_ip(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -83,7 +83,7 @@ def test_elastic_network_interfaces_with_private_ip(): eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip) -@mock_ec2 +@mock_ec2_deprecated def test_elastic_network_interfaces_with_groups(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -101,7 +101,7 @@ def test_elastic_network_interfaces_with_groups(): @requires_boto_gte("2.12.0") -@mock_ec2 +@mock_ec2_deprecated def test_elastic_network_interfaces_modify_attribute(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -133,7 +133,7 @@ def test_elastic_network_interfaces_modify_attribute(): eni.groups[0].id.should.equal(security_group2.id) -@mock_ec2 +@mock_ec2_deprecated def test_elastic_network_interfaces_filtering(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -281,8 +281,8 @@ def test_elastic_network_interfaces_get_by_subnet_id(): enis.should.have.length_of(0) -@mock_ec2 -@mock_cloudformation +@mock_ec2_deprecated +@mock_cloudformation_deprecated def test_elastic_network_interfaces_cloudformation(): template = vpc_eni.template template_json = json.dumps(template) diff --git a/tests/test_ec2/test_general.py b/tests/test_ec2/test_general.py index 83225bc0e..1dc77df82 100644 --- a/tests/test_ec2/test_general.py +++ b/tests/test_ec2/test_general.py @@ -7,10 +7,10 @@ import boto from boto.exception import EC2ResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_console_output(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -20,7 +20,7 @@ def test_console_output(): output.output.should_not.equal(None) -@mock_ec2 +@mock_ec2_deprecated def test_console_output_without_instance(): conn = boto.connect_ec2('the_key', 'the_secret') diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 7b0d734ea..a310c05a4 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -12,7 +12,7 @@ from boto.exception import EC2ResponseError, JSONResponseError from freezegun import freeze_time import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated from tests.helpers import requires_boto_gte @@ -23,7 +23,7 @@ def add_servers(ami_id, count): conn.run_instances(ami_id) -@mock_ec2 +@mock_ec2_deprecated def test_add_servers(): add_servers('ami-1234abcd', 2) @@ -37,7 +37,7 @@ def test_add_servers(): @freeze_time("2014-01-01 05:00:00") -@mock_ec2 +@mock_ec2_deprecated def test_instance_launch_and_terminate(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -87,14 +87,14 @@ def test_instance_launch_and_terminate(): instance.state.should.equal('terminated') -@mock_ec2 +@mock_ec2_deprecated def test_terminate_empty_instances(): conn = boto.connect_ec2('the_key', 'the_secret') conn.terminate_instances.when.called_with([]).should.throw(EC2ResponseError) @freeze_time("2014-01-01 05:00:00") -@mock_ec2 +@mock_ec2_deprecated def test_instance_attach_volume(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -123,7 +123,7 @@ def test_instance_attach_volume(): v.status.should.equal('in-use') -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_by_id(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=2) @@ -150,7 +150,7 @@ def test_get_instances_by_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_state(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -178,7 +178,7 @@ def test_get_instances_filtering_by_state(): conn.get_all_instances.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_instance_id(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -197,7 +197,7 @@ def test_get_instances_filtering_by_instance_id(): reservations.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_instance_type(): conn = boto.connect_ec2() reservation1 = conn.run_instances('ami-1234abcd', instance_type='m1.small') @@ -238,7 +238,7 @@ def test_get_instances_filtering_by_instance_type(): #bogus instance-type should return none reservations.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_reason_code(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -257,7 +257,7 @@ def test_get_instances_filtering_by_reason_code(): reservations[0].instances[0].id.should.equal(instance3.id) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_source_dest_check(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=2) @@ -274,7 +274,7 @@ def test_get_instances_filtering_by_source_dest_check(): source_dest_check_true[0].instances[0].id.should.equal(instance2.id) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_vpc_id(): conn = boto.connect_vpc('the_key', 'the_secret') vpc1 = conn.create_vpc("10.0.0.0/16") @@ -298,7 +298,7 @@ def test_get_instances_filtering_by_vpc_id(): reservations2[0].instances[0].id.should.equal(instance2.id) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_architecture(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=1) @@ -309,7 +309,7 @@ def test_get_instances_filtering_by_architecture(): reservations[0].instances.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_tag(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -351,7 +351,7 @@ def test_get_instances_filtering_by_tag(): reservations[0].instances[1].id.should.equal(instance3.id) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_tag_value(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -388,7 +388,7 @@ def test_get_instances_filtering_by_tag_value(): reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance3.id) -@mock_ec2 +@mock_ec2_deprecated def test_get_instances_filtering_by_tag_name(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -418,7 +418,7 @@ def test_get_instances_filtering_by_tag_name(): reservations[0].instances[1].id.should.equal(instance2.id) reservations[0].instances[2].id.should.equal(instance3.id) -@mock_ec2 +@mock_ec2_deprecated def test_instance_start_and_stop(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd', min_count=2) @@ -448,7 +448,7 @@ def test_instance_start_and_stop(): started_instances[0].state.should.equal('pending') -@mock_ec2 +@mock_ec2_deprecated def test_instance_reboot(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -464,7 +464,7 @@ def test_instance_reboot(): instance.state.should.equal('pending') -@mock_ec2 +@mock_ec2_deprecated def test_instance_attribute_instance_type(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -482,7 +482,7 @@ def test_instance_attribute_instance_type(): instance_attribute.should.be.a(InstanceAttribute) instance_attribute.get('instanceType').should.equal("m1.small") -@mock_ec2 +@mock_ec2_deprecated def test_modify_instance_attribute_security_groups(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -506,7 +506,7 @@ def test_modify_instance_attribute_security_groups(): any(g.id == sg_id2 for g in group_list).should.be.ok -@mock_ec2 +@mock_ec2_deprecated def test_instance_attribute_user_data(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -525,7 +525,7 @@ def test_instance_attribute_user_data(): instance_attribute.get("userData").should.equal("this is my user data") -@mock_ec2 +@mock_ec2_deprecated def test_instance_attribute_source_dest_check(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -566,7 +566,7 @@ def test_instance_attribute_source_dest_check(): instance_attribute.get("sourceDestCheck").should.equal(True) -@mock_ec2 +@mock_ec2_deprecated def test_user_data_with_run_instance(): user_data = b"some user data" conn = boto.connect_ec2('the_key', 'the_secret') @@ -580,7 +580,7 @@ def test_user_data_with_run_instance(): decoded_user_data.should.equal(b"some user data") -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_security_group_name(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -600,7 +600,7 @@ def test_run_instance_with_security_group_name(): instance.groups[0].name.should.equal("group1") -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_security_group_id(): conn = boto.connect_ec2('the_key', 'the_secret') group = conn.create_security_group('group1', "some description") @@ -612,7 +612,7 @@ def test_run_instance_with_security_group_id(): instance.groups[0].name.should.equal("group1") -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_instance_type(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd', instance_type="t1.micro") @@ -621,7 +621,7 @@ def test_run_instance_with_instance_type(): instance.instance_type.should.equal("t1.micro") -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_default_placement(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -630,7 +630,7 @@ def test_run_instance_with_default_placement(): instance.placement.should.equal("us-east-1a") -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_placement(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd', placement="us-east-1b") @@ -639,7 +639,7 @@ def test_run_instance_with_placement(): instance.placement.should.equal("us-east-1b") -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_subnet(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -653,7 +653,7 @@ def test_run_instance_with_subnet(): all_enis.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_nic_autocreated(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -686,7 +686,7 @@ def test_run_instance_with_nic_autocreated(): eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip) -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_nic_preexisting(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -724,7 +724,7 @@ def test_run_instance_with_nic_preexisting(): @requires_boto_gte("2.32.0") -@mock_ec2 +@mock_ec2_deprecated def test_instance_with_nic_attach_detach(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -790,7 +790,7 @@ def test_instance_with_nic_attach_detach(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_ec2_classic_has_public_ip_address(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd', key_name="keypair_name") @@ -802,7 +802,7 @@ def test_ec2_classic_has_public_ip_address(): instance.private_dns_name.should.contain(instance.private_ip_address) -@mock_ec2 +@mock_ec2_deprecated def test_run_instance_with_keypair(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd', key_name="keypair_name") @@ -811,14 +811,14 @@ def test_run_instance_with_keypair(): instance.key_name.should.equal("keypair_name") -@mock_ec2 +@mock_ec2_deprecated def test_describe_instance_status_no_instances(): conn = boto.connect_ec2('the_key', 'the_secret') all_status = conn.get_all_instance_status() len(all_status).should.equal(0) -@mock_ec2 +@mock_ec2_deprecated def test_describe_instance_status_with_instances(): conn = boto.connect_ec2('the_key', 'the_secret') conn.run_instances('ami-1234abcd', key_name="keypair_name") @@ -829,7 +829,7 @@ def test_describe_instance_status_with_instances(): all_status[0].system_status.status.should.equal('ok') -@mock_ec2 +@mock_ec2_deprecated def test_describe_instance_status_with_instance_filter(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -852,7 +852,7 @@ def test_describe_instance_status_with_instance_filter(): cm.exception.request_id.should_not.be.none @requires_boto_gte("2.32.0") -@mock_ec2 +@mock_ec2_deprecated def test_describe_instance_status_with_non_running_instances(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd', min_count=3) @@ -877,7 +877,7 @@ def test_describe_instance_status_with_non_running_instances(): status3 = next((s for s in all_status if s.id == instance3.id), None) status3.state_name.should.equal('running') -@mock_ec2 +@mock_ec2_deprecated def test_get_instance_by_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') diff --git a/tests/test_ec2/test_internet_gateways.py b/tests/test_ec2/test_internet_gateways.py index 4a08fe108..12b37860e 100644 --- a/tests/test_ec2/test_internet_gateways.py +++ b/tests/test_ec2/test_internet_gateways.py @@ -10,14 +10,14 @@ from boto.exception import EC2ResponseError, JSONResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated VPC_CIDR="10.0.0.0/16" BAD_VPC="vpc-deadbeef" BAD_IGW="igw-deadbeef" -@mock_ec2 +@mock_ec2_deprecated def test_igw_create(): """ internet gateway create """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -37,7 +37,7 @@ def test_igw_create(): igw = conn.get_all_internet_gateways()[0] igw.attachments.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_igw_attach(): """ internet gateway attach """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -55,7 +55,7 @@ def test_igw_attach(): igw = conn.get_all_internet_gateways()[0] igw.attachments[0].vpc_id.should.be.equal(vpc.id) -@mock_ec2 +@mock_ec2_deprecated def test_igw_attach_bad_vpc(): """ internet gateway fail to attach w/ bad vpc """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -67,7 +67,7 @@ def test_igw_attach_bad_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_attach_twice(): """ internet gateway fail to attach twice """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -82,7 +82,7 @@ def test_igw_attach_twice(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_detach(): """ internet gateway detach""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -100,7 +100,7 @@ def test_igw_detach(): igw = conn.get_all_internet_gateways()[0] igw.attachments.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_igw_detach_wrong_vpc(): """ internet gateway fail to detach w/ wrong vpc """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -115,7 +115,7 @@ def test_igw_detach_wrong_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_detach_invalid_vpc(): """ internet gateway fail to detach w/ invalid vpc """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -129,7 +129,7 @@ def test_igw_detach_invalid_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_detach_unattached(): """ internet gateway fail to detach unattached """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -142,7 +142,7 @@ def test_igw_detach_unattached(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_delete(): """ internet gateway delete""" conn = boto.connect_vpc('the_key', 'the_secret') @@ -160,7 +160,7 @@ def test_igw_delete(): conn.delete_internet_gateway(igw.id) conn.get_all_internet_gateways().should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_igw_delete_attached(): """ internet gateway fail to delete attached """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -174,7 +174,7 @@ def test_igw_delete_attached(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_desribe(): """ internet gateway fetch by id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -182,7 +182,7 @@ def test_igw_desribe(): igw_by_search = conn.get_all_internet_gateways([igw.id])[0] igw.id.should.equal(igw_by_search.id) -@mock_ec2 +@mock_ec2_deprecated def test_igw_desribe_bad_id(): """ internet gateway fail to fetch by bad id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -193,7 +193,7 @@ def test_igw_desribe_bad_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_igw_filter_by_vpc_id(): """ internet gateway filter by vpc id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -208,7 +208,7 @@ def test_igw_filter_by_vpc_id(): result[0].id.should.equal(igw1.id) -@mock_ec2 +@mock_ec2_deprecated def test_igw_filter_by_tags(): """ internet gateway filter by vpc id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -222,7 +222,7 @@ def test_igw_filter_by_tags(): result[0].id.should.equal(igw1.id) -@mock_ec2 +@mock_ec2_deprecated def test_igw_filter_by_internet_gateway_id(): """ internet gateway filter by internet gateway id """ conn = boto.connect_vpc('the_key', 'the_secret') @@ -235,7 +235,7 @@ def test_igw_filter_by_internet_gateway_id(): result[0].id.should.equal(igw1.id) -@mock_ec2 +@mock_ec2_deprecated def test_igw_filter_by_attachment_state(): """ internet gateway filter by attachment state """ conn = boto.connect_vpc('the_key', 'the_secret') diff --git a/tests/test_ec2/test_key_pairs.py b/tests/test_ec2/test_key_pairs.py index 7d45e79db..a35f0b962 100644 --- a/tests/test_ec2/test_key_pairs.py +++ b/tests/test_ec2/test_key_pairs.py @@ -8,16 +8,16 @@ import six import sure # noqa from boto.exception import EC2ResponseError, JSONResponseError -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_empty(): conn = boto.connect_ec2('the_key', 'the_secret') assert len(conn.get_all_key_pairs()) == 0 -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_invalid_id(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -28,7 +28,7 @@ def test_key_pairs_invalid_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_create(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -45,7 +45,7 @@ def test_key_pairs_create(): assert kps[0].name == 'foo' -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_create_two(): conn = boto.connect_ec2('the_key', 'the_secret') kp = conn.create_key_pair('foo') @@ -60,7 +60,7 @@ def test_key_pairs_create_two(): kps[0].name.should.equal('foo') -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_create_exist(): conn = boto.connect_ec2('the_key', 'the_secret') kp = conn.create_key_pair('foo') @@ -74,7 +74,7 @@ def test_key_pairs_create_exist(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_delete_no_exist(): conn = boto.connect_ec2('the_key', 'the_secret') assert len(conn.get_all_key_pairs()) == 0 @@ -82,7 +82,7 @@ def test_key_pairs_delete_no_exist(): r.should.be.ok -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_delete_exist(): conn = boto.connect_ec2('the_key', 'the_secret') conn.create_key_pair('foo') @@ -98,7 +98,7 @@ def test_key_pairs_delete_exist(): assert len(conn.get_all_key_pairs()) == 0 -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_import(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -115,7 +115,7 @@ def test_key_pairs_import(): assert kps[0].name == 'foo' -@mock_ec2 +@mock_ec2_deprecated def test_key_pairs_import_exist(): conn = boto.connect_ec2('the_key', 'the_secret') kp = conn.import_key_pair('foo', b'content') diff --git a/tests/test_ec2/test_network_acls.py b/tests/test_ec2/test_network_acls.py index 5ab16b51b..91158e0bf 100644 --- a/tests/test_ec2/test_network_acls.py +++ b/tests/test_ec2/test_network_acls.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals import boto import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_default_network_acl_created_with_vpc(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -13,7 +13,7 @@ def test_default_network_acl_created_with_vpc(): all_network_acls.should.have.length_of(2) -@mock_ec2 +@mock_ec2_deprecated def test_network_acls(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -22,7 +22,7 @@ def test_network_acls(): all_network_acls.should.have.length_of(3) -@mock_ec2 +@mock_ec2_deprecated def test_new_subnet_associates_with_default_network_acl(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.get_all_vpcs()[0] @@ -36,7 +36,7 @@ def test_new_subnet_associates_with_default_network_acl(): [a.subnet_id for a in acl.associations].should.contain(subnet.id) -@mock_ec2 +@mock_ec2_deprecated def test_network_acl_entries(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -62,7 +62,7 @@ def test_network_acl_entries(): entries[0].rule_action.should.equal('ALLOW') -@mock_ec2 +@mock_ec2_deprecated def test_associate_new_network_acl_with_subnet(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -81,7 +81,7 @@ def test_associate_new_network_acl_with_subnet(): test_network_acl.associations[0].subnet_id.should.equal(subnet.id) -@mock_ec2 +@mock_ec2_deprecated def test_delete_network_acl(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -101,7 +101,7 @@ def test_delete_network_acl(): any(acl.id == network_acl.id for acl in updated_network_acls).shouldnt.be.ok -@mock_ec2 +@mock_ec2_deprecated def test_network_acl_tagging(): conn = boto.connect_vpc('the_key', 'the secret') vpc = conn.create_vpc("10.0.0.0/16") diff --git a/tests/test_ec2/test_regions.py b/tests/test_ec2/test_regions.py index 9375314d4..07e02c526 100644 --- a/tests/test_ec2/test_regions.py +++ b/tests/test_ec2/test_regions.py @@ -3,7 +3,7 @@ import boto.ec2 import boto.ec2.autoscale import boto.ec2.elb import sure -from moto import mock_ec2, mock_autoscaling, mock_elb +from moto import mock_ec2_deprecated, mock_autoscaling_deprecated, mock_elb_deprecated def add_servers_to_region(ami_id, count, region): @@ -12,7 +12,7 @@ def add_servers_to_region(ami_id, count, region): conn.run_instances(ami_id) -@mock_ec2 +@mock_ec2_deprecated def test_add_servers_to_a_single_region(): region = 'ap-northeast-1' add_servers_to_region('ami-1234abcd', 1, region) @@ -27,7 +27,7 @@ def test_add_servers_to_a_single_region(): reservations[1].instances[0].image_id.should.equal('ami-5678efgh') -@mock_ec2 +@mock_ec2_deprecated def test_add_servers_to_multiple_regions(): region1 = 'us-east-1' region2 = 'ap-northeast-1' @@ -46,8 +46,8 @@ def test_add_servers_to_multiple_regions(): ap_reservations[0].instances[0].image_id.should.equal('ami-5678efgh') -@mock_autoscaling -@mock_elb +@mock_autoscaling_deprecated +@mock_elb_deprecated def test_create_autoscaling_group(): elb_conn = boto.ec2.elb.connect_to_region('us-east-1') elb_conn.create_load_balancer('us_test_lb', zones=[], listeners=[(80, 8080, 'http')]) diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 41e5786e6..3aa4b460a 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -8,11 +8,11 @@ import boto3 from boto.exception import EC2ResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated from tests.helpers import requires_boto_gte -@mock_ec2 +@mock_ec2_deprecated def test_route_tables_defaults(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -37,7 +37,7 @@ def test_route_tables_defaults(): all_route_tables.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_route_tables_additional(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -77,7 +77,7 @@ def test_route_tables_additional(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_route_tables_filters_standard(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -115,7 +115,7 @@ def test_route_tables_filters_standard(): conn.get_all_route_tables.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) -@mock_ec2 +@mock_ec2_deprecated def test_route_tables_filters_associations(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -152,7 +152,7 @@ def test_route_tables_filters_associations(): association1_route_tables[0].associations.should.have.length_of(2) -@mock_ec2 +@mock_ec2_deprecated def test_route_table_associations(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -219,7 +219,7 @@ def test_route_table_associations(): @requires_boto_gte("2.16.0") -@mock_ec2 +@mock_ec2_deprecated def test_route_table_replace_route_table_association(): """ Note: Boto has deprecated replace_route_table_assocation (which returns status) @@ -289,7 +289,7 @@ def test_route_table_replace_route_table_association(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_route_table_get_by_tag(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -326,7 +326,7 @@ def test_route_table_get_by_tag_boto3(): route_tables[0].tags[0].should.equal({'Key': 'Name', 'Value': 'TestRouteTable'}) -@mock_ec2 +@mock_ec2_deprecated def test_routes_additional(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -364,7 +364,7 @@ def test_routes_additional(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_routes_replace(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -412,7 +412,7 @@ def test_routes_replace(): @requires_boto_gte("2.19.0") -@mock_ec2 +@mock_ec2_deprecated def test_routes_not_supported(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -431,7 +431,7 @@ def test_routes_not_supported(): @requires_boto_gte("2.34.0") -@mock_ec2 +@mock_ec2_deprecated def test_routes_vpc_peering_connection(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -458,7 +458,7 @@ def test_routes_vpc_peering_connection(): @requires_boto_gte("2.34.0") -@mock_ec2 +@mock_ec2_deprecated def test_routes_vpn_gateway(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -480,7 +480,7 @@ def test_routes_vpn_gateway(): new_route.vpc_peering_connection_id.should.be.none -@mock_ec2 +@mock_ec2_deprecated def test_network_acl_tagging(): conn = boto.connect_vpc('the_key', 'the secret') diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py index 19f43862d..3968d9151 100644 --- a/tests/test_ec2/test_security_groups.py +++ b/tests/test_ec2/test_security_groups.py @@ -12,10 +12,10 @@ from botocore.exceptions import ClientError from boto.exception import EC2ResponseError, JSONResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_create_and_describe_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -43,7 +43,7 @@ def test_create_and_describe_security_group(): set(group_names).should.equal(set(["default", "test security group"])) -@mock_ec2 +@mock_ec2_deprecated def test_create_security_group_without_description_raises_error(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -54,7 +54,7 @@ def test_create_security_group_without_description_raises_error(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_default_security_group(): conn = boto.ec2.connect_to_region('us-east-1') groups = conn.get_all_security_groups() @@ -62,7 +62,7 @@ def test_default_security_group(): groups[0].name.should.equal("default") -@mock_ec2 +@mock_ec2_deprecated def test_create_and_describe_vpc_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = 'vpc-5300000c' @@ -88,7 +88,7 @@ def test_create_and_describe_vpc_security_group(): all_groups[0].name.should.equal('test security group') -@mock_ec2 +@mock_ec2_deprecated def test_create_two_security_groups_with_same_name_in_different_vpc(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = 'vpc-5300000c' @@ -105,7 +105,7 @@ def test_create_two_security_groups_with_same_name_in_different_vpc(): set(group_names).should.equal(set(["default", "test security group"])) -@mock_ec2 +@mock_ec2_deprecated def test_deleting_security_groups(): conn = boto.connect_ec2('the_key', 'the_secret') security_group1 = conn.create_security_group('test1', 'test1') @@ -135,7 +135,7 @@ def test_deleting_security_groups(): conn.get_all_security_groups().should.have.length_of(2) -@mock_ec2 +@mock_ec2_deprecated def test_delete_security_group_in_vpc(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = "vpc-12345" @@ -145,7 +145,7 @@ def test_delete_security_group_in_vpc(): conn.delete_security_group(group_id=security_group1.id) -@mock_ec2 +@mock_ec2_deprecated def test_authorize_ip_range_and_revoke(): conn = boto.connect_ec2('the_key', 'the_secret') security_group = conn.create_security_group('test', 'test') @@ -216,7 +216,7 @@ def test_authorize_ip_range_and_revoke(): egress_security_group.rules_egress.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_authorize_other_group_and_revoke(): conn = boto.connect_ec2('the_key', 'the_secret') security_group = conn.create_security_group('test', 'test') @@ -269,7 +269,7 @@ def test_authorize_other_group_egress_and_revoke(): sg01.ip_permissions_egress.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_authorize_group_in_vpc(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = "vpc-12345" @@ -295,7 +295,7 @@ def test_authorize_group_in_vpc(): security_group.rules.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_get_all_security_groups(): conn = boto.connect_ec2() sg1 = conn.create_security_group(name='test1', description='test1', vpc_id='vpc-mjm05d27') @@ -321,7 +321,7 @@ def test_get_all_security_groups(): resp.should.have.length_of(4) -@mock_ec2 +@mock_ec2_deprecated def test_authorize_bad_cidr_throws_invalid_parameter_value(): conn = boto.connect_ec2('the_key', 'the_secret') security_group = conn.create_security_group('test', 'test') @@ -332,7 +332,7 @@ def test_authorize_bad_cidr_throws_invalid_parameter_value(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_security_group_tagging(): conn = boto.connect_vpc() vpc = conn.create_vpc("10.0.0.0/16") @@ -356,7 +356,7 @@ def test_security_group_tagging(): group.tags["Test"].should.equal("Tag") -@mock_ec2 +@mock_ec2_deprecated def test_security_group_tag_filtering(): conn = boto.connect_ec2() sg = conn.create_security_group("test-sg", "Test SG") @@ -366,7 +366,7 @@ def test_security_group_tag_filtering(): groups.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_authorize_all_protocols_with_no_port_specification(): conn = boto.connect_ec2() sg = conn.create_security_group('test', 'test') @@ -379,7 +379,7 @@ def test_authorize_all_protocols_with_no_port_specification(): sg.rules[0].to_port.should.equal(None) -@mock_ec2 +@mock_ec2_deprecated def test_sec_group_rule_limit(): ec2_conn = boto.connect_ec2() sg = ec2_conn.create_security_group('test', 'test') @@ -441,7 +441,7 @@ def test_sec_group_rule_limit(): cm.exception.error_code.should.equal('RulesPerSecurityGroupLimitExceeded') -@mock_ec2 +@mock_ec2_deprecated def test_sec_group_rule_limit_vpc(): ec2_conn = boto.connect_ec2() vpc_conn = boto.connect_vpc() @@ -611,7 +611,7 @@ def test_authorize_and_revoke_in_bulk(): for ip_permission in expected_ip_permissions: sg01.ip_permissions_egress.shouldnt.contain(ip_permission) -@mock_ec2 +@mock_ec2_deprecated def test_get_all_security_groups_filter_with_same_vpc_id(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = 'vpc-5300000c' diff --git a/tests/test_ec2/test_spot_instances.py b/tests/test_ec2/test_spot_instances.py index 790ffeb65..1933613e8 100644 --- a/tests/test_ec2/test_spot_instances.py +++ b/tests/test_ec2/test_spot_instances.py @@ -7,12 +7,13 @@ import boto3 import sure # noqa from boto.exception import JSONResponseError -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated from moto.backends import get_model from moto.core.utils import iso_8601_datetime_with_milliseconds @mock_ec2 +@mock_ec2_deprecated def test_request_spot_instances(): conn = boto3.client('ec2', 'us-east-1') vpc = conn.create_vpc(CidrBlock="10.0.0.0/8")['Vpc'] @@ -73,7 +74,7 @@ def test_request_spot_instances(): request.launch_specification.subnet_id.should.equal(subnet_id) -@mock_ec2 +@mock_ec2_deprecated def test_request_spot_instances_default_arguments(): """ Test that moto set the correct default arguments @@ -106,7 +107,7 @@ def test_request_spot_instances_default_arguments(): request.launch_specification.subnet_id.should.equal(None) -@mock_ec2 +@mock_ec2_deprecated def test_cancel_spot_instance_request(): conn = boto.connect_ec2() @@ -130,7 +131,7 @@ def test_cancel_spot_instance_request(): requests.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_request_spot_instances_fulfilled(): """ Test that moto correctly fullfills a spot instance request @@ -156,7 +157,7 @@ def test_request_spot_instances_fulfilled(): request.state.should.equal("active") -@mock_ec2 +@mock_ec2_deprecated def test_tag_spot_instance_request(): """ Test that moto correctly tags a spot instance request @@ -177,7 +178,7 @@ def test_tag_spot_instance_request(): tag_dict.should.equal({'tag1': 'value1', 'tag2': 'value2'}) -@mock_ec2 +@mock_ec2_deprecated def test_get_all_spot_instance_requests_filtering(): """ Test that moto correctly filters spot instance requests @@ -211,7 +212,7 @@ def test_get_all_spot_instance_requests_filtering(): requests.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_request_spot_instances_setting_instance_id(): conn = boto.ec2.connect_to_region("us-east-1") request = conn.request_spot_instances( diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py index 8e6a2a4ea..0a9b41b8e 100644 --- a/tests/test_ec2/test_subnets.py +++ b/tests/test_ec2/test_subnets.py @@ -11,10 +11,10 @@ from botocore.exceptions import ParamValidationError import json import sure # noqa -from moto import mock_cloudformation, mock_ec2 +from moto import mock_cloudformation_deprecated, mock_ec2, mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_subnets(): ec2 = boto.connect_ec2('the_key', 'the_secret') conn = boto.connect_vpc('the_key', 'the_secret') @@ -36,7 +36,7 @@ def test_subnets(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_subnet_create_vpc_validation(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -47,7 +47,7 @@ def test_subnet_create_vpc_validation(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_subnet_tagging(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -65,7 +65,7 @@ def test_subnet_tagging(): subnet.tags["a key"].should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_subnet_should_have_proper_availability_zone_set(): conn = boto.vpc.connect_to_region('us-west-1') vpcA = conn.create_vpc("10.0.0.0/16") @@ -87,7 +87,7 @@ def test_default_subnet(): subnet.map_public_ip_on_launch.shouldnt.be.ok -@mock_ec2 +@mock_ec2_deprecated def test_non_default_subnet(): vpc_cli = boto.vpc.connect_to_region('us-west-1') @@ -150,7 +150,7 @@ def test_modify_subnet_attribute_validation(): client.modify_subnet_attribute(SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': 'invalid'}) -@mock_ec2 +@mock_ec2_deprecated def test_get_subnets_filtering(): ec2 = boto.ec2.connect_to_region('us-west-1') conn = boto.vpc.connect_to_region('us-west-1') @@ -205,8 +205,8 @@ def test_get_subnets_filtering(): conn.get_all_subnets.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) -@mock_ec2 -@mock_cloudformation +@mock_ec2_deprecated +@mock_cloudformation_deprecated def test_subnet_tags_through_cloudformation(): vpc_conn = boto.vpc.connect_to_region('us-west-1') vpc = vpc_conn.create_vpc("10.0.0.0/16") diff --git a/tests/test_ec2/test_tags.py b/tests/test_ec2/test_tags.py index 4a85eb6e1..1084e44c4 100644 --- a/tests/test_ec2/test_tags.py +++ b/tests/test_ec2/test_tags.py @@ -8,11 +8,11 @@ from boto.exception import EC2ResponseError, JSONResponseError from boto.ec2.instance import Reservation import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated from nose.tools import assert_raises -@mock_ec2 +@mock_ec2_deprecated def test_add_tag(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -32,7 +32,7 @@ def test_add_tag(): existing_instance.tags["a key"].should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_remove_tag(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -59,7 +59,7 @@ def test_remove_tag(): instance.remove_tag("a key", "some value") -@mock_ec2 +@mock_ec2_deprecated def test_get_all_tags(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -73,7 +73,7 @@ def test_get_all_tags(): tag.value.should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_get_all_tags_with_special_characters(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -87,7 +87,7 @@ def test_get_all_tags_with_special_characters(): tag.value.should.equal("some<> value") -@mock_ec2 +@mock_ec2_deprecated def test_create_tags(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -108,7 +108,7 @@ def test_create_tags(): set([tag_dict[key] for key in tag_dict]).should.equal(set([tag.value for tag in tags])) -@mock_ec2 +@mock_ec2_deprecated def test_tag_limit_exceeded(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -137,7 +137,7 @@ def test_tag_limit_exceeded(): tag.value.should.equal("a value") -@mock_ec2 +@mock_ec2_deprecated def test_invalid_parameter_tag_null(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -150,7 +150,7 @@ def test_invalid_parameter_tag_null(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_invalid_id(): conn = boto.connect_ec2('the_key', 'the_secret') with assert_raises(EC2ResponseError) as cm: @@ -166,7 +166,7 @@ def test_invalid_id(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_get_all_tags_resource_id_filter(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -193,7 +193,7 @@ def test_get_all_tags_resource_id_filter(): tag.value.should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_get_all_tags_resource_type_filter(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -220,7 +220,7 @@ def test_get_all_tags_resource_type_filter(): tag.value.should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_get_all_tags_key_filter(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -239,7 +239,7 @@ def test_get_all_tags_key_filter(): tag.value.should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_get_all_tags_value_filter(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -283,7 +283,7 @@ def test_get_all_tags_value_filter(): tags.should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_retrieved_instances_must_contain_their_tags(): tag_key = 'Tag name' tag_value = 'Tag value' @@ -314,7 +314,7 @@ def test_retrieved_instances_must_contain_their_tags(): retrieved_tags[tag_key].should.equal(tag_value) -@mock_ec2 +@mock_ec2_deprecated def test_retrieved_volumes_must_contain_their_tags(): tag_key = 'Tag name' tag_value = 'Tag value' @@ -337,7 +337,7 @@ def test_retrieved_volumes_must_contain_their_tags(): retrieved_tags[tag_key].should.equal(tag_value) -@mock_ec2 +@mock_ec2_deprecated def test_retrieved_snapshots_must_contain_their_tags(): tag_key = 'Tag name' tag_value = 'Tag value' @@ -359,7 +359,7 @@ def test_retrieved_snapshots_must_contain_their_tags(): retrieved_tags[tag_key].should.equal(tag_value) -@mock_ec2 +@mock_ec2_deprecated def test_filter_instances_by_wildcard_tags(): conn = boto.connect_ec2(aws_access_key_id='the_key', aws_secret_access_key='the_secret') reservation = conn.run_instances('ami-1234abcd') diff --git a/tests/test_ec2/test_virtual_private_gateways.py b/tests/test_ec2/test_virtual_private_gateways.py index 8050559f1..0a7e34ea5 100644 --- a/tests/test_ec2/test_virtual_private_gateways.py +++ b/tests/test_ec2/test_virtual_private_gateways.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals import boto import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_virtual_private_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -16,7 +16,7 @@ def test_virtual_private_gateways(): vpn_gateway.state.should.equal('available') vpn_gateway.availability_zone.should.equal('us-east-1a') -@mock_ec2 +@mock_ec2_deprecated def test_describe_vpn_gateway(): conn = boto.connect_vpc('the_key', 'the_secret') vpn_gateway = conn.create_vpn_gateway('ipsec.1', 'us-east-1a') @@ -32,7 +32,7 @@ def test_describe_vpn_gateway(): vpn_gateway.availability_zone.should.equal('us-east-1a') -@mock_ec2 +@mock_ec2_deprecated def test_vpn_gateway_vpc_attachment(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -50,7 +50,7 @@ def test_vpn_gateway_vpc_attachment(): attachments[0].state.should.equal('attached') -@mock_ec2 +@mock_ec2_deprecated def test_delete_vpn_gateway(): conn = boto.connect_vpc('the_key', 'the_secret') vpn_gateway = conn.create_vpn_gateway('ipsec.1', 'us-east-1a') @@ -60,7 +60,7 @@ def test_delete_vpn_gateway(): vgws.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_vpn_gateway_tagging(): conn = boto.connect_vpc('the_key', 'the_secret') vpn_gateway = conn.create_vpn_gateway('ipsec.1', 'us-east-1a') @@ -76,7 +76,7 @@ def test_vpn_gateway_tagging(): vpn_gateway.tags["a key"].should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_detach_vpn_gateway(): conn = boto.connect_vpc('the_key', 'the_secret') diff --git a/tests/test_ec2/test_vpc_peering.py b/tests/test_ec2/test_vpc_peering.py index d41c3ab7b..c6a2feffb 100644 --- a/tests/test_ec2/test_vpc_peering.py +++ b/tests/test_ec2/test_vpc_peering.py @@ -7,12 +7,12 @@ import boto from boto.exception import EC2ResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2_deprecated from tests.helpers import requires_boto_gte @requires_boto_gte("2.32.0") -@mock_ec2 +@mock_ec2_deprecated def test_vpc_peering_connections(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -25,7 +25,7 @@ def test_vpc_peering_connections(): @requires_boto_gte("2.32.0") -@mock_ec2 +@mock_ec2_deprecated def test_vpc_peering_connections_get_all(): conn = boto.connect_vpc('the_key', 'the_secret') vpc_pcx = test_vpc_peering_connections() @@ -37,7 +37,7 @@ def test_vpc_peering_connections_get_all(): @requires_boto_gte("2.32.0") -@mock_ec2 +@mock_ec2_deprecated def test_vpc_peering_connections_accept(): conn = boto.connect_vpc('the_key', 'the_secret') vpc_pcx = test_vpc_peering_connections() @@ -57,7 +57,7 @@ def test_vpc_peering_connections_accept(): @requires_boto_gte("2.32.0") -@mock_ec2 +@mock_ec2_deprecated def test_vpc_peering_connections_reject(): conn = boto.connect_vpc('the_key', 'the_secret') vpc_pcx = test_vpc_peering_connections() @@ -77,7 +77,7 @@ def test_vpc_peering_connections_reject(): @requires_boto_gte("2.32.1") -@mock_ec2 +@mock_ec2_deprecated def test_vpc_peering_connections_delete(): conn = boto.connect_vpc('the_key', 'the_secret') vpc_pcx = test_vpc_peering_connections() diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py index 513238001..c4dbf788e 100644 --- a/tests/test_ec2/test_vpcs.py +++ b/tests/test_ec2/test_vpcs.py @@ -8,13 +8,13 @@ import boto from boto.exception import EC2ResponseError import sure # noqa -from moto import mock_ec2 +from moto import mock_ec2, mock_ec2_deprecated SAMPLE_DOMAIN_NAME = u'example.com' SAMPLE_NAME_SERVERS = [u'10.0.0.6', u'10.0.0.7'] -@mock_ec2 +@mock_ec2_deprecated def test_vpcs(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -35,7 +35,7 @@ def test_vpcs(): cm.exception.request_id.should_not.be.none -@mock_ec2 +@mock_ec2_deprecated def test_vpc_defaults(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -50,7 +50,7 @@ def test_vpc_defaults(): conn.get_all_route_tables().should.have.length_of(1) conn.get_all_security_groups(filters={'vpc-id': [vpc.id]}).should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_isdefault_filter(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -59,7 +59,7 @@ def test_vpc_isdefault_filter(): conn.get_all_vpcs(filters={'isDefault': 'true'}).should.have.length_of(1) -@mock_ec2 +@mock_ec2_deprecated def test_multiple_vpcs_default_filter(): conn = boto.connect_vpc('the_key', 'the_secret') conn.create_vpc("10.8.0.0/16") @@ -71,7 +71,7 @@ def test_multiple_vpcs_default_filter(): vpc[0].cidr_block.should.equal('172.31.0.0/16') -@mock_ec2 +@mock_ec2_deprecated def test_vpc_state_available_filter(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") @@ -80,7 +80,7 @@ def test_vpc_state_available_filter(): vpc.delete() conn.get_all_vpcs(filters={'state': 'available'}).should.have.length_of(2) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_tagging(): conn = boto.connect_vpc() vpc = conn.create_vpc("10.0.0.0/16") @@ -96,7 +96,7 @@ def test_vpc_tagging(): vpc.tags["a key"].should.equal("some value") -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_id(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -110,7 +110,7 @@ def test_vpc_get_by_id(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_cidr_block(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -124,7 +124,7 @@ def test_vpc_get_by_cidr_block(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_dhcp_options_id(): conn = boto.connect_vpc() dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) @@ -142,7 +142,7 @@ def test_vpc_get_by_dhcp_options_id(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_tag(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -160,7 +160,7 @@ def test_vpc_get_by_tag(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_tag_key_superset(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -180,7 +180,7 @@ def test_vpc_get_by_tag_key_superset(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_tag_key_subset(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -200,7 +200,7 @@ def test_vpc_get_by_tag_key_subset(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_tag_value_superset(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -220,7 +220,7 @@ def test_vpc_get_by_tag_value_superset(): vpc2.id.should.be.within(vpc_ids) -@mock_ec2 +@mock_ec2_deprecated def test_vpc_get_by_tag_value_subset(): conn = boto.connect_vpc() vpc1 = conn.create_vpc("10.0.0.0/16") @@ -339,7 +339,7 @@ def test_vpc_modify_enable_dns_hostnames(): attr = response.get('EnableDnsHostnames') attr.get('Value').should.be.ok -@mock_ec2 +@mock_ec2_deprecated def test_vpc_associate_dhcp_options(): conn = boto.connect_vpc() dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) diff --git a/tests/test_ec2/test_vpn_connections.py b/tests/test_ec2/test_vpn_connections.py index dd96e7b65..864c1c3ee 100644 --- a/tests/test_ec2/test_vpn_connections.py +++ b/tests/test_ec2/test_vpn_connections.py @@ -4,10 +4,10 @@ from nose.tools import assert_raises import sure # noqa from boto.exception import EC2ResponseError -from moto import mock_ec2 +from moto import mock_ec2_deprecated -@mock_ec2 +@mock_ec2_deprecated def test_create_vpn_connections(): conn = boto.connect_vpc('the_key', 'the_secret') vpn_connection = conn.create_vpn_connection('ipsec.1', 'vgw-0123abcd', 'cgw-0123abcd') @@ -15,7 +15,7 @@ def test_create_vpn_connections(): vpn_connection.id.should.match(r'vpn-\w+') vpn_connection.type.should.equal('ipsec.1') -@mock_ec2 +@mock_ec2_deprecated def test_delete_vpn_connections(): conn = boto.connect_vpc('the_key', 'the_secret') vpn_connection = conn.create_vpn_connection('ipsec.1', 'vgw-0123abcd', 'cgw-0123abcd') @@ -25,13 +25,13 @@ def test_delete_vpn_connections(): list_of_vpn_connections = conn.get_all_vpn_connections() list_of_vpn_connections.should.have.length_of(0) -@mock_ec2 +@mock_ec2_deprecated def test_delete_vpn_connections_bad_id(): conn = boto.connect_vpc('the_key', 'the_secret') with assert_raises(EC2ResponseError): conn.delete_vpn_connection('vpn-0123abcd') -@mock_ec2 +@mock_ec2_deprecated def test_describe_vpn_connections(): conn = boto.connect_vpc('the_key', 'the_secret') list_of_vpn_connections = conn.get_all_vpn_connections() diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 4f7687941..fa13fc23b 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -18,9 +18,9 @@ from boto.ec2.elb.policies import ( from boto.exception import BotoServerError import sure # noqa -from moto import mock_elb, mock_ec2 +from moto import mock_elb, mock_ec2, mock_elb_deprecated, mock_ec2_deprecated -@mock_elb +@mock_elb_deprecated def test_create_load_balancer(): conn = boto.connect_elb() @@ -43,13 +43,13 @@ def test_create_load_balancer(): listener2.protocol.should.equal("TCP") -@mock_elb +@mock_elb_deprecated def test_getting_missing_elb(): conn = boto.connect_elb() conn.get_all_load_balancers.when.called_with(load_balancer_names='aaa').should.throw(BotoServerError) -@mock_elb +@mock_elb_deprecated def test_create_elb_in_multiple_region(): zones = ['us-east-1a', 'us-east-1b'] ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -63,7 +63,7 @@ def test_create_elb_in_multiple_region(): list(west1_conn.get_all_load_balancers()).should.have.length_of(1) list(west2_conn.get_all_load_balancers()).should.have.length_of(1) -@mock_elb +@mock_elb_deprecated def test_create_load_balancer_with_certificate(): conn = boto.connect_elb() @@ -99,7 +99,7 @@ def test_create_and_delete_boto3_support(): ) list(client.describe_load_balancers()['LoadBalancerDescriptions']).should.have.length_of(0) -@mock_elb +@mock_elb_deprecated def test_add_listener(): conn = boto.connect_elb() zones = ['us-east-1a', 'us-east-1b'] @@ -119,7 +119,7 @@ def test_add_listener(): listener2.protocol.should.equal("TCP") -@mock_elb +@mock_elb_deprecated def test_delete_listener(): conn = boto.connect_elb() @@ -161,7 +161,7 @@ def test_create_and_delete_listener_boto3_support(): balancer['ListenerDescriptions'][1]['Listener']['InstancePort'].should.equal(8443) -@mock_elb +@mock_elb_deprecated def test_set_sslcertificate(): conn = boto.connect_elb() @@ -178,7 +178,7 @@ def test_set_sslcertificate(): listener1.ssl_certificate_id.should.equal("arn:certificate") -@mock_elb +@mock_elb_deprecated def test_get_load_balancers_by_name(): conn = boto.connect_elb() @@ -193,7 +193,7 @@ def test_get_load_balancers_by_name(): conn.get_all_load_balancers(load_balancer_names=['my-lb1', 'my-lb2']).should.have.length_of(2) -@mock_elb +@mock_elb_deprecated def test_delete_load_balancer(): conn = boto.connect_elb() @@ -209,7 +209,7 @@ def test_delete_load_balancer(): balancers.should.have.length_of(0) -@mock_elb +@mock_elb_deprecated def test_create_health_check(): conn = boto.connect_elb() @@ -262,8 +262,8 @@ def test_create_health_check_boto3(): balancer['HealthCheck']['UnhealthyThreshold'].should.equal(5) -@mock_ec2 -@mock_elb +@mock_ec2_deprecated +@mock_elb_deprecated def test_register_instances(): ec2_conn = boto.connect_ec2() reservation = ec2_conn.run_instances('ami-1234abcd', 2) @@ -307,8 +307,8 @@ def test_register_instances_boto3(): set(instance_ids).should.equal(set([instance_id1, instance_id2])) -@mock_ec2 -@mock_elb +@mock_ec2_deprecated +@mock_elb_deprecated def test_deregister_instances(): ec2_conn = boto.connect_ec2() reservation = ec2_conn.run_instances('ami-1234abcd', 2) @@ -365,7 +365,7 @@ def test_deregister_instances_boto3(): balancer['Instances'][0]['InstanceId'].should.equal(instance_id2) -@mock_elb +@mock_elb_deprecated def test_default_attributes(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -378,7 +378,7 @@ def test_default_attributes(): attributes.connecting_settings.idle_timeout.should.equal(60) -@mock_elb +@mock_elb_deprecated def test_cross_zone_load_balancing_attribute(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -393,7 +393,7 @@ def test_cross_zone_load_balancing_attribute(): attributes.cross_zone_load_balancing.enabled.should.be.false -@mock_elb +@mock_elb_deprecated def test_connection_draining_attribute(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -419,7 +419,7 @@ def test_connection_draining_attribute(): attributes.connection_draining.enabled.should.be.false -@mock_elb +@mock_elb_deprecated def test_access_log_attribute(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -444,7 +444,7 @@ def test_access_log_attribute(): attributes.access_log.enabled.should.be.false -@mock_elb +@mock_elb_deprecated def test_connection_settings_attribute(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -462,7 +462,7 @@ def test_connection_settings_attribute(): attributes = lb.get_attributes(force=True) attributes.connecting_settings.idle_timeout.should.equal(60) -@mock_elb +@mock_elb_deprecated def test_create_lb_cookie_stickiness_policy(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -482,7 +482,7 @@ def test_create_lb_cookie_stickiness_policy(): int(cookie_expiration_period_response_str).should.equal(cookie_expiration_period) lb.policies.lb_cookie_stickiness_policies[0].policy_name.should.equal(policy_name) -@mock_elb +@mock_elb_deprecated def test_create_lb_cookie_stickiness_policy_no_expiry(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -495,7 +495,7 @@ def test_create_lb_cookie_stickiness_policy_no_expiry(): lb.policies.lb_cookie_stickiness_policies[0].cookie_expiration_period.should.be.none lb.policies.lb_cookie_stickiness_policies[0].policy_name.should.equal(policy_name) -@mock_elb +@mock_elb_deprecated def test_create_app_cookie_stickiness_policy(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -509,7 +509,7 @@ def test_create_app_cookie_stickiness_policy(): lb.policies.app_cookie_stickiness_policies[0].cookie_name.should.equal(cookie_name) lb.policies.app_cookie_stickiness_policies[0].policy_name.should.equal(policy_name) -@mock_elb +@mock_elb_deprecated def test_create_lb_policy(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -521,7 +521,7 @@ def test_create_lb_policy(): lb = conn.get_all_load_balancers()[0] lb.policies.other_policies[0].policy_name.should.equal(policy_name) -@mock_elb +@mock_elb_deprecated def test_set_policies_of_listener(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -543,7 +543,7 @@ def test_set_policies_of_listener(): # by contrast to a backend, a listener stores only policy name strings listener.policy_names[0].should.equal(policy_name) -@mock_elb +@mock_elb_deprecated def test_set_policies_of_backend_server(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] @@ -562,8 +562,8 @@ def test_set_policies_of_backend_server(): # by contrast to a listener, a backend stores OtherPolicy objects backend.policies[0].policy_name.should.equal(policy_name) -@mock_ec2 -@mock_elb +@mock_ec2_deprecated +@mock_elb_deprecated def test_describe_instance_health(): ec2_conn = boto.connect_ec2() reservation = ec2_conn.run_instances('ami-1234abcd', 2) @@ -765,7 +765,7 @@ def test_subnets(): lb.should.have.key('VPCId').which.should.equal(vpc.id) -@mock_elb +@mock_elb_deprecated def test_create_load_balancer_duplicate(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] diff --git a/tests/test_emr/test_emr.py b/tests/test_emr/test_emr.py index 71b3b8ec5..a24aa4bd4 100644 --- a/tests/test_emr/test_emr.py +++ b/tests/test_emr/test_emr.py @@ -11,7 +11,7 @@ from boto.emr.step import StreamingStep import six import sure # noqa -from moto import mock_emr +from moto import mock_emr_deprecated from tests.helpers import requires_boto_gte @@ -35,7 +35,7 @@ input_instance_groups = [ ] -@mock_emr +@mock_emr_deprecated def test_describe_cluster(): conn = boto.connect_emr() args = run_jobflow_args.copy() @@ -106,7 +106,7 @@ def test_describe_cluster(): cluster.visibletoallusers.should.equal('true') -@mock_emr +@mock_emr_deprecated def test_describe_jobflows(): conn = boto.connect_emr() args = run_jobflow_args.copy() @@ -158,7 +158,7 @@ def test_describe_jobflows(): resp.should.have.length_of(200) -@mock_emr +@mock_emr_deprecated def test_describe_jobflow(): conn = boto.connect_emr() args = run_jobflow_args.copy() @@ -241,7 +241,7 @@ def test_describe_jobflow(): jf.visibletoallusers.should.equal('true') -@mock_emr +@mock_emr_deprecated def test_list_clusters(): conn = boto.connect_emr() args = run_jobflow_args.copy() @@ -309,7 +309,7 @@ def test_list_clusters(): resp.clusters.should.have.length_of(30) -@mock_emr +@mock_emr_deprecated def test_run_jobflow(): conn = boto.connect_emr() args = run_jobflow_args.copy() @@ -326,7 +326,7 @@ def test_run_jobflow(): job_flow.steps.should.have.length_of(0) -@mock_emr +@mock_emr_deprecated def test_run_jobflow_in_multiple_regions(): regions = {} for region in ['us-east-1', 'eu-west-1']: @@ -343,7 +343,7 @@ def test_run_jobflow_in_multiple_regions(): @requires_boto_gte("2.8") -@mock_emr +@mock_emr_deprecated def test_run_jobflow_with_new_params(): # Test that run_jobflow works with newer params conn = boto.connect_emr() @@ -351,7 +351,7 @@ def test_run_jobflow_with_new_params(): @requires_boto_gte("2.8") -@mock_emr +@mock_emr_deprecated def test_run_jobflow_with_visible_to_all_users(): conn = boto.connect_emr() for expected in (True, False): @@ -364,7 +364,7 @@ def test_run_jobflow_with_visible_to_all_users(): @requires_boto_gte("2.8") -@mock_emr +@mock_emr_deprecated def test_run_jobflow_with_instance_groups(): input_groups = dict((g.name, g) for g in input_instance_groups) conn = boto.connect_emr() @@ -384,7 +384,7 @@ def test_run_jobflow_with_instance_groups(): @requires_boto_gte("2.8") -@mock_emr +@mock_emr_deprecated def test_set_termination_protection(): conn = boto.connect_emr() job_id = conn.run_jobflow(**run_jobflow_args) @@ -401,7 +401,7 @@ def test_set_termination_protection(): @requires_boto_gte("2.8") -@mock_emr +@mock_emr_deprecated def test_set_visible_to_all_users(): conn = boto.connect_emr() args = run_jobflow_args.copy() @@ -419,7 +419,7 @@ def test_set_visible_to_all_users(): job_flow.visibletoallusers.should.equal('false') -@mock_emr +@mock_emr_deprecated def test_terminate_jobflow(): conn = boto.connect_emr() job_id = conn.run_jobflow(**run_jobflow_args) @@ -433,7 +433,7 @@ def test_terminate_jobflow(): # testing multiple end points for each feature -@mock_emr +@mock_emr_deprecated def test_bootstrap_actions(): bootstrap_actions = [ BootstrapAction( @@ -466,7 +466,7 @@ def test_bootstrap_actions(): list(arg.value for arg in x.args).should.equal(y.args()) -@mock_emr +@mock_emr_deprecated def test_instance_groups(): input_groups = dict((g.name, g) for g in input_instance_groups) @@ -536,7 +536,7 @@ def test_instance_groups(): int(igs['task-2'].instancerunningcount).should.equal(3) -@mock_emr +@mock_emr_deprecated def test_steps(): input_steps = [ StreamingStep( @@ -633,7 +633,7 @@ def test_steps(): test_list_steps_with_states() -@mock_emr +@mock_emr_deprecated def test_tags(): input_tags = {"tag1": "val1", "tag2": "val2"} diff --git a/tests/test_glacier/test_glacier_archives.py b/tests/test_glacier/test_glacier_archives.py index 6a139a91c..e8fa6045e 100644 --- a/tests/test_glacier/test_glacier_archives.py +++ b/tests/test_glacier/test_glacier_archives.py @@ -4,10 +4,10 @@ from tempfile import NamedTemporaryFile import boto.glacier import sure # noqa -from moto import mock_glacier +from moto import mock_glacier_deprecated -@mock_glacier +@mock_glacier_deprecated def test_create_and_delete_archive(): the_file = NamedTemporaryFile(delete=False) the_file.write(b"some stuff") diff --git a/tests/test_glacier/test_glacier_jobs.py b/tests/test_glacier/test_glacier_jobs.py index 7eff3566e..ef4a00b75 100644 --- a/tests/test_glacier/test_glacier_jobs.py +++ b/tests/test_glacier/test_glacier_jobs.py @@ -5,10 +5,10 @@ import json from boto.glacier.layer1 import Layer1 import sure # noqa -from moto import mock_glacier +from moto import mock_glacier_deprecated -@mock_glacier +@mock_glacier_deprecated def test_init_glacier_job(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" @@ -23,7 +23,7 @@ def test_init_glacier_job(): job_response['Location'].should.equal("//vaults/my_vault/jobs/{0}".format(job_id)) -@mock_glacier +@mock_glacier_deprecated def test_describe_job(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" @@ -56,7 +56,7 @@ def test_describe_job(): }) -@mock_glacier +@mock_glacier_deprecated def test_list_glacier_jobs(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" @@ -77,7 +77,7 @@ def test_list_glacier_jobs(): len(jobs['JobList']).should.equal(2) -@mock_glacier +@mock_glacier_deprecated def test_get_job_output(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" diff --git a/tests/test_glacier/test_glacier_vaults.py b/tests/test_glacier/test_glacier_vaults.py index 40f20e58e..e64f40a90 100644 --- a/tests/test_glacier/test_glacier_vaults.py +++ b/tests/test_glacier/test_glacier_vaults.py @@ -3,10 +3,10 @@ from __future__ import unicode_literals import boto.glacier import sure # noqa -from moto import mock_glacier +from moto import mock_glacier_deprecated -@mock_glacier +@mock_glacier_deprecated def test_create_vault(): conn = boto.glacier.connect_to_region("us-west-2") @@ -17,7 +17,7 @@ def test_create_vault(): vaults[0].name.should.equal("my_vault") -@mock_glacier +@mock_glacier_deprecated def test_delete_vault(): conn = boto.glacier.connect_to_region("us-west-2") diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index de8f89a59..a51240b2f 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -6,7 +6,7 @@ import boto3 import sure # noqa from boto.exception import BotoServerError from botocore.exceptions import ClientError -from moto import mock_iam +from moto import mock_iam, mock_iam_deprecated from moto.iam.models import aws_managed_policies from nose.tools import assert_raises, assert_equals, assert_not_equals from nose.tools import raises @@ -14,7 +14,7 @@ from nose.tools import raises from tests.helpers import requires_boto_gte -@mock_iam() +@mock_iam_deprecated() def test_get_all_server_certs(): conn = boto.connect_iam() @@ -26,7 +26,7 @@ def test_get_all_server_certs(): cert1.arn.should.equal("arn:aws:iam::123456789012:server-certificate/certname") -@mock_iam() +@mock_iam_deprecated() def test_get_server_cert_doesnt_exist(): conn = boto.connect_iam() @@ -34,7 +34,7 @@ def test_get_server_cert_doesnt_exist(): conn.get_server_certificate("NonExistant") -@mock_iam() +@mock_iam_deprecated() def test_get_server_cert(): conn = boto.connect_iam() @@ -44,7 +44,7 @@ def test_get_server_cert(): cert.arn.should.equal("arn:aws:iam::123456789012:server-certificate/certname") -@mock_iam() +@mock_iam_deprecated() def test_upload_server_cert(): conn = boto.connect_iam() @@ -54,7 +54,7 @@ def test_upload_server_cert(): cert.arn.should.equal("arn:aws:iam::123456789012:server-certificate/certname") -@mock_iam() +@mock_iam_deprecated() @raises(BotoServerError) def test_get_role__should_throw__when_role_does_not_exist(): conn = boto.connect_iam() @@ -62,7 +62,7 @@ def test_get_role__should_throw__when_role_does_not_exist(): conn.get_role('unexisting_role') -@mock_iam() +@mock_iam_deprecated() @raises(BotoServerError) def test_get_instance_profile__should_throw__when_instance_profile_does_not_exist(): conn = boto.connect_iam() @@ -70,7 +70,7 @@ def test_get_instance_profile__should_throw__when_instance_profile_does_not_exis conn.get_instance_profile('unexisting_instance_profile') -@mock_iam() +@mock_iam_deprecated() def test_create_role_and_instance_profile(): conn = boto.connect_iam() conn.create_instance_profile("my-profile", path="my-path") @@ -91,7 +91,7 @@ def test_create_role_and_instance_profile(): conn.list_roles().roles[0].role_name.should.equal('my-role') -@mock_iam() +@mock_iam_deprecated() def test_remove_role_from_instance_profile(): conn = boto.connect_iam() conn.create_instance_profile("my-profile", path="my-path") @@ -108,7 +108,7 @@ def test_remove_role_from_instance_profile(): dict(profile.roles).should.be.empty -@mock_iam() +@mock_iam_deprecated() def test_list_instance_profiles(): conn = boto.connect_iam() conn.create_instance_profile("my-profile", path="my-path") @@ -123,7 +123,7 @@ def test_list_instance_profiles(): profiles[0].roles.role_name.should.equal("my-role") -@mock_iam() +@mock_iam_deprecated() def test_list_instance_profiles_for_role(): conn = boto.connect_iam() @@ -153,7 +153,7 @@ def test_list_instance_profiles_for_role(): len(profile_list).should.equal(0) -@mock_iam() +@mock_iam_deprecated() def test_list_role_policies(): conn = boto.connect_iam() conn.create_role("my-role") @@ -162,7 +162,7 @@ def test_list_role_policies(): role.policy_names[0].should.equal("test policy") -@mock_iam() +@mock_iam_deprecated() def test_put_role_policy(): conn = boto.connect_iam() conn.create_role("my-role", assume_role_policy_document="some policy", path="my-path") @@ -171,7 +171,7 @@ def test_put_role_policy(): policy.should.equal("test policy") -@mock_iam() +@mock_iam_deprecated() def test_update_assume_role_policy(): conn = boto.connect_iam() role = conn.create_role("my-role") @@ -180,7 +180,7 @@ def test_update_assume_role_policy(): role.assume_role_policy_document.should.equal("my-policy") -@mock_iam() +@mock_iam_deprecated() def test_create_user(): conn = boto.connect_iam() conn.create_user('my-user') @@ -188,7 +188,7 @@ def test_create_user(): conn.create_user('my-user') -@mock_iam() +@mock_iam_deprecated() def test_get_user(): conn = boto.connect_iam() with assert_raises(BotoServerError): @@ -210,7 +210,7 @@ def test_list_users(): user['Arn'].should.equal('arn:aws:iam::123456789012:user/my-user') -@mock_iam() +@mock_iam_deprecated() def test_create_login_profile(): conn = boto.connect_iam() with assert_raises(BotoServerError): @@ -221,7 +221,7 @@ def test_create_login_profile(): conn.create_login_profile('my-user', 'my-pass') -@mock_iam() +@mock_iam_deprecated() def test_delete_login_profile(): conn = boto.connect_iam() conn.create_user('my-user') @@ -231,7 +231,7 @@ def test_delete_login_profile(): conn.delete_login_profile('my-user') -@mock_iam() +@mock_iam_deprecated() def test_create_access_key(): conn = boto.connect_iam() with assert_raises(BotoServerError): @@ -240,7 +240,7 @@ def test_create_access_key(): conn.create_access_key('my-user') -@mock_iam() +@mock_iam_deprecated() def test_get_all_access_keys(): conn = boto.connect_iam() conn.create_user('my-user') @@ -257,7 +257,7 @@ def test_get_all_access_keys(): ) -@mock_iam() +@mock_iam_deprecated() def test_delete_access_key(): conn = boto.connect_iam() conn.create_user('my-user') @@ -265,7 +265,7 @@ def test_delete_access_key(): conn.delete_access_key(access_key_id, 'my-user') -@mock_iam() +@mock_iam_deprecated() def test_delete_user(): conn = boto.connect_iam() with assert_raises(BotoServerError): @@ -274,7 +274,7 @@ def test_delete_user(): conn.delete_user('my-user') -@mock_iam() +@mock_iam_deprecated() def test_generate_credential_report(): conn = boto.connect_iam() result = conn.generate_credential_report() @@ -283,7 +283,7 @@ def test_generate_credential_report(): result['generate_credential_report_response']['generate_credential_report_result']['state'].should.equal('COMPLETE') -@mock_iam() +@mock_iam_deprecated() def test_get_credential_report(): conn = boto.connect_iam() conn.create_user('my-user') @@ -298,7 +298,7 @@ def test_get_credential_report(): @requires_boto_gte('2.39') -@mock_iam() +@mock_iam_deprecated() def test_managed_policy(): conn = boto.connect_iam() diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index 412484a70..6fd0f47dd 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -4,10 +4,10 @@ import sure # noqa from nose.tools import assert_raises from boto.exception import BotoServerError -from moto import mock_iam +from moto import mock_iam, mock_iam_deprecated -@mock_iam() +@mock_iam_deprecated() def test_create_group(): conn = boto.connect_iam() conn.create_group('my-group') @@ -15,7 +15,7 @@ def test_create_group(): conn.create_group('my-group') -@mock_iam() +@mock_iam_deprecated() def test_get_group(): conn = boto.connect_iam() conn.create_group('my-group') @@ -24,7 +24,7 @@ def test_get_group(): conn.get_group('not-group') -@mock_iam() +@mock_iam_deprecated() def test_get_all_groups(): conn = boto.connect_iam() conn.create_group('my-group1') @@ -33,7 +33,7 @@ def test_get_all_groups(): groups.should.have.length_of(2) -@mock_iam() +@mock_iam_deprecated() def test_add_user_to_group(): conn = boto.connect_iam() with assert_raises(BotoServerError): @@ -45,7 +45,7 @@ def test_add_user_to_group(): conn.add_user_to_group('my-group', 'my-user') -@mock_iam() +@mock_iam_deprecated() def test_remove_user_from_group(): conn = boto.connect_iam() with assert_raises(BotoServerError): @@ -58,7 +58,7 @@ def test_remove_user_from_group(): conn.remove_user_from_group('my-group', 'my-user') -@mock_iam() +@mock_iam_deprecated() def test_get_groups_for_user(): conn = boto.connect_iam() conn.create_group('my-group1') diff --git a/tests/test_kinesis/test_kinesis.py b/tests/test_kinesis/test_kinesis.py index 0e4f29625..a86bce44c 100644 --- a/tests/test_kinesis/test_kinesis.py +++ b/tests/test_kinesis/test_kinesis.py @@ -4,10 +4,10 @@ import boto.kinesis from boto.kinesis.exceptions import ResourceNotFoundException, InvalidArgumentException import sure # noqa -from moto import mock_kinesis +from moto import mock_kinesis_deprecated -@mock_kinesis +@mock_kinesis_deprecated def test_create_cluster(): conn = boto.kinesis.connect_to_region("us-west-2") @@ -25,13 +25,13 @@ def test_create_cluster(): shards.should.have.length_of(2) -@mock_kinesis +@mock_kinesis_deprecated def test_describe_non_existant_stream(): conn = boto.kinesis.connect_to_region("us-east-1") conn.describe_stream.when.called_with("not-a-stream").should.throw(ResourceNotFoundException) -@mock_kinesis +@mock_kinesis_deprecated def test_list_and_delete_stream(): conn = boto.kinesis.connect_to_region("us-west-2") @@ -48,7 +48,7 @@ def test_list_and_delete_stream(): conn.delete_stream.when.called_with("not-a-stream").should.throw(ResourceNotFoundException) -@mock_kinesis +@mock_kinesis_deprecated def test_basic_shard_iterator(): conn = boto.kinesis.connect_to_region("us-west-2") @@ -66,7 +66,7 @@ def test_basic_shard_iterator(): response['Records'].should.equal([]) -@mock_kinesis +@mock_kinesis_deprecated def test_get_invalid_shard_iterator(): conn = boto.kinesis.connect_to_region("us-west-2") @@ -76,7 +76,7 @@ def test_get_invalid_shard_iterator(): conn.get_shard_iterator.when.called_with(stream_name, "123", 'TRIM_HORIZON').should.throw(ResourceNotFoundException) -@mock_kinesis +@mock_kinesis_deprecated def test_put_records(): conn = boto.kinesis.connect_to_region("us-west-2") @@ -107,7 +107,7 @@ def test_put_records(): record["SequenceNumber"].should.equal("1") -@mock_kinesis +@mock_kinesis_deprecated def test_get_records_limit(): conn = boto.kinesis.connect_to_region("us-west-2") @@ -136,7 +136,7 @@ def test_get_records_limit(): response['Records'].should.have.length_of(2) -@mock_kinesis +@mock_kinesis_deprecated def test_get_records_at_sequence_number(): # AT_SEQUENCE_NUMBER - Start reading exactly from the position denoted by a specific sequence number. conn = boto.kinesis.connect_to_region("us-west-2") @@ -167,7 +167,7 @@ def test_get_records_at_sequence_number(): response['Records'][0]['Data'].should.equal('2') -@mock_kinesis +@mock_kinesis_deprecated def test_get_records_after_sequence_number(): # AFTER_SEQUENCE_NUMBER - Start reading right after the position denoted by a specific sequence number. conn = boto.kinesis.connect_to_region("us-west-2") @@ -197,7 +197,7 @@ def test_get_records_after_sequence_number(): response['Records'][0]['Data'].should.equal('3') -@mock_kinesis +@mock_kinesis_deprecated def test_get_records_latest(): # LATEST - Start reading just after the most recent record in the shard, so that you always read the most recent data in the shard. conn = boto.kinesis.connect_to_region("us-west-2") @@ -232,7 +232,7 @@ def test_get_records_latest(): response['Records'][0]['Data'].should.equal('last_record') -@mock_kinesis +@mock_kinesis_deprecated def test_invalid_shard_iterator_type(): conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" @@ -244,7 +244,7 @@ def test_invalid_shard_iterator_type(): stream_name, shard_id, 'invalid-type').should.throw(InvalidArgumentException) -@mock_kinesis +@mock_kinesis_deprecated def test_add_tags(): conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" @@ -257,7 +257,7 @@ def test_add_tags(): conn.add_tags_to_stream(stream_name, {'tag2':'val4'}) -@mock_kinesis +@mock_kinesis_deprecated def test_list_tags(): conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" @@ -278,7 +278,7 @@ def test_list_tags(): tags.get('tag2').should.equal('val4') -@mock_kinesis +@mock_kinesis_deprecated def test_remove_tags(): conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" @@ -300,7 +300,7 @@ def test_remove_tags(): tags.get('tag2').should.equal(None) -@mock_kinesis +@mock_kinesis_deprecated def test_split_shard(): conn = boto.kinesis.connect_to_region("us-west-2") stream_name = 'my_stream' @@ -341,7 +341,7 @@ def test_split_shard(): sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) -@mock_kinesis +@mock_kinesis_deprecated def test_merge_shards(): conn = boto.kinesis.connect_to_region("us-west-2") stream_name = 'my_stream' diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index 04e6fbb4b..27850d4ad 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -5,10 +5,10 @@ import boto.kms from boto.exception import JSONResponseError from boto.kms.exceptions import AlreadyExistsException, NotFoundException import sure # noqa -from moto import mock_kms +from moto import mock_kms_deprecated from nose.tools import assert_raises -@mock_kms +@mock_kms_deprecated def test_create_key(): conn = boto.kms.connect_to_region("us-west-2") @@ -19,7 +19,7 @@ def test_create_key(): key['KeyMetadata']['Enabled'].should.equal(True) -@mock_kms +@mock_kms_deprecated def test_describe_key(): conn = boto.kms.connect_to_region("us-west-2") key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') @@ -30,7 +30,7 @@ def test_describe_key(): key['KeyMetadata']['KeyUsage'].should.equal("ENCRYPT_DECRYPT") -@mock_kms +@mock_kms_deprecated def test_describe_key_via_alias(): conn = boto.kms.connect_to_region("us-west-2") key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') @@ -42,7 +42,7 @@ def test_describe_key_via_alias(): alias_key['KeyMetadata']['Arn'].should.equal(key['KeyMetadata']['Arn']) -@mock_kms +@mock_kms_deprecated def test_describe_key_via_alias_not_found(): conn = boto.kms.connect_to_region("us-west-2") key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') @@ -51,7 +51,7 @@ def test_describe_key_via_alias_not_found(): conn.describe_key.when.called_with('alias/not-found-alias').should.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test_describe_key_via_arn(): conn = boto.kms.connect_to_region("us-west-2") key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') @@ -63,13 +63,13 @@ def test_describe_key_via_arn(): the_key['KeyMetadata']['KeyId'].should.equal(key['KeyMetadata']['KeyId']) -@mock_kms +@mock_kms_deprecated def test_describe_missing_key(): conn = boto.kms.connect_to_region("us-west-2") conn.describe_key.when.called_with("not-a-key").should.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test_list_keys(): conn = boto.kms.connect_to_region("us-west-2") @@ -80,7 +80,7 @@ def test_list_keys(): keys['Keys'].should.have.length_of(2) -@mock_kms +@mock_kms_deprecated def test_enable_key_rotation(): conn = boto.kms.connect_to_region("us-west-2") @@ -91,7 +91,7 @@ def test_enable_key_rotation(): conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(True) -@mock_kms +@mock_kms_deprecated def test_enable_key_rotation_via_arn(): conn = boto.kms.connect_to_region("us-west-2") @@ -104,13 +104,13 @@ def test_enable_key_rotation_via_arn(): -@mock_kms +@mock_kms_deprecated def test_enable_key_rotation_with_missing_key(): conn = boto.kms.connect_to_region("us-west-2") conn.enable_key_rotation.when.called_with("not-a-key").should.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test_enable_key_rotation_with_alias_name_should_fail(): conn = boto.kms.connect_to_region("us-west-2") key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') @@ -122,7 +122,7 @@ def test_enable_key_rotation_with_alias_name_should_fail(): conn.enable_key_rotation.when.called_with('alias/my-alias').should.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test_disable_key_rotation(): conn = boto.kms.connect_to_region("us-west-2") @@ -136,7 +136,7 @@ def test_disable_key_rotation(): conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) -@mock_kms +@mock_kms_deprecated def test_encrypt(): """ test_encrypt @@ -147,26 +147,26 @@ def test_encrypt(): response['CiphertextBlob'].should.equal(b'ZW5jcnlwdG1l') -@mock_kms +@mock_kms_deprecated def test_decrypt(): conn = boto.kms.connect_to_region('us-west-2') response = conn.decrypt('ZW5jcnlwdG1l'.encode('utf-8')) response['Plaintext'].should.equal(b'encryptme') -@mock_kms +@mock_kms_deprecated def test_disable_key_rotation_with_missing_key(): conn = boto.kms.connect_to_region("us-west-2") conn.disable_key_rotation.when.called_with("not-a-key").should.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test_get_key_rotation_status_with_missing_key(): conn = boto.kms.connect_to_region("us-west-2") conn.get_key_rotation_status.when.called_with("not-a-key").should.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test_get_key_rotation_status(): conn = boto.kms.connect_to_region("us-west-2") @@ -176,7 +176,7 @@ def test_get_key_rotation_status(): conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) -@mock_kms +@mock_kms_deprecated def test_create_key_defaults_key_rotation(): conn = boto.kms.connect_to_region("us-west-2") @@ -186,7 +186,7 @@ def test_create_key_defaults_key_rotation(): conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) -@mock_kms +@mock_kms_deprecated def test_get_key_policy(): conn = boto.kms.connect_to_region('us-west-2') @@ -196,7 +196,7 @@ def test_get_key_policy(): policy = conn.get_key_policy(key_id, 'default') policy['Policy'].should.equal('my policy') -@mock_kms +@mock_kms_deprecated def test_get_key_policy_via_arn(): conn = boto.kms.connect_to_region('us-west-2') @@ -205,7 +205,7 @@ def test_get_key_policy_via_arn(): policy['Policy'].should.equal('my policy') -@mock_kms +@mock_kms_deprecated def test_put_key_policy(): conn = boto.kms.connect_to_region('us-west-2') @@ -217,7 +217,7 @@ def test_put_key_policy(): policy['Policy'].should.equal('new policy') -@mock_kms +@mock_kms_deprecated def test_put_key_policy_via_arn(): conn = boto.kms.connect_to_region('us-west-2') @@ -229,7 +229,7 @@ def test_put_key_policy_via_arn(): policy['Policy'].should.equal('new policy') -@mock_kms +@mock_kms_deprecated def test_put_key_policy_via_alias_should_not_update(): conn = boto.kms.connect_to_region('us-west-2') @@ -242,7 +242,7 @@ def test_put_key_policy_via_alias_should_not_update(): policy['Policy'].should.equal('my policy') -@mock_kms +@mock_kms_deprecated def test_put_key_policy(): conn = boto.kms.connect_to_region('us-west-2') @@ -253,7 +253,7 @@ def test_put_key_policy(): policy['Policy'].should.equal('new policy') -@mock_kms +@mock_kms_deprecated def test_list_key_policies(): conn = boto.kms.connect_to_region('us-west-2') @@ -264,7 +264,7 @@ def test_list_key_policies(): policies['PolicyNames'].should.equal(['default']) -@mock_kms +@mock_kms_deprecated def test__create_alias__returns_none_if_correct(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -275,7 +275,7 @@ def test__create_alias__returns_none_if_correct(): resp.should.be.none -@mock_kms +@mock_kms_deprecated def test__create_alias__raises_if_reserved_alias(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -300,7 +300,7 @@ def test__create_alias__raises_if_reserved_alias(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__create_alias__can_create_multiple_aliases_for_same_key_id(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -311,7 +311,7 @@ def test__create_alias__can_create_multiple_aliases_for_same_key_id(): kms.create_alias('alias/my-alias5', key_id).should.be.none -@mock_kms +@mock_kms_deprecated def test__create_alias__raises_if_wrong_prefix(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -328,7 +328,7 @@ def test__create_alias__raises_if_wrong_prefix(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__create_alias__raises_if_duplicate(): region = 'us-west-2' kms = boto.kms.connect_to_region(region) @@ -354,7 +354,7 @@ def test__create_alias__raises_if_duplicate(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__create_alias__raises_if_alias_has_restricted_characters(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -378,7 +378,7 @@ def test__create_alias__raises_if_alias_has_restricted_characters(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__create_alias__raises_if_alias_has_colon_character(): # For some reason, colons are not accepted for an alias, even though they are accepted by regex ^[a-zA-Z0-9:/_-]+$ kms = boto.connect_kms() @@ -401,7 +401,7 @@ def test__create_alias__raises_if_alias_has_colon_character(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__create_alias__accepted_characters(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -416,7 +416,7 @@ def test__create_alias__accepted_characters(): kms.create_alias(alias_name, key_id) -@mock_kms +@mock_kms_deprecated def test__create_alias__raises_if_target_key_id_is_existing_alias(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -437,7 +437,7 @@ def test__create_alias__raises_if_target_key_id_is_existing_alias(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__delete_alias(): kms = boto.connect_kms() create_resp = kms.create_key() @@ -454,7 +454,7 @@ def test__delete_alias(): kms.create_alias(alias, key_id) -@mock_kms +@mock_kms_deprecated def test__delete_alias__raises_if_wrong_prefix(): kms = boto.connect_kms() @@ -470,7 +470,7 @@ def test__delete_alias__raises_if_wrong_prefix(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__delete_alias__raises_if_alias_is_not_found(): region = 'us-west-2' kms = boto.kms.connect_to_region(region) @@ -490,7 +490,7 @@ def test__delete_alias__raises_if_alias_is_not_found(): ex.status.should.equal(400) -@mock_kms +@mock_kms_deprecated def test__list_aliases(): region = "eu-west-1" kms = boto.kms.connect_to_region(region) @@ -532,7 +532,7 @@ def test__list_aliases(): len(aliases).should.equal(7) -@mock_kms +@mock_kms_deprecated def test__assert_valid_key_id(): from moto.kms.responses import _assert_valid_key_id import uuid @@ -541,7 +541,7 @@ def test__assert_valid_key_id(): _assert_valid_key_id.when.called_with(str(uuid.uuid4())).should_not.throw(JSONResponseError) -@mock_kms +@mock_kms_deprecated def test__assert_default_policy(): from moto.kms.responses import _assert_default_policy diff --git a/tests/test_opsworks/test_layers.py b/tests/test_opsworks/test_layers.py index 1392d8c6e..dc268bbe5 100644 --- a/tests/test_opsworks/test_layers.py +++ b/tests/test_opsworks/test_layers.py @@ -66,7 +66,7 @@ def test_describe_layers(): rv1 = client.describe_layers(StackId=stack_id) rv2 = client.describe_layers(LayerIds=[layer_id]) - rv1.should.equal(rv2) + rv1['Layers'].should.equal(rv2['Layers']) rv1['Layers'][0]['Name'].should.equal("TestLayer") diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index 6078b5f6b..7a6cab633 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -5,12 +5,12 @@ import boto.vpc from boto.exception import BotoServerError import sure # noqa -from moto import mock_ec2, mock_rds +from moto import mock_ec2_deprecated, mock_rds_deprecated from tests.helpers import disable_on_py3 @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_create_database(): conn = boto.rds.connect_to_region("us-west-2") @@ -27,7 +27,7 @@ def test_create_database(): @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_get_databases(): conn = boto.rds.connect_to_region("us-west-2") @@ -44,14 +44,14 @@ def test_get_databases(): databases[0].id.should.equal("db-master-1") -@mock_rds +@mock_rds_deprecated def test_describe_non_existant_database(): conn = boto.rds.connect_to_region("us-west-2") conn.get_all_dbinstances.when.called_with("not-a-db").should.throw(BotoServerError) @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_delete_database(): conn = boto.rds.connect_to_region("us-west-2") list(conn.get_all_dbinstances()).should.have.length_of(0) @@ -63,13 +63,13 @@ def test_delete_database(): list(conn.get_all_dbinstances()).should.have.length_of(0) -@mock_rds +@mock_rds_deprecated def test_delete_non_existant_database(): conn = boto.rds.connect_to_region("us-west-2") conn.delete_dbinstance.when.called_with("not-a-db").should.throw(BotoServerError) -@mock_rds +@mock_rds_deprecated def test_create_database_security_group(): conn = boto.rds.connect_to_region("us-west-2") @@ -79,7 +79,7 @@ def test_create_database_security_group(): list(security_group.ip_ranges).should.equal([]) -@mock_rds +@mock_rds_deprecated def test_get_security_groups(): conn = boto.rds.connect_to_region("us-west-2") @@ -96,13 +96,13 @@ def test_get_security_groups(): databases[0].name.should.equal("db_sg1") -@mock_rds +@mock_rds_deprecated def test_get_non_existant_security_group(): conn = boto.rds.connect_to_region("us-west-2") conn.get_all_dbsecurity_groups.when.called_with("not-a-sg").should.throw(BotoServerError) -@mock_rds +@mock_rds_deprecated def test_delete_database_security_group(): conn = boto.rds.connect_to_region("us-west-2") conn.create_dbsecurity_group('db_sg', 'DB Security Group') @@ -113,14 +113,14 @@ def test_delete_database_security_group(): list(conn.get_all_dbsecurity_groups()).should.have.length_of(0) -@mock_rds +@mock_rds_deprecated def test_delete_non_existant_security_group(): conn = boto.rds.connect_to_region("us-west-2") conn.delete_dbsecurity_group.when.called_with("not-a-db").should.throw(BotoServerError) @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_security_group_authorize(): conn = boto.rds.connect_to_region("us-west-2") security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') @@ -133,7 +133,7 @@ def test_security_group_authorize(): @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_add_security_group_to_database(): conn = boto.rds.connect_to_region("us-west-2") @@ -147,8 +147,8 @@ def test_add_security_group_to_database(): database.security_groups[0].name.should.equal("db_sg") -@mock_ec2 -@mock_rds +@mock_ec2_deprecated +@mock_rds_deprecated def test_add_database_subnet_group(): vpc_conn = boto.vpc.connect_to_region("us-west-2") vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -163,8 +163,8 @@ def test_add_database_subnet_group(): list(subnet_group.subnet_ids).should.equal(subnet_ids) -@mock_ec2 -@mock_rds +@mock_ec2_deprecated +@mock_rds_deprecated def test_describe_database_subnet_group(): vpc_conn = boto.vpc.connect_to_region("us-west-2") vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -180,8 +180,8 @@ def test_describe_database_subnet_group(): conn.get_all_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) -@mock_ec2 -@mock_rds +@mock_ec2_deprecated +@mock_rds_deprecated def test_delete_database_subnet_group(): vpc_conn = boto.vpc.connect_to_region("us-west-2") vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -198,8 +198,8 @@ def test_delete_database_subnet_group(): @disable_on_py3() -@mock_ec2 -@mock_rds +@mock_ec2_deprecated +@mock_rds_deprecated def test_create_database_in_subnet_group(): vpc_conn = boto.vpc.connect_to_region("us-west-2") vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -216,7 +216,7 @@ def test_create_database_in_subnet_group(): @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_create_database_replica(): conn = boto.rds.connect_to_region("us-west-2") @@ -239,7 +239,7 @@ def test_create_database_replica(): list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_create_cross_region_database_replica(): west_1_conn = boto.rds.connect_to_region("us-west-1") west_2_conn = boto.rds.connect_to_region("us-west-2") @@ -266,7 +266,7 @@ def test_create_cross_region_database_replica(): @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_connecting_to_us_east_1(): # boto does not use us-east-1 in the URL for RDS, # and that broke moto in the past: @@ -286,7 +286,7 @@ def test_connecting_to_us_east_1(): @disable_on_py3() -@mock_rds +@mock_rds_deprecated def test_create_database_with_iops(): conn = boto.rds.connect_to_region("us-west-2") diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 4e1c2b73c..581209655 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import boto.rds2 -import boto.vpc from botocore.exceptions import ClientError, ParamValidationError import boto3 import sure # noqa diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 700301418..13acf6d7c 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -10,10 +10,10 @@ from boto.redshift.exceptions import ( ) import sure # noqa -from moto import mock_ec2, mock_redshift +from moto import mock_ec2_deprecated, mock_redshift_deprecated -@mock_redshift +@mock_redshift_deprecated def test_create_cluster(): conn = boto.redshift.connect_to_region("us-east-1") cluster_identifier = 'my_cluster' @@ -54,7 +54,7 @@ def test_create_cluster(): cluster['NumberOfNodes'].should.equal(3) -@mock_redshift +@mock_redshift_deprecated def test_create_single_node_cluster(): conn = boto.redshift.connect_to_region("us-east-1") cluster_identifier = 'my_cluster' @@ -78,7 +78,7 @@ def test_create_single_node_cluster(): cluster['NumberOfNodes'].should.equal(1) -@mock_redshift +@mock_redshift_deprecated def test_default_cluster_attibutes(): conn = boto.redshift.connect_to_region("us-east-1") cluster_identifier = 'my_cluster' @@ -105,8 +105,8 @@ def test_default_cluster_attibutes(): cluster['NumberOfNodes'].should.equal(1) -@mock_redshift -@mock_ec2 +@mock_redshift_deprecated +@mock_ec2_deprecated def test_create_cluster_in_subnet_group(): vpc_conn = boto.connect_vpc() vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -131,7 +131,7 @@ def test_create_cluster_in_subnet_group(): cluster['ClusterSubnetGroupName'].should.equal('my_subnet_group') -@mock_redshift +@mock_redshift_deprecated def test_create_cluster_with_security_group(): conn = boto.redshift.connect_to_region("us-east-1") conn.create_cluster_security_group( @@ -158,8 +158,8 @@ def test_create_cluster_with_security_group(): set(group_names).should.equal(set(["security_group1", "security_group2"])) -@mock_redshift -@mock_ec2 +@mock_redshift_deprecated +@mock_ec2_deprecated def test_create_cluster_with_vpc_security_groups(): vpc_conn = boto.connect_vpc() ec2_conn = boto.connect_ec2() @@ -181,7 +181,7 @@ def test_create_cluster_with_vpc_security_groups(): list(group_ids).should.equal([security_group.id]) -@mock_redshift +@mock_redshift_deprecated def test_create_cluster_with_parameter_group(): conn = boto.connect_redshift() conn.create_cluster_parameter_group( @@ -203,13 +203,13 @@ def test_create_cluster_with_parameter_group(): cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("my_parameter_group") -@mock_redshift +@mock_redshift_deprecated def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") conn.describe_clusters.when.called_with("not-a-cluster").should.throw(ClusterNotFound) -@mock_redshift +@mock_redshift_deprecated def test_delete_cluster(): conn = boto.connect_redshift() cluster_identifier = 'my_cluster' @@ -233,7 +233,7 @@ def test_delete_cluster(): conn.delete_cluster.when.called_with("not-a-cluster").should.throw(ClusterNotFound) -@mock_redshift +@mock_redshift_deprecated def test_modify_cluster(): conn = boto.connect_redshift() cluster_identifier = 'my_cluster' @@ -281,8 +281,8 @@ def test_modify_cluster(): cluster['NumberOfNodes'].should.equal(2) -@mock_redshift -@mock_ec2 +@mock_redshift_deprecated +@mock_ec2_deprecated def test_create_cluster_subnet_group(): vpc_conn = boto.connect_vpc() vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -306,8 +306,8 @@ def test_create_cluster_subnet_group(): set(subnet_ids).should.equal(set([subnet1.id, subnet2.id])) -@mock_redshift -@mock_ec2 +@mock_redshift_deprecated +@mock_ec2_deprecated def test_create_invalid_cluster_subnet_group(): redshift_conn = boto.connect_redshift() redshift_conn.create_cluster_subnet_group.when.called_with( @@ -317,14 +317,14 @@ def test_create_invalid_cluster_subnet_group(): ).should.throw(InvalidSubnet) -@mock_redshift +@mock_redshift_deprecated def test_describe_non_existant_subnet_group(): conn = boto.redshift.connect_to_region("us-east-1") conn.describe_cluster_subnet_groups.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) -@mock_redshift -@mock_ec2 +@mock_redshift_deprecated +@mock_ec2_deprecated def test_delete_cluster_subnet_group(): vpc_conn = boto.connect_vpc() vpc = vpc_conn.create_vpc("10.0.0.0/16") @@ -351,7 +351,7 @@ def test_delete_cluster_subnet_group(): redshift_conn.delete_cluster_subnet_group.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) -@mock_redshift +@mock_redshift_deprecated def test_create_cluster_security_group(): conn = boto.connect_redshift() conn.create_cluster_security_group( @@ -367,13 +367,13 @@ def test_create_cluster_security_group(): list(my_group['IPRanges']).should.equal([]) -@mock_redshift +@mock_redshift_deprecated def test_describe_non_existant_security_group(): conn = boto.redshift.connect_to_region("us-east-1") conn.describe_cluster_security_groups.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) -@mock_redshift +@mock_redshift_deprecated def test_delete_cluster_security_group(): conn = boto.connect_redshift() conn.create_cluster_security_group( @@ -395,7 +395,7 @@ def test_delete_cluster_security_group(): conn.delete_cluster_security_group.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) -@mock_redshift +@mock_redshift_deprecated def test_create_cluster_parameter_group(): conn = boto.connect_redshift() conn.create_cluster_parameter_group( @@ -412,13 +412,13 @@ def test_create_cluster_parameter_group(): my_group['Description'].should.equal("This is my parameter group") -@mock_redshift +@mock_redshift_deprecated def test_describe_non_existant_parameter_group(): conn = boto.redshift.connect_to_region("us-east-1") conn.describe_cluster_parameter_groups.when.called_with("not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) -@mock_redshift +@mock_redshift_deprecated def test_delete_cluster_parameter_group(): conn = boto.connect_redshift() conn.create_cluster_parameter_group( diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index b5b2000cc..dd68eec0e 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -9,10 +9,10 @@ import sure # noqa import uuid -from moto import mock_route53 +from moto import mock_route53, mock_route53_deprecated -@mock_route53 +@mock_route53_deprecated def test_hosted_zone(): conn = boto.connect_route53('the_key', 'the_secret') firstzone = conn.create_hosted_zone("testdns.aws.com") @@ -34,7 +34,7 @@ def test_hosted_zone(): conn.get_hosted_zone.when.called_with("abcd").should.throw(boto.route53.exception.DNSServerError, "404 Not Found") -@mock_route53 +@mock_route53_deprecated def test_rrset(): conn = boto.connect_route53('the_key', 'the_secret') @@ -117,7 +117,7 @@ def test_rrset(): rrsets.should.have.length_of(0) -@mock_route53 +@mock_route53_deprecated def test_rrset_with_multiple_values(): conn = boto.connect_route53('the_key', 'the_secret') zone = conn.create_hosted_zone("testdns.aws.com") @@ -134,7 +134,7 @@ def test_rrset_with_multiple_values(): set(rrsets[0].resource_records).should.equal(set(['1.2.3.4', '5.6.7.8'])) -@mock_route53 +@mock_route53_deprecated def test_alias_rrset(): conn = boto.connect_route53('the_key', 'the_secret') zone = conn.create_hosted_zone("testdns.aws.com") @@ -153,7 +153,7 @@ def test_alias_rrset(): rrsets[0].resource_records[0].should.equal('bar.testdns.aws.com') -@mock_route53 +@mock_route53_deprecated def test_create_health_check(): conn = boto.connect_route53('the_key', 'the_secret') @@ -183,7 +183,7 @@ def test_create_health_check(): config['FailureThreshold'].should.equal("2") -@mock_route53 +@mock_route53_deprecated def test_delete_health_check(): conn = boto.connect_route53('the_key', 'the_secret') @@ -204,7 +204,7 @@ def test_delete_health_check(): list(checks).should.have.length_of(0) -@mock_route53 +@mock_route53_deprecated def test_use_health_check_in_resource_record_set(): conn = boto.connect_route53('the_key', 'the_secret') @@ -229,7 +229,7 @@ def test_use_health_check_in_resource_record_set(): record_sets[0].health_check.should.equal(check_id) -@mock_route53 +@mock_route53_deprecated def test_hosted_zone_comment_preserved(): conn = boto.connect_route53('the_key', 'the_secret') @@ -246,7 +246,7 @@ def test_hosted_zone_comment_preserved(): zone.config["Comment"].should.equal("test comment") -@mock_route53 +@mock_route53_deprecated def test_deleting_weighted_route(): conn = boto.connect_route53() @@ -266,7 +266,7 @@ def test_deleting_weighted_route(): cname.identifier.should.equal('success-test-bar') -@mock_route53 +@mock_route53_deprecated def test_deleting_latency_route(): conn = boto.connect_route53() @@ -288,7 +288,7 @@ def test_deleting_latency_route(): cname.region.should.equal('us-west-1') -@mock_route53 +@mock_route53_deprecated def test_hosted_zone_private_zone_preserved(): conn = boto.connect_route53('the_key', 'the_secret') diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 4990d7324..874230737 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -20,7 +20,7 @@ from nose.tools import assert_raises import sure # noqa -from moto import mock_s3 +from moto import mock_s3, mock_s3_deprecated REDUCED_PART_SIZE = 256 @@ -56,7 +56,7 @@ class MyModel(object): k.set_contents_from_string(self.value) -@mock_s3 +@mock_s3_deprecated def test_my_model_save(): # Create Bucket so that test can run conn = boto.connect_s3('the_key', 'the_secret') @@ -69,7 +69,7 @@ def test_my_model_save(): conn.get_bucket('mybucket').get_key('steve').get_contents_as_string().should.equal(b'is awesome') -@mock_s3 +@mock_s3_deprecated def test_key_etag(): # Create Bucket so that test can run conn = boto.connect_s3('the_key', 'the_secret') @@ -83,7 +83,7 @@ def test_key_etag(): '"d32bda93738f7e03adb22e66c90fbc04"') -@mock_s3 +@mock_s3_deprecated def test_multipart_upload_too_small(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -95,7 +95,7 @@ def test_multipart_upload_too_small(): multipart.complete_upload.should.throw(S3ResponseError) -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_upload(): conn = boto.connect_s3('the_key', 'the_secret') @@ -112,7 +112,7 @@ def test_multipart_upload(): bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_upload_out_of_order(): conn = boto.connect_s3('the_key', 'the_secret') @@ -129,7 +129,7 @@ def test_multipart_upload_out_of_order(): bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_upload_with_headers(): conn = boto.connect_s3('the_key', 'the_secret') @@ -144,7 +144,7 @@ def test_multipart_upload_with_headers(): key.metadata.should.equal({"foo": "bar"}) -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_upload_with_copy_key(): conn = boto.connect_s3('the_key', 'the_secret') @@ -161,7 +161,7 @@ def test_multipart_upload_with_copy_key(): bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + b"key_") -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_upload_cancel(): conn = boto.connect_s3('the_key', 'the_secret') @@ -175,7 +175,7 @@ def test_multipart_upload_cancel(): # have the ability to list mulipart uploads for a bucket. -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_etag(): # Create Bucket so that test can run @@ -194,7 +194,7 @@ def test_multipart_etag(): '"66d1a1a2ed08fd05c137f316af4ff255-2"') -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_invalid_order(): # Create Bucket so that test can run @@ -214,7 +214,7 @@ def test_multipart_invalid_order(): multipart.key_name, multipart.id, xml).should.throw(S3ResponseError) -@mock_s3 +@mock_s3_deprecated @reduced_min_part_size def test_multipart_duplicate_upload(): conn = boto.connect_s3('the_key', 'the_secret') @@ -232,7 +232,7 @@ def test_multipart_duplicate_upload(): bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) -@mock_s3 +@mock_s3_deprecated def test_list_multiparts(): # Create Bucket so that test can run conn = boto.connect_s3('the_key', 'the_secret') @@ -253,7 +253,7 @@ def test_list_multiparts(): uploads.should.be.empty -@mock_s3 +@mock_s3_deprecated def test_key_save_to_missing_bucket(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.get_bucket('mybucket', validate=False) @@ -263,14 +263,14 @@ def test_key_save_to_missing_bucket(): key.set_contents_from_string.when.called_with("foobar").should.throw(S3ResponseError) -@mock_s3 +@mock_s3_deprecated def test_missing_key(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") bucket.get_key("the-key").should.equal(None) -@mock_s3 +@mock_s3_deprecated def test_missing_key_urllib2(): conn = boto.connect_s3('the_key', 'the_secret') conn.create_bucket("foobar") @@ -278,7 +278,7 @@ def test_missing_key_urllib2(): urlopen.when.called_with("http://foobar.s3.amazonaws.com/the-key").should.throw(HTTPError) -@mock_s3 +@mock_s3_deprecated def test_empty_key(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -291,7 +291,7 @@ def test_empty_key(): key.get_contents_as_string().should.equal(b'') -@mock_s3 +@mock_s3_deprecated def test_empty_key_set_on_existing_key(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -307,7 +307,7 @@ def test_empty_key_set_on_existing_key(): bucket.get_key("the-key").get_contents_as_string().should.equal(b'') -@mock_s3 +@mock_s3_deprecated def test_large_key_save(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -318,7 +318,7 @@ def test_large_key_save(): bucket.get_key("the-key").get_contents_as_string().should.equal(b'foobar' * 100000) -@mock_s3 +@mock_s3_deprecated def test_copy_key(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -332,7 +332,7 @@ def test_copy_key(): bucket.get_key("new-key").get_contents_as_string().should.equal(b"some value") -@mock_s3 +@mock_s3_deprecated def test_copy_key_with_version(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -348,7 +348,7 @@ def test_copy_key_with_version(): bucket.get_key("new-key").get_contents_as_string().should.equal(b"some value") -@mock_s3 +@mock_s3_deprecated def test_set_metadata(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -360,7 +360,7 @@ def test_set_metadata(): bucket.get_key('the-key').get_metadata('md').should.equal('Metadatastring') -@mock_s3 +@mock_s3_deprecated def test_copy_key_replace_metadata(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -377,7 +377,7 @@ def test_copy_key_replace_metadata(): @freeze_time("2012-01-01 12:00:00") -@mock_s3 +@mock_s3_deprecated def test_last_modified(): # See https://github.com/boto/boto/issues/466 conn = boto.connect_s3() @@ -392,19 +392,19 @@ def test_last_modified(): bucket.get_key("the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') -@mock_s3 +@mock_s3_deprecated def test_missing_bucket(): conn = boto.connect_s3('the_key', 'the_secret') conn.get_bucket.when.called_with('mybucket').should.throw(S3ResponseError) -@mock_s3 +@mock_s3_deprecated def test_bucket_with_dash(): conn = boto.connect_s3('the_key', 'the_secret') conn.get_bucket.when.called_with('mybucket-test').should.throw(S3ResponseError) -@mock_s3 +@mock_s3_deprecated def test_create_existing_bucket(): "Trying to create a bucket that already exists should raise an Error" conn = boto.s3.connect_to_region("us-west-2") @@ -413,7 +413,7 @@ def test_create_existing_bucket(): conn.create_bucket('foobar') -@mock_s3 +@mock_s3_deprecated def test_create_existing_bucket_in_us_east_1(): "Trying to create a bucket that already exists in us-east-1 returns the bucket" @@ -430,14 +430,14 @@ def test_create_existing_bucket_in_us_east_1(): bucket.name.should.equal("foobar") -@mock_s3 +@mock_s3_deprecated def test_other_region(): conn = S3Connection('key', 'secret', host='s3-website-ap-southeast-2.amazonaws.com') conn.create_bucket("foobar") list(conn.get_bucket("foobar").get_all_keys()).should.equal([]) -@mock_s3 +@mock_s3_deprecated def test_bucket_deletion(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -459,7 +459,7 @@ def test_bucket_deletion(): conn.delete_bucket.when.called_with("foobar").should.throw(S3ResponseError) -@mock_s3 +@mock_s3_deprecated def test_get_all_buckets(): conn = boto.connect_s3('the_key', 'the_secret') conn.create_bucket("foobar") @@ -470,6 +470,7 @@ def test_get_all_buckets(): @mock_s3 +@mock_s3_deprecated def test_post_to_bucket(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -483,6 +484,7 @@ def test_post_to_bucket(): @mock_s3 +@mock_s3_deprecated def test_post_with_metadata_to_bucket(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -496,7 +498,7 @@ def test_post_with_metadata_to_bucket(): bucket.get_key('the-key').get_metadata('test').should.equal('metadata') -@mock_s3 +@mock_s3_deprecated def test_delete_missing_key(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket('foobar') @@ -505,7 +507,7 @@ def test_delete_missing_key(): deleted_key.key.should.equal("foobar") -@mock_s3 +@mock_s3_deprecated def test_delete_keys(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket('foobar') @@ -523,7 +525,7 @@ def test_delete_keys(): keys[0].name.should.equal('file1') -@mock_s3 +@mock_s3_deprecated def test_delete_keys_with_invalid(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket('foobar') @@ -552,7 +554,7 @@ def test_key_method_not_implemented(): requests.post.when.called_with("https://foobar.s3.amazonaws.com/foo").should.throw(NotImplementedError) -@mock_s3 +@mock_s3_deprecated def test_bucket_name_with_dot(): conn = boto.connect_s3() bucket = conn.create_bucket('firstname.lastname') @@ -561,7 +563,7 @@ def test_bucket_name_with_dot(): k.set_contents_from_string('somedata') -@mock_s3 +@mock_s3_deprecated def test_key_with_special_characters(): conn = boto.connect_s3() bucket = conn.create_bucket('test_bucket_name') @@ -574,7 +576,7 @@ def test_key_with_special_characters(): keys[0].name.should.equal("test_list_keys_2/x?y") -@mock_s3 +@mock_s3_deprecated def test_unicode_key_with_slash(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -586,7 +588,7 @@ def test_unicode_key_with_slash(): key.get_contents_as_string().should.equal(b'value') -@mock_s3 +@mock_s3_deprecated def test_bucket_key_listing_order(): conn = boto.connect_s3() bucket = conn.create_bucket('test_bucket') @@ -628,7 +630,7 @@ def test_bucket_key_listing_order(): keys.should.equal([u'toplevel/x/']) -@mock_s3 +@mock_s3_deprecated def test_key_with_reduced_redundancy(): conn = boto.connect_s3() bucket = conn.create_bucket('test_bucket_name') @@ -640,7 +642,7 @@ def test_key_with_reduced_redundancy(): list(bucket)[0].storage_class.should.equal('REDUCED_REDUNDANCY') -@mock_s3 +@mock_s3_deprecated def test_copy_key_reduced_redundancy(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -658,7 +660,7 @@ def test_copy_key_reduced_redundancy(): @freeze_time("2012-01-01 12:00:00") -@mock_s3 +@mock_s3_deprecated def test_restore_key(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -679,7 +681,7 @@ def test_restore_key(): @freeze_time("2012-01-01 12:00:00") -@mock_s3 +@mock_s3_deprecated def test_restore_key_headers(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -693,7 +695,7 @@ def test_restore_key_headers(): key.expiry_date.should.equal("Mon, 02 Jan 2012 12:00:00 GMT") -@mock_s3 +@mock_s3_deprecated def test_get_versioning_status(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket('foobar') @@ -710,7 +712,7 @@ def test_get_versioning_status(): d.should.have.key('Versioning').being.equal('Suspended') -@mock_s3 +@mock_s3_deprecated def test_key_version(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket('foobar') @@ -728,7 +730,7 @@ def test_key_version(): key.version_id.should.equal('1') -@mock_s3 +@mock_s3_deprecated def test_list_versions(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket('foobar') @@ -754,7 +756,7 @@ def test_list_versions(): versions[1].get_contents_as_string().should.equal(b"Version 2") -@mock_s3 +@mock_s3_deprecated def test_acl_setting(): conn = boto.connect_s3() bucket = conn.create_bucket('foobar') @@ -775,7 +777,7 @@ def test_acl_setting(): g.permission == 'READ' for g in grants), grants -@mock_s3 +@mock_s3_deprecated def test_acl_setting_via_headers(): conn = boto.connect_s3() bucket = conn.create_bucket('foobar') @@ -797,7 +799,7 @@ def test_acl_setting_via_headers(): g.permission == 'FULL_CONTROL' for g in grants), grants -@mock_s3 +@mock_s3_deprecated def test_acl_switching(): conn = boto.connect_s3() bucket = conn.create_bucket('foobar') @@ -814,7 +816,7 @@ def test_acl_switching(): g.permission == 'READ' for g in grants), grants -@mock_s3 +@mock_s3_deprecated def test_bucket_acl_setting(): conn = boto.connect_s3() bucket = conn.create_bucket('foobar') @@ -826,7 +828,7 @@ def test_bucket_acl_setting(): g.permission == 'READ' for g in grants), grants -@mock_s3 +@mock_s3_deprecated def test_bucket_acl_switching(): conn = boto.connect_s3() bucket = conn.create_bucket('foobar') @@ -839,7 +841,7 @@ def test_bucket_acl_switching(): g.permission == 'READ' for g in grants), grants -@mock_s3 +@mock_s3_deprecated def test_unicode_key(): conn = boto.connect_s3() bucket = conn.create_bucket('mybucket') @@ -852,7 +854,7 @@ def test_unicode_key(): assert fetched_key.get_contents_as_string().decode("utf-8") == 'Hello world!' -@mock_s3 +@mock_s3_deprecated def test_unicode_value(): conn = boto.connect_s3() bucket = conn.create_bucket('mybucket') @@ -864,7 +866,7 @@ def test_unicode_value(): assert key.get_contents_as_string().decode("utf-8") == u'こんにちは.jpg' -@mock_s3 +@mock_s3_deprecated def test_setting_content_encoding(): conn = boto.connect_s3() bucket = conn.create_bucket('mybucket') @@ -877,14 +879,14 @@ def test_setting_content_encoding(): key.content_encoding.should.equal("gzip") -@mock_s3 +@mock_s3_deprecated def test_bucket_location(): conn = boto.s3.connect_to_region("us-west-2") bucket = conn.create_bucket('mybucket') bucket.get_location().should.equal("us-west-2") -@mock_s3 +@mock_s3_deprecated def test_ranged_get(): conn = boto.connect_s3() bucket = conn.create_bucket('mybucket') @@ -926,7 +928,7 @@ def test_ranged_get(): key.size.should.equal(100) -@mock_s3 +@mock_s3_deprecated def test_policy(): conn = boto.connect_s3() bucket_name = 'mybucket' @@ -976,6 +978,31 @@ def test_policy(): bucket.get_policy() +@mock_s3_deprecated +def test_website_configuration_xml(): + conn = boto.connect_s3() + bucket = conn.create_bucket('test-bucket') + bucket.set_website_configuration_xml(TEST_XML) + bucket.get_website_configuration_xml().should.equal(TEST_XML) + + +@mock_s3_deprecated +def test_key_with_trailing_slash_in_ordinary_calling_format(): + conn = boto.connect_s3( + 'access_key', + 'secret_key', + calling_format=boto.s3.connection.OrdinaryCallingFormat() + ) + bucket = conn.create_bucket('test_bucket_name') + + key_name = 'key_with_slash/' + + key = Key(bucket, key_name) + key.set_contents_from_string('some value') + + [k.name for k in bucket.get_all_keys()].should.contain(key_name) + + """ boto3 """ @@ -1235,28 +1262,3 @@ TEST_XML = """\ """ - - -@mock_s3 -def test_website_configuration_xml(): - conn = boto.connect_s3() - bucket = conn.create_bucket('test-bucket') - bucket.set_website_configuration_xml(TEST_XML) - bucket.get_website_configuration_xml().should.equal(TEST_XML) - - -@mock_s3 -def test_key_with_trailing_slash_in_ordinary_calling_format(): - conn = boto.connect_s3( - 'access_key', - 'secret_key', - calling_format=boto.s3.connection.OrdinaryCallingFormat() - ) - bucket = conn.create_bucket('test_bucket_name') - - key_name = 'key_with_slash/' - - key = Key(bucket, key_name) - key.set_contents_from_string('some value') - - [k.name for k in bucket.get_all_keys()].should.contain(key_name) diff --git a/tests/test_s3/test_s3_lifecycle.py b/tests/test_s3/test_s3_lifecycle.py index 60613de44..f0a70bc6f 100644 --- a/tests/test_s3/test_s3_lifecycle.py +++ b/tests/test_s3/test_s3_lifecycle.py @@ -6,10 +6,10 @@ from boto.s3.lifecycle import Lifecycle, Transition, Expiration, Rule import sure # noqa -from moto import mock_s3 +from moto import mock_s3_deprecated -@mock_s3 +@mock_s3_deprecated def test_lifecycle_create(): conn = boto.s3.connect_to_region("us-west-1") bucket = conn.create_bucket("foobar") @@ -26,7 +26,7 @@ def test_lifecycle_create(): list(lifecycle.transition).should.equal([]) -@mock_s3 +@mock_s3_deprecated def test_lifecycle_with_glacier_transition(): conn = boto.s3.connect_to_region("us-west-1") bucket = conn.create_bucket("foobar") @@ -44,7 +44,7 @@ def test_lifecycle_with_glacier_transition(): transition.date.should.equal(None) -@mock_s3 +@mock_s3_deprecated def test_lifecycle_multi(): conn = boto.s3.connect_to_region("us-west-1") bucket = conn.create_bucket("foobar") @@ -86,7 +86,7 @@ def test_lifecycle_multi(): assert False, "Invalid rule id" -@mock_s3 +@mock_s3_deprecated def test_lifecycle_delete(): conn = boto.s3.connect_to_region("us-west-1") bucket = conn.create_bucket("foobar") diff --git a/tests/test_s3bucket_path/test_s3bucket_path.py b/tests/test_s3bucket_path/test_s3bucket_path.py index eff01bf55..24c5f7fa5 100644 --- a/tests/test_s3bucket_path/test_s3bucket_path.py +++ b/tests/test_s3bucket_path/test_s3bucket_path.py @@ -12,7 +12,7 @@ import requests import sure # noqa -from moto import mock_s3bucket_path +from moto import mock_s3, mock_s3_deprecated def create_connection(key=None, secret=None): @@ -32,7 +32,7 @@ class MyModel(object): k.set_contents_from_string(self.value) -@mock_s3bucket_path +@mock_s3_deprecated def test_my_model_save(): # Create Bucket so that test can run conn = create_connection('the_key', 'the_secret') @@ -45,14 +45,14 @@ def test_my_model_save(): conn.get_bucket('mybucket').get_key('steve').get_contents_as_string().should.equal(b'is awesome') -@mock_s3bucket_path +@mock_s3_deprecated def test_missing_key(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") bucket.get_key("the-key").should.equal(None) -@mock_s3bucket_path +@mock_s3_deprecated def test_missing_key_urllib2(): conn = create_connection('the_key', 'the_secret') conn.create_bucket("foobar") @@ -60,7 +60,7 @@ def test_missing_key_urllib2(): urlopen.when.called_with("http://s3.amazonaws.com/foobar/the-key").should.throw(HTTPError) -@mock_s3bucket_path +@mock_s3_deprecated def test_empty_key(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -71,7 +71,7 @@ def test_empty_key(): bucket.get_key("the-key").get_contents_as_string().should.equal(b'') -@mock_s3bucket_path +@mock_s3_deprecated def test_empty_key_set_on_existing_key(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -85,7 +85,7 @@ def test_empty_key_set_on_existing_key(): bucket.get_key("the-key").get_contents_as_string().should.equal(b'') -@mock_s3bucket_path +@mock_s3_deprecated def test_large_key_save(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -96,7 +96,7 @@ def test_large_key_save(): bucket.get_key("the-key").get_contents_as_string().should.equal(b'foobar' * 100000) -@mock_s3bucket_path +@mock_s3_deprecated def test_copy_key(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -110,7 +110,7 @@ def test_copy_key(): bucket.get_key("new-key").get_contents_as_string().should.equal(b"some value") -@mock_s3bucket_path +@mock_s3_deprecated def test_set_metadata(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -123,7 +123,7 @@ def test_set_metadata(): @freeze_time("2012-01-01 12:00:00") -@mock_s3bucket_path +@mock_s3_deprecated def test_last_modified(): # See https://github.com/boto/boto/issues/466 conn = create_connection() @@ -138,19 +138,19 @@ def test_last_modified(): bucket.get_key("the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') -@mock_s3bucket_path +@mock_s3_deprecated def test_missing_bucket(): conn = create_connection('the_key', 'the_secret') conn.get_bucket.when.called_with('mybucket').should.throw(S3ResponseError) -@mock_s3bucket_path +@mock_s3_deprecated def test_bucket_with_dash(): conn = create_connection('the_key', 'the_secret') conn.get_bucket.when.called_with('mybucket-test').should.throw(S3ResponseError) -@mock_s3bucket_path +@mock_s3_deprecated def test_bucket_deletion(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -172,7 +172,7 @@ def test_bucket_deletion(): conn.delete_bucket.when.called_with("foobar").should.throw(S3ResponseError) -@mock_s3bucket_path +@mock_s3_deprecated def test_get_all_buckets(): conn = create_connection('the_key', 'the_secret') conn.create_bucket("foobar") @@ -182,7 +182,8 @@ def test_get_all_buckets(): buckets.should.have.length_of(2) -@mock_s3bucket_path +@mock_s3 +@mock_s3_deprecated def test_post_to_bucket(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -195,7 +196,8 @@ def test_post_to_bucket(): bucket.get_key('the-key').get_contents_as_string().should.equal(b'nothing') -@mock_s3bucket_path +@mock_s3 +@mock_s3_deprecated def test_post_with_metadata_to_bucket(): conn = create_connection('the_key', 'the_secret') bucket = conn.create_bucket("foobar") @@ -209,17 +211,17 @@ def test_post_with_metadata_to_bucket(): bucket.get_key('the-key').get_metadata('test').should.equal('metadata') -@mock_s3bucket_path +@mock_s3 def test_bucket_method_not_implemented(): requests.patch.when.called_with("https://s3.amazonaws.com/foobar").should.throw(NotImplementedError) -@mock_s3bucket_path +@mock_s3 def test_key_method_not_implemented(): requests.post.when.called_with("https://s3.amazonaws.com/foobar/foo").should.throw(NotImplementedError) -@mock_s3bucket_path +@mock_s3_deprecated def test_bucket_name_with_dot(): conn = create_connection() bucket = conn.create_bucket('firstname.lastname') @@ -228,7 +230,7 @@ def test_bucket_name_with_dot(): k.set_contents_from_string('somedata') -@mock_s3bucket_path +@mock_s3_deprecated def test_key_with_special_characters(): conn = create_connection() bucket = conn.create_bucket('test_bucket_name') @@ -241,7 +243,7 @@ def test_key_with_special_characters(): keys[0].name.should.equal("test_list_keys_2/*x+?^@~!y") -@mock_s3bucket_path +@mock_s3_deprecated def test_bucket_key_listing_order(): conn = create_connection() bucket = conn.create_bucket('test_bucket') @@ -283,7 +285,7 @@ def test_bucket_key_listing_order(): keys.should.equal(['toplevel/x/']) -@mock_s3bucket_path +@mock_s3_deprecated def test_delete_keys(): conn = create_connection() bucket = conn.create_bucket('foobar') @@ -301,7 +303,7 @@ def test_delete_keys(): keys[0].name.should.equal('file1') -@mock_s3bucket_path +@mock_s3_deprecated def test_delete_keys_with_invalid(): conn = create_connection() bucket = conn.create_bucket('foobar') diff --git a/tests/test_s3bucket_path/test_s3bucket_path_combo.py b/tests/test_s3bucket_path/test_s3bucket_path_combo.py index 48d65d497..e1b1075ee 100644 --- a/tests/test_s3bucket_path/test_s3bucket_path_combo.py +++ b/tests/test_s3bucket_path/test_s3bucket_path_combo.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import boto from boto.s3.connection import OrdinaryCallingFormat -from moto import mock_s3bucket_path, mock_s3 +from moto import mock_s3_deprecated def create_connection(key=None, secret=None): @@ -11,12 +11,12 @@ def create_connection(key=None, secret=None): def test_bucketpath_combo_serial(): - @mock_s3bucket_path + @mock_s3_deprecated def make_bucket_path(): conn = create_connection() conn.create_bucket('mybucketpath') - @mock_s3 + @mock_s3_deprecated def make_bucket(): conn = boto.connect_s3('the_key', 'the_secret') conn.create_bucket('mybucket') diff --git a/tests/test_ses/test_ses.py b/tests/test_ses/test_ses.py index e9b64b78b..7771b9a65 100644 --- a/tests/test_ses/test_ses.py +++ b/tests/test_ses/test_ses.py @@ -6,10 +6,10 @@ from boto.exception import BotoServerError import sure # noqa -from moto import mock_ses +from moto import mock_ses_deprecated -@mock_ses +@mock_ses_deprecated def test_verify_email_identity(): conn = boto.connect_ses('the_key', 'the_secret') conn.verify_email_identity("test@example.com") @@ -19,7 +19,7 @@ def test_verify_email_identity(): address.should.equal('test@example.com') -@mock_ses +@mock_ses_deprecated def test_domain_verify(): conn = boto.connect_ses('the_key', 'the_secret') @@ -31,7 +31,7 @@ def test_domain_verify(): domains.should.equal(['domain1.com', 'domain2.com']) -@mock_ses +@mock_ses_deprecated def test_delete_identity(): conn = boto.connect_ses('the_key', 'the_secret') conn.verify_email_identity("test@example.com") @@ -41,7 +41,7 @@ def test_delete_identity(): conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'].should.have.length_of(0) -@mock_ses +@mock_ses_deprecated def test_send_email(): conn = boto.connect_ses('the_key', 'the_secret') @@ -56,7 +56,7 @@ def test_send_email(): sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours']) sent_count.should.equal(1) -@mock_ses +@mock_ses_deprecated def test_send_html_email(): conn = boto.connect_ses('the_key', 'the_secret') @@ -71,7 +71,7 @@ def test_send_html_email(): sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours']) sent_count.should.equal(1) -@mock_ses +@mock_ses_deprecated def test_send_raw_email(): conn = boto.connect_ses('the_key', 'the_secret') diff --git a/tests/test_sns/test_application.py b/tests/test_sns/test_application.py index 0566adeb3..31db73f62 100644 --- a/tests/test_sns/test_application.py +++ b/tests/test_sns/test_application.py @@ -2,11 +2,11 @@ from __future__ import unicode_literals import boto from boto.exception import BotoServerError -from moto import mock_sns +from moto import mock_sns_deprecated import sure # noqa -@mock_sns +@mock_sns_deprecated def test_create_platform_application(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -21,7 +21,7 @@ def test_create_platform_application(): application_arn.should.equal('arn:aws:sns:us-east-1:123456789012:app/APNS/my-application') -@mock_sns +@mock_sns_deprecated def test_get_platform_application_attributes(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -40,13 +40,13 @@ def test_get_platform_application_attributes(): }) -@mock_sns +@mock_sns_deprecated def test_get_missing_platform_application_attributes(): conn = boto.connect_sns() conn.get_platform_application_attributes.when.called_with("a-fake-arn").should.throw(BotoServerError) -@mock_sns +@mock_sns_deprecated def test_set_platform_application_attributes(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -68,7 +68,7 @@ def test_set_platform_application_attributes(): }) -@mock_sns +@mock_sns_deprecated def test_list_platform_applications(): conn = boto.connect_sns() conn.create_platform_application( @@ -85,7 +85,7 @@ def test_list_platform_applications(): applications.should.have.length_of(2) -@mock_sns +@mock_sns_deprecated def test_delete_platform_application(): conn = boto.connect_sns() conn.create_platform_application( @@ -109,7 +109,7 @@ def test_delete_platform_application(): applications.should.have.length_of(1) -@mock_sns +@mock_sns_deprecated def test_create_platform_endpoint(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -131,7 +131,7 @@ def test_create_platform_endpoint(): endpoint_arn.should.contain("arn:aws:sns:us-east-1:123456789012:endpoint/APNS/my-application/") -@mock_sns +@mock_sns_deprecated def test_get_list_endpoints_by_platform_application(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -159,7 +159,7 @@ def test_get_list_endpoints_by_platform_application(): endpoint_list[0]['EndpointArn'].should.equal(endpoint_arn) -@mock_sns +@mock_sns_deprecated def test_get_endpoint_attributes(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -187,13 +187,13 @@ def test_get_endpoint_attributes(): }) -@mock_sns +@mock_sns_deprecated def test_get_missing_endpoint_attributes(): conn = boto.connect_sns() conn.get_endpoint_attributes.when.called_with("a-fake-arn").should.throw(BotoServerError) -@mock_sns +@mock_sns_deprecated def test_set_endpoint_attributes(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -224,7 +224,7 @@ def test_set_endpoint_attributes(): }) -@mock_sns +@mock_sns_deprecated def test_delete_endpoint(): conn = boto.connect_sns() platform_application = conn.create_platform_application( @@ -258,7 +258,7 @@ def test_delete_endpoint(): endpoint_list.should.have.length_of(0) -@mock_sns +@mock_sns_deprecated def test_publish_to_platform_endpoint(): conn = boto.connect_sns() platform_application = conn.create_platform_application( diff --git a/tests/test_sns/test_publishing.py b/tests/test_sns/test_publishing.py index 3805d9e5e..8f8bfb0a1 100644 --- a/tests/test_sns/test_publishing.py +++ b/tests/test_sns/test_publishing.py @@ -3,14 +3,14 @@ from six.moves.urllib.parse import parse_qs import boto from freezegun import freeze_time -import httpretty import sure # noqa -from moto import mock_sns, mock_sqs +from moto.packages.responses import responses +from moto import mock_sns, mock_sns_deprecated, mock_sqs_deprecated -@mock_sqs -@mock_sns +@mock_sqs_deprecated +@mock_sns_deprecated def test_publish_to_sqs(): conn = boto.connect_sns() conn.create_topic("some-topic") @@ -29,8 +29,8 @@ def test_publish_to_sqs(): message.get_body().should.equal('my message') -@mock_sqs -@mock_sns +@mock_sqs_deprecated +@mock_sns_deprecated def test_publish_to_sqs_in_different_region(): conn = boto.sns.connect_to_region("us-west-1") conn.create_topic("some-topic") @@ -51,10 +51,11 @@ def test_publish_to_sqs_in_different_region(): @freeze_time("2013-01-01") @mock_sns +@mock_sns_deprecated def test_publish_to_http(): - httpretty.HTTPretty.register_uri( + responses.add( method="POST", - uri="http://example.com/foobar", + url="http://example.com/foobar", ) conn = boto.connect_sns() @@ -67,7 +68,7 @@ def test_publish_to_http(): response = conn.publish(topic=topic_arn, message="my message", subject="my subject") message_id = response['PublishResponse']['PublishResult']['MessageId'] - last_request = httpretty.last_request() + last_request = responses.calls[-1].request last_request.method.should.equal("POST") parse_qs(last_request.body.decode('utf-8')).should.equal({ "Type": ["Notification"], @@ -81,3 +82,5 @@ def test_publish_to_http(): "SigningCertURL": ["https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem"], "UnsubscribeURL": ["https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55"], }) + + diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index 90d063971..b37522641 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -3,9 +3,9 @@ from six.moves.urllib.parse import parse_qs import boto3 from freezegun import freeze_time -import httpretty import sure # noqa +from moto.packages.responses import responses from moto import mock_sns, mock_sqs @@ -56,9 +56,9 @@ def test_publish_to_sqs_in_different_region(): @freeze_time("2013-01-01") @mock_sns def test_publish_to_http(): - httpretty.HTTPretty.register_uri( + responses.add( method="POST", - uri="http://example.com/foobar", + url="http://example.com/foobar", ) conn = boto3.client('sns', region_name='us-east-1') @@ -73,7 +73,7 @@ def test_publish_to_http(): response = conn.publish(TopicArn=topic_arn, Message="my message", Subject="my subject") message_id = response['MessageId'] - last_request = httpretty.last_request() + last_request = responses.calls[-2].request last_request.method.should.equal("POST") parse_qs(last_request.body.decode('utf-8')).should.equal({ "Type": ["Notification"], diff --git a/tests/test_sns/test_subscriptions.py b/tests/test_sns/test_subscriptions.py index a202edf36..e141c503a 100644 --- a/tests/test_sns/test_subscriptions.py +++ b/tests/test_sns/test_subscriptions.py @@ -3,11 +3,11 @@ import boto import sure # noqa -from moto import mock_sns +from moto import mock_sns_deprecated from moto.sns.models import DEFAULT_PAGE_SIZE -@mock_sns +@mock_sns_deprecated def test_creating_subscription(): conn = boto.connect_sns() conn.create_topic("some-topic") @@ -32,7 +32,7 @@ def test_creating_subscription(): subscriptions.should.have.length_of(0) -@mock_sns +@mock_sns_deprecated def test_getting_subscriptions_by_topic(): conn = boto.connect_sns() conn.create_topic("topic1") @@ -51,7 +51,7 @@ def test_getting_subscriptions_by_topic(): topic1_subscriptions[0]['Endpoint'].should.equal("http://example1.com/") -@mock_sns +@mock_sns_deprecated def test_subscription_paging(): conn = boto.connect_sns() conn.create_topic("topic1") diff --git a/tests/test_sns/test_topics.py b/tests/test_sns/test_topics.py index a2a8092ee..ab2f06382 100644 --- a/tests/test_sns/test_topics.py +++ b/tests/test_sns/test_topics.py @@ -5,11 +5,11 @@ import six import sure # noqa from boto.exception import BotoServerError -from moto import mock_sns +from moto import mock_sns_deprecated from moto.sns.models import DEFAULT_TOPIC_POLICY, DEFAULT_EFFECTIVE_DELIVERY_POLICY, DEFAULT_PAGE_SIZE -@mock_sns +@mock_sns_deprecated def test_create_and_delete_topic(): conn = boto.connect_sns() conn.create_topic("some-topic") @@ -31,20 +31,20 @@ def test_create_and_delete_topic(): topics.should.have.length_of(0) -@mock_sns +@mock_sns_deprecated def test_get_missing_topic(): conn = boto.connect_sns() conn.get_topic_attributes.when.called_with("a-fake-arn").should.throw(BotoServerError) -@mock_sns +@mock_sns_deprecated def test_create_topic_in_multiple_regions(): for region in ['us-west-1', 'us-west-2']: conn = boto.sns.connect_to_region(region) conn.create_topic("some-topic") list(conn.get_all_topics()["ListTopicsResponse"]["ListTopicsResult"]["Topics"]).should.have.length_of(1) -@mock_sns +@mock_sns_deprecated def test_topic_corresponds_to_region(): for region in ['us-east-1', 'us-west-2']: conn = boto.sns.connect_to_region(region) @@ -53,7 +53,7 @@ def test_topic_corresponds_to_region(): topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] topic_arn.should.equal("arn:aws:sns:{0}:123456789012:some-topic".format(region)) -@mock_sns +@mock_sns_deprecated def test_topic_attributes(): conn = boto.connect_sns() conn.create_topic("some-topic") @@ -95,7 +95,7 @@ def test_topic_attributes(): attributes["DisplayName"].should.equal("My display name") attributes["DeliveryPolicy"].should.equal("{'http': {'defaultHealthyRetryPolicy': {'numRetries': 5}}}") -@mock_sns +@mock_sns_deprecated def test_topic_paging(): conn = boto.connect_sns() for index in range(DEFAULT_PAGE_SIZE + int(DEFAULT_PAGE_SIZE / 2)): diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 32b026a46..b3eaaab75 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -10,16 +10,15 @@ import requests import sure # noqa import time -from moto import mock_sqs +from moto import mock_sqs, mock_sqs_deprecated from tests.helpers import requires_boto_gte import tests.backport_assert_raises # noqa from nose.tools import assert_raises -sqs = boto3.resource('sqs', region_name='us-east-1') - @mock_sqs def test_create_queue(): + sqs = boto3.resource('sqs', region_name='us-east-1') new_queue = sqs.create_queue(QueueName='test-queue') new_queue.should_not.be.none new_queue.should.have.property('url').should.contain('test-queue') @@ -34,11 +33,13 @@ def test_create_queue(): @mock_sqs def test_get_inexistent_queue(): + sqs = boto3.resource('sqs', region_name='us-east-1') sqs.get_queue_by_name.when.called_with(QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) @mock_sqs def test_message_send(): + sqs = boto3.resource('sqs', region_name='us-east-1') queue = sqs.create_queue(QueueName="blah") msg = queue.send_message(MessageBody="derp") @@ -52,6 +53,8 @@ def test_message_send(): @mock_sqs def test_set_queue_attributes(): + sqs = boto3.resource('sqs', region_name='us-east-1') + conn = boto3.client('sqs', region_name='us-west-1') queue = sqs.create_queue(QueueName="blah") queue.attributes['VisibilityTimeout'].should.equal("30") @@ -90,6 +93,7 @@ def test_get_queue_with_prefix(): @mock_sqs def test_delete_queue(): + sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": "60"}) queue = sqs.Queue('test-queue') @@ -105,6 +109,7 @@ def test_delete_queue(): @mock_sqs def test_set_queue_attribute(): + sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": '60'}) @@ -118,6 +123,7 @@ def test_set_queue_attribute(): @mock_sqs def test_send_message(): + sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue") queue = sqs.Queue("test-queue") @@ -134,7 +140,7 @@ def test_send_message(): messages[1]['Body'].should.equal(body_two) -@mock_sqs +@mock_sqs_deprecated def test_send_message_with_xml_characters(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -150,7 +156,7 @@ def test_send_message_with_xml_characters(): @requires_boto_gte("2.28") -@mock_sqs +@mock_sqs_deprecated def test_send_message_with_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -175,7 +181,7 @@ def test_send_message_with_attributes(): dict(messages[0].message_attributes[name]).should.equal(value) -@mock_sqs +@mock_sqs_deprecated def test_send_message_with_delay(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -196,7 +202,7 @@ def test_send_message_with_delay(): queue.count().should.equal(0) -@mock_sqs +@mock_sqs_deprecated def test_send_large_message_fails(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -208,7 +214,7 @@ def test_send_large_message_fails(): queue.write.when.called_with(huge_message).should.throw(SQSError) -@mock_sqs +@mock_sqs_deprecated def test_message_becomes_inflight_when_received(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=2) @@ -229,7 +235,7 @@ def test_message_becomes_inflight_when_received(): queue.count().should.equal(1) -@mock_sqs +@mock_sqs_deprecated def test_receive_message_with_explicit_visibility_timeout(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -246,7 +252,7 @@ def test_receive_message_with_explicit_visibility_timeout(): # Message should remain visible queue.count().should.equal(1) -@mock_sqs +@mock_sqs_deprecated def test_change_message_visibility(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=2) @@ -280,7 +286,7 @@ def test_change_message_visibility(): queue.count().should.equal(0) -@mock_sqs +@mock_sqs_deprecated def test_message_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=2) @@ -304,7 +310,7 @@ def test_message_attributes(): assert message_attributes.get('SenderId') -@mock_sqs +@mock_sqs_deprecated def test_read_message_from_queue(): conn = boto.connect_sqs() queue = conn.create_queue('testqueue') @@ -316,7 +322,7 @@ def test_read_message_from_queue(): message.get_body().should.equal(body) -@mock_sqs +@mock_sqs_deprecated def test_queue_length(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -327,7 +333,7 @@ def test_queue_length(): queue.count().should.equal(2) -@mock_sqs +@mock_sqs_deprecated def test_delete_message(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -348,7 +354,7 @@ def test_delete_message(): queue.count().should.equal(0) -@mock_sqs +@mock_sqs_deprecated def test_send_batch_operation(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -370,7 +376,7 @@ def test_send_batch_operation(): @requires_boto_gte("2.28") -@mock_sqs +@mock_sqs_deprecated def test_send_batch_operation_with_message_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -386,7 +392,7 @@ def test_send_batch_operation_with_message_attributes(): dict(messages[0].message_attributes[name]).should.equal(value) -@mock_sqs +@mock_sqs_deprecated def test_delete_batch_operation(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=60) @@ -408,7 +414,7 @@ def test_sqs_method_not_implemented(): requests.post.when.called_with("https://sqs.amazonaws.com/?Action=[foobar]").should.throw(NotImplementedError) -@mock_sqs +@mock_sqs_deprecated def test_queue_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') @@ -438,7 +444,7 @@ def test_queue_attributes(): attribute_names.should.contain('QueueArn') -@mock_sqs +@mock_sqs_deprecated def test_change_message_visibility_on_invalid_receipt(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=1) @@ -465,7 +471,7 @@ def test_change_message_visibility_on_invalid_receipt(): original_message.change_visibility.when.called_with(100).should.throw(SQSError) -@mock_sqs +@mock_sqs_deprecated def test_change_message_visibility_on_visible_message(): conn = boto.connect_sqs('the_key', 'the_secret') queue = conn.create_queue("test-queue", visibility_timeout=1) @@ -488,7 +494,7 @@ def test_change_message_visibility_on_visible_message(): original_message.change_visibility.when.called_with(100).should.throw(SQSError) -@mock_sqs +@mock_sqs_deprecated def test_purge_action(): conn = boto.sqs.connect_to_region("us-east-1") @@ -501,7 +507,7 @@ def test_purge_action(): queue.count().should.equal(0) -@mock_sqs +@mock_sqs_deprecated def test_delete_message_after_visibility_timeout(): VISIBILITY_TIMEOUT = 1 conn = boto.sqs.connect_to_region("us-east-1") diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index 9bd02ce12..870f14860 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -6,11 +6,11 @@ import boto3 from freezegun import freeze_time import sure # noqa -from moto import mock_sts +from moto import mock_sts, mock_sts_deprecated @freeze_time("2012-01-01 12:00:00") -@mock_sts +@mock_sts_deprecated def test_get_session_token(): conn = boto.connect_sts() token = conn.get_session_token(duration=123) @@ -22,7 +22,7 @@ def test_get_session_token(): @freeze_time("2012-01-01 12:00:00") -@mock_sts +@mock_sts_deprecated def test_get_federation_token(): conn = boto.connect_sts() token = conn.get_federation_token(duration=123, name="Bob") @@ -36,7 +36,7 @@ def test_get_federation_token(): @freeze_time("2012-01-01 12:00:00") -@mock_sts +@mock_sts_deprecated def test_assume_role(): conn = boto.connect_sts() diff --git a/tests/test_swf/responses/test_activity_tasks.py b/tests/test_swf/responses/test_activity_tasks.py index 31eaeeddd..e6671e9e9 100644 --- a/tests/test_swf/responses/test_activity_tasks.py +++ b/tests/test_swf/responses/test_activity_tasks.py @@ -1,14 +1,14 @@ from boto.swf.exceptions import SWFResponseError from freezegun import freeze_time -from moto import mock_swf +from moto import mock_swf_deprecated from moto.swf import swf_backend from ..utils import setup_workflow, SCHEDULE_ACTIVITY_TASK_DECISION # PollForActivityTask endpoint -@mock_swf +@mock_swf_deprecated def test_poll_for_activity_task_when_one(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -26,14 +26,14 @@ def test_poll_for_activity_task_when_one(): ) -@mock_swf +@mock_swf_deprecated def test_poll_for_activity_task_when_none(): conn = setup_workflow() resp = conn.poll_for_activity_task("test-domain", "activity-task-list") resp.should.equal({"startedEventId": 0}) -@mock_swf +@mock_swf_deprecated def test_poll_for_activity_task_on_non_existent_queue(): conn = setup_workflow() resp = conn.poll_for_activity_task("test-domain", "non-existent-queue") @@ -41,7 +41,7 @@ def test_poll_for_activity_task_on_non_existent_queue(): # CountPendingActivityTasks endpoint -@mock_swf +@mock_swf_deprecated def test_count_pending_activity_tasks(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -53,7 +53,7 @@ def test_count_pending_activity_tasks(): resp.should.equal({"count": 1, "truncated": False}) -@mock_swf +@mock_swf_deprecated def test_count_pending_decision_tasks_on_non_existent_task_list(): conn = setup_workflow() resp = conn.count_pending_activity_tasks("test-domain", "non-existent") @@ -61,7 +61,7 @@ def test_count_pending_decision_tasks_on_non_existent_task_list(): # RespondActivityTaskCompleted endpoint -@mock_swf +@mock_swf_deprecated def test_respond_activity_task_completed(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -80,7 +80,7 @@ def test_respond_activity_task_completed(): ) -@mock_swf +@mock_swf_deprecated def test_respond_activity_task_completed_on_closed_workflow_execution(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -99,7 +99,7 @@ def test_respond_activity_task_completed_on_closed_workflow_execution(): ).should.throw(SWFResponseError, "WorkflowExecution=") -@mock_swf +@mock_swf_deprecated def test_respond_activity_task_completed_with_task_already_completed(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -116,7 +116,7 @@ def test_respond_activity_task_completed_with_task_already_completed(): # RespondActivityTaskFailed endpoint -@mock_swf +@mock_swf_deprecated def test_respond_activity_task_failed(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -138,7 +138,7 @@ def test_respond_activity_task_failed(): ) -@mock_swf +@mock_swf_deprecated def test_respond_activity_task_completed_with_wrong_token(): # NB: we just test ONE failure case for RespondActivityTaskFailed # because the safeguards are shared with RespondActivityTaskCompleted, so @@ -155,7 +155,7 @@ def test_respond_activity_task_completed_with_wrong_token(): # RecordActivityTaskHeartbeat endpoint -@mock_swf +@mock_swf_deprecated def test_record_activity_task_heartbeat(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -168,7 +168,7 @@ def test_record_activity_task_heartbeat(): resp.should.equal({"cancelRequested": False}) -@mock_swf +@mock_swf_deprecated def test_record_activity_task_heartbeat_with_wrong_token(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] @@ -182,7 +182,7 @@ def test_record_activity_task_heartbeat_with_wrong_token(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_record_activity_task_heartbeat_sets_details_in_case_of_timeout(): conn = setup_workflow() decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] diff --git a/tests/test_swf/responses/test_activity_types.py b/tests/test_swf/responses/test_activity_types.py index 872cd7f64..20c44dc5f 100644 --- a/tests/test_swf/responses/test_activity_types.py +++ b/tests/test_swf/responses/test_activity_types.py @@ -1,11 +1,11 @@ import boto from boto.swf.exceptions import SWFResponseError -from moto import mock_swf +from moto import mock_swf_deprecated # RegisterActivityType endpoint -@mock_swf +@mock_swf_deprecated def test_register_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -17,7 +17,7 @@ def test_register_activity_type(): actype["activityType"]["version"].should.equal("v1.0") -@mock_swf +@mock_swf_deprecated def test_register_already_existing_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -28,7 +28,7 @@ def test_register_already_existing_activity_type(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_register_with_wrong_parameter_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -39,7 +39,7 @@ def test_register_with_wrong_parameter_type(): # ListActivityTypes endpoint -@mock_swf +@mock_swf_deprecated def test_list_activity_types(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -52,7 +52,7 @@ def test_list_activity_types(): names.should.equal(["a-test-activity", "b-test-activity", "c-test-activity"]) -@mock_swf +@mock_swf_deprecated def test_list_activity_types_reverse_order(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -67,7 +67,7 @@ def test_list_activity_types_reverse_order(): # DeprecateActivityType endpoint -@mock_swf +@mock_swf_deprecated def test_deprecate_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -80,7 +80,7 @@ def test_deprecate_activity_type(): actype["activityType"]["version"].should.equal("v1.0") -@mock_swf +@mock_swf_deprecated def test_deprecate_already_deprecated_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -92,7 +92,7 @@ def test_deprecate_already_deprecated_activity_type(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_deprecate_non_existent_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -103,7 +103,7 @@ def test_deprecate_non_existent_activity_type(): # DescribeActivityType endpoint -@mock_swf +@mock_swf_deprecated def test_describe_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -118,7 +118,7 @@ def test_describe_activity_type(): infos["status"].should.equal("REGISTERED") -@mock_swf +@mock_swf_deprecated def test_describe_non_existent_activity_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") diff --git a/tests/test_swf/responses/test_decision_tasks.py b/tests/test_swf/responses/test_decision_tasks.py index b16a6441a..b552723cb 100644 --- a/tests/test_swf/responses/test_decision_tasks.py +++ b/tests/test_swf/responses/test_decision_tasks.py @@ -1,14 +1,14 @@ from boto.swf.exceptions import SWFResponseError from freezegun import freeze_time -from moto import mock_swf +from moto import mock_swf_deprecated from moto.swf import swf_backend from ..utils import setup_workflow # PollForDecisionTask endpoint -@mock_swf +@mock_swf_deprecated def test_poll_for_decision_task_when_one(): conn = setup_workflow() @@ -23,7 +23,7 @@ def test_poll_for_decision_task_when_one(): resp["events"][-1]["decisionTaskStartedEventAttributes"]["identity"].should.equal("srv01") -@mock_swf +@mock_swf_deprecated def test_poll_for_decision_task_when_none(): conn = setup_workflow() conn.poll_for_decision_task("test-domain", "queue") @@ -34,14 +34,14 @@ def test_poll_for_decision_task_when_none(): resp.should.equal({"previousStartedEventId": 0, "startedEventId": 0}) -@mock_swf +@mock_swf_deprecated def test_poll_for_decision_task_on_non_existent_queue(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "non-existent-queue") resp.should.equal({"previousStartedEventId": 0, "startedEventId": 0}) -@mock_swf +@mock_swf_deprecated def test_poll_for_decision_task_with_reverse_order(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue", reverse_order=True) @@ -50,7 +50,7 @@ def test_poll_for_decision_task_with_reverse_order(): # CountPendingDecisionTasks endpoint -@mock_swf +@mock_swf_deprecated def test_count_pending_decision_tasks(): conn = setup_workflow() conn.poll_for_decision_task("test-domain", "queue") @@ -58,14 +58,14 @@ def test_count_pending_decision_tasks(): resp.should.equal({"count": 1, "truncated": False}) -@mock_swf +@mock_swf_deprecated def test_count_pending_decision_tasks_on_non_existent_task_list(): conn = setup_workflow() resp = conn.count_pending_decision_tasks("test-domain", "non-existent") resp.should.equal({"count": 0, "truncated": False}) -@mock_swf +@mock_swf_deprecated def test_count_pending_decision_tasks_after_decision_completes(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -76,7 +76,7 @@ def test_count_pending_decision_tasks_after_decision_completes(): # RespondDecisionTaskCompleted endpoint -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_no_decision(): conn = setup_workflow() @@ -108,7 +108,7 @@ def test_respond_decision_task_completed_with_no_decision(): resp["latestExecutionContext"].should.equal("free-form context") -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_wrong_token(): conn = setup_workflow() conn.poll_for_decision_task("test-domain", "queue") @@ -117,7 +117,7 @@ def test_respond_decision_task_completed_with_wrong_token(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_on_close_workflow_execution(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -133,7 +133,7 @@ def test_respond_decision_task_completed_on_close_workflow_execution(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_task_already_completed(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -145,7 +145,7 @@ def test_respond_decision_task_completed_with_task_already_completed(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_complete_workflow_execution(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -170,7 +170,7 @@ def test_respond_decision_task_completed_with_complete_workflow_execution(): resp["events"][-1]["workflowExecutionCompletedEventAttributes"]["result"].should.equal("foo bar") -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_close_decision_not_last(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -186,7 +186,7 @@ def test_respond_decision_task_completed_with_close_decision_not_last(): ).should.throw(SWFResponseError, r"Close must be last decision in list") -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_invalid_decision_type(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -204,7 +204,7 @@ def test_respond_decision_task_completed_with_invalid_decision_type(): ) -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_missing_attributes(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -226,7 +226,7 @@ def test_respond_decision_task_completed_with_missing_attributes(): ) -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_missing_attributes_totally(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -245,7 +245,7 @@ def test_respond_decision_task_completed_with_missing_attributes_totally(): ) -@mock_swf +@mock_swf_deprecated def test_respond_decision_task_completed_with_fail_workflow_execution(): conn = setup_workflow() resp = conn.poll_for_decision_task("test-domain", "queue") @@ -272,7 +272,7 @@ def test_respond_decision_task_completed_with_fail_workflow_execution(): attrs["details"].should.equal("foo") -@mock_swf +@mock_swf_deprecated @freeze_time("2015-01-01 12:00:00") def test_respond_decision_task_completed_with_schedule_activity_task(): conn = setup_workflow() diff --git a/tests/test_swf/responses/test_domains.py b/tests/test_swf/responses/test_domains.py index fc89ea752..1f785095c 100644 --- a/tests/test_swf/responses/test_domains.py +++ b/tests/test_swf/responses/test_domains.py @@ -1,11 +1,11 @@ import boto from boto.swf.exceptions import SWFResponseError -from moto import mock_swf +from moto import mock_swf_deprecated # RegisterDomain endpoint -@mock_swf +@mock_swf_deprecated def test_register_domain(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") @@ -18,7 +18,7 @@ def test_register_domain(): domain["description"].should.equal("A test domain") -@mock_swf +@mock_swf_deprecated def test_register_already_existing_domain(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") @@ -28,7 +28,7 @@ def test_register_already_existing_domain(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_register_with_wrong_parameter_type(): conn = boto.connect_swf("the_key", "the_secret") @@ -38,7 +38,7 @@ def test_register_with_wrong_parameter_type(): # ListDomains endpoint -@mock_swf +@mock_swf_deprecated def test_list_domains_order(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("b-test-domain", "60") @@ -50,7 +50,7 @@ def test_list_domains_order(): names.should.equal(["a-test-domain", "b-test-domain", "c-test-domain"]) -@mock_swf +@mock_swf_deprecated def test_list_domains_reverse_order(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("b-test-domain", "60") @@ -63,7 +63,7 @@ def test_list_domains_reverse_order(): # DeprecateDomain endpoint -@mock_swf +@mock_swf_deprecated def test_deprecate_domain(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") @@ -75,7 +75,7 @@ def test_deprecate_domain(): domain["name"].should.equal("test-domain") -@mock_swf +@mock_swf_deprecated def test_deprecate_already_deprecated_domain(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") @@ -86,7 +86,7 @@ def test_deprecate_already_deprecated_domain(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_deprecate_non_existent_domain(): conn = boto.connect_swf("the_key", "the_secret") @@ -96,7 +96,7 @@ def test_deprecate_non_existent_domain(): # DescribeDomain endpoint -@mock_swf +@mock_swf_deprecated def test_describe_domain(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") @@ -108,7 +108,7 @@ def test_describe_domain(): domain["domainInfo"]["status"].should.equal("REGISTERED") -@mock_swf +@mock_swf_deprecated def test_describe_non_existent_domain(): conn = boto.connect_swf("the_key", "the_secret") diff --git a/tests/test_swf/responses/test_timeouts.py b/tests/test_swf/responses/test_timeouts.py index afa130c21..726410e76 100644 --- a/tests/test_swf/responses/test_timeouts.py +++ b/tests/test_swf/responses/test_timeouts.py @@ -1,13 +1,13 @@ from freezegun import freeze_time -from moto import mock_swf +from moto import mock_swf_deprecated from ..utils import setup_workflow, SCHEDULE_ACTIVITY_TASK_DECISION # Activity Task Heartbeat timeout # Default value in workflow helpers: 5 mins -@mock_swf +@mock_swf_deprecated def test_activity_task_heartbeat_timeout(): with freeze_time("2015-01-01 12:00:00"): conn = setup_workflow() @@ -36,7 +36,7 @@ def test_activity_task_heartbeat_timeout(): # Decision Task Start to Close timeout # Default value in workflow helpers: 5 mins -@mock_swf +@mock_swf_deprecated def test_decision_task_start_to_close_timeout(): pass with freeze_time("2015-01-01 12:00:00"): @@ -70,7 +70,7 @@ def test_decision_task_start_to_close_timeout(): # Workflow Execution Start to Close timeout # Default value in workflow helpers: 2 hours -@mock_swf +@mock_swf_deprecated def test_workflow_execution_start_to_close_timeout(): pass with freeze_time("2015-01-01 12:00:00"): diff --git a/tests/test_swf/responses/test_workflow_executions.py b/tests/test_swf/responses/test_workflow_executions.py index f4a949687..d5dc44a38 100644 --- a/tests/test_swf/responses/test_workflow_executions.py +++ b/tests/test_swf/responses/test_workflow_executions.py @@ -6,12 +6,12 @@ import sure # noqa # Ensure 'assert_raises' context manager support for Python 2.6 import tests.backport_assert_raises # noqa -from moto import mock_swf +from moto import mock_swf_deprecated from moto.core.utils import unix_time # Utils -@mock_swf +@mock_swf_deprecated def setup_swf_environment(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") @@ -26,7 +26,7 @@ def setup_swf_environment(): # StartWorkflowExecution endpoint -@mock_swf +@mock_swf_deprecated def test_start_workflow_execution(): conn = setup_swf_environment() @@ -34,7 +34,7 @@ def test_start_workflow_execution(): wf.should.contain("runId") -@mock_swf +@mock_swf_deprecated def test_start_already_started_workflow_execution(): conn = setup_swf_environment() conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") @@ -44,7 +44,7 @@ def test_start_already_started_workflow_execution(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_start_workflow_execution_on_deprecated_type(): conn = setup_swf_environment() conn.deprecate_workflow_type("test-domain", "test-workflow", "v1.0") @@ -55,7 +55,7 @@ def test_start_workflow_execution_on_deprecated_type(): # DescribeWorkflowExecution endpoint -@mock_swf +@mock_swf_deprecated def test_describe_workflow_execution(): conn = setup_swf_environment() hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") @@ -66,7 +66,7 @@ def test_describe_workflow_execution(): wfe["executionInfo"]["executionStatus"].should.equal("OPEN") -@mock_swf +@mock_swf_deprecated def test_describe_non_existent_workflow_execution(): conn = setup_swf_environment() @@ -76,7 +76,7 @@ def test_describe_non_existent_workflow_execution(): # GetWorkflowExecutionHistory endpoint -@mock_swf +@mock_swf_deprecated def test_get_workflow_execution_history(): conn = setup_swf_environment() hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") @@ -87,7 +87,7 @@ def test_get_workflow_execution_history(): types.should.equal(["WorkflowExecutionStarted", "DecisionTaskScheduled"]) -@mock_swf +@mock_swf_deprecated def test_get_workflow_execution_history_with_reverse_order(): conn = setup_swf_environment() hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") @@ -99,7 +99,7 @@ def test_get_workflow_execution_history_with_reverse_order(): types.should.equal(["DecisionTaskScheduled", "WorkflowExecutionStarted"]) -@mock_swf +@mock_swf_deprecated def test_get_workflow_execution_history_on_non_existent_workflow_execution(): conn = setup_swf_environment() @@ -109,7 +109,7 @@ def test_get_workflow_execution_history_on_non_existent_workflow_execution(): # ListOpenWorkflowExecutions endpoint -@mock_swf +@mock_swf_deprecated def test_list_open_workflow_executions(): conn = setup_swf_environment() # One open workflow execution @@ -143,7 +143,7 @@ def test_list_open_workflow_executions(): # ListClosedWorkflowExecutions endpoint -@mock_swf +@mock_swf_deprecated def test_list_closed_workflow_executions(): conn = setup_swf_environment() # Leave one workflow execution open to make sure it isn't displayed @@ -178,7 +178,7 @@ def test_list_closed_workflow_executions(): # TerminateWorkflowExecution endpoint -@mock_swf +@mock_swf_deprecated def test_terminate_workflow_execution(): conn = setup_swf_environment() run_id = conn.start_workflow_execution( @@ -200,7 +200,7 @@ def test_terminate_workflow_execution(): attrs["cause"].should.equal("OPERATOR_INITIATED") -@mock_swf +@mock_swf_deprecated def test_terminate_workflow_execution_with_wrong_workflow_or_run_id(): conn = setup_swf_environment() run_id = conn.start_workflow_execution( diff --git a/tests/test_swf/responses/test_workflow_types.py b/tests/test_swf/responses/test_workflow_types.py index 04521ff6e..1e838c2ee 100644 --- a/tests/test_swf/responses/test_workflow_types.py +++ b/tests/test_swf/responses/test_workflow_types.py @@ -1,11 +1,12 @@ +import sure import boto -from moto import mock_swf +from moto import mock_swf_deprecated from boto.swf.exceptions import SWFResponseError # RegisterWorkflowType endpoint -@mock_swf +@mock_swf_deprecated def test_register_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -17,7 +18,7 @@ def test_register_workflow_type(): actype["workflowType"]["version"].should.equal("v1.0") -@mock_swf +@mock_swf_deprecated def test_register_already_existing_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -28,7 +29,7 @@ def test_register_already_existing_workflow_type(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_register_with_wrong_parameter_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -39,7 +40,7 @@ def test_register_with_wrong_parameter_type(): # ListWorkflowTypes endpoint -@mock_swf +@mock_swf_deprecated def test_list_workflow_types(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -52,7 +53,7 @@ def test_list_workflow_types(): names.should.equal(["a-test-workflow", "b-test-workflow", "c-test-workflow"]) -@mock_swf +@mock_swf_deprecated def test_list_workflow_types_reverse_order(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -67,7 +68,7 @@ def test_list_workflow_types_reverse_order(): # DeprecateWorkflowType endpoint -@mock_swf +@mock_swf_deprecated def test_deprecate_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -80,7 +81,7 @@ def test_deprecate_workflow_type(): actype["workflowType"]["version"].should.equal("v1.0") -@mock_swf +@mock_swf_deprecated def test_deprecate_already_deprecated_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -92,7 +93,7 @@ def test_deprecate_already_deprecated_workflow_type(): ).should.throw(SWFResponseError) -@mock_swf +@mock_swf_deprecated def test_deprecate_non_existent_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -103,7 +104,7 @@ def test_deprecate_non_existent_workflow_type(): # DescribeWorkflowType endpoint -@mock_swf +@mock_swf_deprecated def test_describe_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") @@ -120,7 +121,7 @@ def test_describe_workflow_type(): infos["status"].should.equal("REGISTERED") -@mock_swf +@mock_swf_deprecated def test_describe_non_existent_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60") From 678f73389fd8ce7d1383620ff1e3587f80c6662f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Feb 2017 22:45:20 -0500 Subject: [PATCH 020/274] Fix package being submodule. --- moto/packages/responses | 1 - moto/packages/responses/.gitignore | 12 + moto/packages/responses/.travis.yml | 27 ++ moto/packages/responses/CHANGES | 32 ++ moto/packages/responses/LICENSE | 201 ++++++++++ moto/packages/responses/MANIFEST.in | 2 + moto/packages/responses/Makefile | 16 + moto/packages/responses/README.rst | 190 ++++++++++ moto/packages/responses/__init__.py | 0 moto/packages/responses/responses.py | 321 ++++++++++++++++ moto/packages/responses/setup.cfg | 5 + moto/packages/responses/setup.py | 98 +++++ moto/packages/responses/test_responses.py | 443 ++++++++++++++++++++++ moto/packages/responses/tox.ini | 11 + 14 files changed, 1358 insertions(+), 1 deletion(-) delete mode 160000 moto/packages/responses create mode 100644 moto/packages/responses/.gitignore create mode 100644 moto/packages/responses/.travis.yml create mode 100644 moto/packages/responses/CHANGES create mode 100644 moto/packages/responses/LICENSE create mode 100644 moto/packages/responses/MANIFEST.in create mode 100644 moto/packages/responses/Makefile create mode 100644 moto/packages/responses/README.rst create mode 100644 moto/packages/responses/__init__.py create mode 100644 moto/packages/responses/responses.py create mode 100644 moto/packages/responses/setup.cfg create mode 100644 moto/packages/responses/setup.py create mode 100644 moto/packages/responses/test_responses.py create mode 100644 moto/packages/responses/tox.ini diff --git a/moto/packages/responses b/moto/packages/responses deleted file mode 160000 index 8d500447e..000000000 --- a/moto/packages/responses +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8d500447e3d5c2b96ace2eb7ab0f60158e921ed8 diff --git a/moto/packages/responses/.gitignore b/moto/packages/responses/.gitignore new file mode 100644 index 000000000..5d4406b8d --- /dev/null +++ b/moto/packages/responses/.gitignore @@ -0,0 +1,12 @@ +.arcconfig +.coverage +.DS_Store +.idea +*.db +*.egg-info +*.pyc +/htmlcov +/dist +/build +/.cache +/.tox diff --git a/moto/packages/responses/.travis.yml b/moto/packages/responses/.travis.yml new file mode 100644 index 000000000..9ab219db0 --- /dev/null +++ b/moto/packages/responses/.travis.yml @@ -0,0 +1,27 @@ +language: python +sudo: false +python: + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "3.5" +cache: + directories: + - .pip_download_cache +env: + matrix: + - REQUESTS=requests==2.0 + - REQUESTS=-U requests + - REQUESTS="-e git+git://github.com/kennethreitz/requests.git#egg=requests" + global: + - PIP_DOWNLOAD_CACHE=".pip_download_cache" +matrix: + allow_failures: + - env: 'REQUESTS="-e git+git://github.com/kennethreitz/requests.git#egg=requests"' +install: + - "pip install ${REQUESTS}" + - make develop +script: + - if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then make lint; fi + - py.test . --cov responses --cov-report term-missing diff --git a/moto/packages/responses/CHANGES b/moto/packages/responses/CHANGES new file mode 100644 index 000000000..1bfd7ead8 --- /dev/null +++ b/moto/packages/responses/CHANGES @@ -0,0 +1,32 @@ +Unreleased +---------- + +- Allow empty list/dict as json object (GH-100) + +0.5.1 +----- + +- Add LICENSE, README and CHANGES to the PyPI distribution (GH-97). + +0.5.0 +----- + +- Allow passing a JSON body to `response.add` (GH-82) +- Improve ConnectionError emulation (GH-73) +- Correct assertion in assert_all_requests_are_fired (GH-71) + +0.4.0 +----- + +- Requests 2.0+ is required +- Mocking now happens on the adapter instead of the session + +0.3.0 +----- + +- Add the ability to mock errors (GH-22) +- Add responses.mock context manager (GH-36) +- Support custom adapters (GH-33) +- Add support for regexp error matching (GH-25) +- Add support for dynamic bodies via `responses.add_callback` (GH-24) +- Preserve argspec when using `responses.activate` decorator (GH-18) diff --git a/moto/packages/responses/LICENSE b/moto/packages/responses/LICENSE new file mode 100644 index 000000000..52b44b20a --- /dev/null +++ b/moto/packages/responses/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2015 David Cramer + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/moto/packages/responses/MANIFEST.in b/moto/packages/responses/MANIFEST.in new file mode 100644 index 000000000..ef901684c --- /dev/null +++ b/moto/packages/responses/MANIFEST.in @@ -0,0 +1,2 @@ +include README.rst CHANGES LICENSE +global-exclude *~ diff --git a/moto/packages/responses/Makefile b/moto/packages/responses/Makefile new file mode 100644 index 000000000..9da42c6d1 --- /dev/null +++ b/moto/packages/responses/Makefile @@ -0,0 +1,16 @@ +develop: + pip install -e . + make install-test-requirements + +install-test-requirements: + pip install "file://`pwd`#egg=responses[tests]" + +test: develop lint + @echo "Running Python tests" + py.test . + @echo "" + +lint: + @echo "Linting Python files" + PYFLAKES_NODOCTEST=1 flake8 . + @echo "" diff --git a/moto/packages/responses/README.rst b/moto/packages/responses/README.rst new file mode 100644 index 000000000..5f946fcde --- /dev/null +++ b/moto/packages/responses/README.rst @@ -0,0 +1,190 @@ +Responses +========= + +.. image:: https://travis-ci.org/getsentry/responses.svg?branch=master + :target: https://travis-ci.org/getsentry/responses + +A utility library for mocking out the `requests` Python library. + +.. note:: Responses requires Requests >= 2.0 + +Response body as string +----------------------- + +.. code-block:: python + + import responses + import requests + + @responses.activate + def test_my_api(): + responses.add(responses.GET, 'http://twitter.com/api/1/foobar', + body='{"error": "not found"}', status=404, + content_type='application/json') + + resp = requests.get('http://twitter.com/api/1/foobar') + + assert resp.json() == {"error": "not found"} + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' + assert responses.calls[0].response.text == '{"error": "not found"}' + +You can also specify a JSON object instead of a body string. + +.. code-block:: python + + import responses + import requests + + @responses.activate + def test_my_api(): + responses.add(responses.GET, 'http://twitter.com/api/1/foobar', + json={"error": "not found"}, status=404) + + resp = requests.get('http://twitter.com/api/1/foobar') + + assert resp.json() == {"error": "not found"} + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' + assert responses.calls[0].response.text == '{"error": "not found"}' + +Request callback +---------------- + +.. code-block:: python + + import json + + import responses + import requests + + @responses.activate + def test_calc_api(): + + def request_callback(request): + payload = json.loads(request.body) + resp_body = {'value': sum(payload['numbers'])} + headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'} + return (200, headers, json.dumps(resp_body)) + + responses.add_callback( + responses.POST, 'http://calc.com/sum', + callback=request_callback, + content_type='application/json', + ) + + resp = requests.post( + 'http://calc.com/sum', + json.dumps({'numbers': [1, 2, 3]}), + headers={'content-type': 'application/json'}, + ) + + assert resp.json() == {'value': 6} + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == 'http://calc.com/sum' + assert responses.calls[0].response.text == '{"value": 6}' + assert ( + responses.calls[0].response.headers['request-id'] == + '728d329e-0e86-11e4-a748-0c84dc037c13' + ) + +Instead of passing a string URL into `responses.add` or `responses.add_callback` +you can also supply a compiled regular expression. + +.. code-block:: python + + import re + import responses + import requests + + # Instead of + responses.add(responses.GET, 'http://twitter.com/api/1/foobar', + body='{"error": "not found"}', status=404, + content_type='application/json') + + # You can do the following + url_re = re.compile(r'https?://twitter\.com/api/\d+/foobar') + responses.add(responses.GET, url_re, + body='{"error": "not found"}', status=404, + content_type='application/json') + +A response can also throw an exception as follows. + +.. code-block:: python + + import responses + import requests + from requests.exceptions import HTTPError + + exception = HTTPError('Something went wrong') + responses.add(responses.GET, 'http://twitter.com/api/1/foobar', + body=exception) + # All calls to 'http://twitter.com/api/1/foobar' will throw exception. + + +Responses as a context manager +------------------------------ + +.. code-block:: python + + import responses + import requests + + + def test_my_api(): + with responses.RequestsMock() as rsps: + rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', + body='{}', status=200, + content_type='application/json') + resp = requests.get('http://twitter.com/api/1/foobar') + + assert resp.status_code == 200 + + # outside the context manager requests will hit the remote server + resp = requests.get('http://twitter.com/api/1/foobar') + resp.status_code == 404 + + +Assertions on declared responses +-------------------------------- + +When used as a context manager, Responses will, by default, raise an assertion +error if a url was registered but not accessed. This can be disabled by passing +the ``assert_all_requests_are_fired`` value: + +.. code-block:: python + + import responses + import requests + + + def test_my_api(): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', + body='{}', status=200, + content_type='application/json') + +Multiple Responses +------------------ +You can also use ``assert_all_requests_are_fired`` to add multiple responses for the same url: + +.. code-block:: python + + import responses + import requests + + + def test_my_api(): + with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: + rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) + rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', + body='{}', status=200, + content_type='application/json') + + resp = requests.get('http://twitter.com/api/1/foobar') + assert resp.status_code == 500 + resp = requests.get('http://twitter.com/api/1/foobar') + assert resp.status_code == 200 diff --git a/moto/packages/responses/__init__.py b/moto/packages/responses/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/moto/packages/responses/responses.py b/moto/packages/responses/responses.py new file mode 100644 index 000000000..735655664 --- /dev/null +++ b/moto/packages/responses/responses.py @@ -0,0 +1,321 @@ +from __future__ import ( + absolute_import, print_function, division, unicode_literals +) + +import inspect +import json as json_module +import re +import six + +from collections import namedtuple, Sequence, Sized +from functools import update_wrapper +from cookies import Cookies +from requests.utils import cookiejar_from_dict +from requests.exceptions import ConnectionError +from requests.sessions import REDIRECT_STATI + +try: + from requests.packages.urllib3.response import HTTPResponse +except ImportError: + from urllib3.response import HTTPResponse + +if six.PY2: + from urlparse import urlparse, parse_qsl +else: + from urllib.parse import urlparse, parse_qsl + +if six.PY2: + try: + from six import cStringIO as BufferIO + except ImportError: + from six import StringIO as BufferIO +else: + from io import BytesIO as BufferIO + + +Call = namedtuple('Call', ['request', 'response']) + +_wrapper_template = """\ +def wrapper%(signature)s: + with responses: + return func%(funcargs)s +""" + + +def _is_string(s): + return isinstance(s, (six.string_types, six.text_type)) + + +def _is_redirect(response): + try: + # 2.0.0 <= requests <= 2.2 + return response.is_redirect + except AttributeError: + # requests > 2.2 + return ( + # use request.sessions conditional + response.status_code in REDIRECT_STATI and + 'location' in response.headers + ) + + +def get_wrapped(func, wrapper_template, evaldict): + # Preserve the argspec for the wrapped function so that testing + # tools such as pytest can continue to use their fixture injection. + args, a, kw, defaults = inspect.getargspec(func) + + signature = inspect.formatargspec(args, a, kw, defaults) + is_bound_method = hasattr(func, '__self__') + if is_bound_method: + args = args[1:] # Omit 'self' + callargs = inspect.formatargspec(args, a, kw, None) + + ctx = {'signature': signature, 'funcargs': callargs} + six.exec_(wrapper_template % ctx, evaldict) + + wrapper = evaldict['wrapper'] + + update_wrapper(wrapper, func) + if is_bound_method: + wrapper = wrapper.__get__(func.__self__, type(func.__self__)) + return wrapper + + +class CallList(Sequence, Sized): + def __init__(self): + self._calls = [] + + def __iter__(self): + return iter(self._calls) + + def __len__(self): + return len(self._calls) + + def __getitem__(self, idx): + return self._calls[idx] + + def add(self, request, response): + self._calls.append(Call(request, response)) + + def reset(self): + self._calls = [] + + +def _ensure_url_default_path(url, match_querystring): + if _is_string(url) and url.count('/') == 2: + if match_querystring: + return url.replace('?', '/?', 1) + else: + return url + '/' + return url + + +class RequestsMock(object): + DELETE = 'DELETE' + GET = 'GET' + HEAD = 'HEAD' + OPTIONS = 'OPTIONS' + PATCH = 'PATCH' + POST = 'POST' + PUT = 'PUT' + + def __init__(self, assert_all_requests_are_fired=True): + self._calls = CallList() + self.reset() + self.assert_all_requests_are_fired = assert_all_requests_are_fired + + def reset(self): + self._urls = [] + self._calls.reset() + + def add(self, method, url, body='', match_querystring=False, + status=200, adding_headers=None, stream=False, + content_type='text/plain', json=None): + + # if we were passed a `json` argument, + # override the body and content_type + if json is not None: + body = json_module.dumps(json) + content_type = 'application/json' + + # ensure the url has a default path set if the url is a string + url = _ensure_url_default_path(url, match_querystring) + + # body must be bytes + if isinstance(body, six.text_type): + body = body.encode('utf-8') + + self._urls.append({ + 'url': url, + 'method': method, + 'body': body, + 'content_type': content_type, + 'match_querystring': match_querystring, + 'status': status, + 'adding_headers': adding_headers, + 'stream': stream, + }) + + def add_callback(self, method, url, callback, match_querystring=False, + content_type='text/plain'): + # ensure the url has a default path set if the url is a string + # url = _ensure_url_default_path(url, match_querystring) + + self._urls.append({ + 'url': url, + 'method': method, + 'callback': callback, + 'content_type': content_type, + 'match_querystring': match_querystring, + }) + + @property + def calls(self): + return self._calls + + def __enter__(self): + self.start() + return self + + def __exit__(self, type, value, traceback): + success = type is None + self.stop(allow_assert=success) + self.reset() + return success + + def activate(self, func): + evaldict = {'responses': self, 'func': func} + return get_wrapped(func, _wrapper_template, evaldict) + + def _find_match(self, request): + for match in self._urls: + if request.method != match['method']: + continue + + if not self._has_url_match(match, request.url): + continue + + break + else: + return None + if self.assert_all_requests_are_fired: + # for each found match remove the url from the stack + self._urls.remove(match) + return match + + def _has_url_match(self, match, request_url): + url = match['url'] + + if not match['match_querystring']: + request_url = request_url.split('?', 1)[0] + + if _is_string(url): + if match['match_querystring']: + return self._has_strict_url_match(url, request_url) + else: + return url == request_url + elif isinstance(url, re._pattern_type) and url.match(request_url): + return True + else: + return False + + def _has_strict_url_match(self, url, other): + url_parsed = urlparse(url) + other_parsed = urlparse(other) + + if url_parsed[:3] != other_parsed[:3]: + return False + + url_qsl = sorted(parse_qsl(url_parsed.query)) + other_qsl = sorted(parse_qsl(other_parsed.query)) + return url_qsl == other_qsl + + def _on_request(self, adapter, request, **kwargs): + match = self._find_match(request) + # TODO(dcramer): find the correct class for this + if match is None: + error_msg = 'Connection refused: {0} {1}'.format(request.method, + request.url) + response = ConnectionError(error_msg) + response.request = request + + self._calls.add(request, response) + raise response + + if 'body' in match and isinstance(match['body'], Exception): + self._calls.add(request, match['body']) + raise match['body'] + + headers = {} + if match['content_type'] is not None: + headers['Content-Type'] = match['content_type'] + + if 'callback' in match: # use callback + status, r_headers, body = match['callback'](request) + if isinstance(body, six.text_type): + body = body.encode('utf-8') + body = BufferIO(body) + headers.update(r_headers) + + elif 'body' in match: + if match['adding_headers']: + headers.update(match['adding_headers']) + status = match['status'] + body = BufferIO(match['body']) + + response = HTTPResponse( + status=status, + reason=six.moves.http_client.responses[status], + body=body, + headers=headers, + preload_content=False, + ) + + response = adapter.build_response(request, response) + if not match.get('stream'): + response.content # NOQA + + try: + resp_cookies = Cookies.from_request(response.headers['set-cookie']) + response.cookies = cookiejar_from_dict(dict( + (v.name, v.value) + for _, v + in resp_cookies.items() + )) + except (KeyError, TypeError): + pass + + self._calls.add(request, response) + + return response + + def start(self): + try: + from unittest import mock + except ImportError: + import mock + + def unbound_on_send(adapter, request, *a, **kwargs): + return self._on_request(adapter, request, *a, **kwargs) + self._patcher1 = mock.patch('botocore.vendored.requests.adapters.HTTPAdapter.send', + unbound_on_send) + self._patcher1.start() + self._patcher2 = mock.patch('requests.adapters.HTTPAdapter.send', + unbound_on_send) + self._patcher2.start() + + def stop(self, allow_assert=True): + self._patcher1.stop() + self._patcher2.stop() + if allow_assert and self.assert_all_requests_are_fired and self._urls: + raise AssertionError( + 'Not all requests have been executed {0!r}'.format( + [(url['method'], url['url']) for url in self._urls])) + + +# expose default mock namespace +mock = _default_mock = RequestsMock(assert_all_requests_are_fired=False) +__all__ = [] +for __attr in (a for a in dir(_default_mock) if not a.startswith('_')): + __all__.append(__attr) + globals()[__attr] = getattr(_default_mock, __attr) diff --git a/moto/packages/responses/setup.cfg b/moto/packages/responses/setup.cfg new file mode 100644 index 000000000..9b6594f2e --- /dev/null +++ b/moto/packages/responses/setup.cfg @@ -0,0 +1,5 @@ +[pytest] +addopts=--tb=short + +[bdist_wheel] +universal=1 diff --git a/moto/packages/responses/setup.py b/moto/packages/responses/setup.py new file mode 100644 index 000000000..bab522865 --- /dev/null +++ b/moto/packages/responses/setup.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +""" +responses +========= + +A utility library for mocking out the `requests` Python library. + +:copyright: (c) 2015 David Cramer +:license: Apache 2.0 +""" + +import sys +import logging + +from setuptools import setup +from setuptools.command.test import test as TestCommand +import pkg_resources + + +setup_requires = [] + +if 'test' in sys.argv: + setup_requires.append('pytest') + +install_requires = [ + 'requests>=2.0', + 'cookies', + 'six', +] + +tests_require = [ + 'pytest', + 'coverage >= 3.7.1, < 5.0.0', + 'pytest-cov', + 'flake8', +] + + +extras_require = { + ':python_version in "2.6, 2.7, 3.2"': ['mock'], + 'tests': tests_require, +} + +try: + if 'bdist_wheel' not in sys.argv: + for key, value in extras_require.items(): + if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]): + install_requires.extend(value) +except Exception: + logging.getLogger(__name__).exception( + 'Something went wrong calculating platform specific dependencies, so ' + "you're getting them all!" + ) + for key, value in extras_require.items(): + if key.startswith(':'): + install_requires.extend(value) + + +class PyTest(TestCommand): + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = ['test_responses.py'] + self.test_suite = True + + def run_tests(self): + # import here, cause outside the eggs aren't loaded + import pytest + errno = pytest.main(self.test_args) + sys.exit(errno) + + +setup( + name='responses', + version='0.6.0', + author='David Cramer', + description=( + 'A utility library for mocking out the `requests` Python library.' + ), + url='https://github.com/getsentry/responses', + license='Apache 2.0', + long_description=open('README.rst').read(), + py_modules=['responses', 'test_responses'], + zip_safe=False, + install_requires=install_requires, + extras_require=extras_require, + tests_require=tests_require, + setup_requires=setup_requires, + cmdclass={'test': PyTest}, + include_package_data=True, + classifiers=[ + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development' + ], +) diff --git a/moto/packages/responses/test_responses.py b/moto/packages/responses/test_responses.py new file mode 100644 index 000000000..ba0126ad5 --- /dev/null +++ b/moto/packages/responses/test_responses.py @@ -0,0 +1,443 @@ +from __future__ import ( + absolute_import, print_function, division, unicode_literals +) + +import re +import requests +import responses +import pytest + +from inspect import getargspec +from requests.exceptions import ConnectionError, HTTPError + + +def assert_reset(): + assert len(responses._default_mock._urls) == 0 + assert len(responses.calls) == 0 + + +def assert_response(resp, body=None, content_type='text/plain'): + assert resp.status_code == 200 + assert resp.reason == 'OK' + if content_type is not None: + assert resp.headers['Content-Type'] == content_type + else: + assert 'Content-Type' not in resp.headers + assert resp.text == body + + +def test_response(): + @responses.activate + def run(): + responses.add(responses.GET, 'http://example.com', body=b'test') + resp = requests.get('http://example.com') + assert_response(resp, 'test') + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == 'http://example.com/' + assert responses.calls[0].response.content == b'test' + + resp = requests.get('http://example.com?foo=bar') + assert_response(resp, 'test') + assert len(responses.calls) == 2 + assert responses.calls[1].request.url == 'http://example.com/?foo=bar' + assert responses.calls[1].response.content == b'test' + + run() + assert_reset() + + +def test_connection_error(): + @responses.activate + def run(): + responses.add(responses.GET, 'http://example.com') + + with pytest.raises(ConnectionError): + requests.get('http://example.com/foo') + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == 'http://example.com/foo' + assert type(responses.calls[0].response) is ConnectionError + assert responses.calls[0].response.request + + run() + assert_reset() + + +def test_match_querystring(): + @responses.activate + def run(): + url = 'http://example.com?test=1&foo=bar' + responses.add( + responses.GET, url, + match_querystring=True, body=b'test') + resp = requests.get('http://example.com?test=1&foo=bar') + assert_response(resp, 'test') + resp = requests.get('http://example.com?foo=bar&test=1') + assert_response(resp, 'test') + + run() + assert_reset() + + +def test_match_querystring_error(): + @responses.activate + def run(): + responses.add( + responses.GET, 'http://example.com/?test=1', + match_querystring=True) + + with pytest.raises(ConnectionError): + requests.get('http://example.com/foo/?test=2') + + run() + assert_reset() + + +def test_match_querystring_regex(): + @responses.activate + def run(): + """Note that `match_querystring` value shouldn't matter when passing a + regular expression""" + + responses.add( + responses.GET, re.compile(r'http://example\.com/foo/\?test=1'), + body='test1', match_querystring=True) + + resp = requests.get('http://example.com/foo/?test=1') + assert_response(resp, 'test1') + + responses.add( + responses.GET, re.compile(r'http://example\.com/foo/\?test=2'), + body='test2', match_querystring=False) + + resp = requests.get('http://example.com/foo/?test=2') + assert_response(resp, 'test2') + + run() + assert_reset() + + +def test_match_querystring_error_regex(): + @responses.activate + def run(): + """Note that `match_querystring` value shouldn't matter when passing a + regular expression""" + + responses.add( + responses.GET, re.compile(r'http://example\.com/foo/\?test=1'), + match_querystring=True) + + with pytest.raises(ConnectionError): + requests.get('http://example.com/foo/?test=3') + + responses.add( + responses.GET, re.compile(r'http://example\.com/foo/\?test=2'), + match_querystring=False) + + with pytest.raises(ConnectionError): + requests.get('http://example.com/foo/?test=4') + + run() + assert_reset() + + +def test_accept_string_body(): + @responses.activate + def run(): + url = 'http://example.com/' + responses.add( + responses.GET, url, body='test') + resp = requests.get(url) + assert_response(resp, 'test') + + run() + assert_reset() + + +def test_accept_json_body(): + @responses.activate + def run(): + content_type = 'application/json' + + url = 'http://example.com/' + responses.add( + responses.GET, url, json={"message": "success"}) + resp = requests.get(url) + assert_response(resp, '{"message": "success"}', content_type) + + url = 'http://example.com/1/' + responses.add(responses.GET, url, json=[]) + resp = requests.get(url) + assert_response(resp, '[]', content_type) + + run() + assert_reset() + + +def test_no_content_type(): + @responses.activate + def run(): + url = 'http://example.com/' + responses.add( + responses.GET, url, body='test', content_type=None) + resp = requests.get(url) + assert_response(resp, 'test', content_type=None) + + run() + assert_reset() + + +def test_throw_connection_error_explicit(): + @responses.activate + def run(): + url = 'http://example.com' + exception = HTTPError('HTTP Error') + responses.add( + responses.GET, url, exception) + + with pytest.raises(HTTPError) as HE: + requests.get(url) + + assert str(HE.value) == 'HTTP Error' + + run() + assert_reset() + + +def test_callback(): + body = b'test callback' + status = 400 + reason = 'Bad Request' + headers = {'foo': 'bar'} + url = 'http://example.com/' + + def request_callback(request): + return (status, headers, body) + + @responses.activate + def run(): + responses.add_callback(responses.GET, url, request_callback) + resp = requests.get(url) + assert resp.text == "test callback" + assert resp.status_code == status + assert resp.reason == reason + assert 'foo' in resp.headers + assert resp.headers['foo'] == 'bar' + + run() + assert_reset() + + +def test_callback_no_content_type(): + body = b'test callback' + status = 400 + reason = 'Bad Request' + headers = {'foo': 'bar'} + url = 'http://example.com/' + + def request_callback(request): + return (status, headers, body) + + @responses.activate + def run(): + responses.add_callback( + responses.GET, url, request_callback, content_type=None) + resp = requests.get(url) + assert resp.text == "test callback" + assert resp.status_code == status + assert resp.reason == reason + assert 'foo' in resp.headers + assert 'Content-Type' not in resp.headers + + run() + assert_reset() + + +def test_regular_expression_url(): + @responses.activate + def run(): + url = re.compile(r'https?://(.*\.)?example.com') + responses.add(responses.GET, url, body=b'test') + + resp = requests.get('http://example.com') + assert_response(resp, 'test') + + resp = requests.get('https://example.com') + assert_response(resp, 'test') + + resp = requests.get('https://uk.example.com') + assert_response(resp, 'test') + + with pytest.raises(ConnectionError): + requests.get('https://uk.exaaample.com') + + run() + assert_reset() + + +def test_custom_adapter(): + @responses.activate + def run(): + url = "http://example.com" + responses.add(responses.GET, url, body=b'test') + + calls = [0] + + class DummyAdapter(requests.adapters.HTTPAdapter): + def send(self, *a, **k): + calls[0] += 1 + return super(DummyAdapter, self).send(*a, **k) + + # Test that the adapter is actually used + session = requests.Session() + session.mount("http://", DummyAdapter()) + + resp = session.get(url, allow_redirects=False) + assert calls[0] == 1 + + # Test that the response is still correctly emulated + session = requests.Session() + session.mount("http://", DummyAdapter()) + + resp = session.get(url) + assert_response(resp, 'test') + + run() + + +def test_responses_as_context_manager(): + def run(): + with responses.mock: + responses.add(responses.GET, 'http://example.com', body=b'test') + resp = requests.get('http://example.com') + assert_response(resp, 'test') + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == 'http://example.com/' + assert responses.calls[0].response.content == b'test' + + resp = requests.get('http://example.com?foo=bar') + assert_response(resp, 'test') + assert len(responses.calls) == 2 + assert (responses.calls[1].request.url == + 'http://example.com/?foo=bar') + assert responses.calls[1].response.content == b'test' + + run() + assert_reset() + + +def test_activate_doesnt_change_signature(): + def test_function(a, b=None): + return (a, b) + + decorated_test_function = responses.activate(test_function) + assert getargspec(test_function) == getargspec(decorated_test_function) + assert decorated_test_function(1, 2) == test_function(1, 2) + assert decorated_test_function(3) == test_function(3) + + +def test_activate_doesnt_change_signature_for_method(): + class TestCase(object): + + def test_function(self, a, b=None): + return (self, a, b) + + test_case = TestCase() + argspec = getargspec(test_case.test_function) + decorated_test_function = responses.activate(test_case.test_function) + assert argspec == getargspec(decorated_test_function) + assert decorated_test_function(1, 2) == test_case.test_function(1, 2) + assert decorated_test_function(3) == test_case.test_function(3) + + +def test_response_cookies(): + body = b'test callback' + status = 200 + headers = {'set-cookie': 'session_id=12345; a=b; c=d'} + url = 'http://example.com/' + + def request_callback(request): + return (status, headers, body) + + @responses.activate + def run(): + responses.add_callback(responses.GET, url, request_callback) + resp = requests.get(url) + assert resp.text == "test callback" + assert resp.status_code == status + assert 'session_id' in resp.cookies + assert resp.cookies['session_id'] == '12345' + assert resp.cookies['a'] == 'b' + assert resp.cookies['c'] == 'd' + run() + assert_reset() + + +def test_assert_all_requests_are_fired(): + def run(): + with pytest.raises(AssertionError) as excinfo: + with responses.RequestsMock( + assert_all_requests_are_fired=True) as m: + m.add(responses.GET, 'http://example.com', body=b'test') + assert 'http://example.com' in str(excinfo.value) + assert responses.GET in str(excinfo) + + # check that assert_all_requests_are_fired default to True + with pytest.raises(AssertionError): + with responses.RequestsMock() as m: + m.add(responses.GET, 'http://example.com', body=b'test') + + # check that assert_all_requests_are_fired doesn't swallow exceptions + with pytest.raises(ValueError): + with responses.RequestsMock() as m: + m.add(responses.GET, 'http://example.com', body=b'test') + raise ValueError() + + run() + assert_reset() + + +def test_allow_redirects_samehost(): + redirecting_url = 'http://example.com' + final_url_path = '/1' + final_url = '{0}{1}'.format(redirecting_url, final_url_path) + url_re = re.compile(r'^http://example.com(/)?(\d+)?$') + + def request_callback(request): + # endpoint of chained redirect + if request.url.endswith(final_url_path): + return 200, (), b'test' + # otherwise redirect to an integer path + else: + if request.url.endswith('/0'): + n = 1 + else: + n = 0 + redirect_headers = {'location': '/{0!s}'.format(n)} + return 301, redirect_headers, None + + def run(): + # setup redirect + with responses.mock: + responses.add_callback(responses.GET, url_re, request_callback) + resp_no_redirects = requests.get(redirecting_url, + allow_redirects=False) + assert resp_no_redirects.status_code == 301 + assert len(responses.calls) == 1 # 1x300 + assert responses.calls[0][1].status_code == 301 + assert_reset() + + with responses.mock: + responses.add_callback(responses.GET, url_re, request_callback) + resp_yes_redirects = requests.get(redirecting_url, + allow_redirects=True) + assert len(responses.calls) == 3 # 2x300 + 1x200 + assert len(resp_yes_redirects.history) == 2 + assert resp_yes_redirects.status_code == 200 + assert final_url == resp_yes_redirects.url + status_codes = [call[1].status_code for call in responses.calls] + assert status_codes == [301, 301, 200] + assert_reset() + + run() + assert_reset() diff --git a/moto/packages/responses/tox.ini b/moto/packages/responses/tox.ini new file mode 100644 index 000000000..0a31c03ab --- /dev/null +++ b/moto/packages/responses/tox.ini @@ -0,0 +1,11 @@ + +[tox] +envlist = {py26,py27,py32,py33,py34,py35} + +[testenv] +deps = + pytest + pytest-cov + pytest-flakes +commands = + py.test . --cov responses --cov-report term-missing --flakes From 468a1b970c8fcf6e00a6809e345e9304d9af935c Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Feb 2017 22:47:33 -0500 Subject: [PATCH 021/274] Add responses dependencies. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 52635d00b..ee9c07aed 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,8 @@ from setuptools import setup, find_packages install_requires = [ "Jinja2>=2.8", "boto>=2.36.0", - "requests", + "cookies", + "requests>=2.0", "xmltodict", "six", "werkzeug", From cad185c74da5ec14cbd3f3212ad9e6f7b8303c6d Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 16 Feb 2017 22:51:04 -0500 Subject: [PATCH 022/274] Cleanup headers and encoding. --- moto/apigateway/models.py | 2 +- moto/apigateway/responses.py | 40 +++++----- moto/awslambda/models.py | 2 +- moto/awslambda/responses.py | 23 +++--- moto/core/models.py | 10 ++- moto/core/responses.py | 10 +-- moto/core/utils.py | 28 ++++++- moto/datapipeline/responses.py | 2 +- moto/dynamodb/responses.py | 2 +- moto/dynamodb2/responses.py | 2 +- moto/ecs/responses.py | 2 +- moto/events/responses.py | 2 +- moto/kinesis/responses.py | 2 +- moto/kms/responses.py | 2 +- moto/opsworks/responses.py | 2 +- moto/s3/responses.py | 102 +++++++++++++----------- moto/swf/responses.py | 2 +- tests/test_sns/test_publishing.py | 2 +- tests/test_sns/test_publishing_boto3.py | 2 +- 19 files changed, 138 insertions(+), 101 deletions(-) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index be0bfa434..bab0bc1d0 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -331,7 +331,7 @@ class RestAPI(object): def update_integration_mocks(self, stage_name): stage_url = STAGE_URL.format(api_id=self.id, region_name=self.region_name, stage_name=stage_name) - responses.add_callback(responses.GET, stage_url, callback=self.resource_callback) + responses.add_callback(responses.GET, stage_url.lower(), callback=self.resource_callback) def create_stage(self, name, deployment_id,variables=None,description='',cacheClusterEnabled=None,cacheClusterSize=None): if variables is None: diff --git a/moto/apigateway/responses.py b/moto/apigateway/responses.py index e8c353f4e..a7bb28c6e 100644 --- a/moto/apigateway/responses.py +++ b/moto/apigateway/responses.py @@ -10,11 +10,11 @@ from .exceptions import StageNotFoundException class APIGatewayResponse(BaseResponse): def _get_param(self, key): - return json.loads(self.body.decode("ascii")).get(key) + return json.loads(self.body).get(key) def _get_param_with_default_value(self, key, default): - jsonbody = json.loads(self.body.decode("ascii")) + jsonbody = json.loads(self.body) if key in jsonbody: return jsonbody.get(key) @@ -30,14 +30,14 @@ class APIGatewayResponse(BaseResponse): if self.method == 'GET': apis = self.backend.list_apis() - return 200, headers, json.dumps({"item": [ + return 200, {}, json.dumps({"item": [ api.to_dict() for api in apis ]}) elif self.method == 'POST': name = self._get_param('name') description = self._get_param('description') rest_api = self.backend.create_rest_api(name, description) - return 200, headers, json.dumps(rest_api.to_dict()) + return 200, {}, json.dumps(rest_api.to_dict()) def restapis_individual(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -45,10 +45,10 @@ class APIGatewayResponse(BaseResponse): if self.method == 'GET': rest_api = self.backend.get_rest_api(function_id) - return 200, headers, json.dumps(rest_api.to_dict()) + return 200, {}, json.dumps(rest_api.to_dict()) elif self.method == 'DELETE': rest_api = self.backend.delete_rest_api(function_id) - return 200, headers, json.dumps(rest_api.to_dict()) + return 200, {}, json.dumps(rest_api.to_dict()) def resources(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -56,7 +56,7 @@ class APIGatewayResponse(BaseResponse): if self.method == 'GET': resources = self.backend.list_resources(function_id) - return 200, headers, json.dumps({"item": [ + return 200, {}, json.dumps({"item": [ resource.to_dict() for resource in resources ]}) @@ -72,7 +72,7 @@ class APIGatewayResponse(BaseResponse): resource = self.backend.create_resource(function_id, resource_id, path_part) elif self.method == 'DELETE': resource = self.backend.delete_resource(function_id, resource_id) - return 200, headers, json.dumps(resource.to_dict()) + return 200, {}, json.dumps(resource.to_dict()) def resource_methods(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -83,11 +83,11 @@ class APIGatewayResponse(BaseResponse): if self.method == 'GET': method = self.backend.get_method(function_id, resource_id, method_type) - return 200, headers, json.dumps(method) + return 200, {}, json.dumps(method) elif self.method == 'PUT': authorization_type = self._get_param("authorizationType") method = self.backend.create_method(function_id, resource_id, method_type, authorization_type) - return 200, headers, json.dumps(method) + return 200, {}, json.dumps(method) def resource_method_responses(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -103,7 +103,7 @@ class APIGatewayResponse(BaseResponse): method_response = self.backend.create_method_response(function_id, resource_id, method_type, response_code) elif self.method == 'DELETE': method_response = self.backend.delete_method_response(function_id, resource_id, method_type, response_code) - return 200, headers, json.dumps(method_response) + return 200, {}, json.dumps(method_response) def restapis_stages(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -123,9 +123,9 @@ class APIGatewayResponse(BaseResponse): cacheClusterEnabled=cacheClusterEnabled, cacheClusterSize=cacheClusterSize) elif self.method == 'GET': stages = self.backend.get_stages(function_id) - return 200, headers, json.dumps({"item": stages}) + return 200, {}, json.dumps({"item": stages}) - return 200, headers, json.dumps(stage_response) + return 200, {}, json.dumps(stage_response) def stages(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -137,11 +137,11 @@ class APIGatewayResponse(BaseResponse): try: stage_response = self.backend.get_stage(function_id, stage_name) except StageNotFoundException as error: - return error.code, headers,'{{"message":"{0}","code":"{1}"}}'.format(error.message,error.error_type) + return error.code, {},'{{"message":"{0}","code":"{1}"}}'.format(error.message,error.error_type) elif self.method == 'PATCH': patch_operations = self._get_param('patchOperations') stage_response = self.backend.update_stage(function_id, stage_name, patch_operations) - return 200, headers, json.dumps(stage_response) + return 200, {}, json.dumps(stage_response) def integrations(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -159,7 +159,7 @@ class APIGatewayResponse(BaseResponse): integration_response = self.backend.create_integration(function_id, resource_id, method_type, integration_type, uri, request_templates=request_templates) elif self.method == 'DELETE': integration_response = self.backend.delete_integration(function_id, resource_id, method_type) - return 200, headers, json.dumps(integration_response) + return 200, {}, json.dumps(integration_response) def integration_responses(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -182,7 +182,7 @@ class APIGatewayResponse(BaseResponse): integration_response = self.backend.delete_integration_response( function_id, resource_id, method_type, status_code ) - return 200, headers, json.dumps(integration_response) + return 200, {}, json.dumps(integration_response) def deployments(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -190,13 +190,13 @@ class APIGatewayResponse(BaseResponse): if self.method == 'GET': deployments = self.backend.get_deployments(function_id) - return 200, headers, json.dumps({"item": deployments}) + return 200, {}, json.dumps({"item": deployments}) elif self.method == 'POST': name = self._get_param("stageName") description = self._get_param_with_default_value("description","") stage_variables = self._get_param_with_default_value('variables',{}) deployment = self.backend.create_deployment(function_id, name, description,stage_variables) - return 200, headers, json.dumps(deployment) + return 200, {}, json.dumps(deployment) def individual_deployment(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -208,4 +208,4 @@ class APIGatewayResponse(BaseResponse): deployment = self.backend.get_deployment(function_id, deployment_id) elif self.method == 'DELETE': deployment = self.backend.delete_deployment(function_id, deployment_id) - return 200, headers, json.dumps(deployment) + return 200, {}, json.dumps(deployment) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 069717ca4..e8595cc22 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -196,7 +196,7 @@ class LambdaBackend(BaseBackend): def __init__(self): self._functions = {} - + def has_function(self, function_name): return function_name in self._functions diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 708a8796e..0cd7c57ea 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -36,6 +36,7 @@ class LambdaResponse(BaseResponse): raise ValueError("Cannot handle request") def _invoke(self, request, full_url, headers): + response_headers = {} lambda_backend = self.get_lambda_backend(full_url) path = request.path if hasattr(request, 'path') else request.path_url @@ -43,15 +44,15 @@ class LambdaResponse(BaseResponse): if lambda_backend.has_function(function_name): fn = lambda_backend.get_function(function_name) - payload = fn.invoke(request, headers) - headers['Content-Length'] = str(len(payload)) - return 202, headers, payload + payload = fn.invoke(request, response_headers) + response_headers['Content-Length'] = str(len(payload)) + return 202, response_headers, payload else: - return 404, headers, "{}" + return 404, response_headers, "{}" def _list_functions(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - return 200, headers, json.dumps({ + return 200, {}, json.dumps({ "Functions": [fn.get_configuration() for fn in lambda_backend.list_functions()], # "NextMarker": str(uuid.uuid4()), }) @@ -62,10 +63,10 @@ class LambdaResponse(BaseResponse): try: fn = lambda_backend.create_function(spec) except ValueError as e: - return 400, headers, json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}) + return 400, {}, json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}) else: config = fn.get_configuration() - return 201, headers, json.dumps(config) + return 201, {}, json.dumps(config) def _delete_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) @@ -75,9 +76,9 @@ class LambdaResponse(BaseResponse): if lambda_backend.has_function(function_name): lambda_backend.delete_function(function_name) - return 204, headers, "" + return 204, {}, "" else: - return 404, headers, "{}" + return 404, {}, "{}" def _get_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) @@ -88,9 +89,9 @@ class LambdaResponse(BaseResponse): if lambda_backend.has_function(function_name): fn = lambda_backend.get_function(function_name) code = fn.get_code() - return 200, headers, json.dumps(code) + return 200, {}, json.dumps(code) else: - return 404, headers, "{}" + return 404, {}, "{}" def get_lambda_backend(self, full_url): from moto.awslambda.models import lambda_backends diff --git a/moto/core/models.py b/moto/core/models.py index fa6b74834..9570a86d4 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -8,7 +8,11 @@ import re from moto.packages.responses import responses from moto.packages.httpretty import HTTPretty from .responses import metadata_response -from .utils import convert_regex_to_flask_path, convert_flask_to_responses_response +from .utils import ( + convert_httpretty_response, + convert_regex_to_flask_path, + convert_flask_to_responses_response, +) class BaseMockAWS(object): nested_count = 0 @@ -93,14 +97,14 @@ class HttprettyMockAWS(BaseMockAWS): HTTPretty.register_uri( method=method, uri=re.compile(key), - body=value, + body=convert_httpretty_response(value), ) # Mock out localhost instance metadata HTTPretty.register_uri( method=method, uri=re.compile('http://169.254.169.254/latest/meta-data/.*'), - body=metadata_response + body=convert_httpretty_response(metadata_response), ) def disable_patching(self): diff --git a/moto/core/responses.py b/moto/core/responses.py index 337227d3c..05c882ba1 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -123,14 +123,14 @@ class BaseResponse(_TemplateEnvironmentMixin): for key, value in request.form.items(): querystring[key] = [value, ] + if isinstance(self.body, six.binary_type): + self.body = self.body.decode('utf-8') + if not querystring: querystring.update(parse_qs(urlparse(full_url).query, keep_blank_values=True)) if not querystring: if 'json' in request.headers.get('content-type', []) and self.aws_service_spec: - if isinstance(self.body, six.binary_type): - decoded = json.loads(self.body.decode('utf-8')) - else: - decoded = json.loads(self.body) + decoded = json.loads(self.body) target = request.headers.get('x-amz-target') or request.headers.get('X-Amz-Target') service, method = target.split('.') @@ -154,7 +154,7 @@ class BaseResponse(_TemplateEnvironmentMixin): self.headers = request.headers if 'host' not in self.headers: self.headers['host'] = urlparse(full_url).netloc - self.response_headers = headers + self.response_headers = {"server": "amazon.com"} def get_region_from_url(self, full_url): match = re.search(self.region_regex, full_url) diff --git a/moto/core/utils.py b/moto/core/utils.py index 0f4b20b6d..451d1a761 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -79,6 +79,29 @@ def convert_regex_to_flask_path(url_path): return url_path +class convert_httpretty_response(object): + + def __init__(self, callback): + self.callback = callback + + @property + def __name__(self): + # For instance methods, use class and method names. Otherwise + # use module and method name + if inspect.ismethod(self.callback): + outer = self.callback.__self__.__class__.__name__ + else: + outer = self.callback.__module__ + return "{0}.{1}".format(outer, self.callback.__name__) + + def __call__(self, request, url, headers, **kwargs): + result = self.callback(request, url, headers) + status, headers, response = result + if 'server' not in headers: + headers["server"] = "amazon.com" + return status, headers, response + + class convert_flask_to_httpretty_response(object): def __init__(self, callback): @@ -119,8 +142,11 @@ class convert_flask_to_responses_response(object): return "{0}.{1}".format(outer, self.callback.__name__) def __call__(self, request, *args, **kwargs): + for key, val in request.headers.items(): + if isinstance(val, six.binary_type): + request.headers[key] = val.decode("utf-8") + result = self.callback(request, request.url, request.headers) - # result is a status, headers, response tuple status, headers, response = result return status, headers, response diff --git a/moto/datapipeline/responses.py b/moto/datapipeline/responses.py index 70d19d189..2607f685d 100644 --- a/moto/datapipeline/responses.py +++ b/moto/datapipeline/responses.py @@ -12,7 +12,7 @@ class DataPipelineResponse(BaseResponse): def parameters(self): # TODO this should really be moved to core/responses.py if self.body: - return json.loads(self.body.decode("utf-8")) + return json.loads(self.body) else: return self.querystring diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 226d5d11a..59cff0395 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -51,7 +51,7 @@ class DynamoHandler(BaseResponse): return status, self.response_headers, dynamo_json_dump({'__type': type_}) def call_action(self): - body = self.body.decode('utf-8') + body = self.body if 'GetSessionToken' in body: return 200, self.response_headers, sts_handler() diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 081afc2c4..0957bfa89 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -52,7 +52,7 @@ class DynamoHandler(BaseResponse): return status, self.response_headers, dynamo_json_dump({'__type': type_}) def call_action(self): - body = self.body.decode('utf-8') + body = self.body if 'GetSessionToken' in body: return 200, self.response_headers, sts_handler() diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index ce90de379..a8c0dddac 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -14,7 +14,7 @@ class EC2ContainerServiceResponse(BaseResponse): @property def request_params(self): try: - return json.loads(self.body.decode()) + return json.loads(self.body) except ValueError: return {} diff --git a/moto/events/responses.py b/moto/events/responses.py index 7d63388b7..75e703706 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -19,7 +19,7 @@ class EventsHandler(BaseResponse): } def load_body(self): - decoded_body = self.body.decode('utf-8') + decoded_body = self.body return json.loads(decoded_body or '{}') def error(self, type_, message='', status=400): diff --git a/moto/kinesis/responses.py b/moto/kinesis/responses.py index d0a90a61e..9bc9fe94c 100644 --- a/moto/kinesis/responses.py +++ b/moto/kinesis/responses.py @@ -11,7 +11,7 @@ class KinesisResponse(BaseResponse): @property def parameters(self): - return json.loads(self.body.decode("utf-8")) + return json.loads(self.body) @property def kinesis_backend(self): diff --git a/moto/kms/responses.py b/moto/kms/responses.py index bc928f6f3..7f0659a64 100644 --- a/moto/kms/responses.py +++ b/moto/kms/responses.py @@ -22,7 +22,7 @@ class KmsResponse(BaseResponse): @property def parameters(self): - return json.loads(self.body.decode("utf-8")) + return json.loads(self.body) @property def kms_backend(self): diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py index 47fed3016..4e0979154 100644 --- a/moto/opsworks/responses.py +++ b/moto/opsworks/responses.py @@ -10,7 +10,7 @@ class OpsWorksResponse(BaseResponse): @property def parameters(self): - return json.loads(self.body.decode("utf-8")) + return json.loads(self.body) @property def opsworks_backend(self): diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 3fbd058f2..07be98e7b 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -104,10 +104,10 @@ class ResponseObject(_TemplateEnvironmentMixin): try: response = self._bucket_response(request, full_url, headers) except S3ClientError as s3error: - response = s3error.code, headers, s3error.description + response = s3error.code, {}, s3error.description if isinstance(response, six.string_types): - return 200, headers, response.encode("utf-8") + return 200, {}, response.encode("utf-8") else: status_code, headers, response_content = response return status_code, headers, response_content.encode("utf-8") @@ -133,8 +133,9 @@ class ResponseObject(_TemplateEnvironmentMixin): # Flask server body = request.data if body is None: - body = '' - body = body.decode('utf-8') + body = b'' + if isinstance(body, six.binary_type): + body = body.decode('utf-8') if method == 'HEAD': return self._bucket_response_head(bucket_name, headers) @@ -151,7 +152,7 @@ class ResponseObject(_TemplateEnvironmentMixin): def _bucket_response_head(self, bucket_name, headers): self.backend.get_bucket(bucket_name) - return 200, headers, "" + return 200, {}, "" def _bucket_response_get(self, bucket_name, querystring, headers): if 'uploads' in querystring: @@ -173,7 +174,7 @@ class ResponseObject(_TemplateEnvironmentMixin): elif 'lifecycle' in querystring: bucket = self.backend.get_bucket(bucket_name) if not bucket.rules: - return 404, headers, "NoSuchLifecycleConfiguration" + return 404, {}, "NoSuchLifecycleConfiguration" template = self.response_template(S3_BUCKET_LIFECYCLE_CONFIGURATION) return template.render(rules=bucket.rules) elif 'versioning' in querystring: @@ -184,8 +185,8 @@ class ResponseObject(_TemplateEnvironmentMixin): policy = self.backend.get_bucket_policy(bucket_name) if not policy: template = self.response_template(S3_NO_POLICY) - return 404, headers, template.render(bucket_name=bucket_name) - return 200, headers, policy + return 404, {}, template.render(bucket_name=bucket_name) + return 200, {}, policy elif 'website' in querystring: website_configuration = self.backend.get_bucket_website_configuration(bucket_name) return website_configuration @@ -211,7 +212,7 @@ class ResponseObject(_TemplateEnvironmentMixin): version_id_marker=version_id_marker ) template = self.response_template(S3_BUCKET_GET_VERSIONS) - return 200, headers, template.render( + return 200, {}, template.render( key_list=versions, bucket=bucket, prefix='', @@ -220,14 +221,14 @@ class ResponseObject(_TemplateEnvironmentMixin): is_truncated='false', ) elif querystring.get('list-type', [None])[0] == '2': - return 200, headers, self._handle_list_objects_v2(bucket_name, querystring) + return 200, {}, self._handle_list_objects_v2(bucket_name, querystring) bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] delimiter = querystring.get('delimiter', [None])[0] result_keys, result_folders = self.backend.prefix_query(bucket, prefix, delimiter) template = self.response_template(S3_BUCKET_GET_RESPONSE) - return 200, headers, template.render( + return 200, {}, template.render( bucket=bucket, prefix=prefix, delimiter=delimiter, @@ -286,7 +287,7 @@ class ResponseObject(_TemplateEnvironmentMixin): template = self.response_template(S3_BUCKET_VERSIONING) return template.render(bucket_versioning_status=ver.group(1)) else: - return 404, headers, "" + return 404, {}, "" elif 'lifecycle' in querystring: rules = xmltodict.parse(body)['LifecycleConfiguration']['Rule'] if not isinstance(rules, list): @@ -315,27 +316,27 @@ class ResponseObject(_TemplateEnvironmentMixin): else: raise template = self.response_template(S3_BUCKET_CREATE_RESPONSE) - return 200, headers, template.render(bucket=new_bucket) + return 200, {}, template.render(bucket=new_bucket) def _bucket_response_delete(self, body, bucket_name, querystring, headers): if 'policy' in querystring: self.backend.delete_bucket_policy(bucket_name, body) - return 204, headers, "" + return 204, {}, "" elif 'lifecycle' in querystring: bucket = self.backend.get_bucket(bucket_name) bucket.delete_lifecycle() - return 204, headers, "" + return 204, {}, "" removed_bucket = self.backend.delete_bucket(bucket_name) if removed_bucket: # Bucket exists template = self.response_template(S3_DELETE_BUCKET_SUCCESS) - return 204, headers, template.render(bucket=removed_bucket) + return 204, {}, template.render(bucket=removed_bucket) else: # Tried to delete a bucket that still has keys template = self.response_template(S3_DELETE_BUCKET_WITH_ITEMS_ERROR) - return 409, headers, template.render(bucket=removed_bucket) + return 409, {}, template.render(bucket=removed_bucket) def _bucket_response_post(self, request, body, bucket_name, headers): path = request.path if hasattr(request, 'path') else request.path_url @@ -349,7 +350,7 @@ class ResponseObject(_TemplateEnvironmentMixin): else: # HTTPretty, build new form object form = {} - for kv in body.decode('utf-8').split('&'): + for kv in body.split('&'): k, v = kv.split('=') form[k] = v @@ -365,7 +366,7 @@ class ResponseObject(_TemplateEnvironmentMixin): metadata = metadata_from_headers(form) new_key.set_metadata(metadata) - return 200, headers, "" + return 200, {}, "" def _bucket_response_delete_keys(self, request, body, bucket_name, headers): template = self.response_template(S3_DELETE_KEYS_RESPONSE) @@ -382,9 +383,10 @@ class ResponseObject(_TemplateEnvironmentMixin): else: error_names.append(key_name) - return 200, headers, template.render(deleted=deleted_names, delete_errors=error_names) + return 200, {}, template.render(deleted=deleted_names, delete_errors=error_names) def _handle_range_header(self, request, headers, response_content): + response_headers = {} length = len(response_content) last = length - 1 _, rspec = request.headers.get('range').split('=') @@ -399,28 +401,29 @@ class ResponseObject(_TemplateEnvironmentMixin): begin = length - min(end, length) end = last else: - return 400, headers, "" + return 400, response_headers, "" if begin < 0 or end > last or begin > min(end, last): - return 416, headers, "" - headers['content-range'] = "bytes {0}-{1}/{2}".format( + return 416, response_headers, "" + response_headers['content-range'] = "bytes {0}-{1}/{2}".format( begin, end, length) - return 206, headers, response_content[begin:end + 1] + return 206, response_headers, response_content[begin:end + 1] def key_response(self, request, full_url, headers): + response_headers = {} try: response = self._key_response(request, full_url, headers) except S3ClientError as s3error: - response = s3error.code, headers, s3error.description + response = s3error.code, {}, s3error.description if isinstance(response, six.string_types): status_code = 200 response_content = response else: - status_code, headers, response_content = response + status_code, response_headers, response_content = response if status_code == 200 and 'range' in request.headers: - return self._handle_range_header(request, headers, response_content) - return status_code, headers, response_content + return self._handle_range_header(request, response_headers, response_content) + return status_code, response_headers, response_content def _key_response(self, request, full_url, headers): parsed_url = urlparse(full_url) @@ -455,11 +458,12 @@ class ResponseObject(_TemplateEnvironmentMixin): raise NotImplementedError("Method {0} has not been impelemented in the S3 backend yet".format(method)) def _key_response_get(self, bucket_name, query, key_name, headers): + response_headers = {} if query.get('uploadId'): upload_id = query['uploadId'][0] parts = self.backend.list_multipart(bucket_name, upload_id) template = self.response_template(S3_MULTIPART_LIST_RESPONSE) - return 200, headers, template.render( + return 200, response_headers, template.render( bucket_name=bucket_name, key_name=key_name, upload_id=upload_id, @@ -471,13 +475,14 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket_name, key_name, version_id=version_id) if 'acl' in query: template = self.response_template(S3_OBJECT_ACL_RESPONSE) - return 200, headers, template.render(obj=key) + return 200, response_headers, template.render(obj=key) - headers.update(key.metadata) - headers.update(key.response_dict) - return 200, headers, key.value + response_headers.update(key.metadata) + response_headers.update(key.response_dict) + return 200, response_headers, key.value def _key_response_put(self, request, body, bucket_name, query, key_name, headers): + response_headers = {} if query.get('uploadId') and query.get('partNumber'): upload_id = query['uploadId'][0] part_number = int(query['partNumber'][0]) @@ -501,8 +506,8 @@ class ResponseObject(_TemplateEnvironmentMixin): key = self.backend.set_part( bucket_name, upload_id, part_number, body) response = "" - headers.update(key.response_dict) - return 200, headers, response + response_headers.update(key.response_dict) + return 200, response_headers, response storage_class = request.headers.get('x-amz-storage-class', 'STANDARD') acl = self._acl_from_headers(request.headers) @@ -511,7 +516,7 @@ class ResponseObject(_TemplateEnvironmentMixin): key = self.backend.get_key(bucket_name, key_name) # TODO: Support the XML-based ACL format key.set_acl(acl) - return 200, headers, "" + return 200, response_headers, "" if 'x-amz-copy-source' in request.headers: # Copy key @@ -526,8 +531,8 @@ class ResponseObject(_TemplateEnvironmentMixin): metadata = metadata_from_headers(request.headers) new_key.set_metadata(metadata, replace=True) template = self.response_template(S3_OBJECT_COPY_RESPONSE) - headers.update(new_key.response_dict) - return 200, headers, template.render(key=new_key) + response_headers.update(new_key.response_dict) + return 200, response_headers, template.render(key=new_key) streaming_request = hasattr(request, 'streaming') and request.streaming closing_connection = headers.get('connection') == 'close' if closing_connection and streaming_request: @@ -546,18 +551,19 @@ class ResponseObject(_TemplateEnvironmentMixin): new_key.set_acl(acl) template = self.response_template(S3_OBJECT_RESPONSE) - headers.update(new_key.response_dict) - return 200, headers, template.render(key=new_key) + response_headers.update(new_key.response_dict) + return 200, response_headers, template.render(key=new_key) def _key_response_head(self, bucket_name, query, key_name, headers): + response_headers = {} version_id = query.get('versionId', [None])[0] key = self.backend.get_key(bucket_name, key_name, version_id=version_id) if key: - headers.update(key.metadata) - headers.update(key.response_dict) - return 200, headers, "" + response_headers.update(key.metadata) + response_headers.update(key.response_dict) + return 200, response_headers, "" else: - return 404, headers, "" + return 404, response_headers, "" def _acl_from_headers(self, headers): canned_acl = headers.get('x-amz-acl', '') @@ -595,10 +601,10 @@ class ResponseObject(_TemplateEnvironmentMixin): if query.get('uploadId'): upload_id = query['uploadId'][0] self.backend.cancel_multipart(bucket_name, upload_id) - return 204, headers, "" + return 204, {}, "" self.backend.delete_key(bucket_name, key_name) template = self.response_template(S3_DELETE_OBJECT_SUCCESS) - return 204, headers, template.render() + return 204, {}, template.render() def _complete_multipart_body(self, body): ps = minidom.parseString(body).getElementsByTagName('Part') @@ -620,7 +626,7 @@ class ResponseObject(_TemplateEnvironmentMixin): key_name=key_name, upload_id=multipart.id, ) - return 200, headers, response + return 200, {}, response if query.get('uploadId'): body = self._complete_multipart_body(body) @@ -640,7 +646,7 @@ class ResponseObject(_TemplateEnvironmentMixin): if key.expiry_date is not None: r = 200 key.restore(int(days)) - return r, headers, "" + return r, {}, "" else: raise NotImplementedError("Method POST had only been implemented for multipart uploads and restore operations, so far") diff --git a/moto/swf/responses.py b/moto/swf/responses.py index 47d00901c..92d4957fd 100644 --- a/moto/swf/responses.py +++ b/moto/swf/responses.py @@ -16,7 +16,7 @@ class SWFResponse(BaseResponse): # SWF parameters are passed through a JSON body, so let's ease retrieval @property def _params(self): - return json.loads(self.body.decode("utf-8")) + return json.loads(self.body) def _check_int(self, parameter): if not isinstance(parameter, int): diff --git a/tests/test_sns/test_publishing.py b/tests/test_sns/test_publishing.py index 8f8bfb0a1..dae7e2b83 100644 --- a/tests/test_sns/test_publishing.py +++ b/tests/test_sns/test_publishing.py @@ -70,7 +70,7 @@ def test_publish_to_http(): last_request = responses.calls[-1].request last_request.method.should.equal("POST") - parse_qs(last_request.body.decode('utf-8')).should.equal({ + parse_qs(last_request.body).should.equal({ "Type": ["Notification"], "MessageId": [message_id], "TopicArn": ["arn:aws:sns:{0}:123456789012:some-topic".format(conn.region.name)], diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index b37522641..e31b969f1 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -75,7 +75,7 @@ def test_publish_to_http(): last_request = responses.calls[-2].request last_request.method.should.equal("POST") - parse_qs(last_request.body.decode('utf-8')).should.equal({ + parse_qs(last_request.body).should.equal({ "Type": ["Notification"], "MessageId": [message_id], "TopicArn": ["arn:aws:sns:{0}:123456789012:some-topic".format(conn._client_config.region_name)], From d28f083a0baeb8500eb50e15a84e75e724c65071 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 18 Feb 2017 09:19:08 -0500 Subject: [PATCH 023/274] Cleanup apigateway callback. --- moto/apigateway/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index bab0bc1d0..6ce831186 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -316,9 +316,8 @@ class RestAPI(object): return resource # TODO deal with no matching resource - def resource_callback(self, request, full_url=None, headers=None): - if not headers: - headers = request.headers + def resource_callback(self, request): + headers = request.headers path = request.path if hasattr(request, 'path') else request.path_url path_after_stage_name = '/'.join(path.split("/")[2:]) From 480c1bba1468852d41f2f656fd27ad82e8f30047 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 18 Feb 2017 09:24:09 -0500 Subject: [PATCH 024/274] Add rest of deprecated decorators. --- moto/__init__.py | 8 ++++---- moto/opsworks/__init__.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/moto/__init__.py b/moto/__init__.py index 4accf1d0c..5a16a0a8e 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -5,20 +5,20 @@ import logging __title__ = 'moto' __version__ = '0.4.31' -from .apigateway import mock_apigateway # flake8: noqa +from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa from .autoscaling import mock_autoscaling, mock_autoscaling_deprecated # flake8: noqa -from .awslambda import mock_lambda # flake8: noqa +from .awslambda import mock_lambda, mock_lambda_deprecated # flake8: noqa from .cloudformation import mock_cloudformation, mock_cloudformation_deprecated # flake8: noqa from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # flake8: noqa from .datapipeline import mock_datapipeline, mock_datapipeline_deprecated # flake8: noqa from .dynamodb import mock_dynamodb, mock_dynamodb_deprecated # flake8: noqa from .dynamodb2 import mock_dynamodb2, mock_dynamodb2_deprecated # flake8: noqa from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa -from .ecs import mock_ecs # flake8: noqa +from .ecs import mock_ecs, mock_ecs_deprecated # flake8: noqa from .elb import mock_elb, mock_elb_deprecated # flake8: noqa from .emr import mock_emr, mock_emr_deprecated # flake8: noqa from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa -from .opsworks import mock_opsworks # flake8: noqa +from .opsworks import mock_opsworks, mock_opsworks_deprecated # flake8: noqa from .iam import mock_iam, mock_iam_deprecated # flake8: noqa from .kinesis import mock_kinesis, mock_kinesis_deprecated # flake8: noqa from .kms import mock_kms, mock_kms_deprecated # flake8: noqa diff --git a/moto/opsworks/__init__.py b/moto/opsworks/__init__.py index 75f49eba5..d2da1a6a8 100644 --- a/moto/opsworks/__init__.py +++ b/moto/opsworks/__init__.py @@ -4,3 +4,4 @@ from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_ opsworks_backend = opsworks_backends['us-east-1'] mock_opsworks = base_decorator(opsworks_backends) +mock_opsworks_deprecated = deprecated_base_decorator(opsworks_backends) From 6785d359d30aa4a9e03116232e011132fb9d8bc8 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 18 Feb 2017 09:25:42 -0500 Subject: [PATCH 025/274] Cleanup apigateway callback. --- moto/apigateway/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 6ce831186..b6fa2df02 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -317,16 +317,13 @@ class RestAPI(object): # TODO deal with no matching resource def resource_callback(self, request): - headers = request.headers - - path = request.path if hasattr(request, 'path') else request.path_url - path_after_stage_name = '/'.join(path.split("/")[2:]) + path_after_stage_name = '/'.join(request.path_url.split("/")[2:]) if not path_after_stage_name: path_after_stage_name = '/' resource = self.get_resource_for_path(path_after_stage_name) status_code, response = resource.get_response(request) - return status_code, headers, response + return status_code, {}, response def update_integration_mocks(self, stage_name): stage_url = STAGE_URL.format(api_id=self.id, region_name=self.region_name, stage_name=stage_name) From d0fe1a09560114837d0a2fee76d36380f66378c1 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 18 Feb 2017 09:31:47 -0500 Subject: [PATCH 026/274] Remove pdb. --- moto/kinesis/responses.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/moto/kinesis/responses.py b/moto/kinesis/responses.py index 9bc9fe94c..9aed719d5 100644 --- a/moto/kinesis/responses.py +++ b/moto/kinesis/responses.py @@ -19,10 +19,7 @@ class KinesisResponse(BaseResponse): @property def is_firehose(self): - try: - host = self.headers.get('host') or self.headers['Host'] - except KeyError: - import pdb;pdb.set_trace() + host = self.headers.get('host') or self.headers['Host'] return host.startswith('firehose') def create_stream(self): From 51df02e7cf922d17e3fcb34993ada166444f11cd Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 20 Feb 2017 14:31:19 -0500 Subject: [PATCH 027/274] Cleanup Server host parsing. --- moto/core/models.py | 23 +++++++++++++---------- moto/server.py | 12 +++++++++--- tests/test_core/test_server.py | 6 +++--- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/moto/core/models.py b/moto/core/models.py index 9570a86d4..9675d514a 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import functools import inspect +import os import re from moto.packages.responses import responses @@ -48,7 +49,9 @@ class BaseMockAWS(object): if self.__class__.nested_count < 0: raise RuntimeError('Called stop() before start().') - self.disable_patching() + + if self.__class__.nested_count == 0: + self.disable_patching() def decorate_callable(self, func, reset): def wrapper(*args, **kwargs): @@ -108,9 +111,8 @@ class HttprettyMockAWS(BaseMockAWS): ) def disable_patching(self): - if self.__class__.nested_count == 0: - HTTPretty.disable() - HTTPretty.reset() + HTTPretty.disable() + HTTPretty.reset() RESPONSES_METHODS = [responses.GET, responses.DELETE, responses.HEAD, @@ -142,14 +144,15 @@ class ResponsesMockAWS(BaseMockAWS): pattern['stream'] = True def disable_patching(self): - if self.__class__.nested_count == 0: - try: - responses.stop() - except AttributeError: - pass - responses.reset() + try: + responses.stop() + except AttributeError: + pass + responses.reset() + MockAWS = ResponsesMockAWS + class Model(type): def __new__(self, clsname, bases, namespace): cls = super(Model, self).__new__(self, clsname, bases, namespace) diff --git a/moto/server.py b/moto/server.py index 1780083d8..321f5a9ea 100644 --- a/moto/server.py +++ b/moto/server.py @@ -42,8 +42,14 @@ class DomainDispatcherApplication(object): raise RuntimeError('Invalid host: "%s"' % host) - def get_application(self, host): - host = host.split(':')[0] + def get_application(self, environ): + host = environ['HTTP_HOST'].split(':')[0] + if host == "localhost": + # Fall back to parsing auth header to find service + # ['Credential=sdffdsa', '20170220', 'us-east-1', 'sns', 'aws4_request'] + _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[1].split("/") + host = "{service}.{region}.amazonaws.com".format(service=service, region=region) + with self.lock: backend = self.get_backend_for_host(host) app = self.app_instances.get(backend, None) @@ -53,7 +59,7 @@ class DomainDispatcherApplication(object): return app def __call__(self, environ, start_response): - backend_app = self.get_application(environ['HTTP_HOST']) + backend_app = self.get_application(environ) return backend_app(environ, start_response) diff --git a/tests/test_core/test_server.py b/tests/test_core/test_server.py index 3ee08465b..a0fb328cf 100644 --- a/tests/test_core/test_server.py +++ b/tests/test_core/test_server.py @@ -32,19 +32,19 @@ def test_port_argument(run_simple): def test_domain_dispatched(): dispatcher = DomainDispatcherApplication(create_backend_app) - backend_app = dispatcher.get_application("email.us-east1.amazonaws.com") + backend_app = dispatcher.get_application({"HTTP_HOST": "email.us-east1.amazonaws.com"}) keys = list(backend_app.view_functions.keys()) keys[0].should.equal('EmailResponse.dispatch') def test_domain_without_matches(): dispatcher = DomainDispatcherApplication(create_backend_app) - dispatcher.get_application.when.called_with("not-matching-anything.com").should.throw(RuntimeError) + dispatcher.get_application.when.called_with({"HTTP_HOST": "not-matching-anything.com"}).should.throw(RuntimeError) def test_domain_dispatched_with_service(): # If we pass a particular service, always return that. dispatcher = DomainDispatcherApplication(create_backend_app, service="s3") - backend_app = dispatcher.get_application("s3.us-east1.amazonaws.com") + backend_app = dispatcher.get_application({"HTTP_HOST": "s3.us-east1.amazonaws.com"}) keys = set(backend_app.view_functions.keys()) keys.should.contain('ResponseObject.key_response') From fe46b4c5b92285aab7367a40714693281b6d3380 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 20 Feb 2017 15:50:49 -0500 Subject: [PATCH 028/274] Remove extra line in test. --- moto/core/urls.py | 12 ++++++++++++ tests/test_sqs/test_sqs.py | 1 - 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 moto/core/urls.py diff --git a/moto/core/urls.py b/moto/core/urls.py new file mode 100644 index 000000000..ece486058 --- /dev/null +++ b/moto/core/urls.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .responses import MotoAPIResponse + +url_bases = [ + "https?://motoapi.amazonaws.com" +] + +response_instance = MotoAPIResponse() + +url_paths = { + '{0}/moto-api/reset': response_instance.reset_response, +} diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index b3eaaab75..fd496c214 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -54,7 +54,6 @@ def test_message_send(): @mock_sqs def test_set_queue_attributes(): sqs = boto3.resource('sqs', region_name='us-east-1') - conn = boto3.client('sqs', region_name='us-west-1') queue = sqs.create_queue(QueueName="blah") queue.attributes['VisibilityTimeout'].should.equal("30") From cb28eeefbbe75a289fe3ca165bfc73ca2baddd9e Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 20 Feb 2017 18:25:10 -0500 Subject: [PATCH 029/274] Add moto reset API. --- moto/backends.py | 4 ++++ moto/core/__init__.py | 2 +- moto/core/models.py | 50 ++++++++++++++++++++++++++++++++++++++++++ moto/core/responses.py | 8 +++++++ moto/server.py | 8 ++++++- moto/sns/urls.py | 2 +- 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/moto/backends.py b/moto/backends.py index 0cbcf4810..4cebe560a 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -5,10 +5,12 @@ from moto.autoscaling import autoscaling_backend from moto.awslambda import lambda_backend from moto.cloudformation import cloudformation_backend from moto.cloudwatch import cloudwatch_backend +from moto.core import moto_api_backend from moto.datapipeline import datapipeline_backend from moto.dynamodb import dynamodb_backend from moto.dynamodb2 import dynamodb_backend2 from moto.ec2 import ec2_backend +from moto.ecs import ecs_backend from moto.elb import elb_backend from moto.emr import emr_backend from moto.events import events_backend @@ -35,11 +37,13 @@ BACKENDS = { 'dynamodb': dynamodb_backend, 'dynamodb2': dynamodb_backend2, 'ec2': ec2_backend, + 'ecs': ecs_backend, 'elb': elb_backend, 'events': events_backend, 'emr': emr_backend, 'glacier': glacier_backend, 'iam': iam_backend, + 'moto_api': moto_api_backend, 'opsworks': opsworks_backend, 'kinesis': kinesis_backend, 'kms': kms_backend, diff --git a/moto/core/__init__.py b/moto/core/__init__.py index 1b909183e..664637b76 100644 --- a/moto/core/__init__.py +++ b/moto/core/__init__.py @@ -1,2 +1,2 @@ from __future__ import unicode_literals -from .models import BaseBackend # flake8: noqa +from .models import BaseBackend, moto_api_backend # flake8: noqa diff --git a/moto/core/models.py b/moto/core/models.py index 9675d514a..8fac8a990 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -153,6 +153,38 @@ class ResponsesMockAWS(BaseMockAWS): MockAWS = ResponsesMockAWS +class ServerModeMockAWS(BaseMockAWS): + + def reset(self): + import requests + requests.post("http://localhost:8086/moto-api/reset") + + def enable_patching(self): + if self.__class__.nested_count == 1: + # Just started + self.reset() + + from boto3 import client as real_boto3_client, resource as real_boto3_resource + import mock + + def fake_boto3_client(*args, **kwargs): + if 'endpoint_url' not in kwargs: + kwargs['endpoint_url'] = "http://localhost:8086" + return real_boto3_client(*args, **kwargs) + def fake_boto3_resource(*args, **kwargs): + if 'endpoint_url' not in kwargs: + kwargs['endpoint_url'] = "http://localhost:8086" + return real_boto3_resource(*args, **kwargs) + self._client_patcher = mock.patch('boto3.client', fake_boto3_client) + self._resource_patcher = mock.patch('boto3.resource', fake_boto3_resource) + self._client_patcher.start() + self._resource_patcher.start() + + def disable_patching(self): + if self._client_patcher: + self._client_patcher.stop() + self._resource_patcher.stop() + class Model(type): def __new__(self, clsname, bases, namespace): cls = super(Model, self).__new__(self, clsname, bases, namespace) @@ -257,6 +289,9 @@ class base_decorator(object): self.backends = backends def __call__(self, func=None): + if self.mock_backend == MockAWS and os.environ.get('TEST_SERVER_MODE', '0').lower() == 'true': + self.mock_backend = ServerModeMockAWS + if func: return self.mock_backend(self.backends)(func) else: @@ -265,3 +300,18 @@ class base_decorator(object): class deprecated_base_decorator(base_decorator): mock_backend = HttprettyMockAWS + + +class MotoAPIBackend(BaseBackend): + def __init__(self): + super(MotoAPIBackend, self).__init__() + + def reset(self): + from moto.backends import BACKENDS + for name, backend in BACKENDS.items(): + if name == "moto_api": + continue + backend.reset() + self.__init__() + +moto_api_backend = MotoAPIBackend() diff --git a/moto/core/responses.py b/moto/core/responses.py index 05c882ba1..9b22b58cf 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -366,6 +366,14 @@ def metadata_response(request, full_url, headers): return 200, headers, result +class MotoAPIResponse(BaseResponse): + + def reset_response(self, request, full_url, headers): + from .models import moto_api_backend + moto_api_backend.reset() + return 200, {}, json.dumps({"status": "ok"}) + + class _RecursiveDictRef(object): """Store a recursive reference to dict.""" def __init__(self): diff --git a/moto/server.py b/moto/server.py index 321f5a9ea..0b5ff7cae 100644 --- a/moto/server.py +++ b/moto/server.py @@ -35,6 +35,9 @@ class DomainDispatcherApplication(object): if self.service: return self.service + if host in BACKENDS: + return host + for backend_name, backend in BACKENDS.items(): for url_base in backend.url_bases: if re.match(url_base, 'http://%s' % host): @@ -43,7 +46,10 @@ class DomainDispatcherApplication(object): raise RuntimeError('Invalid host: "%s"' % host) def get_application(self, environ): - host = environ['HTTP_HOST'].split(':')[0] + if environ.get('PATH_INFO', '').startswith("/moto-api"): + host = "moto_api" + else: + host = environ['HTTP_HOST'].split(':')[0] if host == "localhost": # Fall back to parsing auth header to find service # ['Credential=sdffdsa', '20170220', 'us-east-1', 'sns', 'aws4_request'] diff --git a/moto/sns/urls.py b/moto/sns/urls.py index 769c0c89c..518531c55 100644 --- a/moto/sns/urls.py +++ b/moto/sns/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from .responses import SNSResponse url_bases = [ - "https?://sns.(.+).amazonaws.com" + "https?://sns.(.+).amazonaws.com", ] url_paths = { From 81836b698107debc9dffe056530820c9694c8c77 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 23 Feb 2017 19:43:48 -0500 Subject: [PATCH 030/274] Get standalone server mode working for all tests. --- CHANGELOG.md | 3 + Makefile | 3 + README.md | 35 +-- moto/apigateway/models.py | 4 +- moto/awslambda/models.py | 10 +- moto/awslambda/responses.py | 28 +- moto/awslambda/urls.py | 9 +- moto/backends.py | 124 ++++---- moto/cloudformation/responses.py | 7 +- moto/core/__init__.py | 2 + moto/core/exceptions.py | 6 + moto/core/models.py | 81 ++--- moto/core/responses.py | 59 +--- moto/core/utils.py | 10 +- moto/dynamodb/__init__.py | 2 + moto/dynamodb2/__init__.py | 2 + moto/ec2/responses/instances.py | 1 - moto/ec2/responses/ip_addresses.py | 1 - moto/ec2/responses/spot_instances.py | 6 +- moto/emr/exceptions.py | 7 + moto/emr/models.py | 1 + moto/emr/responses.py | 11 +- moto/events/__init__.py | 1 + moto/events/urls.py | 2 +- moto/iam/__init__.py | 2 + moto/iam/urls.py | 2 +- moto/instance_metadata/__init__.py | 4 + moto/instance_metadata/models.py | 7 + moto/instance_metadata/responses.py | 47 +++ moto/instance_metadata/urls.py | 12 + moto/kinesis/responses.py | 2 +- moto/route53/__init__.py | 2 + moto/route53/responses.py | 294 +++++++++--------- moto/route53/urls.py | 26 +- moto/s3/__init__.py | 2 + moto/s3/models.py | 10 +- moto/server.py | 15 +- moto/ses/__init__.py | 2 + moto/ses/urls.py | 3 +- moto/settings.py | 3 + moto/sts/__init__.py | 2 + moto/sts/urls.py | 2 +- other_langs/sqsSample.java | 52 ++++ other_langs/test.js | 26 ++ other_langs/test.rb | 6 + setup.cfg | 2 +- tests/test_apigateway/test_apigateway.py | 7 +- tests/test_awslambda/test_lambda.py | 23 +- .../test_cloudformation_stack_crud_boto3.py | 22 +- .../test_cloudformation_stack_integration.py | 44 +-- tests/test_core/test_instance_metadata.py | 21 +- tests/test_core/test_moto_api.py | 21 ++ tests/test_dynamodb/test_dynamodb.py | 7 - tests/test_dynamodb2/test_dynamodb.py | 8 - tests/test_ec2/test_amis.py | 22 +- tests/test_ec2/test_ec2_core.py | 10 - tests/test_ec2/test_elastic_block_store.py | 46 +-- tests/test_ec2/test_elastic_ip_addresses.py | 31 +- .../test_elastic_network_interfaces.py | 27 +- tests/test_ec2/test_instances.py | 54 ++-- tests/test_ec2/test_internet_gateways.py | 18 +- tests/test_ec2/test_key_pairs.py | 14 +- tests/test_ec2/test_security_groups.py | 38 +-- tests/test_ec2/test_spot_instances.py | 164 ++++++---- tests/test_ec2/test_tags.py | 14 +- tests/test_emr/test_emr.py | 12 +- tests/test_emr/test_emr_boto3.py | 16 +- tests/test_iam/test_iam.py | 4 +- tests/test_kinesis/test_firehose.py | 19 +- tests/test_route53/test_route53.py | 18 +- tests/test_s3/test_s3.py | 44 ++- .../test_s3bucket_path/test_s3bucket_path.py | 10 - tests/test_sns/test_publishing.py | 17 - tests/test_sns/test_publishing_boto3.py | 15 - tests/test_sqs/test_sqs.py | 5 - tests/test_sts/test_sts.py | 2 +- tests/test_swf/utils.py | 2 - tox.ini | 1 + 78 files changed, 934 insertions(+), 760 deletions(-) create mode 100644 moto/emr/exceptions.py create mode 100644 moto/instance_metadata/__init__.py create mode 100644 moto/instance_metadata/models.py create mode 100644 moto/instance_metadata/responses.py create mode 100644 moto/instance_metadata/urls.py create mode 100644 moto/settings.py create mode 100644 other_langs/sqsSample.java create mode 100644 other_langs/test.js create mode 100644 other_langs/test.rb create mode 100644 tests/test_core/test_moto_api.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 790f6de95..912659875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Latest * The normal @mock_ decorators will no longer work with boto. It is suggested that you upgrade to boto3 or use the standalone-server mode. If you would still like to use boto, you must use the @mock__deprecated decorators which will be removed in a future release. * The @mock_s3bucket_path decorator is now deprecated. Use the @mock_s3 decorator instead. + Added + * Reset API: a reset API has been added to flush all of the current data ex: `requests.post("http://motoapi.amazonaws.com/moto-api/reset")` + 0.4.31 ------ diff --git a/Makefile b/Makefile index a7f08b146..58b74b2fb 100644 --- a/Makefile +++ b/Makefile @@ -9,5 +9,8 @@ test: rm -rf cover @nosetests -sv --with-coverage --cover-html ./tests/ +test_server: + @TEST_SERVER_MODE=true nosetests -sv --with-coverage --cover-html ./tests/ + publish: python setup.py sdist bdist_wheel upload diff --git a/README.md b/README.md index ae161dc5c..5485c63cd 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -# Moto - Mock Boto +# Moto - Mock AWS Services [![Build Status](https://travis-ci.org/spulec/moto.png?branch=master)](https://travis-ci.org/spulec/moto) [![Coverage Status](https://coveralls.io/repos/spulec/moto/badge.png?branch=master)](https://coveralls.io/r/spulec/moto) # In a nutshell -Moto is a library that allows your python tests to easily mock out the boto library. +Moto is a library that allows your tests to easily mock out AWS Services. -Imagine you have the following code that you want to test: +Imagine you have the following python code that you want to test: ```python -import boto -from boto.s3.key import Key +import boto3 class MyModel(object): def __init__(self, name, value): @@ -19,11 +18,9 @@ class MyModel(object): self.value = value def save(self): - conn = boto.connect_s3() - bucket = conn.get_bucket('mybucket') - k = Key(bucket) - k.key = self.name - k.set_contents_from_string(self.value) + s3 = boto3.client('s3', region_name='us-east-1') + s3.put_object(Bucket='mybucket', Key=self.name, Body=self.value) + ``` Take a minute to think how you would have tested that in the past. @@ -31,25 +28,28 @@ Take a minute to think how you would have tested that in the past. Now see how you could test it with Moto: ```python -import boto +import boto3 from moto import mock_s3 from mymodule import MyModel + @mock_s3 def test_my_model_save(): - conn = boto.connect_s3() + conn = boto3.resource('s3', region_name='us-east-1') # We need to create the bucket since this is all in Moto's 'virtual' AWS account - conn.create_bucket('mybucket') + conn.create_bucket(Bucket='mybucket') model_instance = MyModel('steve', 'is awesome') model_instance.save() - assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + body = conn.Object('mybucket', 'steve').get()['Body'].read().decode("utf-8") + + assert body == b'is awesome' ``` With the decorator wrapping the test, all the calls to s3 are automatically mocked out. The mock keeps the state of the buckets and keys. -It gets even better! Moto isn't just S3. Here's the status of the other AWS services implemented. +It gets even better! Moto isn't just for Python code and it isn't just for S3. Look at the [standalone server mode](https://github.com/spulec/moto#stand-alone-server-mode) for more information about running Moto with other languages. Here's the status of the other AWS services implemented: ```gherkin |------------------------------------------------------------------------------| @@ -193,11 +193,6 @@ def test_my_model_save(): mock.stop() ``` -## Use with other libraries (boto3) or languages - -In general, Moto doesn't rely on anything specific to Boto. It only mocks AWS endpoints, so there should be no issue with boto3 or using other languages. Feel free to open an issue if something isn't working though. If you are using another language, you will need to either use the stand-alone server mode (more below) or monkey patch the HTTP calls yourself. - - ## Stand-alone Server Mode Moto also has a stand-alone server mode. This allows you to utilize diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index b6fa2df02..4b09f44bc 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -326,8 +326,8 @@ class RestAPI(object): return status_code, {}, response def update_integration_mocks(self, stage_name): - stage_url = STAGE_URL.format(api_id=self.id, region_name=self.region_name, stage_name=stage_name) - responses.add_callback(responses.GET, stage_url.lower(), callback=self.resource_callback) + stage_url = STAGE_URL.format(api_id=self.id.upper(), region_name=self.region_name, stage_name=stage_name) + responses.add_callback(responses.GET, stage_url, callback=self.resource_callback) def create_stage(self, name, deployment_id,variables=None,description='',cacheClusterEnabled=None,cacheClusterSize=None): if variables is None: diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index e8595cc22..1fc139eb7 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -154,15 +154,15 @@ class LambdaFunction(object): sys.stderr = sys.__stderr__ return self.convert(result) - def invoke(self, request, headers): + def invoke(self, body, request_headers, response_headers): payload = dict() # Get the invocation type: - r = self._invoke_lambda(code=self.code, event=request.body) - if request.headers.get("x-amz-invocation-type") == "RequestResponse": + r = self._invoke_lambda(code=self.code, event=body) + if request_headers.get("x-amz-invocation-type") == "RequestResponse": encoded = base64.b64encode(r.encode('utf-8')) - headers["x-amz-log-result"] = encoded.decode('utf-8') - payload['result'] = headers["x-amz-log-result"] + response_headers["x-amz-log-result"] = encoded.decode('utf-8') + payload['result'] = response_headers["x-amz-log-result"] result = r.encode('utf-8') else: result = json.dumps(payload) diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 0cd7c57ea..3fc756efa 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -10,32 +10,32 @@ from .models import lambda_backends class LambdaResponse(BaseResponse): - @classmethod - def root(cls, request, full_url, headers): + def root(self, request, full_url, headers): + self.setup_class(request, full_url, headers) if request.method == 'GET': - return cls()._list_functions(request, full_url, headers) + return self._list_functions(request, full_url, headers) elif request.method == 'POST': - return cls()._create_function(request, full_url, headers) + return self._create_function(request, full_url, headers) else: raise ValueError("Cannot handle request") - @classmethod - def function(cls, request, full_url, headers): + def function(self, request, full_url, headers): + self.setup_class(request, full_url, headers) if request.method == 'GET': - return cls()._get_function(request, full_url, headers) + return self._get_function(request, full_url, headers) elif request.method == 'DELETE': - return cls()._delete_function(request, full_url, headers) + return self._delete_function(request, full_url, headers) else: raise ValueError("Cannot handle request") - @classmethod - def invoke(cls, request, full_url, headers): + def invoke(self, request, full_url, headers): + self.setup_class(request, full_url, headers) if request.method == 'POST': - return cls()._invoke(request, full_url, headers) + return self._invoke(request, full_url) else: raise ValueError("Cannot handle request") - def _invoke(self, request, full_url, headers): + def _invoke(self, request, full_url): response_headers = {} lambda_backend = self.get_lambda_backend(full_url) @@ -44,7 +44,7 @@ class LambdaResponse(BaseResponse): if lambda_backend.has_function(function_name): fn = lambda_backend.get_function(function_name) - payload = fn.invoke(request, response_headers) + payload = fn.invoke(self.body, self.headers, response_headers) response_headers['Content-Length'] = str(len(payload)) return 202, response_headers, payload else: @@ -59,7 +59,7 @@ class LambdaResponse(BaseResponse): def _create_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - spec = json.loads(request.body.decode('utf-8')) + spec = json.loads(self.body.decode('utf-8')) try: fn = lambda_backend.create_function(spec) except ValueError as e: diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py index 79a99c9f8..c63135766 100644 --- a/moto/awslambda/urls.py +++ b/moto/awslambda/urls.py @@ -5,9 +5,10 @@ url_bases = [ "https?://lambda.(.+).amazonaws.com", ] +response = LambdaResponse() + url_paths = { - # double curly braces because the `format()` method is called on the strings - '{0}/\d{{4}}-\d{{2}}-\d{{2}}/functions/?$': LambdaResponse.root, - '{0}/\d{{4}}-\d{{2}}-\d{{2}}/functions/(?P[\w_-]+)/?$': LambdaResponse.function, - '{0}/\d{{4}}-\d{{2}}-\d{{2}}/functions/(?P[\w_-]+)/invocations?$': LambdaResponse.invoke, + '{0}/(?P[^/]+)/functions/?$': response.root, + '{0}/(?P[^/]+)/functions/(?P[\w_-]+)/?$': response.function, + '{0}/(?P[^/]+)/functions/(?P[\w_-]+)/invocations/?$': response.invoke, } diff --git a/moto/backends.py b/moto/backends.py index 4cebe560a..5b1695e3b 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -1,67 +1,71 @@ from __future__ import unicode_literals -from moto.apigateway import apigateway_backend -from moto.autoscaling import autoscaling_backend -from moto.awslambda import lambda_backend -from moto.cloudformation import cloudformation_backend -from moto.cloudwatch import cloudwatch_backend -from moto.core import moto_api_backend -from moto.datapipeline import datapipeline_backend -from moto.dynamodb import dynamodb_backend -from moto.dynamodb2 import dynamodb_backend2 -from moto.ec2 import ec2_backend -from moto.ecs import ecs_backend -from moto.elb import elb_backend -from moto.emr import emr_backend -from moto.events import events_backend -from moto.glacier import glacier_backend -from moto.iam import iam_backend -from moto.kinesis import kinesis_backend -from moto.kms import kms_backend -from moto.opsworks import opsworks_backend -from moto.rds import rds_backend -from moto.redshift import redshift_backend -from moto.route53 import route53_backend -from moto.s3 import s3_backend -from moto.ses import ses_backend -from moto.sns import sns_backend -from moto.sqs import sqs_backend -from moto.sts import sts_backend +from moto.apigateway import apigateway_backends +from moto.autoscaling import autoscaling_backends +from moto.awslambda import lambda_backends +from moto.cloudformation import cloudformation_backends +from moto.cloudwatch import cloudwatch_backends +from moto.core import moto_api_backends +from moto.datapipeline import datapipeline_backends +from moto.dynamodb import dynamodb_backends +from moto.dynamodb2 import dynamodb_backends2 +from moto.ec2 import ec2_backends +from moto.ecs import ecs_backends +from moto.elb import elb_backends +from moto.emr import emr_backends +from moto.events import events_backends +from moto.glacier import glacier_backends +from moto.iam import iam_backends +from moto.instance_metadata import instance_metadata_backends +from moto.kinesis import kinesis_backends +from moto.kms import kms_backends +from moto.opsworks import opsworks_backends +from moto.rds2 import rds2_backends +from moto.redshift import redshift_backends +from moto.route53 import route53_backends +from moto.s3 import s3_backends +from moto.ses import ses_backends +from moto.sns import sns_backends +from moto.sqs import sqs_backends +from moto.sts import sts_backends BACKENDS = { - 'apigateway': apigateway_backend, - 'autoscaling': autoscaling_backend, - 'cloudformation': cloudformation_backend, - 'cloudwatch': cloudwatch_backend, - 'datapipeline': datapipeline_backend, - 'dynamodb': dynamodb_backend, - 'dynamodb2': dynamodb_backend2, - 'ec2': ec2_backend, - 'ecs': ecs_backend, - 'elb': elb_backend, - 'events': events_backend, - 'emr': emr_backend, - 'glacier': glacier_backend, - 'iam': iam_backend, - 'moto_api': moto_api_backend, - 'opsworks': opsworks_backend, - 'kinesis': kinesis_backend, - 'kms': kms_backend, - 'redshift': redshift_backend, - 'rds': rds_backend, - 's3': s3_backend, - 's3bucket_path': s3_backend, - 'ses': ses_backend, - 'sns': sns_backend, - 'sqs': sqs_backend, - 'sts': sts_backend, - 'route53': route53_backend, - 'lambda': lambda_backend, + 'apigateway': apigateway_backends, + 'autoscaling': autoscaling_backends, + 'cloudformation': cloudformation_backends, + 'cloudwatch': cloudwatch_backends, + 'datapipeline': datapipeline_backends, + 'dynamodb': dynamodb_backends, + 'dynamodb2': dynamodb_backends2, + 'ec2': ec2_backends, + 'ecs': ecs_backends, + 'elb': elb_backends, + 'events': events_backends, + 'emr': emr_backends, + 'glacier': glacier_backends, + 'iam': iam_backends, + 'moto_api': moto_api_backends, + 'instance_metadata': instance_metadata_backends, + 'opsworks': opsworks_backends, + 'kinesis': kinesis_backends, + 'kms': kms_backends, + 'redshift': redshift_backends, + 'rds': rds2_backends, + 's3': s3_backends, + 's3bucket_path': s3_backends, + 'ses': ses_backends, + 'sns': sns_backends, + 'sqs': sqs_backends, + 'sts': sts_backends, + 'route53': route53_backends, + 'lambda': lambda_backends, } -def get_model(name): - for backend in BACKENDS.values(): - models = getattr(backend.__class__, '__models__', {}) - if name in models: - return list(getattr(backend, models[name])()) +def get_model(name, region): + for backends in BACKENDS.values(): + for region, backend in backends.items(): + if region == region: + models = getattr(backend.__class__, '__models__', {}) + if name in models: + return list(getattr(backend, models[name])()) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index d16b3560c..3b8f53895 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -17,8 +17,11 @@ class CloudFormationResponse(BaseResponse): def _get_stack_from_s3_url(self, template_url): template_url_parts = urlparse(template_url) - bucket_name = template_url_parts.netloc.split(".")[0] - key_name = template_url_parts.path.lstrip("/") + if "localhost" in template_url: + bucket_name, key_name = template_url_parts.path.lstrip("/").split("/") + else: + bucket_name = template_url_parts.netloc.split(".")[0] + key_name = template_url_parts.path.lstrip("/") key = s3_backend.get_key(bucket_name, key_name) return key.value.decode("utf-8") diff --git a/moto/core/__init__.py b/moto/core/__init__.py index 664637b76..4f783d46c 100644 --- a/moto/core/__init__.py +++ b/moto/core/__init__.py @@ -1,2 +1,4 @@ from __future__ import unicode_literals from .models import BaseBackend, moto_api_backend # flake8: noqa + +moto_api_backends = {"global": moto_api_backend} diff --git a/moto/core/exceptions.py b/moto/core/exceptions.py index c66b8f257..d3a87e299 100644 --- a/moto/core/exceptions.py +++ b/moto/core/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from werkzeug.exceptions import HTTPException from jinja2 import DictLoader, Environment from six import text_type @@ -47,6 +49,10 @@ class RESTError(HTTPException): error_type=error_type, message=message, **kwargs) +class DryRunClientError(RESTError): + code = 400 + + class JsonRESTError(RESTError): def __init__(self, error_type, message, template='error_json', **kwargs): super(JsonRESTError, self).__init__(error_type, message, template, **kwargs) diff --git a/moto/core/models.py b/moto/core/models.py index 8fac8a990..04ff709e0 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -6,9 +6,9 @@ import inspect import os import re +from moto import settings from moto.packages.responses import responses from moto.packages.httpretty import HTTPretty -from .responses import metadata_response from .utils import ( convert_httpretty_response, convert_regex_to_flask_path, @@ -21,6 +21,15 @@ class BaseMockAWS(object): def __init__(self, backends): self.backends = backends + self.backends_for_urls = {} + from moto.backends import BACKENDS + default_backends = { + "instance_metadata": BACKENDS['instance_metadata']['global'], + "moto_api": BACKENDS['moto_api']['global'], + } + self.backends_for_urls.update(self.backends) + self.backends_for_urls.update(default_backends) + if self.__class__.nested_count == 0: self.reset() @@ -95,20 +104,13 @@ class HttprettyMockAWS(BaseMockAWS): HTTPretty.enable() for method in HTTPretty.METHODS: - backend = list(self.backends.values())[0] - for key, value in backend.urls.items(): - HTTPretty.register_uri( - method=method, - uri=re.compile(key), - body=convert_httpretty_response(value), - ) - - # Mock out localhost instance metadata - HTTPretty.register_uri( - method=method, - uri=re.compile('http://169.254.169.254/latest/meta-data/.*'), - body=convert_httpretty_response(metadata_response), - ) + for backend in self.backends_for_urls.values(): + for key, value in backend.urls.items(): + HTTPretty.register_uri( + method=method, + uri=re.compile(key), + body=convert_httpretty_response(value), + ) def disable_patching(self): HTTPretty.disable() @@ -126,20 +128,14 @@ class ResponsesMockAWS(BaseMockAWS): def enable_patching(self): responses.start() for method in RESPONSES_METHODS: - backend = list(self.backends.values())[0] - for key, value in backend.urls.items(): - responses.add_callback( - method=method, - url=re.compile(key), - callback=convert_flask_to_responses_response(value), - ) + for backend in self.backends_for_urls.values(): + for key, value in backend.urls.items(): + responses.add_callback( + method=method, + url=re.compile(key), + callback=convert_flask_to_responses_response(value), + ) - # Mock out localhost instance metadata - responses.add_callback( - method=method, - url=re.compile('http://169.254.169.254/latest/meta-data/.*'), - callback=convert_flask_to_responses_response(metadata_response), - ) for pattern in responses.mock._urls: pattern['stream'] = True @@ -270,10 +266,15 @@ class BaseBackend(object): return paths def decorator(self, func=None): - if func: - return MockAWS({'global': self})(func) + if settings.TEST_SERVER_MODE: + mocked_backend = ServerModeMockAWS({'global': self}) else: - return MockAWS({'global': self}) + mocked_backend = MockAWS({'global': self}) + + if func: + return mocked_backend(func) + else: + return mocked_backend def deprecated_decorator(self, func=None): if func: @@ -289,13 +290,15 @@ class base_decorator(object): self.backends = backends def __call__(self, func=None): - if self.mock_backend == MockAWS and os.environ.get('TEST_SERVER_MODE', '0').lower() == 'true': - self.mock_backend = ServerModeMockAWS + if self.mock_backend != HttprettyMockAWS and settings.TEST_SERVER_MODE: + mocked_backend = ServerModeMockAWS(self.backends) + else: + mocked_backend = self.mock_backend(self.backends) if func: - return self.mock_backend(self.backends)(func) + return mocked_backend(func) else: - return self.mock_backend(self.backends) + return mocked_backend class deprecated_base_decorator(base_decorator): @@ -303,15 +306,13 @@ class deprecated_base_decorator(base_decorator): class MotoAPIBackend(BaseBackend): - def __init__(self): - super(MotoAPIBackend, self).__init__() - def reset(self): from moto.backends import BACKENDS - for name, backend in BACKENDS.items(): + for name, backends in BACKENDS.items(): if name == "moto_api": continue - backend.reset() + for region_name, backend in backends.items(): + backend.reset() self.__init__() moto_api_backend = MotoAPIBackend() diff --git a/moto/core/responses.py b/moto/core/responses.py index 9b22b58cf..e558eb1dd 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -5,7 +5,7 @@ import logging import re import pytz -from boto.exception import JSONResponseError +from moto.core.exceptions import DryRunClientError from jinja2 import Environment, DictLoader, TemplateNotFound @@ -149,17 +149,19 @@ class BaseResponse(_TemplateEnvironmentMixin): self.path = urlparse(full_url).path self.querystring = querystring self.method = request.method - self.region = self.get_region_from_url(full_url) + self.region = self.get_region_from_url(request, full_url) self.headers = request.headers if 'host' not in self.headers: self.headers['host'] = urlparse(full_url).netloc self.response_headers = {"server": "amazon.com"} - def get_region_from_url(self, full_url): + def get_region_from_url(self, request, full_url): match = re.search(self.region_regex, full_url) if match: region = match.group(1) + elif 'Authorization' in request.headers: + region = request.headers['Authorization'].split(",")[0].split("/")[2] else: region = self.default_region return region @@ -195,6 +197,7 @@ class BaseResponse(_TemplateEnvironmentMixin): if "status" in headers: headers['status'] = str(headers['status']) return status, headers, body + raise NotImplementedError("The {0} action has not been implemented".format(action)) def _get_param(self, param_name, if_none=None): @@ -323,55 +326,19 @@ class BaseResponse(_TemplateEnvironmentMixin): def is_not_dryrun(self, action): if 'true' in self.querystring.get('DryRun', ['false']): - raise JSONResponseError(400, 'DryRunOperation', body={'message': 'An error occurred (DryRunOperation) when calling the %s operation: Request would have succeeded, but DryRun flag is set' % action}) + message = 'An error occurred (DryRunOperation) when calling the %s operation: Request would have succeeded, but DryRun flag is set' % action + raise DryRunClientError(error_type="DryRunOperation", message=message) return True -def metadata_response(request, full_url, headers): - """ - Mock response for localhost metadata - - http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html - """ - - parsed_url = urlparse(full_url) - tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1) - credentials = dict( - AccessKeyId="test-key", - SecretAccessKey="test-secret-key", - Token="test-session-token", - Expiration=tomorrow.strftime("%Y-%m-%dT%H:%M:%SZ") - ) - - path = parsed_url.path - - meta_data_prefix = "/latest/meta-data/" - # Strip prefix if it is there - if path.startswith(meta_data_prefix): - path = path[len(meta_data_prefix):] - - if path == '': - result = 'iam' - elif path == 'iam': - result = json.dumps({ - 'security-credentials': { - 'default-role': credentials - } - }) - elif path == 'iam/security-credentials/': - result = 'default-role' - elif path == 'iam/security-credentials/default-role': - result = json.dumps(credentials) - else: - raise NotImplementedError("The {0} metadata path has not been implemented".format(path)) - return 200, headers, result - class MotoAPIResponse(BaseResponse): def reset_response(self, request, full_url, headers): - from .models import moto_api_backend - moto_api_backend.reset() - return 200, {}, json.dumps({"status": "ok"}) + if request.method == "POST": + from .models import moto_api_backend + moto_api_backend.reset() + return 200, {}, json.dumps({"status": "ok"}) + return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"}) class _RecursiveDictRef(object): diff --git a/moto/core/utils.py b/moto/core/utils.py index 451d1a761..11aafbb89 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -118,12 +118,16 @@ class convert_flask_to_httpretty_response(object): return "{0}.{1}".format(outer, self.callback.__name__) def __call__(self, args=None, **kwargs): - from flask import request + from flask import request, Response result = self.callback(request, request.url, {}) # result is a status, headers, response tuple - status, headers, response = result - return response, status, headers + status, headers, content = result + + response = Response(response=content, status=status, headers=headers) + if request.method == "HEAD" and 'content-length' in headers: + response.headers['Content-Length'] = headers['content-length'] + return response class convert_flask_to_responses_response(object): diff --git a/moto/dynamodb/__init__.py b/moto/dynamodb/__init__.py index 008050317..4c2bc04d9 100644 --- a/moto/dynamodb/__init__.py +++ b/moto/dynamodb/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import dynamodb_backend + +dynamodb_backends = {"global": dynamodb_backend} mock_dynamodb = dynamodb_backend.decorator mock_dynamodb_deprecated = dynamodb_backend.deprecated_decorator diff --git a/moto/dynamodb2/__init__.py b/moto/dynamodb2/__init__.py index f0892d13f..7a1f07352 100644 --- a/moto/dynamodb2/__init__.py +++ b/moto/dynamodb2/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import dynamodb_backend2 + +dynamodb_backends2 = {"global": dynamodb_backend2} mock_dynamodb2 = dynamodb_backend2.decorator mock_dynamodb2_deprecated = dynamodb_backend2.deprecated_decorator \ No newline at end of file diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index 10cdcd07b..3c5a087d9 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from boto.ec2.instancetype import InstanceType -from boto.exception import JSONResponseError from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores from moto.ec2.utils import instance_ids_from_querystring, filters_from_querystring, \ diff --git a/moto/ec2/responses/ip_addresses.py b/moto/ec2/responses/ip_addresses.py index fd58741e2..995719202 100644 --- a/moto/ec2/responses/ip_addresses.py +++ b/moto/ec2/responses/ip_addresses.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from boto.exception import JSONResponseError from moto.core.responses import BaseResponse diff --git a/moto/ec2/responses/spot_instances.py b/moto/ec2/responses/spot_instances.py index 321ecd99a..96e5a1ba4 100644 --- a/moto/ec2/responses/spot_instances.py +++ b/moto/ec2/responses/spot_instances.py @@ -35,8 +35,8 @@ class SpotInstances(BaseResponse): def request_spot_instances(self): price = self._get_param('SpotPrice') image_id = self._get_param('LaunchSpecification.ImageId') - count = self._get_int_param('InstanceCount') - type = self._get_param('Type') + count = self._get_int_param('InstanceCount', 1) + type = self._get_param('Type', 'one-time') valid_from = self._get_param('ValidFrom') valid_until = self._get_param('ValidUntil') launch_group = self._get_param('LaunchGroup') @@ -44,7 +44,7 @@ class SpotInstances(BaseResponse): key_name = self._get_param('LaunchSpecification.KeyName') security_groups = self._get_multi_param('LaunchSpecification.SecurityGroup') user_data = self._get_param('LaunchSpecification.UserData') - instance_type = self._get_param('LaunchSpecification.InstanceType') + instance_type = self._get_param('LaunchSpecification.InstanceType', 'm1.small') placement = self._get_param('LaunchSpecification.Placement.AvailabilityZone') kernel_id = self._get_param('LaunchSpecification.KernelId') ramdisk_id = self._get_param('LaunchSpecification.RamdiskId') diff --git a/moto/emr/exceptions.py b/moto/emr/exceptions.py new file mode 100644 index 000000000..1a3398d4f --- /dev/null +++ b/moto/emr/exceptions.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from moto.core.exceptions import RESTError + + +class EmrError(RESTError): + code = 400 diff --git a/moto/emr/models.py b/moto/emr/models.py index f92428331..155e4a898 100644 --- a/moto/emr/models.py +++ b/moto/emr/models.py @@ -299,6 +299,7 @@ class ElasticMapReduceBackend(BaseBackend): created_before = dtparse(created_before) clusters = [c for c in clusters if c.creation_datetime < created_before] + # Amazon EMR can return a maximum of 512 job flow descriptions return sorted(clusters, key=lambda x: x.id)[:512] def describe_step(self, cluster_id, step_id): diff --git a/moto/emr/responses.py b/moto/emr/responses.py index 427ab48c1..3869c33ff 100644 --- a/moto/emr/responses.py +++ b/moto/emr/responses.py @@ -5,15 +5,14 @@ from datetime import datetime from functools import wraps import pytz -from botocore.exceptions import ClientError from moto.compat import urlparse from moto.core.responses import AWSServiceSpec from moto.core.responses import BaseResponse from moto.core.responses import xml_to_json_response +from .exceptions import EmrError from .models import emr_backends -from .utils import steps_from_query_string -from .utils import tags_from_query_string +from .utils import steps_from_query_string, tags_from_query_string def generate_boto3_response(operation): @@ -46,7 +45,7 @@ class ElasticMapReduceResponse(BaseResponse): aws_service_spec = AWSServiceSpec('data/emr/2009-03-31/service-2.json') - def get_region_from_url(self, full_url): + def get_region_from_url(self, request, full_url): parsed = urlparse(full_url) for regex in self.region_regex: match = regex.search(parsed.netloc) @@ -240,9 +239,7 @@ class ElasticMapReduceResponse(BaseResponse): 'Only one AMI version and release label may be specified. ' 'Provided AMI: {0}, release label: {1}.').format( ami_version, release_label) - raise ClientError( - {'Error': {'Code': 'ValidationException', - 'Message': message}}, 'RunJobFlow') + raise EmrError(error_type="ValidationException", message=message, template='single_error') else: if ami_version: kwargs['requested_ami_version'] = ami_version diff --git a/moto/events/__init__.py b/moto/events/__init__.py index 8b15e852a..5c93c59c8 100644 --- a/moto/events/__init__.py +++ b/moto/events/__init__.py @@ -2,4 +2,5 @@ from __future__ import unicode_literals from .models import events_backend +events_backends = {"global": events_backend} mock_events = events_backend.decorator diff --git a/moto/events/urls.py b/moto/events/urls.py index bff05da3f..a6e533b08 100644 --- a/moto/events/urls.py +++ b/moto/events/urls.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from .responses import EventsHandler url_bases = [ - "https://events.(.+).amazonaws.com" + "https?://events.(.+).amazonaws.com" ] url_paths = { diff --git a/moto/iam/__init__.py b/moto/iam/__init__.py index 02519cbc9..c5110b35d 100644 --- a/moto/iam/__init__.py +++ b/moto/iam/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import iam_backend + +iam_backends = {"global": iam_backend} mock_iam = iam_backend.decorator mock_iam_deprecated = iam_backend.deprecated_decorator \ No newline at end of file diff --git a/moto/iam/urls.py b/moto/iam/urls.py index a591e3ebe..46db41e46 100644 --- a/moto/iam/urls.py +++ b/moto/iam/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from .responses import IamResponse url_bases = [ - "https?://iam.amazonaws.com", + "https?://iam(.*).amazonaws.com", ] url_paths = { diff --git a/moto/instance_metadata/__init__.py b/moto/instance_metadata/__init__.py new file mode 100644 index 000000000..9197bcf7c --- /dev/null +++ b/moto/instance_metadata/__init__.py @@ -0,0 +1,4 @@ +from __future__ import unicode_literals +from .models import instance_metadata_backend + +instance_metadata_backends = {"global": instance_metadata_backend} \ No newline at end of file diff --git a/moto/instance_metadata/models.py b/moto/instance_metadata/models.py new file mode 100644 index 000000000..b86f86376 --- /dev/null +++ b/moto/instance_metadata/models.py @@ -0,0 +1,7 @@ +from moto.core.models import BaseBackend + + +class InstanceMetadataBackend(BaseBackend): + pass + +instance_metadata_backend = InstanceMetadataBackend() diff --git a/moto/instance_metadata/responses.py b/moto/instance_metadata/responses.py new file mode 100644 index 000000000..b2de66e7b --- /dev/null +++ b/moto/instance_metadata/responses.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals +import datetime +import json +from urlparse import urlparse + +from moto.core.responses import BaseResponse + + +class InstanceMetadataResponse(BaseResponse): + def metadata_response(self, request, full_url, headers): + """ + Mock response for localhost metadata + + http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html + """ + + parsed_url = urlparse(full_url) + tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1) + credentials = dict( + AccessKeyId="test-key", + SecretAccessKey="test-secret-key", + Token="test-session-token", + Expiration=tomorrow.strftime("%Y-%m-%dT%H:%M:%SZ") + ) + + path = parsed_url.path + + meta_data_prefix = "/latest/meta-data/" + # Strip prefix if it is there + if path.startswith(meta_data_prefix): + path = path[len(meta_data_prefix):] + + if path == '': + result = 'iam' + elif path == 'iam': + result = json.dumps({ + 'security-credentials': { + 'default-role': credentials + } + }) + elif path == 'iam/security-credentials/': + result = 'default-role' + elif path == 'iam/security-credentials/default-role': + result = json.dumps(credentials) + else: + raise NotImplementedError("The {0} metadata path has not been implemented".format(path)) + return 200, headers, result diff --git a/moto/instance_metadata/urls.py b/moto/instance_metadata/urls.py new file mode 100644 index 000000000..7776b364a --- /dev/null +++ b/moto/instance_metadata/urls.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .responses import InstanceMetadataResponse + +url_bases = [ + "http://169.254.169.254" +] + +instance_metadata = InstanceMetadataResponse() + +url_paths = { + '{0}/(?P.+)': instance_metadata.metadata_response, +} diff --git a/moto/kinesis/responses.py b/moto/kinesis/responses.py index 9aed719d5..29f6c07ff 100644 --- a/moto/kinesis/responses.py +++ b/moto/kinesis/responses.py @@ -20,7 +20,7 @@ class KinesisResponse(BaseResponse): @property def is_firehose(self): host = self.headers.get('host') or self.headers['Host'] - return host.startswith('firehose') + return host.startswith('firehose') or 'firehose' in self.headers.get('Authorization', '') def create_stream(self): stream_name = self.parameters.get('StreamName') diff --git a/moto/route53/__init__.py b/moto/route53/__init__.py index df629880f..e2bbe4c1a 100644 --- a/moto/route53/__init__.py +++ b/moto/route53/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import route53_backend + +route53_backends = {"global": route53_backend} mock_route53 = route53_backend.decorator mock_route53_deprecated = route53_backend.deprecated_decorator diff --git a/moto/route53/responses.py b/moto/route53/responses.py index 429317dae..d796660e1 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -1,174 +1,186 @@ from __future__ import unicode_literals from jinja2 import Template from six.moves.urllib.parse import parse_qs, urlparse + +from moto.core.responses import BaseResponse from .models import route53_backend import xmltodict -def list_or_create_hostzone_response(request, full_url, headers): +class Route53 (BaseResponse): + def list_or_create_hostzone_response(self, request, full_url, headers): + self.setup_class(request, full_url, headers) - if request.method == "POST": - elements = xmltodict.parse(request.body) - if "HostedZoneConfig" in elements["CreateHostedZoneRequest"]: - comment = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["Comment"] - try: - # in boto3, this field is set directly in the xml - private_zone = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["PrivateZone"] - except KeyError: - # if a VPC subsection is only included in xmls params when private_zone=True, - # see boto: boto/route53/connection.py - private_zone = 'VPC' in elements["CreateHostedZoneRequest"] - else: - comment = None - private_zone = False + if request.method == "POST": + elements = xmltodict.parse(self.body) + if "HostedZoneConfig" in elements["CreateHostedZoneRequest"]: + comment = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["Comment"] + try: + # in boto3, this field is set directly in the xml + private_zone = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["PrivateZone"] + except KeyError: + # if a VPC subsection is only included in xmls params when private_zone=True, + # see boto: boto/route53/connection.py + private_zone = 'VPC' in elements["CreateHostedZoneRequest"] + else: + comment = None + private_zone = False + + name = elements["CreateHostedZoneRequest"]["Name"] + + if name[-1] != ".": + name += "." + + new_zone = route53_backend.create_hosted_zone( + name, + comment=comment, + private_zone=private_zone, + ) + template = Template(CREATE_HOSTED_ZONE_RESPONSE) + return 201, headers, template.render(zone=new_zone) + + elif request.method == "GET": + all_zones = route53_backend.get_all_hosted_zones() + template = Template(LIST_HOSTED_ZONES_RESPONSE) + return 200, headers, template.render(zones=all_zones) - name = elements["CreateHostedZoneRequest"]["Name"] + def get_or_delete_hostzone_response(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + parsed_url = urlparse(full_url) + zoneid = parsed_url.path.rstrip('/').rsplit('/', 1)[1] + the_zone = route53_backend.get_hosted_zone(zoneid) + if not the_zone: + return 404, headers, "Zone %s not Found" % zoneid - if name[-1] != ".": - name += "." + if request.method == "GET": + template = Template(GET_HOSTED_ZONE_RESPONSE) - new_zone = route53_backend.create_hosted_zone( - name, - comment=comment, - private_zone=private_zone, - ) - template = Template(CREATE_HOSTED_ZONE_RESPONSE) - return 201, headers, template.render(zone=new_zone) - - elif request.method == "GET": - all_zones = route53_backend.get_all_hosted_zones() - template = Template(LIST_HOSTED_ZONES_RESPONSE) - return 200, headers, template.render(zones=all_zones) + return 200, headers, template.render(zone=the_zone) + elif request.method == "DELETE": + route53_backend.delete_hosted_zone(zoneid) + return 200, headers, DELETE_HOSTED_ZONE_RESPONSE -def get_or_delete_hostzone_response(request, full_url, headers): - parsed_url = urlparse(full_url) - zoneid = parsed_url.path.rstrip('/').rsplit('/', 1)[1] - the_zone = route53_backend.get_hosted_zone(zoneid) - if not the_zone: - return 404, headers, "Zone %s not Found" % zoneid + def rrset_response(self, request, full_url, headers): + self.setup_class(request, full_url, headers) - if request.method == "GET": - template = Template(GET_HOSTED_ZONE_RESPONSE) + parsed_url = urlparse(full_url) + method = request.method - return 200, headers, template.render(zone=the_zone) - elif request.method == "DELETE": - route53_backend.delete_hosted_zone(zoneid) - return 200, headers, DELETE_HOSTED_ZONE_RESPONSE + zoneid = parsed_url.path.rstrip('/').rsplit('/', 2)[1] + the_zone = route53_backend.get_hosted_zone(zoneid) + if not the_zone: + return 404, headers, "Zone %s Not Found" % zoneid + + if method == "POST": + elements = xmltodict.parse(self.body) + + change_list = elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes']['Change'] + if not isinstance(change_list, list): + change_list = [elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes']['Change']] + + for value in change_list: + action = value['Action'] + record_set = value['ResourceRecordSet'] + if action in ('CREATE', 'UPSERT'): + if 'ResourceRecords' in record_set: + resource_records = list(record_set['ResourceRecords'].values())[0] + if not isinstance(resource_records, list): + # Depending on how many records there are, this may or may not be a list + resource_records = [resource_records] + record_values = [x['Value'] for x in resource_records] + elif 'AliasTarget' in record_set: + record_values = [record_set['AliasTarget']['DNSName']] + record_set['ResourceRecords'] = record_values + if action == 'CREATE': + the_zone.add_rrset(record_set) + else: + the_zone.upsert_rrset(record_set) + elif action == "DELETE": + if 'SetIdentifier' in record_set: + the_zone.delete_rrset_by_id(record_set["SetIdentifier"]) + else: + the_zone.delete_rrset_by_name(record_set["Name"]) + + return 200, headers, CHANGE_RRSET_RESPONSE + + elif method == "GET": + querystring = parse_qs(parsed_url.query) + template = Template(LIST_RRSET_REPONSE) + type_filter = querystring.get("type", [None])[0] + name_filter = querystring.get("name", [None])[0] + record_sets = the_zone.get_record_sets(type_filter, name_filter) + return 200, headers, template.render(record_sets=record_sets) -def rrset_response(request, full_url, headers): - parsed_url = urlparse(full_url) - method = request.method + def health_check_response(self, request, full_url, headers): + self.setup_class(request, full_url, headers) - zoneid = parsed_url.path.rstrip('/').rsplit('/', 2)[1] - the_zone = route53_backend.get_hosted_zone(zoneid) - if not the_zone: - return 404, headers, "Zone %s Not Found" % zoneid + parsed_url = urlparse(full_url) + method = request.method - if method == "POST": - elements = xmltodict.parse(request.body) + if method == "POST": + properties = xmltodict.parse(self.body)['CreateHealthCheckRequest']['HealthCheckConfig'] + health_check_args = { + "ip_address": properties.get('IPAddress'), + "port": properties.get('Port'), + "type": properties['Type'], + "resource_path": properties.get('ResourcePath'), + "fqdn": properties.get('FullyQualifiedDomainName'), + "search_string": properties.get('SearchString'), + "request_interval": properties.get('RequestInterval'), + "failure_threshold": properties.get('FailureThreshold'), + } + health_check = route53_backend.create_health_check(health_check_args) + template = Template(CREATE_HEALTH_CHECK_RESPONSE) + return 201, headers, template.render(health_check=health_check) + elif method == "DELETE": + health_check_id = parsed_url.path.split("/")[-1] + route53_backend.delete_health_check(health_check_id) + return 200, headers, DELETE_HEALTH_CHECK_REPONSE + elif method == "GET": + template = Template(LIST_HEALTH_CHECKS_REPONSE) + health_checks = route53_backend.get_health_checks() + return 200, headers, template.render(health_checks=health_checks) - change_list = elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes']['Change'] - if not isinstance(change_list, list): - change_list = [elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes']['Change']] + def not_implemented_response(self, request, full_url, headers): + self.setup_class(request, full_url, headers) - for value in change_list: - action = value['Action'] - record_set = value['ResourceRecordSet'] - if action in ('CREATE', 'UPSERT'): - if 'ResourceRecords' in record_set: - resource_records = list(record_set['ResourceRecords'].values())[0] - if not isinstance(resource_records, list): - # Depending on how many records there are, this may or may not be a list - resource_records = [resource_records] - record_values = [x['Value'] for x in resource_records] - elif 'AliasTarget' in record_set: - record_values = [record_set['AliasTarget']['DNSName']] - record_set['ResourceRecords'] = record_values - if action == 'CREATE': - the_zone.add_rrset(record_set) - else: - the_zone.upsert_rrset(record_set) - elif action == "DELETE": - if 'SetIdentifier' in record_set: - the_zone.delete_rrset_by_id(record_set["SetIdentifier"]) - else: - the_zone.delete_rrset_by_name(record_set["Name"]) - - return 200, headers, CHANGE_RRSET_RESPONSE - - elif method == "GET": - querystring = parse_qs(parsed_url.query) - template = Template(LIST_RRSET_REPONSE) - type_filter = querystring.get("type", [None])[0] - name_filter = querystring.get("name", [None])[0] - record_sets = the_zone.get_record_sets(type_filter, name_filter) - return 200, headers, template.render(record_sets=record_sets) + action = '' + if 'tags' in full_url: + action = 'tags' + elif 'trafficpolicyinstances' in full_url: + action = 'policies' + raise NotImplementedError("The action for {0} has not been implemented for route 53".format(action)) -def health_check_response(request, full_url, headers): - parsed_url = urlparse(full_url) - method = request.method + def list_or_change_tags_for_resource_request(self, request, full_url, headers): + self.setup_class(request, full_url, headers) - if method == "POST": - properties = xmltodict.parse(request.body)['CreateHealthCheckRequest']['HealthCheckConfig'] - health_check_args = { - "ip_address": properties.get('IPAddress'), - "port": properties.get('Port'), - "type": properties['Type'], - "resource_path": properties.get('ResourcePath'), - "fqdn": properties.get('FullyQualifiedDomainName'), - "search_string": properties.get('SearchString'), - "request_interval": properties.get('RequestInterval'), - "failure_threshold": properties.get('FailureThreshold'), - } - health_check = route53_backend.create_health_check(health_check_args) - template = Template(CREATE_HEALTH_CHECK_RESPONSE) - return 201, headers, template.render(health_check=health_check) - elif method == "DELETE": - health_check_id = parsed_url.path.split("/")[-1] - route53_backend.delete_health_check(health_check_id) - return 200, headers, DELETE_HEALTH_CHECK_REPONSE - elif method == "GET": - template = Template(LIST_HEALTH_CHECKS_REPONSE) - health_checks = route53_backend.get_health_checks() - return 200, headers, template.render(health_checks=health_checks) + parsed_url = urlparse(full_url) + id_ = parsed_url.path.split("/")[-1] + type_ = parsed_url.path.split("/")[-2] -def not_implemented_response(request, full_url, headers): - action = '' - if 'tags' in full_url: - action = 'tags' - elif 'trafficpolicyinstances' in full_url: - action = 'policies' - raise NotImplementedError("The action for {0} has not been implemented for route 53".format(action)) + if request.method == "GET": + tags = route53_backend.list_tags_for_resource(id_) + template = Template(LIST_TAGS_FOR_RESOURCE_RESPONSE) + return 200, headers, template.render( + resource_type=type_, resource_id=id_, tags=tags) + if request.method == "POST": + tags = xmltodict.parse( + self.body)['ChangeTagsForResourceRequest'] -def list_or_change_tags_for_resource_request(request, full_url, headers): - parsed_url = urlparse(full_url) - id_ = parsed_url.path.split("/")[-1] - type_ = parsed_url.path.split("/")[-2] + if 'AddTags' in tags: + tags = tags['AddTags'] + elif 'RemoveTagKeys' in tags: + tags = tags['RemoveTagKeys'] - if request.method == "GET": - tags = route53_backend.list_tags_for_resource(id_) - template = Template(LIST_TAGS_FOR_RESOURCE_RESPONSE) - return 200, headers, template.render( - resource_type=type_, resource_id=id_, tags=tags) + route53_backend.change_tags_for_resource(id_, tags) + template = Template(CHANGE_TAGS_FOR_RESOURCE_RESPONSE) - if request.method == "POST": - tags = xmltodict.parse( - request.body)['ChangeTagsForResourceRequest'] - - if 'AddTags' in tags: - tags = tags['AddTags'] - elif 'RemoveTagKeys' in tags: - tags = tags['RemoveTagKeys'] - - route53_backend.change_tags_for_resource(id_, tags) - template = Template(CHANGE_TAGS_FOR_RESOURCE_RESPONSE) - - return 200, headers, template.render() + return 200, headers, template.render() LIST_TAGS_FOR_RESOURCE_RESPONSE = """ diff --git a/moto/route53/urls.py b/moto/route53/urls.py index 361c96317..795f7d807 100644 --- a/moto/route53/urls.py +++ b/moto/route53/urls.py @@ -1,15 +1,25 @@ from __future__ import unicode_literals -from . import responses +from .responses import Route53 url_bases = [ - "https://route53.amazonaws.com/201.-..-../", + "https?://route53(.*).amazonaws.com", ] + +def tag_response1(*args, **kwargs): + return Route53().list_or_change_tags_for_resource_request(*args, **kwargs) + + +def tag_response2(*args, **kwargs): + return Route53().list_or_change_tags_for_resource_request(*args, **kwargs) + + url_paths = { - '{0}hostedzone$': responses.list_or_create_hostzone_response, - '{0}hostedzone/[^/]+$': responses.get_or_delete_hostzone_response, - '{0}hostedzone/[^/]+/rrset/?$': responses.rrset_response, - '{0}healthcheck': responses.health_check_response, - '{0}tags/(healthcheck|hostedzone)/*': responses.list_or_change_tags_for_resource_request, - '{0}trafficpolicyinstances/*': responses.not_implemented_response + '{0}/(?P[\d_-]+)/hostedzone$': Route53().list_or_create_hostzone_response, + '{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)$': Route53().get_or_delete_hostzone_response, + '{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/rrset/?$': Route53().rrset_response, + '{0}/(?P[\d_-]+)/healthcheck': Route53().health_check_response, + '{0}/(?P[\d_-]+)/tags/healthcheck/(?P[^/]+)$': tag_response1, + '{0}/(?P[\d_-]+)/tags/hostedzone/(?P[^/]+)$': tag_response2, + '{0}/(?P[\d_-]+)/trafficpolicyinstances/*': Route53().not_implemented_response } diff --git a/moto/s3/__init__.py b/moto/s3/__init__.py index 7d0df53bd..2c54a8d5a 100644 --- a/moto/s3/__init__.py +++ b/moto/s3/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import s3_backend + +s3_backends = {"global": s3_backend} mock_s3 = s3_backend.decorator mock_s3_deprecated = s3_backend.deprecated_decorator \ No newline at end of file diff --git a/moto/s3/models.py b/moto/s3/models.py index 40370b5dd..d5e156498 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -89,21 +89,21 @@ class FakeKey(object): @property def response_dict(self): - r = { + res = { 'etag': self.etag, 'last-modified': self.last_modified_RFC1123, 'content-length': str(len(self.value)), } if self._storage_class != 'STANDARD': - r['x-amz-storage-class'] = self._storage_class + res['x-amz-storage-class'] = self._storage_class if self._expiry is not None: rhdr = 'ongoing-request="false", expiry-date="{0}"' - r['x-amz-restore'] = rhdr.format(self.expiry_date) + res['x-amz-restore'] = rhdr.format(self.expiry_date) if self._is_versioned: - r['x-amz-version-id'] = str(self._version_id) + res['x-amz-version-id'] = str(self._version_id) - return r + return res @property def size(self): diff --git a/moto/server.py b/moto/server.py index 0b5ff7cae..0bb4eb779 100644 --- a/moto/server.py +++ b/moto/server.py @@ -39,21 +39,28 @@ class DomainDispatcherApplication(object): return host for backend_name, backend in BACKENDS.items(): - for url_base in backend.url_bases: + for url_base in backend.values()[0].url_bases: if re.match(url_base, 'http://%s' % host): return backend_name raise RuntimeError('Invalid host: "%s"' % host) def get_application(self, environ): - if environ.get('PATH_INFO', '').startswith("/moto-api"): + path_info = environ.get('PATH_INFO', '') + if path_info.startswith("/moto-api"): host = "moto_api" + elif path_info.startswith("/latest/meta-data/"): + host = "instance_metadata" else: host = environ['HTTP_HOST'].split(':')[0] if host == "localhost": # Fall back to parsing auth header to find service # ['Credential=sdffdsa', '20170220', 'us-east-1', 'sns', 'aws4_request'] - _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[1].split("/") + try: + _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[1].split("/") + except ValueError: + region = 'us-east-1' + service = 's3' host = "{service}.{region}.amazonaws.com".format(service=service, region=region) with self.lock: @@ -108,7 +115,7 @@ def create_backend_app(service): backend_app.view_functions = {} backend_app.url_map = Map() backend_app.url_map.converters['regex'] = RegexConverter - backend = BACKENDS[service] + backend = BACKENDS[service].values()[0] for url_path, handler in backend.flask_paths.items(): if handler.__name__ == 'dispatch': endpoint = '{0}.dispatch'.format(handler.__self__.__name__) diff --git a/moto/ses/__init__.py b/moto/ses/__init__.py index e1ec4b41a..e105b9929 100644 --- a/moto/ses/__init__.py +++ b/moto/ses/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import ses_backend + +ses_backends = {"global": ses_backend} mock_ses = ses_backend.decorator mock_ses_deprecated = ses_backend.deprecated_decorator \ No newline at end of file diff --git a/moto/ses/urls.py b/moto/ses/urls.py index 18d5874c4..adfb4c6e4 100644 --- a/moto/ses/urls.py +++ b/moto/ses/urls.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals from .responses import EmailResponse url_bases = [ - "https?://email.(.+).amazonaws.com" + "https?://email.(.+).amazonaws.com", + "https?://ses.(.+).amazonaws.com", ] url_paths = { diff --git a/moto/settings.py b/moto/settings.py new file mode 100644 index 000000000..a5240f130 --- /dev/null +++ b/moto/settings.py @@ -0,0 +1,3 @@ +import os + +TEST_SERVER_MODE = os.environ.get('TEST_SERVER_MODE', '0').lower() == 'true' diff --git a/moto/sts/__init__.py b/moto/sts/__init__.py index 57456c1b3..7b46bdfbd 100644 --- a/moto/sts/__init__.py +++ b/moto/sts/__init__.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals from .models import sts_backend + +sts_backends = {"global": sts_backend} mock_sts = sts_backend.decorator mock_sts_deprecated = sts_backend.deprecated_decorator diff --git a/moto/sts/urls.py b/moto/sts/urls.py index c6e310960..2078e0b2c 100644 --- a/moto/sts/urls.py +++ b/moto/sts/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from .responses import TokenResponse url_bases = [ - "https?://sts.amazonaws.com" + "https?://sts(.*).amazonaws.com" ] url_paths = { diff --git a/other_langs/sqsSample.java b/other_langs/sqsSample.java new file mode 100644 index 000000000..23368272c --- /dev/null +++ b/other_langs/sqsSample.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amazonaws.samples; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.AmazonSQSClient; + +public class S3Sample { + + public static void main(String[] args) throws IOException { + AmazonSQS sqs = new AmazonSQSClient(); + Region usWest2 = Region.getRegion(Regions.US_WEST_2); + sqs.setRegion(usWest2); + sqs.setEndpoint("http://localhost:8086"); + + String queueName = "my-first-queue"; + sqs.createQueue(queueName); + + System.out.println("Listing queues"); + for (String queue_url: sqs.listQueues().getQueueUrls()) { + System.out.println(" - " + queue_url); + } + System.out.println(); + + } + +} diff --git a/other_langs/test.js b/other_langs/test.js new file mode 100644 index 000000000..65d65ae70 --- /dev/null +++ b/other_langs/test.js @@ -0,0 +1,26 @@ +var AWS = require('aws-sdk'); + +var s3 = new AWS.S3({endpoint: "http://localhost:8086"}); +var myBucket = 'my.unique.bucket.name'; + +var myKey = 'myBucketKey'; + +s3.createBucket({Bucket: myBucket}, function(err, data) { + if (err) { + console.log(err); + } else { + params = {Bucket: myBucket, Key: myKey, Body: 'Hello!'}; + s3.putObject(params, function(err, data) { + if (err) { + console.log(err) + } else { + console.log("Successfully uploaded data to myBucket/myKey"); + } + }); + } +}); + +s3.listBuckets(function(err, data) { + if (err) console.log(err, err.stack); // an error occurred + else console.log(data); // successful response +}); diff --git a/other_langs/test.rb b/other_langs/test.rb new file mode 100644 index 000000000..dc5b7914b --- /dev/null +++ b/other_langs/test.rb @@ -0,0 +1,6 @@ +require 'aws-sdk' + +sqs = Aws::SQS::Resource.new(region: 'us-west-2', endpoint: 'http://localhost:8086') +my_queue = sqs.create_queue(queue_name: 'my-bucket') + +puts sqs.client.list_queues() diff --git a/setup.cfg b/setup.cfg index 3480374bc..3c6e79cf3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [bdist_wheel] -universal=1 \ No newline at end of file +universal=1 diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index 6bd6eb5e5..e52bfe0d7 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -10,7 +10,7 @@ import sure # noqa from botocore.exceptions import ClientError from moto.packages.responses import responses -from moto import mock_apigateway +from moto import mock_apigateway, settings @freeze_time("2015-01-01") @@ -29,11 +29,11 @@ def test_create_and_get_rest_api(): ) response.pop('ResponseMetadata') + response.pop('createdDate') response.should.equal({ 'id': api_id, 'name': 'my_api', 'description': 'this is my api', - 'createdDate': datetime(2015, 1, 1, tzinfo=tzutc()) }) @@ -930,4 +930,5 @@ def test_http_proxying_integration(): deploy_url = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}".format(api_id=api_id, region_name=region_name, stage_name=stage_name) - requests.get(deploy_url).content.should.equal(b"a fake response") + if not settings.TEST_SERVER_MODE: + requests.get(deploy_url).content.should.equal(b"a fake response") diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index ce8892dc9..74e93c373 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -10,7 +10,7 @@ import zipfile import sure # noqa from freezegun import freeze_time -from moto import mock_lambda, mock_s3, mock_ec2 +from moto import mock_lambda, mock_s3, mock_ec2, settings def _process_lamda(pfunc): @@ -36,16 +36,15 @@ def lambda_handler(event, context): volume_id = event.get('volume_id') print('get volume details for %s' % volume_id) import boto3 - ec2 = boto3.resource('ec2', region_name='us-west-2') + ec2 = boto3.resource('ec2', region_name='us-west-2', endpoint_url="http://{base_url}") vol = ec2.Volume(volume_id) print('Volume - %s state=%s, size=%s' % (volume_id, vol.state, vol.size)) return event -""" +""".format(base_url="localhost:8086" if settings.TEST_SERVER_MODE else "ec2.us-west-2.amazonaws.com") return _process_lamda(pfunc) @mock_lambda -@mock_s3 def test_list_functions(): conn = boto3.client('lambda', 'us-west-2') result = conn.list_functions() @@ -53,7 +52,6 @@ def test_list_functions(): @mock_lambda -@freeze_time('2015-01-01 00:00:00') def test_invoke_requestresponse_function(): conn = boto3.client('lambda', 'us-west-2') conn.create_function( @@ -80,7 +78,6 @@ def test_invoke_requestresponse_function(): @mock_lambda -@freeze_time('2015-01-01 00:00:00') def test_invoke_event_function(): conn = boto3.client('lambda', 'us-west-2') conn.create_function( @@ -111,7 +108,6 @@ def test_invoke_event_function(): @mock_ec2 @mock_lambda -@freeze_time('2015-01-01 00:00:00') def test_invoke_function_get_ec2_volume(): conn = boto3.resource("ec2", "us-west-2") vol = conn.create_volume(Size=99, AvailabilityZone='us-west-2') @@ -141,7 +137,6 @@ def test_invoke_function_get_ec2_volume(): @mock_lambda -@freeze_time('2015-01-01 00:00:00') def test_create_based_on_s3_with_missing_bucket(): conn = boto3.client('lambda', 'us-west-2') @@ -196,6 +191,7 @@ def test_create_function_from_aws_bucket(): ) result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + result.pop('LastModified') result.should.equal({ 'FunctionName': 'testFunction', 'FunctionArn': 'arn:aws:lambda:123456789012:function:testFunction', @@ -207,7 +203,6 @@ def test_create_function_from_aws_bucket(): 'Description': 'test lambda function', 'Timeout': 3, 'MemorySize': 128, - 'LastModified': '2015-01-01 00:00:00', 'Version': '$LATEST', 'VpcConfig': { "SecurityGroupIds": ["sg-123abc"], @@ -238,6 +233,7 @@ def test_create_function_from_zipfile(): ) result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + result.pop('LastModified') result.should.equal({ 'FunctionName': 'testFunction', @@ -249,7 +245,6 @@ def test_create_function_from_zipfile(): 'Description': 'test lambda function', 'Timeout': 3, 'MemorySize': 128, - 'LastModified': '2015-01-01 00:00:00', 'CodeSha256': hashlib.sha256(zip_content).hexdigest(), 'Version': '$LATEST', 'VpcConfig': { @@ -290,6 +285,7 @@ def test_get_function(): result = conn.get_function(FunctionName='testFunction') result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + result['Configuration'].pop('LastModified') result.should.equal({ "Code": { @@ -303,7 +299,6 @@ def test_get_function(): "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", "Handler": "lambda_function.handler", - "LastModified": "2015-01-01 00:00:00", "MemorySize": 128, "Role": "test-iam-role", "Runtime": "python2.7", @@ -395,7 +390,6 @@ def test_list_create_list_get_delete_list(): "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", "Handler": "lambda_function.handler", - "LastModified": "2015-01-01 00:00:00", "MemorySize": 128, "Role": "test-iam-role", "Runtime": "python2.7", @@ -408,11 +402,14 @@ def test_list_create_list_get_delete_list(): }, 'ResponseMetadata': {'HTTPStatusCode': 200}, } - conn.list_functions()['Functions'].should.equal([expected_function_result['Configuration']]) + func = conn.list_functions()['Functions'][0] + func.pop('LastModified') + func.should.equal(expected_function_result['Configuration']) func = conn.get_function(FunctionName='testFunction') func['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it func['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + func['Configuration'].pop('LastModified') func.should.equal(expected_function_result) conn.delete_function(FunctionName='testFunction') diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 95ac6ede4..2ee74f886 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -5,7 +5,7 @@ import boto import boto.s3 import boto.s3.key from botocore.exceptions import ClientError -from moto import mock_cloudformation, mock_s3_deprecated +from moto import mock_cloudformation, mock_s3 import json import sure # noqa @@ -118,14 +118,20 @@ def test_create_stack_with_role_arn(): @mock_cloudformation -@mock_s3_deprecated +@mock_s3 def test_create_stack_from_s3_url(): - s3_conn = boto.s3.connect_to_region('us-west-1') - bucket = s3_conn.create_bucket("foobar") - key = boto.s3.key.Key(bucket) - key.key = "template-key" - key.set_contents_from_string(dummy_template_json) - key_url = key.generate_url(expires_in=0, query_auth=False) + s3 = boto3.client('s3') + s3_conn = boto3.resource('s3') + bucket = s3_conn.create_bucket(Bucket="foobar") + + key = s3_conn.Object('foobar', 'template-key').put(Body=dummy_template_json) + key_url = s3.generate_presigned_url( + ClientMethod='get_object', + Params={ + 'Bucket': 'foobar', + 'Key': 'template-key' + } + ) cf_conn = boto3.client('cloudformation', region_name='us-west-1') cf_conn.create_stack( diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 1b9330a9f..609a0b46d 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -701,27 +701,29 @@ def test_vpc_single_instance_in_subnet(): eip_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] eip_resource.physical_resource_id.should.equal(eip.allocation_id) -@mock_cloudformation_deprecated() -@mock_ec2_deprecated() +@mock_cloudformation() +@mock_ec2() @mock_rds2() def test_rds_db_parameter_groups(): - ec2_conn = boto.ec2.connect_to_region("us-west-1") - ec2_conn.create_security_group('application', 'Our Application Group') + ec2_conn = boto3.client("ec2", region_name="us-west-1") + ec2_conn.create_security_group(GroupName='application', Description='Our Application Group') template_json = json.dumps(rds_mysql_with_db_parameter_group.template) - conn = boto.cloudformation.connect_to_region("us-west-1") - conn.create_stack( - "test_stack", - template_body=template_json, - parameters=[ - ("DBInstanceIdentifier", "master_db"), - ("DBName", "my_db"), - ("DBUser", "my_user"), - ("DBPassword", "my_password"), - ("DBAllocatedStorage", "20"), - ("DBInstanceClass", "db.m1.medium"), - ("EC2SecurityGroup", "application"), - ("MultiAZ", "true"), + cf_conn = boto3.client('cloudformation', 'us-west-1') + cf_conn.create_stack( + StackName="test_stack", + TemplateBody=template_json, + Parameters=[{'ParameterKey': key, 'ParameterValue': value} for + key, value in [ + ("DBInstanceIdentifier", "master_db"), + ("DBName", "my_db"), + ("DBUser", "my_user"), + ("DBPassword", "my_password"), + ("DBAllocatedStorage", "20"), + ("DBInstanceClass", "db.m1.medium"), + ("EC2SecurityGroup", "application"), + ("MultiAZ", "true"), + ] ], ) @@ -1802,7 +1804,7 @@ def lambda_handler(event, context): return _process_lamda(pfunc) -@mock_cloudformation_deprecated +@mock_cloudformation @mock_lambda def test_lambda_function(): # switch this to python as backend lambda only supports python execution. @@ -1826,10 +1828,10 @@ def test_lambda_function(): } template_json = json.dumps(template) - cf_conn = boto.cloudformation.connect_to_region("us-east-1") + cf_conn = boto3.client('cloudformation', 'us-east-1') cf_conn.create_stack( - "test_stack", - template_body=template_json, + StackName="test_stack", + TemplateBody=template_json, ) conn = boto3.client('lambda', 'us-east-1') diff --git a/tests/test_core/test_instance_metadata.py b/tests/test_core/test_instance_metadata.py index aa86b41b3..80dd501e7 100644 --- a/tests/test_core/test_instance_metadata.py +++ b/tests/test_core/test_instance_metadata.py @@ -3,18 +3,23 @@ import sure # noqa from nose.tools import assert_raises import requests -from moto import mock_ec2 +from moto import mock_ec2, settings + +if settings.TEST_SERVER_MODE: + BASE_URL = 'http://localhost:8086' +else: + BASE_URL = 'http://169.254.169.254' @mock_ec2 def test_latest_meta_data(): - res = requests.get("http://169.254.169.254/latest/meta-data/") + res = requests.get("{0}/latest/meta-data/".format(BASE_URL)) res.content.should.equal(b"iam") @mock_ec2 def test_meta_data_iam(): - res = requests.get("http://169.254.169.254/latest/meta-data/iam") + res = requests.get("{0}/latest/meta-data/iam".format(BASE_URL)) json_response = res.json() default_role = json_response['security-credentials']['default-role'] default_role.should.contain('AccessKeyId') @@ -25,21 +30,15 @@ def test_meta_data_iam(): @mock_ec2 def test_meta_data_security_credentials(): - res = requests.get("http://169.254.169.254/latest/meta-data/iam/security-credentials/") + res = requests.get("{0}/latest/meta-data/iam/security-credentials/".format(BASE_URL)) res.content.should.equal(b"default-role") @mock_ec2 def test_meta_data_default_role(): - res = requests.get("http://169.254.169.254/latest/meta-data/iam/security-credentials/default-role") + res = requests.get("{0}/latest/meta-data/iam/security-credentials/default-role".format(BASE_URL)) json_response = res.json() json_response.should.contain('AccessKeyId') json_response.should.contain('SecretAccessKey') json_response.should.contain('Token') json_response.should.contain('Expiration') - - -@mock_ec2 -def test_meta_data_unknown_path(): - with assert_raises(NotImplementedError): - requests.get("http://169.254.169.254/latest/meta-data/badpath") diff --git a/tests/test_core/test_moto_api.py b/tests/test_core/test_moto_api.py new file mode 100644 index 000000000..3b441a3f1 --- /dev/null +++ b/tests/test_core/test_moto_api.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals +import sure # noqa +from nose.tools import assert_raises +import requests + +import boto3 +from moto import mock_sqs, settings + +base_url = "http://localhost:8086" if settings.TEST_SERVER_MODE else "http://motoapi.amazonaws.com" + + +@mock_sqs +def test_reset_api(): + conn = boto3.client("sqs", region_name='us-west-1') + conn.create_queue(QueueName="queue1") + conn.list_queues()['QueueUrls'].should.have.length_of(1) + + res = requests.post("{base_url}/moto-api/reset".format(base_url=base_url)) + res.content.should.equal(b'{"status": "ok"}') + + conn.list_queues().shouldnt.contain('QueueUrls') # No more queues diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 7ea56faa9..f2df39a22 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -42,13 +42,6 @@ def test_describe_missing_table(): conn.describe_table('messages') -@mock_dynamodb -def test_sts_handler(): - res = requests.post("https://sts.amazonaws.com/", data={"GetSessionToken": ""}) - res.ok.should.be.ok - res.text.should.contain("SecretAccessKey") - - @mock_dynamodb_deprecated def test_dynamodb_with_connect_to_region(): # this will work if connected with boto.connect_dynamodb() diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index d66d36d9f..9e92e7985 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -63,11 +63,3 @@ def test_describe_missing_table(): aws_secret_access_key="sk") with assert_raises(JSONResponseError): conn.describe_table('messages') - - -@requires_boto_gte("2.9") -@mock_dynamodb2 -def test_sts_handler(): - res = requests.post("https://sts.amazonaws.com/", data={"GetSessionToken": ""}) - res.ok.should.be.ok - res.text.should.contain("SecretAccessKey") diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 9c3fbd40d..4c154ae84 100755 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -5,7 +5,7 @@ from nose.tools import assert_raises import boto import boto.ec2 -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError, EC2ResponseError import sure # noqa @@ -19,9 +19,9 @@ def test_ami_create_and_delete(): reservation = conn.run_instances('ami-1234abcd') instance = reservation.instances[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: image_id = conn.create_image(instance.id, "test-ami", "this is a test ami", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateImage operation: Request would have succeeded, but DryRun flag is set') @@ -52,9 +52,9 @@ def test_ami_create_and_delete(): snapshot.volume_id.should.equal(volume.id) # Deregister - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: success = conn.deregister_image(image_id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeregisterImage operation: Request would have succeeded, but DryRun flag is set') @@ -80,9 +80,9 @@ def test_ami_copy(): source_image = conn.get_all_images(image_ids=[source_image_id])[0] # Boto returns a 'CopyImage' object with an image_id attribute here. Use the image_id to fetch the full info. - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: copy_image_ref = conn.copy_image(source_image.region.name, source_image.id, "test-copy-ami", "this is a test copy ami", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CopyImage operation: Request would have succeeded, but DryRun flag is set') @@ -127,9 +127,9 @@ def test_ami_tagging(): conn.create_image(instance.id, "test-ami", "this is a test ami") image = conn.get_all_images()[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: image.add_tag("a key", "some value", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') @@ -289,9 +289,9 @@ def test_ami_attribute_group_permissions(): 'groups': 'all'} # Add 'all' group and confirm - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.modify_image_attribute(**dict(ADD_GROUP_ARGS, **{'dry_run': True})) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyImageAttribute operation: Request would have succeeded, but DryRun flag is set') diff --git a/tests/test_ec2/test_ec2_core.py b/tests/test_ec2/test_ec2_core.py index 53c7d6480..baffc4882 100644 --- a/tests/test_ec2/test_ec2_core.py +++ b/tests/test_ec2/test_ec2_core.py @@ -1,11 +1 @@ from __future__ import unicode_literals -import requests -from moto import mock_ec2 - - -@mock_ec2 -def test_not_implemented_method(): - requests.post.when.called_with( - "https://ec2.us-east-1.amazonaws.com/", - data={'Action': ['foobar']} - ).should.throw(NotImplementedError) diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index c4794b1c8..6491412e3 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -5,7 +5,7 @@ from nose.tools import assert_raises from moto.ec2 import ec2_backends import boto -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError import sure # noqa from moto import mock_ec2_deprecated @@ -24,9 +24,9 @@ def test_create_and_delete_volume(): volume = all_volumes[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: volume.delete(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteVolume operation: Request would have succeeded, but DryRun flag is set') @@ -46,9 +46,9 @@ def test_create_and_delete_volume(): @mock_ec2_deprecated def test_create_encrypted_volume_dryrun(): conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.create_volume(80, "us-east-1a", encrypted=True, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') @@ -58,9 +58,9 @@ def test_create_encrypted_volume(): conn = boto.connect_ec2('the_key', 'the_secret') conn.create_volume(80, "us-east-1a", encrypted=True) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.create_volume(80, "us-east-1a", encrypted=True, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') @@ -165,9 +165,9 @@ def test_volume_attach_and_detach(): volume.update() volume.volume_state().should.equal('available') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: volume.attach(instance.id, "/dev/sdh", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AttachVolume operation: Request would have succeeded, but DryRun flag is set') @@ -179,9 +179,9 @@ def test_volume_attach_and_detach(): volume.attach_data.instance_id.should.equal(instance.id) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: volume.detach(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DetachVolume operation: Request would have succeeded, but DryRun flag is set') @@ -214,9 +214,9 @@ def test_create_snapshot(): conn = boto.connect_ec2('the_key', 'the_secret') volume = conn.create_volume(80, "us-east-1a") - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: snapshot = volume.create_snapshot('a dryrun snapshot', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSnapshot operation: Request would have succeeded, but DryRun flag is set') @@ -347,9 +347,9 @@ def test_snapshot_attribute(): # Add 'all' group and confirm - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.modify_snapshot_attribute(**dict(ADD_GROUP_ARGS, **{'dry_run': True})) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifySnapshotAttribute operation: Request would have succeeded, but DryRun flag is set') @@ -363,9 +363,9 @@ def test_snapshot_attribute(): conn.modify_snapshot_attribute.when.called_with(**ADD_GROUP_ARGS).should_not.throw(EC2ResponseError) # Remove 'all' group and confirm - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.modify_snapshot_attribute(**dict(REMOVE_GROUP_ARGS, **{'dry_run': True})) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifySnapshotAttribute operation: Request would have succeeded, but DryRun flag is set') @@ -424,9 +424,9 @@ def test_create_volume_from_snapshot(): volume = conn.create_volume(80, "us-east-1a") snapshot = volume.create_snapshot('a test snapshot') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: snapshot = volume.create_snapshot('a test snapshot', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSnapshot operation: Request would have succeeded, but DryRun flag is set') @@ -468,9 +468,9 @@ def test_modify_attribute_blockDeviceMapping(): instance = reservation.instances[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True}, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceAttribute operation: Request would have succeeded, but DryRun flag is set') @@ -487,9 +487,9 @@ def test_volume_tag_escaping(): vol = conn.create_volume(10, 'us-east-1a') snapshot = conn.create_snapshot(vol.id, 'Desc') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: snapshot.add_tags({'key': ''}, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') dict(conn.get_all_snapshots()[0].tags).should_not.be.equal({'key': ''}) diff --git a/tests/test_ec2/test_elastic_ip_addresses.py b/tests/test_ec2/test_elastic_ip_addresses.py index dc7910379..f92c4df8b 100644 --- a/tests/test_ec2/test_elastic_ip_addresses.py +++ b/tests/test_ec2/test_elastic_ip_addresses.py @@ -5,7 +5,7 @@ from nose.tools import assert_raises import boto import boto3 -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError import six import sure # noqa @@ -20,9 +20,9 @@ def test_eip_allocate_classic(): """Allocate/release Classic EIP""" conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: standard = conn.allocate_address(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AllocateAddress operation: Request would have succeeded, but DryRun flag is set') @@ -32,9 +32,9 @@ def test_eip_allocate_classic(): standard.instance_id.should.be.none standard.domain.should.be.equal("standard") - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: standard.release(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ReleaseAddress operation: Request would have succeeded, but DryRun flag is set') @@ -47,9 +47,9 @@ def test_eip_allocate_vpc(): """Allocate/release VPC EIP""" conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: vpc = conn.allocate_address(domain="vpc", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AllocateAddress operation: Request would have succeeded, but DryRun flag is set') @@ -89,9 +89,9 @@ def test_eip_associate_classic(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.associate_address(instance_id=instance.id, public_ip=eip.public_ip, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AssociateAddress operation: Request would have succeeded, but DryRun flag is set') @@ -99,9 +99,9 @@ def test_eip_associate_classic(): eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): eip.instance_id.should.be.equal(instance.id) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.disassociate_address(public_ip=eip.public_ip, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DisAssociateAddress operation: Request would have succeeded, but DryRun flag is set') @@ -139,9 +139,9 @@ def test_eip_associate_vpc(): eip.instance_id.should.be.equal(u'') eip.association_id.should.be.none - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: eip.release(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ReleaseAddress operation: Request would have succeeded, but DryRun flag is set') @@ -153,9 +153,8 @@ def test_eip_associate_vpc(): @mock_ec2 def test_eip_boto3_vpc_association(): """Associate EIP to VPC instance in a new subnet with boto3""" - session = boto3.session.Session(region_name='us-west-1') - service = session.resource('ec2') - client = session.client('ec2') + service = boto3.resource('ec2', region_name='us-west-1') + client = boto3.client('ec2', region_name='us-west-1') vpc_res = client.create_vpc(CidrBlock='10.0.0.0/24') subnet_res = client.create_subnet( VpcId=vpc_res['Vpc']['VpcId'], CidrBlock='10.0.0.0/24') diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py index 6f60c85a8..9027e0448 100644 --- a/tests/test_ec2/test_elastic_network_interfaces.py +++ b/tests/test_ec2/test_elastic_network_interfaces.py @@ -4,10 +4,11 @@ import tests.backport_assert_raises from nose.tools import assert_raises import boto3 +from botocore.exceptions import ClientError import boto import boto.cloudformation import boto.ec2 -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError import sure # noqa from moto import mock_ec2, mock_cloudformation_deprecated, mock_ec2_deprecated @@ -22,9 +23,9 @@ def test_elastic_network_interfaces(): vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: eni = conn.create_network_interface(subnet.id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateNetworkInterface operation: Request would have succeeded, but DryRun flag is set') @@ -36,9 +37,9 @@ def test_elastic_network_interfaces(): eni.groups.should.have.length_of(0) eni.private_ip_addresses.should.have.length_of(0) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.delete_network_interface(eni.id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteNetworkInterface operation: Request would have succeeded, but DryRun flag is set') @@ -49,7 +50,7 @@ def test_elastic_network_interfaces(): with assert_raises(EC2ResponseError) as cm: conn.delete_network_interface(eni.id) - cm.exception.code.should.equal('InvalidNetworkInterfaceID.NotFound') + cm.exception.error_code.should.equal('InvalidNetworkInterfaceID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none @@ -60,7 +61,7 @@ def test_elastic_network_interfaces_subnet_validation(): with assert_raises(EC2ResponseError) as cm: conn.create_network_interface("subnet-abcd1234") - cm.exception.code.should.equal('InvalidSubnetID.NotFound') + cm.exception.error_code.should.equal('InvalidSubnetID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none @@ -117,9 +118,9 @@ def test_elastic_network_interfaces_modify_attribute(): eni.groups.should.have.length_of(1) eni.groups[0].id.should.equal(security_group1.id) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.modify_network_interface_attribute(eni.id, 'groupset', [security_group2.id], dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyNetworkInterface operation: Request would have succeeded, but DryRun flag is set') @@ -183,11 +184,11 @@ def test_elastic_network_interfaces_get_by_tag_name(): eni1 = ec2.create_network_interface(SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') - with assert_raises(JSONResponseError) as ex: + with assert_raises(ClientError) as ex: eni1.create_tags(Tags=[{'Key': 'Name', 'Value': 'eni1'}], DryRun=True) - ex.exception.reason.should.equal('DryRunOperation') - ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.response['Error']['Code'].should.equal('DryRunOperation') + ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') eni1.create_tags(Tags=[{'Key': 'Name', 'Value': 'eni1'}]) diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index a310c05a4..b6601e87f 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -8,7 +8,7 @@ import datetime import boto from boto.ec2.instance import Reservation, InstanceAttribute -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError, EC2ResponseError from freezegun import freeze_time import sure # noqa @@ -41,9 +41,9 @@ def test_add_servers(): def test_instance_launch_and_terminate(): conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: reservation = conn.run_instances('ami-1234abcd', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RunInstance operation: Request would have succeeded, but DryRun flag is set') @@ -74,9 +74,9 @@ def test_instance_launch_and_terminate(): volume.attach_data.instance_id.should.equal(instance.id) volume.status.should.equal('in-use') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.terminate_instances([instance.id], dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the TerminateInstance operation: Request would have succeeded, but DryRun flag is set') @@ -427,9 +427,9 @@ def test_instance_start_and_stop(): instance_ids = [instance.id for instance in instances] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: stopped_instances = conn.stop_instances(instance_ids, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the StopInstance operation: Request would have succeeded, but DryRun flag is set') @@ -438,9 +438,9 @@ def test_instance_start_and_stop(): for instance in stopped_instances: instance.state.should.equal('stopping') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: started_instances = conn.start_instances([instances[0].id], dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the StartInstance operation: Request would have succeeded, but DryRun flag is set') @@ -454,9 +454,9 @@ def test_instance_reboot(): reservation = conn.run_instances('ami-1234abcd') instance = reservation.instances[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.reboot(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RebootInstance operation: Request would have succeeded, but DryRun flag is set') @@ -470,9 +470,9 @@ def test_instance_attribute_instance_type(): reservation = conn.run_instances('ami-1234abcd') instance = reservation.instances[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.modify_attribute("instanceType", "m1.small", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceType operation: Request would have succeeded, but DryRun flag is set') @@ -491,9 +491,9 @@ def test_modify_instance_attribute_security_groups(): sg_id = 'sg-1234abcd' sg_id2 = 'sg-abcd4321' - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.modify_attribute("groupSet", [sg_id, sg_id2], dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceSecurityGroups operation: Request would have succeeded, but DryRun flag is set') @@ -512,9 +512,9 @@ def test_instance_attribute_user_data(): reservation = conn.run_instances('ami-1234abcd') instance = reservation.instances[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.modify_attribute("userData", "this is my user data", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyUserData operation: Request would have succeeded, but DryRun flag is set') @@ -540,9 +540,9 @@ def test_instance_attribute_source_dest_check(): # Set to false (note: Boto converts bool to string, eg 'false') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.modify_attribute("sourceDestCheck", False, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifySourceDestCheck operation: Request would have succeeded, but DryRun flag is set') @@ -584,9 +584,9 @@ def test_user_data_with_run_instance(): def test_run_instance_with_security_group_name(): conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: group = conn.create_security_group('group1', "some description", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSecurityGroup operation: Request would have succeeded, but DryRun flag is set') @@ -745,9 +745,9 @@ def test_instance_with_nic_attach_detach(): set([group.id for group in eni.groups]).should.equal(set([security_group2.id])) # Attach - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.attach_network_interface(eni.id, instance.id, device_index=1, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AttachNetworkInterface operation: Request would have succeeded, but DryRun flag is set') @@ -766,9 +766,9 @@ def test_instance_with_nic_attach_detach(): set([group.id for group in eni.groups]).should.equal(set([security_group1.id,security_group2.id])) # Detach - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.detach_network_interface(instance_eni.attachment.id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DetachNetworkInterface operation: Request would have succeeded, but DryRun flag is set') @@ -886,9 +886,9 @@ def test_get_instance_by_security_group(): security_group = conn.create_security_group('test', 'test') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.modify_instance_attribute(instance.id, "groupSet", [security_group.id], dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceSecurityGroups operation: Request would have succeeded, but DryRun flag is set') diff --git a/tests/test_ec2/test_internet_gateways.py b/tests/test_ec2/test_internet_gateways.py index 12b37860e..fe5e4945d 100644 --- a/tests/test_ec2/test_internet_gateways.py +++ b/tests/test_ec2/test_internet_gateways.py @@ -6,7 +6,7 @@ from nose.tools import assert_raises import re import boto -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError import sure # noqa @@ -24,9 +24,9 @@ def test_igw_create(): conn.get_all_internet_gateways().should.have.length_of(0) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: igw = conn.create_internet_gateway(dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateInternetGateway operation: Request would have succeeded, but DryRun flag is set') @@ -44,9 +44,9 @@ def test_igw_attach(): igw = conn.create_internet_gateway() vpc = conn.create_vpc(VPC_CIDR) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.attach_internet_gateway(igw.id, vpc.id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AttachInternetGateway operation: Request would have succeeded, but DryRun flag is set') @@ -90,9 +90,9 @@ def test_igw_detach(): vpc = conn.create_vpc(VPC_CIDR) conn.attach_internet_gateway(igw.id, vpc.id) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.detach_internet_gateway(igw.id, vpc.id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DetachInternetGateway operation: Request would have succeeded, but DryRun flag is set') @@ -151,9 +151,9 @@ def test_igw_delete(): igw = conn.create_internet_gateway() conn.get_all_internet_gateways().should.have.length_of(1) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.delete_internet_gateway(igw.id, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteInternetGateway operation: Request would have succeeded, but DryRun flag is set') diff --git a/tests/test_ec2/test_key_pairs.py b/tests/test_ec2/test_key_pairs.py index a35f0b962..6c4773200 100644 --- a/tests/test_ec2/test_key_pairs.py +++ b/tests/test_ec2/test_key_pairs.py @@ -7,7 +7,7 @@ import boto import six import sure # noqa -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError from moto import mock_ec2_deprecated @@ -32,9 +32,9 @@ def test_key_pairs_invalid_id(): def test_key_pairs_create(): conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: kp = conn.create_key_pair('foo', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateKeyPair operation: Request would have succeeded, but DryRun flag is set') @@ -87,9 +87,9 @@ def test_key_pairs_delete_exist(): conn = boto.connect_ec2('the_key', 'the_secret') conn.create_key_pair('foo') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: r = conn.delete_key_pair('foo', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteKeyPair operation: Request would have succeeded, but DryRun flag is set') @@ -102,9 +102,9 @@ def test_key_pairs_delete_exist(): def test_key_pairs_import(): conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: kp = conn.import_key_pair('foo', b'content', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ImportKeyPair operation: Request would have succeeded, but DryRun flag is set') diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py index 3968d9151..3056331be 100644 --- a/tests/test_ec2/test_security_groups.py +++ b/tests/test_ec2/test_security_groups.py @@ -9,7 +9,7 @@ from nose.tools import assert_raises import boto3 import boto from botocore.exceptions import ClientError -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError import sure # noqa from moto import mock_ec2, mock_ec2_deprecated @@ -19,9 +19,9 @@ from moto import mock_ec2, mock_ec2_deprecated def test_create_and_describe_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: security_group = conn.create_security_group('test security group', 'this is a test security group', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSecurityGroup operation: Request would have succeeded, but DryRun flag is set') @@ -121,9 +121,9 @@ def test_deleting_security_groups(): cm.exception.request_id.should_not.be.none # Delete by name - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.delete_security_group('test2', dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteSecurityGroup operation: Request would have succeeded, but DryRun flag is set') @@ -150,9 +150,9 @@ def test_authorize_ip_range_and_revoke(): conn = boto.connect_ec2('the_key', 'the_secret') security_group = conn.create_security_group('test', 'test') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: success = security_group.authorize(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the GrantSecurityGroupIngress operation: Request would have succeeded, but DryRun flag is set') @@ -171,9 +171,9 @@ def test_authorize_ip_range_and_revoke(): cm.exception.request_id.should_not.be.none # Actually revoke - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RevokeSecurityGroupIngress operation: Request would have succeeded, but DryRun flag is set') @@ -185,9 +185,9 @@ def test_authorize_ip_range_and_revoke(): # Test for egress as well egress_security_group = conn.create_security_group('testegress', 'testegress', vpc_id='vpc-3432589') - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: success = conn.authorize_security_group_egress(egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the GrantSecurityGroupEgress operation: Request would have succeeded, but DryRun flag is set') @@ -203,9 +203,9 @@ def test_authorize_ip_range_and_revoke(): egress_security_group.revoke.when.called_with(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.122/32").should.throw(EC2ResponseError) # Actually revoke - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.revoke_security_group_egress(egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RevokeSecurityGroupEgress operation: Request would have succeeded, but DryRun flag is set') @@ -339,9 +339,9 @@ def test_security_group_tagging(): sg = conn.create_security_group("test-sg", "Test SG", vpc.id) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: sg.add_tag("Test", "Tag", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') @@ -540,11 +540,11 @@ def test_security_group_tagging_boto3(): sg = conn.create_security_group(GroupName="test-sg", Description="Test SG") - with assert_raises(JSONResponseError) as ex: + with assert_raises(ClientError) as ex: conn.create_tags(Resources=[sg['GroupId']], Tags=[{'Key': 'Test', 'Value': 'Tag'}], DryRun=True) - ex.exception.reason.should.equal('DryRunOperation') - ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.response['Error']['Code'].should.equal('DryRunOperation') + ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') conn.create_tags(Resources=[sg['GroupId']], Tags=[{'Key': 'Test', 'Value': 'Tag'}]) describe = conn.describe_security_groups(Filters=[{'Name': 'tag-value', 'Values': ['Tag']}]) diff --git a/tests/test_ec2/test_spot_instances.py b/tests/test_ec2/test_spot_instances.py index 1933613e8..2d3cb3036 100644 --- a/tests/test_ec2/test_spot_instances.py +++ b/tests/test_ec2/test_spot_instances.py @@ -4,8 +4,10 @@ import datetime import boto import boto3 +from boto.exception import EC2ResponseError +from botocore.exceptions import ClientError +import pytz import sure # noqa -from boto.exception import JSONResponseError from moto import mock_ec2, mock_ec2_deprecated from moto.backends import get_model @@ -13,98 +15,130 @@ from moto.core.utils import iso_8601_datetime_with_milliseconds @mock_ec2 -@mock_ec2_deprecated def test_request_spot_instances(): conn = boto3.client('ec2', 'us-east-1') vpc = conn.create_vpc(CidrBlock="10.0.0.0/8")['Vpc'] subnet = conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] subnet_id = subnet['SubnetId'] - conn = boto.connect_ec2() + conn.create_security_group(GroupName='group1', Description='description') + conn.create_security_group(GroupName='group2', Description='description') - conn.create_security_group('group1', 'description') - conn.create_security_group('group2', 'description') + start_dt = datetime.datetime(2013, 1, 1).replace(tzinfo=pytz.utc) + end_dt = datetime.datetime(2013, 1, 2).replace(tzinfo=pytz.utc) + start = iso_8601_datetime_with_milliseconds(start_dt) + end = iso_8601_datetime_with_milliseconds(end_dt) - start = iso_8601_datetime_with_milliseconds(datetime.datetime(2013, 1, 1)) - end = iso_8601_datetime_with_milliseconds(datetime.datetime(2013, 1, 2)) - - with assert_raises(JSONResponseError) as ex: + with assert_raises(ClientError) as ex: request = conn.request_spot_instances( - price=0.5, image_id='ami-abcd1234', count=1, type='one-time', - valid_from=start, valid_until=end, launch_group="the-group", - availability_zone_group='my-group', key_name="test", - security_groups=['group1', 'group2'], user_data=b"some test data", - instance_type='m1.small', placement='us-east-1c', - kernel_id="test-kernel", ramdisk_id="test-ramdisk", - monitoring_enabled=True, subnet_id=subnet_id, dry_run=True + SpotPrice="0.5", InstanceCount=1, Type='one-time', + ValidFrom=start, ValidUntil=end, LaunchGroup="the-group", + AvailabilityZoneGroup='my-group', + LaunchSpecification={ + "ImageId": 'ami-abcd1234', + "KeyName": "test", + "SecurityGroups": ['group1', 'group2'], + "UserData": b"some test data", + "InstanceType": 'm1.small', + "Placement": { + "AvailabilityZone": 'us-east-1c', + }, + "KernelId": "test-kernel", + "RamdiskId": "test-ramdisk", + "Monitoring": { + "Enabled": True, + }, + "SubnetId": subnet_id, + }, + DryRun=True, ) - ex.exception.reason.should.equal('DryRunOperation') - ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RequestSpotInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.response['Error']['Code'].should.equal('DryRunOperation') + ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal('An error occurred (DryRunOperation) when calling the RequestSpotInstance operation: Request would have succeeded, but DryRun flag is set') request = conn.request_spot_instances( - price=0.5, image_id='ami-abcd1234', count=1, type='one-time', - valid_from=start, valid_until=end, launch_group="the-group", - availability_zone_group='my-group', key_name="test", - security_groups=['group1', 'group2'], user_data=b"some test data", - instance_type='m1.small', placement='us-east-1c', - kernel_id="test-kernel", ramdisk_id="test-ramdisk", - monitoring_enabled=True, subnet_id=subnet_id, + SpotPrice="0.5", InstanceCount=1, Type='one-time', + ValidFrom=start, ValidUntil=end, LaunchGroup="the-group", + AvailabilityZoneGroup='my-group', + LaunchSpecification={ + "ImageId": 'ami-abcd1234', + "KeyName": "test", + "SecurityGroups": ['group1', 'group2'], + "UserData": b"some test data", + "InstanceType": 'm1.small', + "Placement": { + "AvailabilityZone": 'us-east-1c', + }, + "KernelId": "test-kernel", + "RamdiskId": "test-ramdisk", + "Monitoring": { + "Enabled": True, + }, + "SubnetId": subnet_id, + }, ) - requests = conn.get_all_spot_instance_requests() + requests = conn.describe_spot_instance_requests()['SpotInstanceRequests'] requests.should.have.length_of(1) request = requests[0] - request.state.should.equal("open") - request.price.should.equal(0.5) - request.launch_specification.image_id.should.equal('ami-abcd1234') - request.type.should.equal('one-time') - request.valid_from.should.equal(start) - request.valid_until.should.equal(end) - request.launch_group.should.equal("the-group") - request.availability_zone_group.should.equal('my-group') - request.launch_specification.key_name.should.equal("test") - security_group_names = [group.name for group in request.launch_specification.groups] + request['State'].should.equal("open") + request['SpotPrice'].should.equal("0.5") + request['Type'].should.equal('one-time') + request['ValidFrom'].should.equal(start_dt) + request['ValidUntil'].should.equal(end_dt) + request['LaunchGroup'].should.equal("the-group") + request['AvailabilityZoneGroup'].should.equal('my-group') + + launch_spec = request['LaunchSpecification'] + security_group_names = [group['GroupName'] for group in launch_spec['SecurityGroups']] set(security_group_names).should.equal(set(['group1', 'group2'])) - request.launch_specification.instance_type.should.equal('m1.small') - request.launch_specification.placement.should.equal('us-east-1c') - request.launch_specification.kernel.should.equal("test-kernel") - request.launch_specification.ramdisk.should.equal("test-ramdisk") - request.launch_specification.subnet_id.should.equal(subnet_id) + + launch_spec['ImageId'].should.equal('ami-abcd1234') + launch_spec['KeyName'].should.equal("test") + launch_spec['InstanceType'].should.equal('m1.small') + launch_spec['KernelId'].should.equal("test-kernel") + launch_spec['RamdiskId'].should.equal("test-ramdisk") + launch_spec['SubnetId'].should.equal(subnet_id) -@mock_ec2_deprecated +@mock_ec2 def test_request_spot_instances_default_arguments(): """ Test that moto set the correct default arguments """ - conn = boto.connect_ec2() + conn = boto3.client('ec2', 'us-east-1') request = conn.request_spot_instances( - price=0.5, image_id='ami-abcd1234', + SpotPrice="0.5", + LaunchSpecification={ + "ImageId": 'ami-abcd1234', + } ) - requests = conn.get_all_spot_instance_requests() + requests = conn.describe_spot_instance_requests()['SpotInstanceRequests'] requests.should.have.length_of(1) request = requests[0] - request.state.should.equal("open") - request.price.should.equal(0.5) - request.launch_specification.image_id.should.equal('ami-abcd1234') - request.type.should.equal('one-time') - request.valid_from.should.equal(None) - request.valid_until.should.equal(None) - request.launch_group.should.equal(None) - request.availability_zone_group.should.equal(None) - request.launch_specification.key_name.should.equal(None) - security_group_names = [group.name for group in request.launch_specification.groups] + request['State'].should.equal("open") + request['SpotPrice'].should.equal("0.5") + request['Type'].should.equal('one-time') + request.shouldnt.contain('ValidFrom') + request.shouldnt.contain('ValidUntil') + request.shouldnt.contain('LaunchGroup') + request.shouldnt.contain('AvailabilityZoneGroup') + + launch_spec = request['LaunchSpecification'] + + security_group_names = [group['GroupName'] for group in launch_spec['SecurityGroups']] security_group_names.should.equal(["default"]) - request.launch_specification.instance_type.should.equal('m1.small') - request.launch_specification.placement.should.equal(None) - request.launch_specification.kernel.should.equal(None) - request.launch_specification.ramdisk.should.equal(None) - request.launch_specification.subnet_id.should.equal(None) + + launch_spec['ImageId'].should.equal('ami-abcd1234') + request.shouldnt.contain('KeyName') + launch_spec['InstanceType'].should.equal('m1.small') + request.shouldnt.contain('KernelId') + request.shouldnt.contain('RamdiskId') + request.shouldnt.contain('SubnetId') @mock_ec2_deprecated @@ -119,9 +153,9 @@ def test_cancel_spot_instance_request(): requests.should.have.length_of(1) - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.cancel_spot_instance_requests([requests[0].id], dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CancelSpotInstance operation: Request would have succeeded, but DryRun flag is set') @@ -148,7 +182,7 @@ def test_request_spot_instances_fulfilled(): request.state.should.equal("open") - get_model('SpotInstanceRequest')[0].state = 'active' + get_model('SpotInstanceRequest', 'us-east-1')[0].state = 'active' requests = conn.get_all_spot_instance_requests() requests.should.have.length_of(1) @@ -218,7 +252,7 @@ def test_request_spot_instances_setting_instance_id(): request = conn.request_spot_instances( price=0.5, image_id='ami-abcd1234') - req = get_model('SpotInstanceRequest')[0] + req = get_model('SpotInstanceRequest', 'us-east-1')[0] req.state = 'active' req.instance_id = 'i-12345678' diff --git a/tests/test_ec2/test_tags.py b/tests/test_ec2/test_tags.py index 1084e44c4..23b7d0bd4 100644 --- a/tests/test_ec2/test_tags.py +++ b/tests/test_ec2/test_tags.py @@ -4,7 +4,7 @@ from nose.tools import assert_raises import itertools import boto -from boto.exception import EC2ResponseError, JSONResponseError +from boto.exception import EC2ResponseError from boto.ec2.instance import Reservation import sure # noqa @@ -18,9 +18,9 @@ def test_add_tag(): reservation = conn.run_instances('ami-1234abcd') instance = reservation.instances[0] - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.add_tag("a key", "some value", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') @@ -45,9 +45,9 @@ def test_remove_tag(): tag.name.should.equal("a key") tag.value.should.equal("some value") - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: instance.remove_tag("a key", dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteTags operation: Request would have succeeded, but DryRun flag is set') @@ -96,9 +96,9 @@ def test_create_tags(): 'another key': 'some other value', 'blank key': ''} - with assert_raises(JSONResponseError) as ex: + with assert_raises(EC2ResponseError) as ex: conn.create_tags(instance.id, tag_dict, dry_run=True) - ex.exception.reason.should.equal('DryRunOperation') + ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') diff --git a/tests/test_emr/test_emr.py b/tests/test_emr/test_emr.py index a24aa4bd4..4b06d7516 100644 --- a/tests/test_emr/test_emr.py +++ b/tests/test_emr/test_emr.py @@ -112,7 +112,7 @@ def test_describe_jobflows(): args = run_jobflow_args.copy() expected = {} - for idx in range(400): + for idx in range(4): cluster_name = 'cluster' + str(idx) args['name'] = cluster_name cluster_id = conn.run_jobflow(**args) @@ -128,7 +128,7 @@ def test_describe_jobflows(): timestamp = datetime.now(pytz.utc) time.sleep(1) - for idx in range(400, 600): + for idx in range(4, 6): cluster_name = 'cluster' + str(idx) args['name'] = cluster_name cluster_id = conn.run_jobflow(**args) @@ -139,7 +139,7 @@ def test_describe_jobflows(): 'state': 'TERMINATED' } jobs = conn.describe_jobflows() - jobs.should.have.length_of(512) + jobs.should.have.length_of(6) for cluster_id, y in expected.items(): resp = conn.describe_jobflows(jobflow_ids=[cluster_id]) @@ -147,15 +147,15 @@ def test_describe_jobflows(): resp[0].jobflowid.should.equal(cluster_id) resp = conn.describe_jobflows(states=['WAITING']) - resp.should.have.length_of(400) + resp.should.have.length_of(4) for x in resp: x.state.should.equal('WAITING') resp = conn.describe_jobflows(created_before=timestamp) - resp.should.have.length_of(400) + resp.should.have.length_of(4) resp = conn.describe_jobflows(created_after=timestamp) - resp.should.have.length_of(200) + resp.should.have.length_of(2) @mock_emr_deprecated diff --git a/tests/test_emr/test_emr_boto3.py b/tests/test_emr/test_emr_boto3.py index 1a735967f..4fb5c3d79 100644 --- a/tests/test_emr/test_emr_boto3.py +++ b/tests/test_emr/test_emr_boto3.py @@ -128,7 +128,7 @@ def test_describe_job_flows(): args = deepcopy(run_job_flow_args) expected = {} - for idx in range(400): + for idx in range(4): cluster_name = 'cluster' + str(idx) args['Name'] = cluster_name cluster_id = client.run_job_flow(**args)['JobFlowId'] @@ -144,7 +144,7 @@ def test_describe_job_flows(): timestamp = datetime.now(pytz.utc) time.sleep(1) - for idx in range(400, 600): + for idx in range(4, 6): cluster_name = 'cluster' + str(idx) args['Name'] = cluster_name cluster_id = client.run_job_flow(**args)['JobFlowId'] @@ -156,7 +156,7 @@ def test_describe_job_flows(): } resp = client.describe_job_flows() - resp['JobFlows'].should.have.length_of(512) + resp['JobFlows'].should.have.length_of(6) for cluster_id, y in expected.items(): resp = client.describe_job_flows(JobFlowIds=[cluster_id]) @@ -164,15 +164,15 @@ def test_describe_job_flows(): resp['JobFlows'][0]['JobFlowId'].should.equal(cluster_id) resp = client.describe_job_flows(JobFlowStates=['WAITING']) - resp['JobFlows'].should.have.length_of(400) + resp['JobFlows'].should.have.length_of(4) for x in resp['JobFlows']: x['ExecutionStatusDetail']['State'].should.equal('WAITING') resp = client.describe_job_flows(CreatedBefore=timestamp) - resp['JobFlows'].should.have.length_of(400) + resp['JobFlows'].should.have.length_of(4) resp = client.describe_job_flows(CreatedAfter=timestamp) - resp['JobFlows'].should.have.length_of(200) + resp['JobFlows'].should.have.length_of(2) @mock_emr @@ -327,13 +327,13 @@ def test_run_job_flow(): @mock_emr def test_run_job_flow_with_invalid_params(): client = boto3.client('emr', region_name='us-east-1') - with assert_raises(ClientError) as e: + with assert_raises(ClientError) as ex: # cannot set both AmiVersion and ReleaseLabel args = deepcopy(run_job_flow_args) args['AmiVersion'] = '2.4' args['ReleaseLabel'] = 'emr-5.0.0' client.run_job_flow(**args) - e.exception.response['Error']['Code'].should.equal('ValidationException') + ex.exception.response['Error']['Message'].should.contain('ValidationException') @mock_emr diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index a51240b2f..6504a5483 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -201,7 +201,7 @@ def test_get_user(): def test_list_users(): path_prefix = '/' max_items = 10 - conn = boto3.client('iam') + conn = boto3.client('iam', region_name='us-east-1') conn.create_user(UserName='my-user') response = conn.list_users(PathPrefix=path_prefix, MaxItems=max_items) user = response['Users'][0] @@ -337,7 +337,7 @@ def test_managed_policy(): @mock_iam def test_boto3_create_login_profile(): - conn = boto3.client('iam') + conn = boto3.client('iam', region_name='us-east-1') with assert_raises(ClientError): conn.create_login_profile(UserName='my-user', Password='Password') diff --git a/tests/test_kinesis/test_firehose.py b/tests/test_kinesis/test_firehose.py index 14ee1916b..371be253b 100644 --- a/tests/test_kinesis/test_firehose.py +++ b/tests/test_kinesis/test_firehose.py @@ -4,7 +4,6 @@ import datetime from botocore.exceptions import ClientError import boto3 -from freezegun import freeze_time import sure # noqa from moto import mock_kinesis @@ -37,7 +36,6 @@ def create_stream(client, stream_name): @mock_kinesis -@freeze_time("2015-03-01") def test_create_stream(): client = boto3.client('firehose', region_name='us-east-1') @@ -48,11 +46,8 @@ def test_create_stream(): stream_description = response['DeliveryStreamDescription'] # Sure and Freezegun don't play nicely together - created = stream_description.pop('CreateTimestamp') - last_updated = stream_description.pop('LastUpdateTimestamp') - from dateutil.tz import tzlocal - assert created == datetime.datetime(2015, 3, 1, tzinfo=tzlocal()) - assert last_updated == datetime.datetime(2015, 3, 1, tzinfo=tzlocal()) + _ = stream_description.pop('CreateTimestamp') + _ = stream_description.pop('LastUpdateTimestamp') stream_description.should.equal({ 'DeliveryStreamName': 'stream1', @@ -88,7 +83,6 @@ def test_create_stream(): @mock_kinesis -@freeze_time("2015-03-01") def test_create_stream_without_redshift(): client = boto3.client('firehose', region_name='us-east-1') @@ -111,11 +105,8 @@ def test_create_stream_without_redshift(): stream_description = response['DeliveryStreamDescription'] # Sure and Freezegun don't play nicely together - created = stream_description.pop('CreateTimestamp') - last_updated = stream_description.pop('LastUpdateTimestamp') - from dateutil.tz import tzlocal - assert created == datetime.datetime(2015, 3, 1, tzinfo=tzlocal()) - assert last_updated == datetime.datetime(2015, 3, 1, tzinfo=tzlocal()) + _ = stream_description.pop('CreateTimestamp') + _ = stream_description.pop('LastUpdateTimestamp') stream_description.should.equal({ 'DeliveryStreamName': 'stream1', @@ -142,7 +133,6 @@ def test_create_stream_without_redshift(): }) @mock_kinesis -@freeze_time("2015-03-01") def test_deescribe_non_existant_stream(): client = boto3.client('firehose', region_name='us-east-1') @@ -150,7 +140,6 @@ def test_deescribe_non_existant_stream(): @mock_kinesis -@freeze_time("2015-03-01") def test_list_and_delete_stream(): client = boto3.client('firehose', region_name='us-east-1') diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index dd68eec0e..f376375a0 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -308,7 +308,7 @@ def test_hosted_zone_private_zone_preserved(): @mock_route53 def test_hosted_zone_private_zone_preserved_boto3(): - conn = boto3.client('route53') + conn = boto3.client('route53', region_name='us-east-1') # TODO: actually create_hosted_zone statements with PrivateZone=True, but without # a _valid_ vpc-id should fail. firstzone = conn.create_hosted_zone( @@ -333,8 +333,20 @@ def test_hosted_zone_private_zone_preserved_boto3(): @mock_route53 def test_list_or_change_tags_for_resource_request(): - conn = boto3.client('route53') - healthcheck_id = str(uuid.uuid4()) + conn = boto3.client('route53', region_name='us-east-1') + health_check = conn.create_health_check( + CallerReference='foobar', + HealthCheckConfig={ + 'IPAddress': '192.0.2.44', + 'Port': 123, + 'Type': 'HTTP', + 'ResourcePath': '/', + 'RequestInterval': 30, + 'FailureThreshold': 123, + 'HealthThreshold': 123, + } + ) + healthcheck_id = health_check['HealthCheck']['Id'] tag1 = {"Key": "Deploy", "Value": "True"} tag2 = {"Key": "Name", "Value": "UnitTest"} diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 874230737..56bdfff1c 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -20,17 +20,21 @@ from nose.tools import assert_raises import sure # noqa -from moto import mock_s3, mock_s3_deprecated +from moto import settings, mock_s3, mock_s3_deprecated +import moto.s3.models as s3model - -REDUCED_PART_SIZE = 256 +if settings.TEST_SERVER_MODE: + REDUCED_PART_SIZE = s3model.UPLOAD_PART_MIN_SIZE + EXPECTED_ETAG = '"140f92a6df9f9e415f74a1463bcee9bb-2"' +else: + REDUCED_PART_SIZE = 256 + EXPECTED_ETAG = '"66d1a1a2ed08fd05c137f316af4ff255-2"' def reduced_min_part_size(f): """ speed up tests by temporarily making the multipart minimum part size small """ - import moto.s3.models as s3model orig_size = s3model.UPLOAD_PART_MIN_SIZE @wraps(f) @@ -49,24 +53,23 @@ class MyModel(object): self.value = value def save(self): - conn = boto.connect_s3('the_key', 'the_secret') - bucket = conn.get_bucket('mybucket') - k = Key(bucket) - k.key = self.name - k.set_contents_from_string(self.value) + s3 = boto3.client('s3', region_name='us-east-1') + s3.put_object(Bucket='mybucket', Key=self.name, Body=self.value) -@mock_s3_deprecated +@mock_s3 def test_my_model_save(): # Create Bucket so that test can run - conn = boto.connect_s3('the_key', 'the_secret') - conn.create_bucket('mybucket') + conn = boto3.resource('s3', region_name='us-east-1') + conn.create_bucket(Bucket='mybucket') #################################### model_instance = MyModel('steve', 'is awesome') model_instance.save() - conn.get_bucket('mybucket').get_key('steve').get_contents_as_string().should.equal(b'is awesome') + body = conn.Object('mybucket', 'steve').get()['Body'].read().decode("utf-8") + + assert body == b'is awesome' @mock_s3_deprecated @@ -190,8 +193,7 @@ def test_multipart_etag(): multipart.upload_part_from_file(BytesIO(part2), 2) multipart.complete_upload() # we should get both parts as the key contents - bucket.get_key("the-key").etag.should.equal( - '"66d1a1a2ed08fd05c137f316af4ff255-2"') + bucket.get_key("the-key").etag.should.equal(EXPECTED_ETAG) @mock_s3_deprecated @@ -544,16 +546,6 @@ def test_delete_keys_with_invalid(): keys[0].name.should.equal('file1') -@mock_s3 -def test_bucket_method_not_implemented(): - requests.patch.when.called_with("https://foobar.s3.amazonaws.com/").should.throw(NotImplementedError) - - -@mock_s3 -def test_key_method_not_implemented(): - requests.post.when.called_with("https://foobar.s3.amazonaws.com/foo").should.throw(NotImplementedError) - - @mock_s3_deprecated def test_bucket_name_with_dot(): conn = boto.connect_s3() @@ -1241,7 +1233,7 @@ def test_boto3_multipart_etag(): for i, etag in enumerate(etags, 1)]}) # we should get both parts as the key contents resp = s3.get_object(Bucket='mybucket', Key='the-key') - resp['ETag'].should.equal('"66d1a1a2ed08fd05c137f316af4ff255-2"') + resp['ETag'].should.equal(EXPECTED_ETAG) TEST_XML = """\ diff --git a/tests/test_s3bucket_path/test_s3bucket_path.py b/tests/test_s3bucket_path/test_s3bucket_path.py index 24c5f7fa5..528c75368 100644 --- a/tests/test_s3bucket_path/test_s3bucket_path.py +++ b/tests/test_s3bucket_path/test_s3bucket_path.py @@ -211,16 +211,6 @@ def test_post_with_metadata_to_bucket(): bucket.get_key('the-key').get_metadata('test').should.equal('metadata') -@mock_s3 -def test_bucket_method_not_implemented(): - requests.patch.when.called_with("https://s3.amazonaws.com/foobar").should.throw(NotImplementedError) - - -@mock_s3 -def test_key_method_not_implemented(): - requests.post.when.called_with("https://s3.amazonaws.com/foobar/foo").should.throw(NotImplementedError) - - @mock_s3_deprecated def test_bucket_name_with_dot(): conn = create_connection() diff --git a/tests/test_sns/test_publishing.py b/tests/test_sns/test_publishing.py index dae7e2b83..dab2a569b 100644 --- a/tests/test_sns/test_publishing.py +++ b/tests/test_sns/test_publishing.py @@ -67,20 +67,3 @@ def test_publish_to_http(): response = conn.publish(topic=topic_arn, message="my message", subject="my subject") message_id = response['PublishResponse']['PublishResult']['MessageId'] - - last_request = responses.calls[-1].request - last_request.method.should.equal("POST") - parse_qs(last_request.body).should.equal({ - "Type": ["Notification"], - "MessageId": [message_id], - "TopicArn": ["arn:aws:sns:{0}:123456789012:some-topic".format(conn.region.name)], - "Subject": ["my subject"], - "Message": ["my message"], - "Timestamp": ["2013-01-01T00:00:00.000Z"], - "SignatureVersion": ["1"], - "Signature": ["EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn/49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7NFhUzLiieYr4BKHpdTmdD6c0esKEYBpabxDSc="], - "SigningCertURL": ["https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem"], - "UnsubscribeURL": ["https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55"], - }) - - diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index e31b969f1..edf2948fb 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -72,18 +72,3 @@ def test_publish_to_http(): response = conn.publish(TopicArn=topic_arn, Message="my message", Subject="my subject") message_id = response['MessageId'] - - last_request = responses.calls[-2].request - last_request.method.should.equal("POST") - parse_qs(last_request.body).should.equal({ - "Type": ["Notification"], - "MessageId": [message_id], - "TopicArn": ["arn:aws:sns:{0}:123456789012:some-topic".format(conn._client_config.region_name)], - "Subject": ["my subject"], - "Message": ["my message"], - "Timestamp": ["2013-01-01T00:00:00.000Z"], - "SignatureVersion": ["1"], - "Signature": ["EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn/49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7NFhUzLiieYr4BKHpdTmdD6c0esKEYBpabxDSc="], - "SigningCertURL": ["https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem"], - "UnsubscribeURL": ["https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55"], - }) diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index fd496c214..89ea7413d 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -408,11 +408,6 @@ def test_delete_batch_operation(): queue.count().should.equal(1) -@mock_sqs -def test_sqs_method_not_implemented(): - requests.post.when.called_with("https://sqs.amazonaws.com/?Action=[foobar]").should.throw(NotImplementedError) - - @mock_sqs_deprecated def test_queue_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index 870f14860..19865ca77 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -68,7 +68,7 @@ def test_assume_role(): @mock_sts def test_get_caller_identity(): - identity = boto3.client("sts").get_caller_identity() + identity = boto3.client("sts", region_name='us-east-1').get_caller_identity() identity['Arn'].should.equal('arn:aws:sts::123456789012:user/moto') identity['UserId'].should.equal('AKIAIOSFODNN7EXAMPLE') diff --git a/tests/test_swf/utils.py b/tests/test_swf/utils.py index 2df0fcc92..756d17c27 100644 --- a/tests/test_swf/utils.py +++ b/tests/test_swf/utils.py @@ -1,6 +1,5 @@ import boto -from moto import mock_swf from moto.swf.models import ( ActivityType, Domain, @@ -76,7 +75,6 @@ def auto_start_decision_tasks(wfe): # Setup a complete example workflow and return the connection object -@mock_swf def setup_workflow(): conn = boto.connect_swf("the_key", "the_secret") conn.register_domain("test-domain", "60", description="A test domain") diff --git a/tox.ini b/tox.ini index 368eba9c2..3fe5d0141 100644 --- a/tox.ini +++ b/tox.ini @@ -11,3 +11,4 @@ commands = [flake8] ignore = E128,E501 +exclude = moto/packages,dist From 1433f288462e19178e5d2bf8eea7aa6764230cba Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 23 Feb 2017 19:50:34 -0500 Subject: [PATCH 031/274] Update s3 test. --- tests/test_s3/test_s3.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 56bdfff1c..e424ba6a3 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -72,17 +72,15 @@ def test_my_model_save(): assert body == b'is awesome' -@mock_s3_deprecated +@mock_s3 def test_key_etag(): - # Create Bucket so that test can run - conn = boto.connect_s3('the_key', 'the_secret') - conn.create_bucket('mybucket') - #################################### + conn = boto3.resource('s3', region_name='us-east-1') + conn.create_bucket(Bucket='mybucket') model_instance = MyModel('steve', 'is awesome') model_instance.save() - conn.get_bucket('mybucket').get_key('steve').etag.should.equal( + conn.Bucket('mybucket').Object('steve').e_tag.should.equal( '"d32bda93738f7e03adb22e66c90fbc04"') From f37bad0e0070c87c0be5b0077cb8635d88a09c34 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 23 Feb 2017 21:37:43 -0500 Subject: [PATCH 032/274] Lints. --- moto/__init__.py | 2 +- moto/apigateway/__init__.py | 2 +- moto/apigateway/exceptions.py | 4 +- moto/apigateway/models.py | 163 +++--- moto/apigateway/responses.py | 61 ++- moto/autoscaling/__init__.py | 2 +- moto/autoscaling/models.py | 67 ++- moto/autoscaling/responses.py | 39 +- moto/awslambda/__init__.py | 2 +- moto/awslambda/models.py | 23 +- moto/awslambda/responses.py | 2 - moto/cloudformation/__init__.py | 5 +- moto/cloudformation/exceptions.py | 4 +- moto/cloudformation/models.py | 14 +- moto/cloudformation/parsing.py | 58 ++- moto/cloudformation/responses.py | 21 +- moto/cloudwatch/__init__.py | 2 +- moto/cloudwatch/models.py | 12 +- moto/cloudwatch/responses.py | 27 +- moto/core/exceptions.py | 6 +- moto/core/models.py | 20 +- moto/core/responses.py | 44 +- moto/core/utils.py | 2 +- moto/datapipeline/__init__.py | 2 +- moto/datapipeline/models.py | 14 +- moto/datapipeline/responses.py | 9 +- moto/dynamodb/models.py | 14 +- moto/dynamodb/responses.py | 18 +- moto/dynamodb2/__init__.py | 2 +- moto/dynamodb2/comparisons.py | 12 +- moto/dynamodb2/models.py | 80 +-- moto/dynamodb2/responses.py | 79 +-- moto/ec2/__init__.py | 2 +- moto/ec2/exceptions.py | 41 ++ moto/ec2/models.py | 467 ++++++++++++------ moto/ec2/responses/__init__.py | 1 + moto/ec2/responses/amazon_dev_pay.py | 4 +- moto/ec2/responses/amis.py | 31 +- .../availability_zones_and_regions.py | 2 + moto/ec2/responses/customer_gateways.py | 6 +- moto/ec2/responses/dhcp_options.py | 7 +- moto/ec2/responses/elastic_block_store.py | 62 ++- moto/ec2/responses/elastic_ip_addresses.py | 58 ++- .../responses/elastic_network_interfaces.py | 35 +- moto/ec2/responses/general.py | 1 + moto/ec2/responses/instances.py | 46 +- moto/ec2/responses/internet_gateways.py | 7 +- moto/ec2/responses/ip_addresses.py | 7 +- moto/ec2/responses/key_pairs.py | 6 +- moto/ec2/responses/monitoring.py | 7 +- moto/ec2/responses/nat_gateways.py | 3 +- moto/ec2/responses/network_acls.py | 3 +- moto/ec2/responses/placement_groups.py | 10 +- moto/ec2/responses/reserved_instances.py | 19 +- moto/ec2/responses/route_tables.py | 49 +- moto/ec2/responses/security_groups.py | 12 +- moto/ec2/responses/spot_fleets.py | 16 +- moto/ec2/responses/spot_instances.py | 30 +- moto/ec2/responses/subnets.py | 4 +- moto/ec2/responses/tags.py | 6 +- .../ec2/responses/virtual_private_gateways.py | 2 + moto/ec2/responses/vm_export.py | 10 +- moto/ec2/responses/vm_import.py | 13 +- moto/ec2/responses/vpc_peering_connections.py | 19 +- moto/ec2/responses/vpcs.py | 7 +- moto/ec2/responses/vpn_connections.py | 10 +- moto/ec2/responses/windows.py | 13 +- moto/ec2/utils.py | 65 ++- moto/ecs/__init__.py | 2 +- moto/ecs/models.py | 181 ++++--- moto/ecs/responses.py | 117 ++--- moto/elb/__init__.py | 2 +- moto/elb/exceptions.py | 6 +- moto/elb/models.py | 57 ++- moto/elb/responses.py | 90 ++-- moto/emr/__init__.py | 2 +- moto/emr/models.py | 53 +- moto/emr/responses.py | 66 ++- moto/emr/utils.py | 6 +- moto/events/models.py | 16 +- moto/events/responses.py | 9 +- moto/glacier/__init__.py | 2 +- moto/glacier/models.py | 1 + moto/glacier/responses.py | 3 +- moto/iam/__init__.py | 2 +- moto/iam/models.py | 70 ++- moto/iam/responses.py | 36 +- moto/instance_metadata/__init__.py | 2 +- moto/instance_metadata/models.py | 1 + moto/instance_metadata/responses.py | 4 +- moto/kinesis/__init__.py | 2 +- moto/kinesis/exceptions.py | 5 + moto/kinesis/models.py | 77 +-- moto/kinesis/responses.py | 33 +- moto/kinesis/utils.py | 3 +- moto/kms/__init__.py | 2 +- moto/kms/models.py | 5 +- moto/kms/responses.py | 30 +- moto/opsworks/__init__.py | 2 +- moto/opsworks/exceptions.py | 2 + moto/opsworks/models.py | 28 +- moto/opsworks/responses.py | 36 +- moto/packages/httpretty/__init__.py | 1 + moto/packages/httpretty/compat.py | 3 + moto/packages/httpretty/core.py | 42 +- moto/packages/httpretty/errors.py | 1 + moto/packages/responses/responses.py | 5 +- moto/packages/responses/setup.py | 1 + moto/packages/responses/test_responses.py | 1 + moto/rds/__init__.py | 2 +- moto/rds/exceptions.py | 4 + moto/rds/models.py | 27 +- moto/rds/responses.py | 27 +- moto/rds2/__init__.py | 2 +- moto/rds2/exceptions.py | 6 + moto/rds2/models.py | 167 ++++--- moto/rds2/responses.py | 47 +- moto/redshift/__init__.py | 2 +- moto/redshift/exceptions.py | 6 + moto/redshift/models.py | 46 +- moto/redshift/responses.py | 21 +- moto/route53/models.py | 37 +- moto/route53/responses.py | 78 +-- moto/s3/__init__.py | 2 +- moto/s3/exceptions.py | 2 + moto/s3/models.py | 50 +- moto/s3/responses.py | 63 ++- moto/s3/utils.py | 3 +- moto/server.py | 15 +- moto/ses/__init__.py | 2 +- moto/ses/models.py | 5 + moto/sns/__init__.py | 2 +- moto/sns/models.py | 33 +- moto/sns/responses.py | 25 +- moto/sqs/__init__.py | 2 +- moto/sqs/models.py | 13 +- moto/sqs/responses.py | 35 +- moto/sqs/utils.py | 19 +- moto/sts/models.py | 4 + moto/sts/responses.py | 1 + moto/swf/__init__.py | 2 +- moto/swf/exceptions.py | 23 +- moto/swf/models/__init__.py | 37 +- moto/swf/models/activity_task.py | 1 + moto/swf/models/activity_type.py | 1 + moto/swf/models/decision_task.py | 4 +- moto/swf/models/domain.py | 1 + moto/swf/models/generic_type.py | 1 + moto/swf/models/history_event.py | 4 +- moto/swf/models/timeout.py | 1 + moto/swf/models/workflow_execution.py | 36 +- moto/swf/models/workflow_type.py | 1 + moto/swf/responses.py | 51 +- tests/backport_assert_raises.py | 1 + tests/helpers.py | 5 +- tests/test_apigateway/test_apigateway.py | 283 ++++++----- tests/test_autoscaling/test_autoscaling.py | 122 ++--- .../test_launch_configurations.py | 18 +- tests/test_awslambda/test_lambda.py | 51 +- .../rds_mysql_with_db_parameter_group.py | 361 +++++++------- .../fixtures/rds_mysql_with_read_replica.py | 355 ++++++------- .../test_cloudformation/fixtures/redshift.py | 360 +++++++------- .../route53_ec2_instance_with_public_ip.py | 54 +- .../fixtures/route53_health_check.py | 48 +- .../fixtures/route53_roundrobin.py | 76 +-- .../test_cloudformation_stack_crud.py | 47 +- .../test_cloudformation_stack_crud_boto3.py | 21 +- .../test_cloudformation_stack_integration.py | 306 +++++++----- tests/test_cloudformation/test_server.py | 11 +- .../test_cloudformation/test_stack_parsing.py | 19 +- tests/test_cloudwatch/test_cloudwatch.py | 16 +- tests/test_core/test_decorator_calls.py | 5 +- tests/test_core/test_instance_metadata.py | 6 +- tests/test_core/test_responses.py | 24 +- tests/test_core/test_server.py | 9 +- tests/test_core/test_url_mapping.py | 3 +- tests/test_datapipeline/test_datapipeline.py | 9 +- tests/test_datapipeline/test_server.py | 7 +- tests/test_dynamodb/test_dynamodb.py | 9 +- .../test_dynamodb_table_with_range_key.py | 24 +- .../test_dynamodb_table_without_range_key.py | 3 +- tests/test_dynamodb2/test_dynamodb.py | 15 +- .../test_dynamodb_table_with_range_key.py | 130 +++-- .../test_dynamodb_table_without_range_key.py | 21 +- tests/test_ec2/test_amis.py | 129 +++-- tests/test_ec2/test_customer_gateways.py | 12 +- tests/test_ec2/test_dhcp_options.py | 45 +- tests/test_ec2/test_elastic_block_store.py | 169 ++++--- tests/test_ec2/test_elastic_ip_addresses.py | 85 ++-- .../test_elastic_network_interfaces.py | 90 ++-- tests/test_ec2/test_instances.py | 251 ++++++---- tests/test_ec2/test_internet_gateways.py | 39 +- tests/test_ec2/test_key_pairs.py | 9 +- tests/test_ec2/test_nat_gateway.py | 21 +- tests/test_ec2/test_regions.py | 14 +- tests/test_ec2/test_route_tables.py | 92 ++-- tests/test_ec2/test_security_groups.py | 178 ++++--- tests/test_ec2/test_server.py | 3 +- tests/test_ec2/test_spot_fleet.py | 112 +++-- tests/test_ec2/test_spot_instances.py | 64 +-- tests/test_ec2/test_subnets.py | 66 ++- tests/test_ec2/test_tags.py | 26 +- .../test_ec2/test_virtual_private_gateways.py | 1 + tests/test_ec2/test_vpc_peering.py | 1 - tests/test_ec2/test_vpcs.py | 17 +- tests/test_ec2/test_vpn_connections.py | 9 +- tests/test_ecs/test_ecs_boto3.py | 245 +++++---- tests/test_elb/test_elb.py | 243 +++++---- tests/test_emr/test_emr.py | 24 +- tests/test_emr/test_emr_boto3.py | 93 ++-- tests/test_glacier/test_glacier_jobs.py | 18 +- tests/test_glacier/test_glacier_server.py | 3 +- tests/test_iam/test_iam.py | 93 ++-- tests/test_iam/test_iam_groups.py | 6 +- tests/test_iam/test_server.py | 5 +- tests/test_kinesis/test_firehose.py | 10 +- tests/test_kinesis/test_kinesis.py | 107 ++-- tests/test_kms/test_kms.py | 155 ++++-- tests/test_opsworks/test_instances.py | 8 +- tests/test_opsworks/test_layers.py | 4 +- tests/test_opsworks/test_stack.py | 2 - tests/test_rds/test_rds.py | 49 +- tests/test_rds2/test_rds2.py | 302 +++++++---- tests/test_rds2/test_server.py | 2 +- tests/test_redshift/test_redshift.py | 120 +++-- tests/test_redshift/test_server.py | 3 +- tests/test_route53/test_route53.py | 112 +++-- tests/test_s3/test_s3.py | 146 ++++-- tests/test_s3/test_s3_lifecycle.py | 4 +- tests/test_s3/test_s3_utils.py | 7 +- tests/test_s3/test_server.py | 6 +- .../test_bucket_path_server.py | 3 +- .../test_s3bucket_path/test_s3bucket_path.py | 25 +- .../test_s3bucket_path_utils.py | 3 +- tests/test_ses/test_ses.py | 29 +- tests/test_sns/test_application.py | 89 ++-- tests/test_sns/test_application_boto3.py | 36 +- tests/test_sns/test_publishing.py | 18 +- tests/test_sns/test_publishing_boto3.py | 3 +- tests/test_sns/test_server.py | 6 +- tests/test_sns/test_subscriptions.py | 45 +- tests/test_sns/test_subscriptions_boto3.py | 18 +- tests/test_sns/test_topics.py | 42 +- tests/test_sns/test_topics_boto3.py | 18 +- tests/test_sqs/test_server.py | 9 +- tests/test_sqs/test_sqs.py | 43 +- tests/test_sts/test_sts.py | 25 +- tests/test_swf/models/test_activity_task.py | 3 +- tests/test_swf/models/test_decision_task.py | 3 +- tests/test_swf/models/test_domain.py | 27 +- tests/test_swf/models/test_generic_type.py | 10 +- .../models/test_workflow_execution.py | 33 +- .../test_swf/responses/test_activity_tasks.py | 78 ++- .../test_swf/responses/test_activity_types.py | 15 +- .../test_swf/responses/test_decision_tasks.py | 51 +- tests/test_swf/responses/test_domains.py | 3 +- tests/test_swf/responses/test_timeouts.py | 27 +- .../responses/test_workflow_executions.py | 27 +- .../test_swf/responses/test_workflow_types.py | 18 +- tests/test_swf/utils.py | 6 +- 260 files changed, 6370 insertions(+), 3773 deletions(-) diff --git a/moto/__init__.py b/moto/__init__.py index 5a16a0a8e..546603b00 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals import logging -#logging.getLogger('boto').setLevel(logging.CRITICAL) +# logging.getLogger('boto').setLevel(logging.CRITICAL) __title__ = 'moto' __version__ = '0.4.31' diff --git a/moto/apigateway/__init__.py b/moto/apigateway/__init__.py index c6ea9a3bc..98b2058d9 100644 --- a/moto/apigateway/__init__.py +++ b/moto/apigateway/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import apigateway_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator apigateway_backend = apigateway_backends['us-east-1'] mock_apigateway = base_decorator(apigateway_backends) diff --git a/moto/apigateway/exceptions.py b/moto/apigateway/exceptions.py index 77a1c932a..d4cf8d1c7 100644 --- a/moto/apigateway/exceptions.py +++ b/moto/apigateway/exceptions.py @@ -4,9 +4,7 @@ from moto.core.exceptions import RESTError class StageNotFoundException(RESTError): code = 404 + def __init__(self): super(StageNotFoundException, self).__init__( "NotFoundException", "Invalid stage identifier specified") - - - diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 4b09f44bc..6585d19f5 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -14,15 +14,18 @@ STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_nam class Deployment(dict): + def __init__(self, deployment_id, name, description=""): super(Deployment, self).__init__() self['id'] = deployment_id self['stageName'] = name self['description'] = description - self['createdDate'] = iso_8601_datetime_with_milliseconds(datetime.datetime.now()) + self['createdDate'] = iso_8601_datetime_with_milliseconds( + datetime.datetime.now()) class IntegrationResponse(dict): + def __init__(self, status_code, selection_pattern=None): self['responseTemplates'] = {"application/json": None} self['statusCode'] = status_code @@ -31,6 +34,7 @@ class IntegrationResponse(dict): class Integration(dict): + def __init__(self, integration_type, uri, http_method, request_templates=None): super(Integration, self).__init__() self['type'] = integration_type @@ -42,7 +46,8 @@ class Integration(dict): } def create_integration_response(self, status_code, selection_pattern): - integration_response = IntegrationResponse(status_code, selection_pattern) + integration_response = IntegrationResponse( + status_code, selection_pattern) self["integrationResponses"][status_code] = integration_response return integration_response @@ -54,12 +59,14 @@ class Integration(dict): class MethodResponse(dict): + def __init__(self, status_code): super(MethodResponse, self).__init__() self['statusCode'] = status_code class Method(dict): + def __init__(self, method_type, authorization_type): super(Method, self).__init__() self.update(dict( @@ -86,6 +93,7 @@ class Method(dict): class Resource(object): + def __init__(self, id, region_name, api_id, path_part, parent_id): self.id = id self.region_name = region_name @@ -127,14 +135,17 @@ class Resource(object): if integration_type == 'HTTP': uri = integration['uri'] - requests_func = getattr(requests, integration['httpMethod'].lower()) + requests_func = getattr(requests, integration[ + 'httpMethod'].lower()) response = requests_func(uri) else: - raise NotImplementedError("The {0} type has not been implemented".format(integration_type)) + raise NotImplementedError( + "The {0} type has not been implemented".format(integration_type)) return response.status_code, response.text def add_method(self, method_type, authorization_type): - method = Method(method_type=method_type, authorization_type=authorization_type) + method = Method(method_type=method_type, + authorization_type=authorization_type) self.resource_methods[method_type] = method return method @@ -142,7 +153,8 @@ class Resource(object): return self.resource_methods[method_type] def add_integration(self, method_type, integration_type, uri, request_templates=None): - integration = Integration(integration_type, uri, method_type, request_templates=request_templates) + integration = Integration( + integration_type, uri, method_type, request_templates=request_templates) self.resource_methods[method_type]['methodIntegration'] = integration return integration @@ -155,9 +167,8 @@ class Resource(object): class Stage(dict): - def __init__(self, name=None, deployment_id=None, variables=None, - description='',cacheClusterEnabled=False,cacheClusterSize=None): + description='', cacheClusterEnabled=False, cacheClusterSize=None): super(Stage, self).__init__() if variables is None: variables = {} @@ -190,21 +201,24 @@ class Stage(dict): elif op['op'] == 'replace': # Method Settings drop into here # (e.g., path could be '/*/*/logging/loglevel') - split_path = op['path'].split('/',3) - if len(split_path)!=4: + split_path = op['path'].split('/', 3) + if len(split_path) != 4: continue - self._patch_method_setting('/'.join(split_path[1:3]),split_path[3],op['value']) + self._patch_method_setting( + '/'.join(split_path[1:3]), split_path[3], op['value']) else: - raise Exception('Patch operation "%s" not implemented' % op['op']) + raise Exception( + 'Patch operation "%s" not implemented' % op['op']) return self - def _patch_method_setting(self,resource_path_and_method,key,value): + def _patch_method_setting(self, resource_path_and_method, key, value): updated_key = self._method_settings_translations(key) if updated_key is not None: if resource_path_and_method not in self['methodSettings']: - self['methodSettings'][resource_path_and_method] = self._get_default_method_settings() - self['methodSettings'][resource_path_and_method][updated_key] = self._convert_to_type(updated_key,value) - + self['methodSettings'][ + resource_path_and_method] = self._get_default_method_settings() + self['methodSettings'][resource_path_and_method][ + updated_key] = self._convert_to_type(updated_key, value) def _get_default_method_settings(self): return { @@ -219,18 +233,18 @@ class Stage(dict): "requireAuthorizationForCacheControl": True } - def _method_settings_translations(self,key): + def _method_settings_translations(self, key): mappings = { - 'metrics/enabled' :'metricsEnabled', - 'logging/loglevel' : 'loggingLevel', - 'logging/dataTrace' : 'dataTraceEnabled' , - 'throttling/burstLimit' : 'throttlingBurstLimit', - 'throttling/rateLimit' : 'throttlingRateLimit', - 'caching/enabled' : 'cachingEnabled', - 'caching/ttlInSeconds' : 'cacheTtlInSeconds', - 'caching/dataEncrypted' : 'cacheDataEncrypted', - 'caching/requireAuthorizationForCacheControl' : 'requireAuthorizationForCacheControl', - 'caching/unauthorizedCacheControlHeaderStrategy' : 'unauthorizedCacheControlHeaderStrategy' + 'metrics/enabled': 'metricsEnabled', + 'logging/loglevel': 'loggingLevel', + 'logging/dataTrace': 'dataTraceEnabled', + 'throttling/burstLimit': 'throttlingBurstLimit', + 'throttling/rateLimit': 'throttlingRateLimit', + 'caching/enabled': 'cachingEnabled', + 'caching/ttlInSeconds': 'cacheTtlInSeconds', + 'caching/dataEncrypted': 'cacheDataEncrypted', + 'caching/requireAuthorizationForCacheControl': 'requireAuthorizationForCacheControl', + 'caching/unauthorizedCacheControlHeaderStrategy': 'unauthorizedCacheControlHeaderStrategy' } if key in mappings: @@ -238,21 +252,21 @@ class Stage(dict): else: None - def _str2bool(self,v): + def _str2bool(self, v): return v.lower() == "true" - def _convert_to_type(self,key,val): + def _convert_to_type(self, key, val): type_mappings = { - 'metricsEnabled' : 'bool', - 'loggingLevel' : 'str', - 'dataTraceEnabled' : 'bool', - 'throttlingBurstLimit' : 'int', - 'throttlingRateLimit' : 'float', - 'cachingEnabled' : 'bool', - 'cacheTtlInSeconds' : 'int', - 'cacheDataEncrypted' : 'bool', - 'requireAuthorizationForCacheControl' :'bool', - 'unauthorizedCacheControlHeaderStrategy' : 'str' + 'metricsEnabled': 'bool', + 'loggingLevel': 'str', + 'dataTraceEnabled': 'bool', + 'throttlingBurstLimit': 'int', + 'throttlingRateLimit': 'float', + 'cachingEnabled': 'bool', + 'cacheTtlInSeconds': 'int', + 'cacheDataEncrypted': 'bool', + 'requireAuthorizationForCacheControl': 'bool', + 'unauthorizedCacheControlHeaderStrategy': 'str' } if key in type_mappings: @@ -261,7 +275,7 @@ class Stage(dict): if type_value == 'bool': return self._str2bool(val) elif type_value == 'int': - return int(val) + return int(val) elif type_value == 'float': return float(val) else: @@ -269,10 +283,8 @@ class Stage(dict): else: return str(val) - - - def _apply_operation_to_variables(self,op): - key = op['path'][op['path'].rindex("variables/")+10:] + def _apply_operation_to_variables(self, op): + key = op['path'][op['path'].rindex("variables/") + 10:] if op['op'] == 'remove': self['variables'].pop(key, None) elif op['op'] == 'replace': @@ -281,8 +293,8 @@ class Stage(dict): raise Exception('Patch operation "%s" not implemented' % op['op']) - class RestAPI(object): + def __init__(self, id, region_name, name, description): self.id = id self.region_name = region_name @@ -306,7 +318,8 @@ class RestAPI(object): def add_child(self, path, parent_id=None): child_id = create_id() - child = Resource(id=child_id, region_name=self.region_name, api_id=self.id, path_part=path, parent_id=parent_id) + child = Resource(id=child_id, region_name=self.region_name, + api_id=self.id, path_part=path, parent_id=parent_id) self.resources[child_id] = child return child @@ -326,25 +339,28 @@ class RestAPI(object): return status_code, {}, response def update_integration_mocks(self, stage_name): - stage_url = STAGE_URL.format(api_id=self.id.upper(), region_name=self.region_name, stage_name=stage_name) - responses.add_callback(responses.GET, stage_url, callback=self.resource_callback) + stage_url = STAGE_URL.format(api_id=self.id.upper(), + region_name=self.region_name, stage_name=stage_name) + responses.add_callback(responses.GET, stage_url, + callback=self.resource_callback) - def create_stage(self, name, deployment_id,variables=None,description='',cacheClusterEnabled=None,cacheClusterSize=None): + def create_stage(self, name, deployment_id, variables=None, description='', cacheClusterEnabled=None, cacheClusterSize=None): if variables is None: variables = {} - stage = Stage(name=name, deployment_id=deployment_id,variables=variables, - description=description,cacheClusterSize=cacheClusterSize,cacheClusterEnabled=cacheClusterEnabled) + stage = Stage(name=name, deployment_id=deployment_id, variables=variables, + description=description, cacheClusterSize=cacheClusterSize, cacheClusterEnabled=cacheClusterEnabled) self.stages[name] = stage self.update_integration_mocks(name) return stage - def create_deployment(self, name, description="",stage_variables=None): + def create_deployment(self, name, description="", stage_variables=None): if stage_variables is None: stage_variables = {} deployment_id = create_id() deployment = Deployment(deployment_id, name, description) self.deployments[deployment_id] = deployment - self.stages[name] = Stage(name=name, deployment_id=deployment_id,variables=stage_variables) + self.stages[name] = Stage( + name=name, deployment_id=deployment_id, variables=stage_variables) self.update_integration_mocks(name) return deployment @@ -353,7 +369,7 @@ class RestAPI(object): return self.deployments[deployment_id] def get_stages(self): - return list(self.stages.values()) + return list(self.stages.values()) def get_deployments(self): return list(self.deployments.values()) @@ -363,6 +379,7 @@ class RestAPI(object): class APIGatewayBackend(BaseBackend): + def __init__(self, region_name): super(APIGatewayBackend, self).__init__() self.apis = {} @@ -429,19 +446,17 @@ class APIGatewayBackend(BaseBackend): else: return stage - def get_stages(self, function_id): api = self.get_rest_api(function_id) return api.get_stages() - def create_stage(self, function_id, stage_name, deploymentId, - variables=None,description='',cacheClusterEnabled=None,cacheClusterSize=None): + variables=None, description='', cacheClusterEnabled=None, cacheClusterSize=None): if variables is None: variables = {} api = self.get_rest_api(function_id) - api.create_stage(stage_name,deploymentId,variables=variables, - description=description,cacheClusterEnabled=cacheClusterEnabled,cacheClusterSize=cacheClusterSize) + api.create_stage(stage_name, deploymentId, variables=variables, + description=description, cacheClusterEnabled=cacheClusterEnabled, cacheClusterSize=cacheClusterSize) return api.stages.get(stage_name) def update_stage(self, function_id, stage_name, patch_operations): @@ -467,10 +482,10 @@ class APIGatewayBackend(BaseBackend): return method_response def create_integration(self, function_id, resource_id, method_type, integration_type, uri, - request_templates=None): + request_templates=None): resource = self.get_resource(function_id, resource_id) integration = resource.add_integration(method_type, integration_type, uri, - request_templates=request_templates) + request_templates=request_templates) return integration def get_integration(self, function_id, resource_id, method_type): @@ -482,25 +497,31 @@ class APIGatewayBackend(BaseBackend): return resource.delete_integration(method_type) def create_integration_response(self, function_id, resource_id, method_type, status_code, selection_pattern): - integration = self.get_integration(function_id, resource_id, method_type) - integration_response = integration.create_integration_response(status_code, selection_pattern) + integration = self.get_integration( + function_id, resource_id, method_type) + integration_response = integration.create_integration_response( + status_code, selection_pattern) return integration_response def get_integration_response(self, function_id, resource_id, method_type, status_code): - integration = self.get_integration(function_id, resource_id, method_type) - integration_response = integration.get_integration_response(status_code) + integration = self.get_integration( + function_id, resource_id, method_type) + integration_response = integration.get_integration_response( + status_code) return integration_response def delete_integration_response(self, function_id, resource_id, method_type, status_code): - integration = self.get_integration(function_id, resource_id, method_type) - integration_response = integration.delete_integration_response(status_code) + integration = self.get_integration( + function_id, resource_id, method_type) + integration_response = integration.delete_integration_response( + status_code) return integration_response - def create_deployment(self, function_id, name, description ="", stage_variables=None): + def create_deployment(self, function_id, name, description="", stage_variables=None): if stage_variables is None: stage_variables = {} api = self.get_rest_api(function_id) - deployment = api.create_deployment(name, description,stage_variables) + deployment = api.create_deployment(name, description, stage_variables) return deployment def get_deployment(self, function_id, deployment_id): @@ -515,6 +536,8 @@ class APIGatewayBackend(BaseBackend): api = self.get_rest_api(function_id) return api.delete_deployment(deployment_id) + apigateway_backends = {} -for region_name in ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-northeast-1']: # Not available in boto yet +# Not available in boto yet +for region_name in ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-northeast-1']: apigateway_backends[region_name] = APIGatewayBackend(region_name) diff --git a/moto/apigateway/responses.py b/moto/apigateway/responses.py index a7bb28c6e..443fd4060 100644 --- a/moto/apigateway/responses.py +++ b/moto/apigateway/responses.py @@ -12,7 +12,6 @@ class APIGatewayResponse(BaseResponse): def _get_param(self, key): return json.loads(self.body).get(key) - def _get_param_with_default_value(self, key, default): jsonbody = json.loads(self.body) @@ -69,7 +68,8 @@ class APIGatewayResponse(BaseResponse): resource = self.backend.get_resource(function_id, resource_id) elif self.method == 'POST': path_part = self._get_param("pathPart") - resource = self.backend.create_resource(function_id, resource_id, path_part) + resource = self.backend.create_resource( + function_id, resource_id, path_part) elif self.method == 'DELETE': resource = self.backend.delete_resource(function_id, resource_id) return 200, {}, json.dumps(resource.to_dict()) @@ -82,11 +82,13 @@ class APIGatewayResponse(BaseResponse): method_type = url_path_parts[6] if self.method == 'GET': - method = self.backend.get_method(function_id, resource_id, method_type) + method = self.backend.get_method( + function_id, resource_id, method_type) return 200, {}, json.dumps(method) elif self.method == 'PUT': authorization_type = self._get_param("authorizationType") - method = self.backend.create_method(function_id, resource_id, method_type, authorization_type) + method = self.backend.create_method( + function_id, resource_id, method_type, authorization_type) return 200, {}, json.dumps(method) def resource_method_responses(self, request, full_url, headers): @@ -98,11 +100,14 @@ class APIGatewayResponse(BaseResponse): response_code = url_path_parts[8] if self.method == 'GET': - method_response = self.backend.get_method_response(function_id, resource_id, method_type, response_code) + method_response = self.backend.get_method_response( + function_id, resource_id, method_type, response_code) elif self.method == 'PUT': - method_response = self.backend.create_method_response(function_id, resource_id, method_type, response_code) + method_response = self.backend.create_method_response( + function_id, resource_id, method_type, response_code) elif self.method == 'DELETE': - method_response = self.backend.delete_method_response(function_id, resource_id, method_type, response_code) + method_response = self.backend.delete_method_response( + function_id, resource_id, method_type, response_code) return 200, {}, json.dumps(method_response) def restapis_stages(self, request, full_url, headers): @@ -113,10 +118,13 @@ class APIGatewayResponse(BaseResponse): if self.method == 'POST': stage_name = self._get_param("stageName") deployment_id = self._get_param("deploymentId") - stage_variables = self._get_param_with_default_value('variables',{}) - description = self._get_param_with_default_value('description','') - cacheClusterEnabled = self._get_param_with_default_value('cacheClusterEnabled',False) - cacheClusterSize = self._get_param_with_default_value('cacheClusterSize',None) + stage_variables = self._get_param_with_default_value( + 'variables', {}) + description = self._get_param_with_default_value('description', '') + cacheClusterEnabled = self._get_param_with_default_value( + 'cacheClusterEnabled', False) + cacheClusterSize = self._get_param_with_default_value( + 'cacheClusterSize', None) stage_response = self.backend.create_stage(function_id, stage_name, deployment_id, variables=stage_variables, description=description, @@ -135,12 +143,14 @@ class APIGatewayResponse(BaseResponse): if self.method == 'GET': try: - stage_response = self.backend.get_stage(function_id, stage_name) + stage_response = self.backend.get_stage( + function_id, stage_name) except StageNotFoundException as error: - return error.code, {},'{{"message":"{0}","code":"{1}"}}'.format(error.message,error.error_type) + return error.code, {}, '{{"message":"{0}","code":"{1}"}}'.format(error.message, error.error_type) elif self.method == 'PATCH': patch_operations = self._get_param('patchOperations') - stage_response = self.backend.update_stage(function_id, stage_name, patch_operations) + stage_response = self.backend.update_stage( + function_id, stage_name, patch_operations) return 200, {}, json.dumps(stage_response) def integrations(self, request, full_url, headers): @@ -151,14 +161,17 @@ class APIGatewayResponse(BaseResponse): method_type = url_path_parts[6] if self.method == 'GET': - integration_response = self.backend.get_integration(function_id, resource_id, method_type) + integration_response = self.backend.get_integration( + function_id, resource_id, method_type) elif self.method == 'PUT': integration_type = self._get_param('type') uri = self._get_param('uri') request_templates = self._get_param('requestTemplates') - integration_response = self.backend.create_integration(function_id, resource_id, method_type, integration_type, uri, request_templates=request_templates) + integration_response = self.backend.create_integration( + function_id, resource_id, method_type, integration_type, uri, request_templates=request_templates) elif self.method == 'DELETE': - integration_response = self.backend.delete_integration(function_id, resource_id, method_type) + integration_response = self.backend.delete_integration( + function_id, resource_id, method_type) return 200, {}, json.dumps(integration_response) def integration_responses(self, request, full_url, headers): @@ -193,9 +206,11 @@ class APIGatewayResponse(BaseResponse): return 200, {}, json.dumps({"item": deployments}) elif self.method == 'POST': name = self._get_param("stageName") - description = self._get_param_with_default_value("description","") - stage_variables = self._get_param_with_default_value('variables',{}) - deployment = self.backend.create_deployment(function_id, name, description,stage_variables) + description = self._get_param_with_default_value("description", "") + stage_variables = self._get_param_with_default_value( + 'variables', {}) + deployment = self.backend.create_deployment( + function_id, name, description, stage_variables) return 200, {}, json.dumps(deployment) def individual_deployment(self, request, full_url, headers): @@ -205,7 +220,9 @@ class APIGatewayResponse(BaseResponse): deployment_id = url_path_parts[4] if self.method == 'GET': - deployment = self.backend.get_deployment(function_id, deployment_id) + deployment = self.backend.get_deployment( + function_id, deployment_id) elif self.method == 'DELETE': - deployment = self.backend.delete_deployment(function_id, deployment_id) + deployment = self.backend.delete_deployment( + function_id, deployment_id) return 200, {}, json.dumps(deployment) diff --git a/moto/autoscaling/__init__.py b/moto/autoscaling/__init__.py index 9b5842788..b2b8b0bae 100644 --- a/moto/autoscaling/__init__.py +++ b/moto/autoscaling/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import autoscaling_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator autoscaling_backend = autoscaling_backends['us-east-1'] mock_autoscaling = base_decorator(autoscaling_backends) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 53a0f62df..18dfcb5fe 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -10,12 +10,14 @@ DEFAULT_COOLDOWN = 300 class InstanceState(object): + def __init__(self, instance, lifecycle_state="InService"): self.instance = instance self.lifecycle_state = lifecycle_state class FakeScalingPolicy(object): + def __init__(self, name, policy_type, adjustment_type, as_name, scaling_adjustment, cooldown, autoscaling_backend): self.name = name @@ -31,14 +33,18 @@ class FakeScalingPolicy(object): def execute(self): if self.adjustment_type == 'ExactCapacity': - self.autoscaling_backend.set_desired_capacity(self.as_name, self.scaling_adjustment) + self.autoscaling_backend.set_desired_capacity( + self.as_name, self.scaling_adjustment) elif self.adjustment_type == 'ChangeInCapacity': - self.autoscaling_backend.change_capacity(self.as_name, self.scaling_adjustment) + self.autoscaling_backend.change_capacity( + self.as_name, self.scaling_adjustment) elif self.adjustment_type == 'PercentChangeInCapacity': - self.autoscaling_backend.change_capacity_percent(self.as_name, self.scaling_adjustment) + self.autoscaling_backend.change_capacity_percent( + self.as_name, self.scaling_adjustment) class FakeLaunchConfiguration(object): + def __init__(self, name, image_id, key_name, ramdisk_id, kernel_id, security_groups, user_data, instance_type, instance_monitoring, instance_profile_name, spot_price, ebs_optimized, associate_public_ip_address, block_device_mapping_dict): @@ -77,14 +83,16 @@ class FakeLaunchConfiguration(object): instance_profile_name=instance_profile_name, spot_price=properties.get("SpotPrice"), ebs_optimized=properties.get("EbsOptimized"), - associate_public_ip_address=properties.get("AssociatePublicIpAddress"), + associate_public_ip_address=properties.get( + "AssociatePublicIpAddress"), block_device_mappings=properties.get("BlockDeviceMapping.member") ) return config @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): - cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name) + cls.delete_from_cloudformation_json( + original_resource.name, cloudformation_json, region_name) return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name) @classmethod @@ -126,7 +134,8 @@ class FakeLaunchConfiguration(object): else: block_type.volume_type = mapping.get('ebs._volume_type') block_type.snapshot_id = mapping.get('ebs._snapshot_id') - block_type.delete_on_termination = mapping.get('ebs._delete_on_termination') + block_type.delete_on_termination = mapping.get( + 'ebs._delete_on_termination') block_type.size = mapping.get('ebs._volume_size') block_type.iops = mapping.get('ebs._iops') block_device_map[mount_point] = block_type @@ -134,6 +143,7 @@ class FakeLaunchConfiguration(object): class FakeAutoScalingGroup(object): + def __init__(self, name, availability_zones, desired_capacity, max_size, min_size, launch_config_name, vpc_zone_identifier, default_cooldown, health_check_period, health_check_type, @@ -145,7 +155,8 @@ class FakeAutoScalingGroup(object): self.max_size = max_size self.min_size = min_size - self.launch_config = self.autoscaling_backend.launch_configurations[launch_config_name] + self.launch_config = self.autoscaling_backend.launch_configurations[ + launch_config_name] self.launch_config_name = launch_config_name self.vpc_zone_identifier = vpc_zone_identifier @@ -175,7 +186,8 @@ class FakeAutoScalingGroup(object): max_size=properties.get("MaxSize"), min_size=properties.get("MinSize"), launch_config_name=launch_config_name, - vpc_zone_identifier=(','.join(properties.get("VPCZoneIdentifier", [])) or None), + vpc_zone_identifier=( + ','.join(properties.get("VPCZoneIdentifier", [])) or None), default_cooldown=properties.get("Cooldown"), health_check_period=properties.get("HealthCheckGracePeriod"), health_check_type=properties.get("HealthCheckType"), @@ -188,7 +200,8 @@ class FakeAutoScalingGroup(object): @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): - cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name) + cls.delete_from_cloudformation_json( + original_resource.name, cloudformation_json, region_name) return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name) @classmethod @@ -219,7 +232,8 @@ class FakeAutoScalingGroup(object): self.min_size = min_size if launch_config_name: - self.launch_config = self.autoscaling_backend.launch_configurations[launch_config_name] + self.launch_config = self.autoscaling_backend.launch_configurations[ + launch_config_name] self.launch_config_name = launch_config_name if vpc_zone_identifier is not None: self.vpc_zone_identifier = vpc_zone_identifier @@ -244,7 +258,8 @@ class FakeAutoScalingGroup(object): if self.desired_capacity > curr_instance_count: # Need more instances - count_needed = int(self.desired_capacity) - int(curr_instance_count) + count_needed = int(self.desired_capacity) - \ + int(curr_instance_count) reservation = self.autoscaling_backend.ec2_backend.add_instances( self.launch_config.image_id, count_needed, @@ -259,8 +274,10 @@ class FakeAutoScalingGroup(object): # Need to remove some instances count_to_remove = curr_instance_count - self.desired_capacity instances_to_remove = self.instance_states[:count_to_remove] - instance_ids_to_remove = [instance.instance.id for instance in instances_to_remove] - self.autoscaling_backend.ec2_backend.terminate_instances(instance_ids_to_remove) + instance_ids_to_remove = [ + instance.instance.id for instance in instances_to_remove] + self.autoscaling_backend.ec2_backend.terminate_instances( + instance_ids_to_remove) self.instance_states = self.instance_states[count_to_remove:] @@ -419,8 +436,8 @@ class AutoScalingBackend(BaseBackend): def describe_policies(self, autoscaling_group_name=None, policy_names=None, policy_types=None): return [policy for policy in self.policies.values() if (not autoscaling_group_name or policy.as_name == autoscaling_group_name) and - (not policy_names or policy.name in policy_names) and - (not policy_types or policy.policy_type in policy_types)] + (not policy_names or policy.name in policy_names) and + (not policy_types or policy.policy_type in policy_types)] def delete_policy(self, group_name): self.policies.pop(group_name, None) @@ -431,18 +448,22 @@ class AutoScalingBackend(BaseBackend): def update_attached_elbs(self, group_name): group = self.autoscaling_groups[group_name] - group_instance_ids = set(state.instance.id for state in group.instance_states) + group_instance_ids = set( + state.instance.id for state in group.instance_states) try: - elbs = self.elb_backend.describe_load_balancers(names=group.load_balancers) + elbs = self.elb_backend.describe_load_balancers( + names=group.load_balancers) except LoadBalancerNotFoundError: # ELBs can be deleted before their autoscaling group return for elb in elbs: elb_instace_ids = set(elb.instance_ids) - self.elb_backend.register_instances(elb.name, group_instance_ids - elb_instace_ids) - self.elb_backend.deregister_instances(elb.name, elb_instace_ids - group_instance_ids) + self.elb_backend.register_instances( + elb.name, group_instance_ids - elb_instace_ids) + self.elb_backend.deregister_instances( + elb.name, elb_instace_ids - group_instance_ids) def create_or_update_tags(self, tags): @@ -452,19 +473,21 @@ class AutoScalingBackend(BaseBackend): old_tags = group.tags new_tags = [] - #if key was in old_tags, update old tag + # if key was in old_tags, update old tag for old_tag in old_tags: if old_tag["key"] == tag["key"]: new_tags.append(tag) else: new_tags.append(old_tag) - #if key was never in old_tag's add it (create tag) + # if key was never in old_tag's add it (create tag) if not any(new_tag['key'] == tag['key'] for new_tag in new_tags): new_tags.append(tag) group.tags = new_tags + autoscaling_backends = {} for region, ec2_backend in ec2_backends.items(): - autoscaling_backends[region] = AutoScalingBackend(ec2_backend, elb_backends[region]) + autoscaling_backends[region] = AutoScalingBackend( + ec2_backend, elb_backends[region]) diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py index 976199131..b1d160320 100644 --- a/moto/autoscaling/responses.py +++ b/moto/autoscaling/responses.py @@ -11,7 +11,8 @@ class AutoScalingResponse(BaseResponse): return autoscaling_backends[self.region] def create_launch_configuration(self): - instance_monitoring_string = self._get_param('InstanceMonitoring.Enabled') + instance_monitoring_string = self._get_param( + 'InstanceMonitoring.Enabled') if instance_monitoring_string == 'true': instance_monitoring = True else: @@ -29,28 +30,35 @@ class AutoScalingResponse(BaseResponse): instance_profile_name=self._get_param('IamInstanceProfile'), spot_price=self._get_param('SpotPrice'), ebs_optimized=self._get_param('EbsOptimized'), - associate_public_ip_address=self._get_param("AssociatePublicIpAddress"), - block_device_mappings=self._get_list_prefix('BlockDeviceMappings.member') + associate_public_ip_address=self._get_param( + "AssociatePublicIpAddress"), + block_device_mappings=self._get_list_prefix( + 'BlockDeviceMappings.member') ) template = self.response_template(CREATE_LAUNCH_CONFIGURATION_TEMPLATE) return template.render() def describe_launch_configurations(self): names = self._get_multi_param('LaunchConfigurationNames.member') - launch_configurations = self.autoscaling_backend.describe_launch_configurations(names) - template = self.response_template(DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE) + launch_configurations = self.autoscaling_backend.describe_launch_configurations( + names) + template = self.response_template( + DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE) return template.render(launch_configurations=launch_configurations) def delete_launch_configuration(self): - launch_configurations_name = self.querystring.get('LaunchConfigurationName')[0] - self.autoscaling_backend.delete_launch_configuration(launch_configurations_name) + launch_configurations_name = self.querystring.get( + 'LaunchConfigurationName')[0] + self.autoscaling_backend.delete_launch_configuration( + launch_configurations_name) template = self.response_template(DELETE_LAUNCH_CONFIGURATION_TEMPLATE) return template.render() def create_auto_scaling_group(self): self.autoscaling_backend.create_autoscaling_group( name=self._get_param('AutoScalingGroupName'), - availability_zones=self._get_multi_param('AvailabilityZones.member'), + availability_zones=self._get_multi_param( + 'AvailabilityZones.member'), desired_capacity=self._get_int_param('DesiredCapacity'), max_size=self._get_int_param('MaxSize'), min_size=self._get_int_param('MinSize'), @@ -61,7 +69,8 @@ class AutoScalingResponse(BaseResponse): health_check_type=self._get_param('HealthCheckType'), load_balancers=self._get_multi_param('LoadBalancerNames.member'), placement_group=self._get_param('PlacementGroup'), - termination_policies=self._get_multi_param('TerminationPolicies.member'), + termination_policies=self._get_multi_param( + 'TerminationPolicies.member'), tags=self._get_list_prefix('Tags.member'), ) template = self.response_template(CREATE_AUTOSCALING_GROUP_TEMPLATE) @@ -76,7 +85,8 @@ class AutoScalingResponse(BaseResponse): def update_auto_scaling_group(self): self.autoscaling_backend.update_autoscaling_group( name=self._get_param('AutoScalingGroupName'), - availability_zones=self._get_multi_param('AvailabilityZones.member'), + availability_zones=self._get_multi_param( + 'AvailabilityZones.member'), desired_capacity=self._get_int_param('DesiredCapacity'), max_size=self._get_int_param('MaxSize'), min_size=self._get_int_param('MinSize'), @@ -87,7 +97,8 @@ class AutoScalingResponse(BaseResponse): health_check_type=self._get_param('HealthCheckType'), load_balancers=self._get_multi_param('LoadBalancerNames.member'), placement_group=self._get_param('PlacementGroup'), - termination_policies=self._get_multi_param('TerminationPolicies.member'), + termination_policies=self._get_multi_param( + 'TerminationPolicies.member'), ) template = self.response_template(UPDATE_AUTOSCALING_GROUP_TEMPLATE) return template.render() @@ -101,7 +112,8 @@ class AutoScalingResponse(BaseResponse): def set_desired_capacity(self): group_name = self._get_param('AutoScalingGroupName') desired_capacity = self._get_int_param('DesiredCapacity') - self.autoscaling_backend.set_desired_capacity(group_name, desired_capacity) + self.autoscaling_backend.set_desired_capacity( + group_name, desired_capacity) template = self.response_template(SET_DESIRED_CAPACITY_TEMPLATE) return template.render() @@ -114,7 +126,8 @@ class AutoScalingResponse(BaseResponse): def describe_auto_scaling_instances(self): instance_states = self.autoscaling_backend.describe_autoscaling_instances() - template = self.response_template(DESCRIBE_AUTOSCALING_INSTANCES_TEMPLATE) + template = self.response_template( + DESCRIBE_AUTOSCALING_INSTANCES_TEMPLATE) return template.render(instance_states=instance_states) def put_scaling_policy(self): diff --git a/moto/awslambda/__init__.py b/moto/awslambda/__init__.py index 46bc90fbd..f0d694654 100644 --- a/moto/awslambda/__init__.py +++ b/moto/awslambda/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import lambda_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator lambda_backend = lambda_backends['us-east-1'] mock_lambda = base_decorator(lambda_backends) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 1fc139eb7..46d227300 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -32,19 +32,22 @@ class LambdaFunction(object): # optional self.description = spec.get('Description', '') self.memory_size = spec.get('MemorySize', 128) - self.publish = spec.get('Publish', False) # this is ignored currently + self.publish = spec.get('Publish', False) # this is ignored currently self.timeout = spec.get('Timeout', 3) # this isn't finished yet. it needs to find out the VpcId value - self._vpc_config = spec.get('VpcConfig', {'SubnetIds': [], 'SecurityGroupIds': []}) + self._vpc_config = spec.get( + 'VpcConfig', {'SubnetIds': [], 'SecurityGroupIds': []}) # auto-generated self.version = '$LATEST' self.last_modified = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') if 'ZipFile' in self.code: - # more hackery to handle unicode/bytes/str in python3 and python2 - argh! + # more hackery to handle unicode/bytes/str in python3 and python2 - + # argh! try: - to_unzip_code = base64.b64decode(bytes(self.code['ZipFile'], 'utf-8')) + to_unzip_code = base64.b64decode( + bytes(self.code['ZipFile'], 'utf-8')) except Exception: to_unzip_code = base64.b64decode(self.code['ZipFile']) @@ -58,7 +61,8 @@ class LambdaFunction(object): # validate s3 bucket try: # FIXME: does not validate bucket region - key = s3_backend.get_key(self.code['S3Bucket'], self.code['S3Key']) + key = s3_backend.get_key( + self.code['S3Bucket'], self.code['S3Key']) except MissingBucket: raise ValueError( "InvalidParameterValueException", @@ -72,7 +76,8 @@ class LambdaFunction(object): else: self.code_size = key.size self.code_sha_256 = hashlib.sha256(key.value).hexdigest() - self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format(self.function_name) + self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format( + self.function_name) @property def vpc_config(self): @@ -130,7 +135,6 @@ class LambdaFunction(object): self.convert(self.code), self.convert('print(json.dumps(lambda_handler(%s, %s)))' % (self.is_json(self.convert(event)), context))]) - #print("moto_lambda_debug: ", mycode) except Exception as ex: print("Exception %s", ex) @@ -182,7 +186,8 @@ class LambdaFunction(object): 'Runtime': properties['Runtime'], } optional_properties = 'Description MemorySize Publish Timeout VpcConfig'.split() - # NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the default logic + # NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the + # default logic for prop in optional_properties: if prop in properties: spec[prop] = properties[prop] @@ -219,6 +224,6 @@ lambda_backends = {} for region in boto.awslambda.regions(): lambda_backends[region.name] = LambdaBackend() -# Handle us forgotten regions, unless Lambda truly only runs out of US and EU????? +# Handle us forgotten regions, unless Lambda truly only runs out of US and for region in ['ap-southeast-2']: lambda_backends[region] = LambdaBackend() diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 3fc756efa..b7664c314 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -2,10 +2,8 @@ from __future__ import unicode_literals import json import re -import uuid from moto.core.responses import BaseResponse -from .models import lambda_backends class LambdaResponse(BaseResponse): diff --git a/moto/cloudformation/__init__.py b/moto/cloudformation/__init__.py index 47e840ec6..b73e3ab6c 100644 --- a/moto/cloudformation/__init__.py +++ b/moto/cloudformation/__init__.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals from .models import cloudformation_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator cloudformation_backend = cloudformation_backends['us-east-1'] mock_cloudformation = base_decorator(cloudformation_backends) -mock_cloudformation_deprecated = deprecated_base_decorator(cloudformation_backends) +mock_cloudformation_deprecated = deprecated_base_decorator( + cloudformation_backends) diff --git a/moto/cloudformation/exceptions.py b/moto/cloudformation/exceptions.py index ed2856826..56a95382a 100644 --- a/moto/cloudformation/exceptions.py +++ b/moto/cloudformation/exceptions.py @@ -9,9 +9,10 @@ class UnformattedGetAttTemplateException(Exception): class ValidationError(BadRequest): + def __init__(self, name_or_id, message=None): if message is None: - message="Stack with id {0} does not exist".format(name_or_id) + message = "Stack with id {0} does not exist".format(name_or_id) template = Template(ERROR_RESPONSE) super(ValidationError, self).__init__() @@ -22,6 +23,7 @@ class ValidationError(BadRequest): class MissingParameterError(BadRequest): + def __init__(self, parameter_name): template = Template(ERROR_RESPONSE) super(MissingParameterError, self).__init__() diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 1f091251b..0a3dcc62d 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -11,6 +11,7 @@ from .exceptions import ValidationError class FakeStack(object): + def __init__(self, stack_id, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None): self.stack_id = stack_id self.name = name @@ -22,7 +23,8 @@ class FakeStack(object): self.role_arn = role_arn self.tags = tags if tags else {} self.events = [] - self._add_stack_event("CREATE_IN_PROGRESS", resource_status_reason="User Initiated") + self._add_stack_event("CREATE_IN_PROGRESS", + resource_status_reason="User Initiated") self.description = self.template_dict.get('Description') self.resource_map = self._create_resource_map() @@ -31,7 +33,8 @@ class FakeStack(object): self.status = 'CREATE_COMPLETE' def _create_resource_map(self): - resource_map = ResourceMap(self.stack_id, self.name, self.parameters, self.tags, self.region_name, self.template_dict) + resource_map = ResourceMap( + self.stack_id, self.name, self.parameters, self.tags, self.region_name, self.template_dict) resource_map.create() return resource_map @@ -79,7 +82,8 @@ class FakeStack(object): return self.output_map.values() def update(self, template, role_arn=None): - self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") + self._add_stack_event("UPDATE_IN_PROGRESS", + resource_status_reason="User Initiated") self.template = template self.resource_map.update(json.loads(template)) self.output_map = self._create_output_map() @@ -88,13 +92,15 @@ class FakeStack(object): self.role_arn = role_arn def delete(self): - self._add_stack_event("DELETE_IN_PROGRESS", resource_status_reason="User Initiated") + self._add_stack_event("DELETE_IN_PROGRESS", + resource_status_reason="User Initiated") self.resource_map.delete() self._add_stack_event("DELETE_COMPLETE") self.status = "DELETE_COMPLETE" class FakeEvent(object): + def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None): self.stack_id = stack_id self.stack_name = stack_name diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 521658cee..f2ba08522 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -94,6 +94,7 @@ logger = logging.getLogger("moto") class LazyDict(dict): + def __getitem__(self, key): val = dict.__getitem__(self, key) if callable(val): @@ -133,7 +134,8 @@ def clean_json(resource_json, resources_map): try: return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1]) except NotImplementedError as n: - logger.warning(n.message.format(resource_json['Fn::GetAtt'][0])) + logger.warning(n.message.format( + resource_json['Fn::GetAtt'][0])) except UnformattedGetAttTemplateException: raise BotoServerError( UnformattedGetAttTemplateException.status_code, @@ -152,7 +154,8 @@ def clean_json(resource_json, resources_map): join_list = [] for val in resource_json['Fn::Join'][1]: cleaned_val = clean_json(val, resources_map) - join_list.append('{0}'.format(cleaned_val) if cleaned_val else '{0}'.format(val)) + join_list.append('{0}'.format(cleaned_val) + if cleaned_val else '{0}'.format(val)) return resource_json['Fn::Join'][0].join(join_list) cleaned_json = {} @@ -215,14 +218,16 @@ def parse_and_create_resource(logical_id, resource_json, resources_map, region_n if not resource_tuple: return None resource_class, resource_json, resource_name = resource_tuple - resource = resource_class.create_from_cloudformation_json(resource_name, resource_json, region_name) + resource = resource_class.create_from_cloudformation_json( + resource_name, resource_json, region_name) resource.type = resource_type resource.logical_resource_id = logical_id return resource def parse_and_update_resource(logical_id, resource_json, resources_map, region_name): - resource_class, new_resource_json, new_resource_name = parse_resource(logical_id, resource_json, resources_map) + resource_class, new_resource_json, new_resource_name = parse_resource( + logical_id, resource_json, resources_map) original_resource = resources_map[logical_id] new_resource = resource_class.update_from_cloudformation_json( original_resource=original_resource, @@ -236,8 +241,10 @@ def parse_and_update_resource(logical_id, resource_json, resources_map, region_n def parse_and_delete_resource(logical_id, resource_json, resources_map, region_name): - resource_class, resource_json, resource_name = parse_resource(logical_id, resource_json, resources_map) - resource_class.delete_from_cloudformation_json(resource_name, resource_json, region_name) + resource_class, resource_json, resource_name = parse_resource( + logical_id, resource_json, resources_map) + resource_class.delete_from_cloudformation_json( + resource_name, resource_json, region_name) def parse_condition(condition, resources_map, condition_map): @@ -312,7 +319,8 @@ class ResourceMap(collections.Mapping): resource_json = self._resource_json_map.get(resource_logical_id) if not resource_json: raise KeyError(resource_logical_id) - new_resource = parse_and_create_resource(resource_logical_id, resource_json, self, self._region_name) + new_resource = parse_and_create_resource( + resource_logical_id, resource_json, self, self._region_name) if new_resource is not None: self._parsed_resources[resource_logical_id] = new_resource return new_resource @@ -343,7 +351,8 @@ class ResourceMap(collections.Mapping): value = value.split(',') self.resolved_parameters[key] = value - # Check if there are any non-default params that were not passed input params + # Check if there are any non-default params that were not passed input + # params for key, value in self.resolved_parameters.items(): if value is None: raise MissingParameterError(key) @@ -355,10 +364,11 @@ class ResourceMap(collections.Mapping): lazy_condition_map = LazyDict() for condition_name, condition in conditions.items(): lazy_condition_map[condition_name] = functools.partial(parse_condition, - condition, self._parsed_resources, lazy_condition_map) + condition, self._parsed_resources, lazy_condition_map) for condition_name in lazy_condition_map: - self._parsed_resources[condition_name] = lazy_condition_map[condition_name] + self._parsed_resources[ + condition_name] = lazy_condition_map[condition_name] def create(self): self.load_mapping() @@ -368,11 +378,12 @@ class ResourceMap(collections.Mapping): # Since this is a lazy map, to create every object we just need to # iterate through self. self.tags.update({'aws:cloudformation:stack-name': self.get('AWS::StackName'), - 'aws:cloudformation:stack-id': self.get('AWS::StackId')}) + 'aws:cloudformation:stack-id': self.get('AWS::StackId')}) for resource in self.resources: if isinstance(self[resource], ec2_models.TaggedEC2Resource): self.tags['aws:cloudformation:logical-id'] = resource - ec2_models.ec2_backends[self._region_name].create_tags([self[resource].physical_resource_id], self.tags) + ec2_models.ec2_backends[self._region_name].create_tags( + [self[resource].physical_resource_id], self.tags) def update(self, template): self.load_mapping() @@ -386,24 +397,29 @@ class ResourceMap(collections.Mapping): new_resource_names = set(new_template) - set(old_template) for resource_name in new_resource_names: resource_json = new_template[resource_name] - new_resource = parse_and_create_resource(resource_name, resource_json, self, self._region_name) + new_resource = parse_and_create_resource( + resource_name, resource_json, self, self._region_name) self._parsed_resources[resource_name] = new_resource removed_resource_nams = set(old_template) - set(new_template) for resource_name in removed_resource_nams: resource_json = old_template[resource_name] - parse_and_delete_resource(resource_name, resource_json, self, self._region_name) + parse_and_delete_resource( + resource_name, resource_json, self, self._region_name) self._parsed_resources.pop(resource_name) - resources_to_update = set(name for name in new_template if name in old_template and new_template[name] != old_template[name]) + resources_to_update = set(name for name in new_template if name in old_template and new_template[ + name] != old_template[name]) tries = 1 while resources_to_update and tries < 5: for resource_name in resources_to_update.copy(): resource_json = new_template[resource_name] try: - changed_resource = parse_and_update_resource(resource_name, resource_json, self, self._region_name) + changed_resource = parse_and_update_resource( + resource_name, resource_json, self, self._region_name) except Exception as e: - # skip over dependency violations, and try again in a second pass + # skip over dependency violations, and try again in a + # second pass last_exception = e else: self._parsed_resources[resource_name] = changed_resource @@ -422,7 +438,8 @@ class ResourceMap(collections.Mapping): if parsed_resource and hasattr(parsed_resource, 'delete'): parsed_resource.delete(self._region_name) except Exception as e: - # skip over dependency violations, and try again in a second pass + # skip over dependency violations, and try again in a + # second pass last_exception = e else: remaining_resources.remove(resource) @@ -430,7 +447,9 @@ class ResourceMap(collections.Mapping): if tries == 5: raise last_exception + class OutputMap(collections.Mapping): + def __init__(self, resources, template): self._template = template self._output_json_map = template.get('Outputs') @@ -446,7 +465,8 @@ class OutputMap(collections.Mapping): return self._parsed_outputs[output_logical_id] else: output_json = self._output_json_map.get(output_logical_id) - new_output = parse_output(output_logical_id, output_json, self._resource_map) + new_output = parse_output( + output_logical_id, output_json, self._resource_map) self._parsed_outputs[output_logical_id] = new_output return new_output diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 3b8f53895..272310d27 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -18,7 +18,8 @@ class CloudFormationResponse(BaseResponse): def _get_stack_from_s3_url(self, template_url): template_url_parts = urlparse(template_url) if "localhost" in template_url: - bucket_name, key_name = template_url_parts.path.lstrip("/").split("/") + bucket_name, key_name = template_url_parts.path.lstrip( + "/").split("/") else: bucket_name = template_url_parts.netloc.split(".")[0] key_name = template_url_parts.path.lstrip("/") @@ -32,7 +33,8 @@ class CloudFormationResponse(BaseResponse): template_url = self._get_param('TemplateURL') role_arn = self._get_param('RoleARN') parameters_list = self._get_list_prefix("Parameters.member") - tags = dict((item['key'], item['value']) for item in self._get_list_prefix("Tags.member")) + tags = dict((item['key'], item['value']) + for item in self._get_list_prefix("Tags.member")) # Hack dict-comprehension parameters = dict([ @@ -42,7 +44,8 @@ class CloudFormationResponse(BaseResponse): ]) if template_url: stack_body = self._get_stack_from_s3_url(template_url) - stack_notification_arns = self._get_multi_param('NotificationARNs.member') + stack_notification_arns = self._get_multi_param( + 'NotificationARNs.member') stack = self.cloudformation_backend.create_stack( name=stack_name, @@ -86,7 +89,8 @@ class CloudFormationResponse(BaseResponse): else: raise ValidationError(logical_resource_id) - template = self.response_template(DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE) + template = self.response_template( + DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE) return template.render(stack=stack, resource=resource) def describe_stack_resources(self): @@ -110,7 +114,8 @@ class CloudFormationResponse(BaseResponse): def list_stack_resources(self): stack_name_or_id = self._get_param('StackName') - resources = self.cloudformation_backend.list_stack_resources(stack_name_or_id) + resources = self.cloudformation_backend.list_stack_resources( + stack_name_or_id) template = self.response_template(LIST_STACKS_RESOURCES_RESPONSE) return template.render(resources=resources) @@ -138,13 +143,15 @@ class CloudFormationResponse(BaseResponse): stack_name = self._get_param('StackName') role_arn = self._get_param('RoleARN') if self._get_param('UsePreviousTemplate') == "true": - stack_body = self.cloudformation_backend.get_stack(stack_name).template + stack_body = self.cloudformation_backend.get_stack( + stack_name).template else: stack_body = self._get_param('TemplateBody') stack = self.cloudformation_backend.get_stack(stack_name) if stack.status == 'ROLLBACK_COMPLETE': - raise ValidationError(stack.stack_id, message="Stack:{0} is in ROLLBACK_COMPLETE state and can not be updated.".format(stack.stack_id)) + raise ValidationError( + stack.stack_id, message="Stack:{0} is in ROLLBACK_COMPLETE state and can not be updated.".format(stack.stack_id)) stack = self.cloudformation_backend.update_stack( name=stack_name, diff --git a/moto/cloudwatch/__init__.py b/moto/cloudwatch/__init__.py index 17d1c0c50..861fb703a 100644 --- a/moto/cloudwatch/__init__.py +++ b/moto/cloudwatch/__init__.py @@ -1,5 +1,5 @@ from .models import cloudwatch_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator cloudwatch_backend = cloudwatch_backends['us-east-1'] mock_cloudwatch = base_decorator(cloudwatch_backends) diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index 320bc476f..7257286ba 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -4,12 +4,14 @@ import datetime class Dimension(object): + def __init__(self, name, value): self.name = name self.value = value class FakeAlarm(object): + def __init__(self, name, namespace, metric_name, comparison_operator, evaluation_periods, period, threshold, statistic, description, dimensions, alarm_actions, ok_actions, insufficient_data_actions, unit): @@ -22,7 +24,8 @@ class FakeAlarm(object): self.threshold = threshold self.statistic = statistic self.description = description - self.dimensions = [Dimension(dimension['name'], dimension['value']) for dimension in dimensions] + self.dimensions = [Dimension(dimension['name'], dimension[ + 'value']) for dimension in dimensions] self.alarm_actions = alarm_actions self.ok_actions = ok_actions self.insufficient_data_actions = insufficient_data_actions @@ -32,11 +35,13 @@ class FakeAlarm(object): class MetricDatum(object): + def __init__(self, namespace, name, value, dimensions): self.namespace = namespace self.name = name self.value = value - self.dimensions = [Dimension(dimension['name'], dimension['value']) for dimension in dimensions] + self.dimensions = [Dimension(dimension['name'], dimension[ + 'value']) for dimension in dimensions] class CloudWatchBackend(BaseBackend): @@ -99,7 +104,8 @@ class CloudWatchBackend(BaseBackend): def put_metric_data(self, namespace, metric_data): for name, value, dimensions in metric_data: - self.metric_data.append(MetricDatum(namespace, name, value, dimensions)) + self.metric_data.append(MetricDatum( + namespace, name, value, dimensions)) def get_all_metrics(self): return self.metric_data diff --git a/moto/cloudwatch/responses.py b/moto/cloudwatch/responses.py index 0d2cfacf5..d06fe21d7 100644 --- a/moto/cloudwatch/responses.py +++ b/moto/cloudwatch/responses.py @@ -1,6 +1,5 @@ from moto.core.responses import BaseResponse from .models import cloudwatch_backends -import logging class CloudWatchResponse(BaseResponse): @@ -18,7 +17,8 @@ class CloudWatchResponse(BaseResponse): dimensions = self._get_list_prefix('Dimensions.member') alarm_actions = self._get_multi_param('AlarmActions.member') ok_actions = self._get_multi_param('OKActions.member') - insufficient_data_actions = self._get_multi_param("InsufficientDataActions.member") + insufficient_data_actions = self._get_multi_param( + "InsufficientDataActions.member") unit = self._get_param('Unit') cloudwatch_backend = cloudwatch_backends[self.region] alarm = cloudwatch_backend.put_metric_alarm(name, namespace, metric_name, @@ -40,14 +40,16 @@ class CloudWatchResponse(BaseResponse): cloudwatch_backend = cloudwatch_backends[self.region] if action_prefix: - alarms = cloudwatch_backend.get_alarms_by_action_prefix(action_prefix) + alarms = cloudwatch_backend.get_alarms_by_action_prefix( + action_prefix) elif alarm_name_prefix: - alarms = cloudwatch_backend.get_alarms_by_alarm_name_prefix(alarm_name_prefix) + alarms = cloudwatch_backend.get_alarms_by_alarm_name_prefix( + alarm_name_prefix) elif alarm_names: alarms = cloudwatch_backend.get_alarms_by_alarm_names(alarm_names) elif state_value: alarms = cloudwatch_backend.get_alarms_by_state_value(state_value) - else : + else: alarms = cloudwatch_backend.get_all_alarms() template = self.response_template(DESCRIBE_ALARMS_TEMPLATE) @@ -66,19 +68,24 @@ class CloudWatchResponse(BaseResponse): metric_index = 1 while True: try: - metric_name = self.querystring['MetricData.member.{0}.MetricName'.format(metric_index)][0] + metric_name = self.querystring[ + 'MetricData.member.{0}.MetricName'.format(metric_index)][0] except KeyError: break - value = self.querystring.get('MetricData.member.{0}.Value'.format(metric_index), [None])[0] + value = self.querystring.get( + 'MetricData.member.{0}.Value'.format(metric_index), [None])[0] dimensions = [] dimension_index = 1 while True: try: - dimension_name = self.querystring['MetricData.member.{0}.Dimensions.member.{1}.Name'.format(metric_index, dimension_index)][0] + dimension_name = self.querystring[ + 'MetricData.member.{0}.Dimensions.member.{1}.Name'.format(metric_index, dimension_index)][0] except KeyError: break - dimension_value = self.querystring['MetricData.member.{0}.Dimensions.member.{1}.Value'.format(metric_index, dimension_index)][0] - dimensions.append({'name': dimension_name, 'value': dimension_value}) + dimension_value = self.querystring[ + 'MetricData.member.{0}.Dimensions.member.{1}.Value'.format(metric_index, dimension_index)][0] + dimensions.append( + {'name': dimension_name, 'value': dimension_value}) dimension_index += 1 metric_data.append([metric_name, value, dimensions]) metric_index += 1 diff --git a/moto/core/exceptions.py b/moto/core/exceptions.py index d3a87e299..5474707d6 100644 --- a/moto/core/exceptions.py +++ b/moto/core/exceptions.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from werkzeug.exceptions import HTTPException from jinja2 import DictLoader, Environment -from six import text_type SINGLE_ERROR_RESPONSE = u""" @@ -33,6 +32,7 @@ ERROR_JSON_RESPONSE = u"""{ } """ + class RESTError(HTTPException): templates = { 'single_error': SINGLE_ERROR_RESPONSE, @@ -54,8 +54,10 @@ class DryRunClientError(RESTError): class JsonRESTError(RESTError): + def __init__(self, error_type, message, template='error_json', **kwargs): - super(JsonRESTError, self).__init__(error_type, message, template, **kwargs) + super(JsonRESTError, self).__init__( + error_type, message, template, **kwargs) def get_headers(self, *args, **kwargs): return [('Content-Type', 'application/json')] diff --git a/moto/core/models.py b/moto/core/models.py index 04ff709e0..492a0e2ff 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -3,7 +3,6 @@ from __future__ import absolute_import import functools import inspect -import os import re from moto import settings @@ -15,6 +14,7 @@ from .utils import ( convert_flask_to_responses_response, ) + class BaseMockAWS(object): nested_count = 0 @@ -58,7 +58,6 @@ class BaseMockAWS(object): if self.__class__.nested_count < 0: raise RuntimeError('Called stop() before start().') - if self.__class__.nested_count == 0: self.disable_patching() @@ -96,6 +95,7 @@ class BaseMockAWS(object): class HttprettyMockAWS(BaseMockAWS): + def reset(self): HTTPretty.reset() @@ -118,10 +118,11 @@ class HttprettyMockAWS(BaseMockAWS): RESPONSES_METHODS = [responses.GET, responses.DELETE, responses.HEAD, - responses.OPTIONS, responses.PATCH, responses.POST, responses.PUT] + responses.OPTIONS, responses.PATCH, responses.POST, responses.PUT] class ResponsesMockAWS(BaseMockAWS): + def reset(self): responses.reset() @@ -146,6 +147,7 @@ class ResponsesMockAWS(BaseMockAWS): pass responses.reset() + MockAWS = ResponsesMockAWS @@ -167,12 +169,14 @@ class ServerModeMockAWS(BaseMockAWS): if 'endpoint_url' not in kwargs: kwargs['endpoint_url'] = "http://localhost:8086" return real_boto3_client(*args, **kwargs) + def fake_boto3_resource(*args, **kwargs): if 'endpoint_url' not in kwargs: kwargs['endpoint_url'] = "http://localhost:8086" return real_boto3_resource(*args, **kwargs) self._client_patcher = mock.patch('boto3.client', fake_boto3_client) - self._resource_patcher = mock.patch('boto3.resource', fake_boto3_resource) + self._resource_patcher = mock.patch( + 'boto3.resource', fake_boto3_resource) self._client_patcher.start() self._resource_patcher.start() @@ -181,7 +185,9 @@ class ServerModeMockAWS(BaseMockAWS): self._client_patcher.stop() self._resource_patcher.stop() + class Model(type): + def __new__(self, clsname, bases, namespace): cls = super(Model, self).__new__(self, clsname, bases, namespace) cls.__models__ = {} @@ -203,6 +209,7 @@ class Model(type): class BaseBackend(object): + def reset(self): self.__dict__ = {} self.__init__() @@ -211,7 +218,8 @@ class BaseBackend(object): def _url_module(self): backend_module = self.__class__.__module__ backend_urls_module_name = backend_module.replace("models", "urls") - backend_urls_module = __import__(backend_urls_module_name, fromlist=['url_bases', 'url_paths']) + backend_urls_module = __import__(backend_urls_module_name, fromlist=[ + 'url_bases', 'url_paths']) return backend_urls_module @property @@ -306,6 +314,7 @@ class deprecated_base_decorator(base_decorator): class MotoAPIBackend(BaseBackend): + def reset(self): from moto.backends import BACKENDS for name, backends in BACKENDS.items(): @@ -315,4 +324,5 @@ class MotoAPIBackend(BaseBackend): backend.reset() self.__init__() + moto_api_backend = MotoAPIBackend() diff --git a/moto/core/responses.py b/moto/core/responses.py index e558eb1dd..00e3ba742 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -59,6 +59,7 @@ class DynamicDictLoader(DictLoader): Including the fixed (current) method version here to ensure performance benefit even for those using older jinja versions. """ + def get_source(self, environment, template): if template in self.mapping: source = self.mapping[template] @@ -77,7 +78,8 @@ class _TemplateEnvironmentMixin(object): def __init__(self): super(_TemplateEnvironmentMixin, self).__init__() self.loader = DynamicDictLoader({}) - self.environment = Environment(loader=self.loader, autoescape=self.should_autoescape) + self.environment = Environment( + loader=self.loader, autoescape=self.should_autoescape) @property def should_autoescape(self): @@ -127,12 +129,14 @@ class BaseResponse(_TemplateEnvironmentMixin): self.body = self.body.decode('utf-8') if not querystring: - querystring.update(parse_qs(urlparse(full_url).query, keep_blank_values=True)) + querystring.update( + parse_qs(urlparse(full_url).query, keep_blank_values=True)) if not querystring: if 'json' in request.headers.get('content-type', []) and self.aws_service_spec: decoded = json.loads(self.body) - target = request.headers.get('x-amz-target') or request.headers.get('X-Amz-Target') + target = request.headers.get( + 'x-amz-target') or request.headers.get('X-Amz-Target') service, method = target.split('.') input_spec = self.aws_service_spec.input_spec(method) flat = flatten_json_request_body('', decoded, input_spec) @@ -161,7 +165,8 @@ class BaseResponse(_TemplateEnvironmentMixin): if match: region = match.group(1) elif 'Authorization' in request.headers: - region = request.headers['Authorization'].split(",")[0].split("/")[2] + region = request.headers['Authorization'].split(",")[ + 0].split("/")[2] else: region = self.default_region return region @@ -175,7 +180,8 @@ class BaseResponse(_TemplateEnvironmentMixin): action = self.querystring.get('Action', [""])[0] if not action: # Some services use a header for the action # Headers are case-insensitive. Probably a better way to do this. - match = self.headers.get('x-amz-target') or self.headers.get('X-Amz-Target') + match = self.headers.get( + 'x-amz-target') or self.headers.get('X-Amz-Target') if match: action = match.split(".")[-1] @@ -198,7 +204,8 @@ class BaseResponse(_TemplateEnvironmentMixin): headers['status'] = str(headers['status']) return status, headers, body - raise NotImplementedError("The {0} action has not been implemented".format(action)) + raise NotImplementedError( + "The {0} action has not been implemented".format(action)) def _get_param(self, param_name, if_none=None): val = self.querystring.get(param_name) @@ -258,7 +265,8 @@ class BaseResponse(_TemplateEnvironmentMixin): params = {} for key, value in self.querystring.items(): if key.startswith(param_prefix): - params[camelcase_to_underscores(key.replace(param_prefix, ""))] = value[0] + params[camelcase_to_underscores( + key.replace(param_prefix, ""))] = value[0] return params def _get_list_prefix(self, param_prefix): @@ -291,7 +299,8 @@ class BaseResponse(_TemplateEnvironmentMixin): new_items = {} for key, value in self.querystring.items(): if key.startswith(index_prefix): - new_items[camelcase_to_underscores(key.replace(index_prefix, ""))] = value[0] + new_items[camelcase_to_underscores( + key.replace(index_prefix, ""))] = value[0] if not new_items: break results.append(new_items) @@ -327,7 +336,8 @@ class BaseResponse(_TemplateEnvironmentMixin): def is_not_dryrun(self, action): if 'true' in self.querystring.get('DryRun', ['false']): message = 'An error occurred (DryRunOperation) when calling the %s operation: Request would have succeeded, but DryRun flag is set' % action - raise DryRunClientError(error_type="DryRunOperation", message=message) + raise DryRunClientError( + error_type="DryRunOperation", message=message) return True @@ -343,6 +353,7 @@ class MotoAPIResponse(BaseResponse): class _RecursiveDictRef(object): """Store a recursive reference to dict.""" + def __init__(self): self.key = None self.dic = {} @@ -502,12 +513,15 @@ def flatten_json_request_body(prefix, dict_body, spec): if node_type == 'list': for idx, v in enumerate(value, 1): pref = key + '.member.' + str(idx) - flat.update(flatten_json_request_body(pref, v, spec[key]['member'])) + flat.update(flatten_json_request_body( + pref, v, spec[key]['member'])) elif node_type == 'map': for idx, (k, v) in enumerate(value.items(), 1): pref = key + '.entry.' + str(idx) - flat.update(flatten_json_request_body(pref + '.key', k, spec[key]['key'])) - flat.update(flatten_json_request_body(pref + '.value', v, spec[key]['value'])) + flat.update(flatten_json_request_body( + pref + '.key', k, spec[key]['key'])) + flat.update(flatten_json_request_body( + pref + '.value', v, spec[key]['value'])) else: flat.update(flatten_json_request_body(key, value, spec[key])) @@ -542,7 +556,8 @@ def xml_to_json_response(service_spec, operation, xml, result_node=None): # this can happen when with an older version of # botocore for which the node in XML template is not # defined in service spec. - log.warning('Field %s is not defined by the botocore version in use', k) + log.warning( + 'Field %s is not defined by the botocore version in use', k) continue if spec[k]['type'] == 'list': @@ -554,7 +569,8 @@ def xml_to_json_response(service_spec, operation, xml, result_node=None): else: od[k] = [transform(v['member'], spec[k]['member'])] elif isinstance(v['member'], list): - od[k] = [transform(o, spec[k]['member']) for o in v['member']] + od[k] = [transform(o, spec[k]['member']) + for o in v['member']] elif isinstance(v['member'], OrderedDict): od[k] = [transform(v['member'], spec[k]['member'])] else: diff --git a/moto/core/utils.py b/moto/core/utils.py index 11aafbb89..d26694014 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -98,7 +98,7 @@ class convert_httpretty_response(object): result = self.callback(request, url, headers) status, headers, response = result if 'server' not in headers: - headers["server"] = "amazon.com" + headers["server"] = "amazon.com" return status, headers, response diff --git a/moto/datapipeline/__init__.py b/moto/datapipeline/__init__.py index cebcf22bf..2565ddd5a 100644 --- a/moto/datapipeline/__init__.py +++ b/moto/datapipeline/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import datapipeline_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator datapipeline_backend = datapipeline_backends['us-east-1'] mock_datapipeline = base_decorator(datapipeline_backends) diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index b6a70b5f1..0cb33e4ed 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -7,6 +7,7 @@ from .utils import get_random_pipeline_id, remove_capitalization_of_dict_keys class PipelineObject(object): + def __init__(self, object_id, name, fields): self.object_id = object_id self.name = name @@ -21,6 +22,7 @@ class PipelineObject(object): class Pipeline(object): + def __init__(self, name, unique_id): self.name = name self.unique_id = unique_id @@ -82,7 +84,8 @@ class Pipeline(object): def set_pipeline_objects(self, pipeline_objects): self.objects = [ - PipelineObject(pipeline_object['id'], pipeline_object['name'], pipeline_object['fields']) + PipelineObject(pipeline_object['id'], pipeline_object[ + 'name'], pipeline_object['fields']) for pipeline_object in remove_capitalization_of_dict_keys(pipeline_objects) ] @@ -95,8 +98,10 @@ class Pipeline(object): properties = cloudformation_json["Properties"] cloudformation_unique_id = "cf-" + properties["Name"] - pipeline = datapipeline_backend.create_pipeline(properties["Name"], cloudformation_unique_id) - datapipeline_backend.put_pipeline_definition(pipeline.pipeline_id, properties["PipelineObjects"]) + pipeline = datapipeline_backend.create_pipeline( + properties["Name"], cloudformation_unique_id) + datapipeline_backend.put_pipeline_definition( + pipeline.pipeline_id, properties["PipelineObjects"]) if properties["Activate"]: pipeline.activate() @@ -117,7 +122,8 @@ class DataPipelineBackend(BaseBackend): return self.pipelines.values() def describe_pipelines(self, pipeline_ids): - pipelines = [pipeline for pipeline in self.pipelines.values() if pipeline.pipeline_id in pipeline_ids] + pipelines = [pipeline for pipeline in self.pipelines.values( + ) if pipeline.pipeline_id in pipeline_ids] return pipelines def get_pipeline(self, pipeline_id): diff --git a/moto/datapipeline/responses.py b/moto/datapipeline/responses.py index 2607f685d..f3644fd5c 100644 --- a/moto/datapipeline/responses.py +++ b/moto/datapipeline/responses.py @@ -52,12 +52,14 @@ class DataPipelineResponse(BaseResponse): pipeline_id = self.parameters["pipelineId"] pipeline_objects = self.parameters["pipelineObjects"] - self.datapipeline_backend.put_pipeline_definition(pipeline_id, pipeline_objects) + self.datapipeline_backend.put_pipeline_definition( + pipeline_id, pipeline_objects) return json.dumps({"errored": False}) def get_pipeline_definition(self): pipeline_id = self.parameters["pipelineId"] - pipeline_definition = self.datapipeline_backend.get_pipeline_definition(pipeline_id) + pipeline_definition = self.datapipeline_backend.get_pipeline_definition( + pipeline_id) return json.dumps({ "pipelineObjects": [pipeline_object.to_json() for pipeline_object in pipeline_definition] }) @@ -66,7 +68,8 @@ class DataPipelineResponse(BaseResponse): pipeline_id = self.parameters["pipelineId"] object_ids = self.parameters["objectIds"] - pipeline_objects = self.datapipeline_backend.describe_objects(object_ids, pipeline_id) + pipeline_objects = self.datapipeline_backend.describe_objects( + object_ids, pipeline_id) return json.dumps({ "hasMoreResults": False, "marker": None, diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index dd58eb4de..db50dbcc6 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -10,6 +10,7 @@ from .comparisons import get_comparison_func class DynamoJsonEncoder(json.JSONEncoder): + def default(self, obj): if hasattr(obj, 'to_json'): return obj.to_json() @@ -53,6 +54,7 @@ class DynamoType(object): class Item(object): + def __init__(self, hash_key, hash_key_type, range_key, range_key_type, attrs): self.hash_key = hash_key self.hash_key_type = hash_key_type @@ -157,7 +159,8 @@ class Table(object): else: range_value = None - item = Item(hash_value, self.hash_key_type, range_value, self.range_key_type, item_attrs) + item = Item(hash_value, self.hash_key_type, range_value, + self.range_key_type, item_attrs) if range_value: self.items[hash_value][range_value] = item @@ -167,7 +170,8 @@ class Table(object): def get_item(self, hash_key, range_key): if self.has_range_key and not range_key: - raise ValueError("Table has a range key, but no range key was passed into get_item") + raise ValueError( + "Table has a range key, but no range key was passed into get_item") try: if range_key: return self.items[hash_key][range_key] @@ -222,7 +226,8 @@ class Table(object): # Comparison is NULL and we don't have the attribute continue else: - # No attribute found and comparison is no NULL. This item fails + # No attribute found and comparison is no NULL. This item + # fails passes_all_conditions = False break @@ -283,7 +288,8 @@ class DynamoDBBackend(BaseBackend): return None, None hash_key = DynamoType(hash_key_dict) - range_values = [DynamoType(range_value) for range_value in range_value_dicts] + range_values = [DynamoType(range_value) + for range_value in range_value_dicts] return table.query(hash_key, range_comparison, range_values) diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 59cff0395..0da3e5045 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -130,7 +130,8 @@ class DynamoHandler(BaseResponse): throughput = self.body["ProvisionedThroughput"] new_read_units = throughput["ReadCapacityUnits"] new_write_units = throughput["WriteCapacityUnits"] - table = dynamodb_backend.update_table_throughput(name, new_read_units, new_write_units) + table = dynamodb_backend.update_table_throughput( + name, new_read_units, new_write_units) return dynamo_json_dump(table.describe) def describe_table(self): @@ -169,7 +170,8 @@ class DynamoHandler(BaseResponse): key = request['Key'] hash_key = key['HashKeyElement'] range_key = key.get('RangeKeyElement') - item = dynamodb_backend.delete_item(table_name, hash_key, range_key) + item = dynamodb_backend.delete_item( + table_name, hash_key, range_key) response = { "Responses": { @@ -221,11 +223,13 @@ class DynamoHandler(BaseResponse): for key in keys: hash_key = key["HashKeyElement"] range_key = key.get("RangeKeyElement") - item = dynamodb_backend.get_item(table_name, hash_key, range_key) + item = dynamodb_backend.get_item( + table_name, hash_key, range_key) if item: item_describe = item.describe_attrs(attributes_to_get) items.append(item_describe) - results["Responses"][table_name] = {"Items": items, "ConsumedCapacityUnits": 1} + results["Responses"][table_name] = { + "Items": items, "ConsumedCapacityUnits": 1} return dynamo_json_dump(results) def query(self): @@ -239,7 +243,8 @@ class DynamoHandler(BaseResponse): range_comparison = None range_values = [] - items, last_page = dynamodb_backend.query(name, hash_key, range_comparison, range_values) + items, last_page = dynamodb_backend.query( + name, hash_key, range_comparison, range_values) if items is None: er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' @@ -265,7 +270,8 @@ class DynamoHandler(BaseResponse): filters = {} scan_filters = self.body.get('ScanFilter', {}) for attribute_name, scan_filter in scan_filters.items(): - # Keys are attribute names. Values are tuples of (comparison, comparison_value) + # Keys are attribute names. Values are tuples of (comparison, + # comparison_value) comparison_operator = scan_filter["ComparisonOperator"] comparison_values = scan_filter.get("AttributeValueList", []) filters[attribute_name] = (comparison_operator, comparison_values) diff --git a/moto/dynamodb2/__init__.py b/moto/dynamodb2/__init__.py index 7a1f07352..ad3f042d2 100644 --- a/moto/dynamodb2/__init__.py +++ b/moto/dynamodb2/__init__.py @@ -3,4 +3,4 @@ from .models import dynamodb_backend2 dynamodb_backends2 = {"global": dynamodb_backend2} mock_dynamodb2 = dynamodb_backend2.decorator -mock_dynamodb2_deprecated = dynamodb_backend2.deprecated_decorator \ No newline at end of file +mock_dynamodb2_deprecated = dynamodb_backend2.deprecated_decorator diff --git a/moto/dynamodb2/comparisons.py b/moto/dynamodb2/comparisons.py index 1dc723df0..0b323ecd5 100644 --- a/moto/dynamodb2/comparisons.py +++ b/moto/dynamodb2/comparisons.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals # TODO add tests for all of these -EQ_FUNCTION = lambda item_value, test_value: item_value == test_value -NE_FUNCTION = lambda item_value, test_value: item_value != test_value -LE_FUNCTION = lambda item_value, test_value: item_value <= test_value -LT_FUNCTION = lambda item_value, test_value: item_value < test_value -GE_FUNCTION = lambda item_value, test_value: item_value >= test_value -GT_FUNCTION = lambda item_value, test_value: item_value > test_value +EQ_FUNCTION = lambda item_value, test_value: item_value == test_value # flake8: noqa +NE_FUNCTION = lambda item_value, test_value: item_value != test_value # flake8: noqa +LE_FUNCTION = lambda item_value, test_value: item_value <= test_value # flake8: noqa +LT_FUNCTION = lambda item_value, test_value: item_value < test_value # flake8: noqa +GE_FUNCTION = lambda item_value, test_value: item_value >= test_value # flake8: noqa +GT_FUNCTION = lambda item_value, test_value: item_value > test_value # flake8: noqa COMPARISON_FUNCS = { 'EQ': EQ_FUNCTION, diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 0adbae946..15c30e590 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -11,6 +11,7 @@ from .comparisons import get_comparison_func class DynamoJsonEncoder(json.JSONEncoder): + def default(self, obj): if hasattr(obj, 'to_json'): return obj.to_json() @@ -76,6 +77,7 @@ class DynamoType(object): class Item(object): + def __init__(self, hash_key, hash_key_type, range_key, range_key_type, attrs): self.hash_key = hash_key self.hash_key_type = hash_key_type @@ -131,14 +133,15 @@ class Item(object): elif action == 'SET' or action == 'set': key, value = value.split("=") if value in expression_attribute_values: - self.attrs[key] = DynamoType(expression_attribute_values[value]) + self.attrs[key] = DynamoType( + expression_attribute_values[value]) else: self.attrs[key] = DynamoType({"S": value}) def update_with_attribute_updates(self, attribute_updates): for attribute_name, update_action in attribute_updates.items(): action = update_action['Action'] - if action == 'DELETE' and not 'Value' in update_action: + if action == 'DELETE' and 'Value' not in update_action: if attribute_name in self.attrs: del self.attrs[attribute_name] continue @@ -158,14 +161,16 @@ class Item(object): self.attrs[attribute_name] = DynamoType({"S": new_value}) elif action == 'ADD': if set(update_action['Value'].keys()) == set(['N']): - existing = self.attrs.get(attribute_name, DynamoType({"N": '0'})) + existing = self.attrs.get( + attribute_name, DynamoType({"N": '0'})) self.attrs[attribute_name] = DynamoType({"N": str( - decimal.Decimal(existing.value) + - decimal.Decimal(new_value) + decimal.Decimal(existing.value) + + decimal.Decimal(new_value) )}) else: # TODO: implement other data types - raise NotImplementedError('ADD not supported for %s' % ', '.join(update_action['Value'].keys())) + raise NotImplementedError( + 'ADD not supported for %s' % ', '.join(update_action['Value'].keys())) class Table(object): @@ -186,7 +191,8 @@ class Table(object): self.range_key_attr = elem["AttributeName"] self.range_key_type = elem["KeyType"] if throughput is None: - self.throughput = {'WriteCapacityUnits': 10, 'ReadCapacityUnits': 10} + self.throughput = { + 'WriteCapacityUnits': 10, 'ReadCapacityUnits': 10} else: self.throughput = throughput self.throughput["NumberOfDecreasesToday"] = 0 @@ -250,14 +256,16 @@ class Table(object): else: range_value = None - item = Item(hash_value, self.hash_key_type, range_value, self.range_key_type, item_attrs) + item = Item(hash_value, self.hash_key_type, range_value, + self.range_key_type, item_attrs) if not overwrite: if expected is None: expected = {} lookup_range_value = range_value else: - expected_range_value = expected.get(self.range_key_attr, {}).get("Value") + expected_range_value = expected.get( + self.range_key_attr, {}).get("Value") if(expected_range_value is None): lookup_range_value = range_value else: @@ -281,8 +289,10 @@ class Table(object): elif 'Value' in val and DynamoType(val['Value']).value != current_attr[key].value: raise ValueError("The conditional request failed") elif 'ComparisonOperator' in val: - comparison_func = get_comparison_func(val['ComparisonOperator']) - dynamo_types = [DynamoType(ele) for ele in val["AttributeValueList"]] + comparison_func = get_comparison_func( + val['ComparisonOperator']) + dynamo_types = [DynamoType(ele) for ele in val[ + "AttributeValueList"]] for t in dynamo_types: if not comparison_func(current_attr[key].value, t.value): raise ValueError('The conditional request failed') @@ -304,7 +314,8 @@ class Table(object): def get_item(self, hash_key, range_key=None): if self.has_range_key and not range_key: - raise ValueError("Table has a range key, but no range key was passed into get_item") + raise ValueError( + "Table has a range key, but no range key was passed into get_item") try: if range_key: return self.items[hash_key][range_key] @@ -339,9 +350,11 @@ class Table(object): index = indexes_by_name[index_name] try: - index_hash_key = [key for key in index['KeySchema'] if key['KeyType'] == 'HASH'][0] + index_hash_key = [key for key in index[ + 'KeySchema'] if key['KeyType'] == 'HASH'][0] except IndexError: - raise ValueError('Missing Hash Key. KeySchema: %s' % index['KeySchema']) + raise ValueError('Missing Hash Key. KeySchema: %s' % + index['KeySchema']) possible_results = [] for item in self.all_items(): @@ -351,17 +364,20 @@ class Table(object): if item_hash_key and item_hash_key == hash_key: possible_results.append(item) else: - possible_results = [item for item in list(self.all_items()) if isinstance(item, Item) and item.hash_key == hash_key] + possible_results = [item for item in list(self.all_items()) if isinstance( + item, Item) and item.hash_key == hash_key] if index_name: try: - index_range_key = [key for key in index['KeySchema'] if key['KeyType'] == 'RANGE'][0] + index_range_key = [key for key in index[ + 'KeySchema'] if key['KeyType'] == 'RANGE'][0] except IndexError: index_range_key = None if range_comparison: if index_name and not index_range_key: - raise ValueError('Range Key comparison but no range key found for index: %s' % index_name) + raise ValueError( + 'Range Key comparison but no range key found for index: %s' % index_name) elif index_name: for result in possible_results: @@ -375,19 +391,21 @@ class Table(object): if filter_kwargs: for result in possible_results: for field, value in filter_kwargs.items(): - dynamo_types = [DynamoType(ele) for ele in value["AttributeValueList"]] + dynamo_types = [DynamoType(ele) for ele in value[ + "AttributeValueList"]] if result.attrs.get(field).compare(value['ComparisonOperator'], dynamo_types): results.append(result) if not range_comparison and not filter_kwargs: - # If we're not filtering on range key or on an index return all values + # If we're not filtering on range key or on an index return all + # values results = possible_results if index_name: if index_range_key: results.sort(key=lambda item: item.attrs[index_range_key['AttributeName']].value - if item.attrs.get(index_range_key['AttributeName']) else None) + if item.attrs.get(index_range_key['AttributeName']) else None) else: results.sort(key=lambda item: item.range_key) @@ -427,7 +445,8 @@ class Table(object): # Comparison is NULL and we don't have the attribute continue else: - # No attribute found and comparison is no NULL. This item fails + # No attribute found and comparison is no NULL. This item + # fails passes_all_conditions = False break @@ -460,7 +479,6 @@ class Table(object): return results, last_evaluated_key - def lookup(self, *args, **kwargs): if not self.schema: self.describe() @@ -517,7 +535,8 @@ class DynamoDBBackend(BaseBackend): if gsi_to_create: if gsi_to_create['IndexName'] in gsis_by_name: - raise ValueError('Global Secondary Index already exists: %s' % gsi_to_create['IndexName']) + raise ValueError( + 'Global Secondary Index already exists: %s' % gsi_to_create['IndexName']) gsis_by_name[gsi_to_create['IndexName']] = gsi_to_create @@ -555,9 +574,11 @@ class DynamoDBBackend(BaseBackend): def get_keys_value(self, table, keys): if table.hash_key_attr not in keys or (table.has_range_key and table.range_key_attr not in keys): - raise ValueError("Table has a range key, but no range key was passed into get_item") + raise ValueError( + "Table has a range key, but no range key was passed into get_item") hash_key = DynamoType(keys[table.hash_key_attr]) - range_key = DynamoType(keys[table.range_key_attr]) if table.has_range_key else None + range_key = DynamoType( + keys[table.range_key_attr]) if table.has_range_key else None return hash_key, range_key def get_table(self, table_name): @@ -577,7 +598,8 @@ class DynamoDBBackend(BaseBackend): return None, None hash_key = DynamoType(hash_key_dict) - range_values = [DynamoType(range_value) for range_value in range_value_dicts] + range_values = [DynamoType(range_value) + for range_value in range_value_dicts] return table.query(hash_key, range_comparison, range_values, limit, exclusive_start_key, scan_index_forward, index_name, **filter_kwargs) @@ -598,7 +620,8 @@ class DynamoDBBackend(BaseBackend): table = self.get_table(table_name) if all([table.hash_key_attr in key, table.range_key_attr in key]): - # Covers cases where table has hash and range keys, ``key`` param will be a dict + # Covers cases where table has hash and range keys, ``key`` param + # will be a dict hash_value = DynamoType(key[table.hash_key_attr]) range_value = DynamoType(key[table.range_key_attr]) elif table.hash_key_attr in key: @@ -629,7 +652,8 @@ class DynamoDBBackend(BaseBackend): item = table.get_item(hash_value, range_value) if update_expression: - item.update(update_expression, expression_attribute_names, expression_attribute_values) + item.update(update_expression, expression_attribute_names, + expression_attribute_values) else: item.update_with_attribute_updates(attribute_updates) return item diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 0957bfa89..3ceda0be1 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -104,11 +104,11 @@ class DynamoHandler(BaseResponse): local_secondary_indexes = body.get("LocalSecondaryIndexes", []) table = dynamodb_backend2.create_table(table_name, - schema=key_schema, - throughput=throughput, - attr=attr, - global_indexes=global_indexes, - indexes=local_secondary_indexes) + schema=key_schema, + throughput=throughput, + attr=attr, + global_indexes=global_indexes, + indexes=local_secondary_indexes) if table is not None: return dynamo_json_dump(table.describe()) else: @@ -127,7 +127,8 @@ class DynamoHandler(BaseResponse): def update_table(self): name = self.body['TableName'] if 'GlobalSecondaryIndexUpdates' in self.body: - table = dynamodb_backend2.update_table_global_indexes(name, self.body['GlobalSecondaryIndexUpdates']) + table = dynamodb_backend2.update_table_global_indexes( + name, self.body['GlobalSecondaryIndexUpdates']) if 'ProvisionedThroughput' in self.body: throughput = self.body["ProvisionedThroughput"] table = dynamodb_backend2.update_table_throughput(name, throughput) @@ -151,17 +152,20 @@ class DynamoHandler(BaseResponse): else: expected = None - # Attempt to parse simple ConditionExpressions into an Expected expression + # Attempt to parse simple ConditionExpressions into an Expected + # expression if not expected: condition_expression = self.body.get('ConditionExpression') if condition_expression and 'OR' not in condition_expression: - cond_items = [c.strip() for c in condition_expression.split('AND')] + cond_items = [c.strip() + for c in condition_expression.split('AND')] if cond_items: expected = {} overwrite = False exists_re = re.compile('^attribute_exists\((.*)\)$') - not_exists_re = re.compile('^attribute_not_exists\((.*)\)$') + not_exists_re = re.compile( + '^attribute_not_exists\((.*)\)$') for cond in cond_items: exists_m = exists_re.match(cond) @@ -172,7 +176,8 @@ class DynamoHandler(BaseResponse): expected[not_exists_m.group(1)] = {'Exists': False} try: - result = dynamodb_backend2.put_item(name, item, expected, overwrite) + result = dynamodb_backend2.put_item( + name, item, expected, overwrite) except Exception: er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' return self.error(er) @@ -249,7 +254,8 @@ class DynamoHandler(BaseResponse): item = dynamodb_backend2.get_item(table_name, key) if item: item_describe = item.describe_attrs(attributes_to_get) - results["Responses"][table_name].append(item_describe["Item"]) + results["Responses"][table_name].append( + item_describe["Item"]) results["ConsumedCapacity"].append({ "CapacityUnits": len(keys), @@ -268,8 +274,10 @@ class DynamoHandler(BaseResponse): table = dynamodb_backend2.get_table(name) index_name = self.body.get('IndexName') if index_name: - all_indexes = (table.global_indexes or []) + (table.indexes or []) - indexes_by_name = dict((i['IndexName'], i) for i in all_indexes) + all_indexes = (table.global_indexes or []) + \ + (table.indexes or []) + indexes_by_name = dict((i['IndexName'], i) + for i in all_indexes) if index_name not in indexes_by_name: raise ValueError('Invalid index: %s for table: %s. Available indexes are: %s' % ( index_name, name, ', '.join(indexes_by_name.keys()) @@ -279,16 +287,21 @@ class DynamoHandler(BaseResponse): else: index = table.schema - key_map = [column for _, column in sorted((k, v) for k, v in self.body['ExpressionAttributeNames'].items())] + key_map = [column for _, column in sorted( + (k, v) for k, v in self.body['ExpressionAttributeNames'].items())] if " AND " in key_condition_expression: expressions = key_condition_expression.split(" AND ", 1) - index_hash_key = [key for key in index if key['KeyType'] == 'HASH'][0] - hash_key_index_in_key_map = key_map.index(index_hash_key['AttributeName']) + index_hash_key = [ + key for key in index if key['KeyType'] == 'HASH'][0] + hash_key_index_in_key_map = key_map.index( + index_hash_key['AttributeName']) - hash_key_expression = expressions.pop(hash_key_index_in_key_map).strip('()') - # TODO implement more than one range expression and OR operators + hash_key_expression = expressions.pop( + hash_key_index_in_key_map).strip('()') + # TODO implement more than one range expression and OR + # operators range_key_expression = expressions[0].strip('()') range_key_expression_components = range_key_expression.split() range_comparison = range_key_expression_components[1] @@ -304,7 +317,8 @@ class DynamoHandler(BaseResponse): value_alias_map[range_key_expression_components[1]], ] else: - range_values = [value_alias_map[range_key_expression_components[2]]] + range_values = [value_alias_map[ + range_key_expression_components[2]]] else: hash_key_expression = key_condition_expression range_comparison = None @@ -316,14 +330,16 @@ class DynamoHandler(BaseResponse): # 'KeyConditions': {u'forum_name': {u'ComparisonOperator': u'EQ', u'AttributeValueList': [{u'S': u'the-key'}]}} key_conditions = self.body.get('KeyConditions') if key_conditions: - hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name, key_conditions.keys()) + hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name( + name, key_conditions.keys()) for key, value in key_conditions.items(): if key not in (hash_key_name, range_key_name): filter_kwargs[key] = value if hash_key_name is None: er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" return self.error(er) - hash_key = key_conditions[hash_key_name]['AttributeValueList'][0] + hash_key = key_conditions[hash_key_name][ + 'AttributeValueList'][0] if len(key_conditions) == 1: range_comparison = None range_values = [] @@ -334,8 +350,10 @@ class DynamoHandler(BaseResponse): else: range_condition = key_conditions.get(range_key_name) if range_condition: - range_comparison = range_condition['ComparisonOperator'] - range_values = range_condition['AttributeValueList'] + range_comparison = range_condition[ + 'ComparisonOperator'] + range_values = range_condition[ + 'AttributeValueList'] else: range_comparison = None range_values = [] @@ -369,7 +387,8 @@ class DynamoHandler(BaseResponse): filters = {} scan_filters = self.body.get('ScanFilter', {}) for attribute_name, scan_filter in scan_filters.items(): - # Keys are attribute names. Values are tuples of (comparison, comparison_value) + # Keys are attribute names. Values are tuples of (comparison, + # comparison_value) comparison_operator = scan_filter["ComparisonOperator"] comparison_values = scan_filter.get("AttributeValueList", []) filters[attribute_name] = (comparison_operator, comparison_values) @@ -416,16 +435,20 @@ class DynamoHandler(BaseResponse): key = self.body['Key'] update_expression = self.body.get('UpdateExpression') attribute_updates = self.body.get('AttributeUpdates') - expression_attribute_names = self.body.get('ExpressionAttributeNames', {}) - expression_attribute_values = self.body.get('ExpressionAttributeValues', {}) + expression_attribute_names = self.body.get( + 'ExpressionAttributeNames', {}) + expression_attribute_values = self.body.get( + 'ExpressionAttributeValues', {}) existing_item = dynamodb_backend2.get_item(name, key) # Support spaces between operators in an update expression # E.g. `a = b + c` -> `a=b+c` if update_expression: - update_expression = re.sub('\s*([=\+-])\s*', '\\1', update_expression) + update_expression = re.sub( + '\s*([=\+-])\s*', '\\1', update_expression) - item = dynamodb_backend2.update_item(name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values) + item = dynamodb_backend2.update_item( + name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values) item_dict = item.to_json() item_dict['ConsumedCapacityUnits'] = 0.5 diff --git a/moto/ec2/__init__.py b/moto/ec2/__init__.py index 608173577..ba8cbe0a0 100644 --- a/moto/ec2/__init__.py +++ b/moto/ec2/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import ec2_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator ec2_backend = ec2_backends['us-east-1'] mock_ec2 = base_decorator(ec2_backends) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 79ceb776f..d32118b82 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -7,12 +7,14 @@ class EC2ClientError(RESTError): class DependencyViolationError(EC2ClientError): + def __init__(self, message): super(DependencyViolationError, self).__init__( "DependencyViolation", message) class MissingParameterError(EC2ClientError): + def __init__(self, parameter): super(MissingParameterError, self).__init__( "MissingParameter", @@ -21,6 +23,7 @@ class MissingParameterError(EC2ClientError): class InvalidDHCPOptionsIdError(EC2ClientError): + def __init__(self, dhcp_options_id): super(InvalidDHCPOptionsIdError, self).__init__( "InvalidDhcpOptionID.NotFound", @@ -29,6 +32,7 @@ class InvalidDHCPOptionsIdError(EC2ClientError): class MalformedDHCPOptionsIdError(EC2ClientError): + def __init__(self, dhcp_options_id): super(MalformedDHCPOptionsIdError, self).__init__( "InvalidDhcpOptionsId.Malformed", @@ -37,6 +41,7 @@ class MalformedDHCPOptionsIdError(EC2ClientError): class InvalidKeyPairNameError(EC2ClientError): + def __init__(self, key): super(InvalidKeyPairNameError, self).__init__( "InvalidKeyPair.NotFound", @@ -45,6 +50,7 @@ class InvalidKeyPairNameError(EC2ClientError): class InvalidKeyPairDuplicateError(EC2ClientError): + def __init__(self, key): super(InvalidKeyPairDuplicateError, self).__init__( "InvalidKeyPair.Duplicate", @@ -53,6 +59,7 @@ class InvalidKeyPairDuplicateError(EC2ClientError): class InvalidVPCIdError(EC2ClientError): + def __init__(self, vpc_id): super(InvalidVPCIdError, self).__init__( "InvalidVpcID.NotFound", @@ -61,6 +68,7 @@ class InvalidVPCIdError(EC2ClientError): class InvalidSubnetIdError(EC2ClientError): + def __init__(self, subnet_id): super(InvalidSubnetIdError, self).__init__( "InvalidSubnetID.NotFound", @@ -69,6 +77,7 @@ class InvalidSubnetIdError(EC2ClientError): class InvalidNetworkAclIdError(EC2ClientError): + def __init__(self, network_acl_id): super(InvalidNetworkAclIdError, self).__init__( "InvalidNetworkAclID.NotFound", @@ -77,6 +86,7 @@ class InvalidNetworkAclIdError(EC2ClientError): class InvalidVpnGatewayIdError(EC2ClientError): + def __init__(self, network_acl_id): super(InvalidVpnGatewayIdError, self).__init__( "InvalidVpnGatewayID.NotFound", @@ -85,6 +95,7 @@ class InvalidVpnGatewayIdError(EC2ClientError): class InvalidVpnConnectionIdError(EC2ClientError): + def __init__(self, network_acl_id): super(InvalidVpnConnectionIdError, self).__init__( "InvalidVpnConnectionID.NotFound", @@ -93,6 +104,7 @@ class InvalidVpnConnectionIdError(EC2ClientError): class InvalidCustomerGatewayIdError(EC2ClientError): + def __init__(self, customer_gateway_id): super(InvalidCustomerGatewayIdError, self).__init__( "InvalidCustomerGatewayID.NotFound", @@ -101,6 +113,7 @@ class InvalidCustomerGatewayIdError(EC2ClientError): class InvalidNetworkInterfaceIdError(EC2ClientError): + def __init__(self, eni_id): super(InvalidNetworkInterfaceIdError, self).__init__( "InvalidNetworkInterfaceID.NotFound", @@ -109,6 +122,7 @@ class InvalidNetworkInterfaceIdError(EC2ClientError): class InvalidNetworkAttachmentIdError(EC2ClientError): + def __init__(self, attachment_id): super(InvalidNetworkAttachmentIdError, self).__init__( "InvalidAttachmentID.NotFound", @@ -117,6 +131,7 @@ class InvalidNetworkAttachmentIdError(EC2ClientError): class InvalidSecurityGroupDuplicateError(EC2ClientError): + def __init__(self, name): super(InvalidSecurityGroupDuplicateError, self).__init__( "InvalidGroup.Duplicate", @@ -125,6 +140,7 @@ class InvalidSecurityGroupDuplicateError(EC2ClientError): class InvalidSecurityGroupNotFoundError(EC2ClientError): + def __init__(self, name): super(InvalidSecurityGroupNotFoundError, self).__init__( "InvalidGroup.NotFound", @@ -133,6 +149,7 @@ class InvalidSecurityGroupNotFoundError(EC2ClientError): class InvalidPermissionNotFoundError(EC2ClientError): + def __init__(self): super(InvalidPermissionNotFoundError, self).__init__( "InvalidPermission.NotFound", @@ -140,6 +157,7 @@ class InvalidPermissionNotFoundError(EC2ClientError): class InvalidRouteTableIdError(EC2ClientError): + def __init__(self, route_table_id): super(InvalidRouteTableIdError, self).__init__( "InvalidRouteTableID.NotFound", @@ -148,6 +166,7 @@ class InvalidRouteTableIdError(EC2ClientError): class InvalidRouteError(EC2ClientError): + def __init__(self, route_table_id, cidr): super(InvalidRouteError, self).__init__( "InvalidRoute.NotFound", @@ -156,6 +175,7 @@ class InvalidRouteError(EC2ClientError): class InvalidInstanceIdError(EC2ClientError): + def __init__(self, instance_id): super(InvalidInstanceIdError, self).__init__( "InvalidInstanceID.NotFound", @@ -164,6 +184,7 @@ class InvalidInstanceIdError(EC2ClientError): class InvalidAMIIdError(EC2ClientError): + def __init__(self, ami_id): super(InvalidAMIIdError, self).__init__( "InvalidAMIID.NotFound", @@ -172,6 +193,7 @@ class InvalidAMIIdError(EC2ClientError): class InvalidAMIAttributeItemValueError(EC2ClientError): + def __init__(self, attribute, value): super(InvalidAMIAttributeItemValueError, self).__init__( "InvalidAMIAttributeItemValue", @@ -180,6 +202,7 @@ class InvalidAMIAttributeItemValueError(EC2ClientError): class MalformedAMIIdError(EC2ClientError): + def __init__(self, ami_id): super(MalformedAMIIdError, self).__init__( "InvalidAMIID.Malformed", @@ -188,6 +211,7 @@ class MalformedAMIIdError(EC2ClientError): class InvalidSnapshotIdError(EC2ClientError): + def __init__(self, snapshot_id): super(InvalidSnapshotIdError, self).__init__( "InvalidSnapshot.NotFound", @@ -195,6 +219,7 @@ class InvalidSnapshotIdError(EC2ClientError): class InvalidVolumeIdError(EC2ClientError): + def __init__(self, volume_id): super(InvalidVolumeIdError, self).__init__( "InvalidVolume.NotFound", @@ -203,6 +228,7 @@ class InvalidVolumeIdError(EC2ClientError): class InvalidVolumeAttachmentError(EC2ClientError): + def __init__(self, volume_id, instance_id): super(InvalidVolumeAttachmentError, self).__init__( "InvalidAttachment.NotFound", @@ -211,6 +237,7 @@ class InvalidVolumeAttachmentError(EC2ClientError): class InvalidDomainError(EC2ClientError): + def __init__(self, domain): super(InvalidDomainError, self).__init__( "InvalidParameterValue", @@ -219,6 +246,7 @@ class InvalidDomainError(EC2ClientError): class InvalidAddressError(EC2ClientError): + def __init__(self, ip): super(InvalidAddressError, self).__init__( "InvalidAddress.NotFound", @@ -227,6 +255,7 @@ class InvalidAddressError(EC2ClientError): class InvalidAllocationIdError(EC2ClientError): + def __init__(self, allocation_id): super(InvalidAllocationIdError, self).__init__( "InvalidAllocationID.NotFound", @@ -235,6 +264,7 @@ class InvalidAllocationIdError(EC2ClientError): class InvalidAssociationIdError(EC2ClientError): + def __init__(self, association_id): super(InvalidAssociationIdError, self).__init__( "InvalidAssociationID.NotFound", @@ -243,6 +273,7 @@ class InvalidAssociationIdError(EC2ClientError): class InvalidVPCPeeringConnectionIdError(EC2ClientError): + def __init__(self, vpc_peering_connection_id): super(InvalidVPCPeeringConnectionIdError, self).__init__( "InvalidVpcPeeringConnectionId.NotFound", @@ -251,6 +282,7 @@ class InvalidVPCPeeringConnectionIdError(EC2ClientError): class InvalidVPCPeeringConnectionStateTransitionError(EC2ClientError): + def __init__(self, vpc_peering_connection_id): super(InvalidVPCPeeringConnectionStateTransitionError, self).__init__( "InvalidStateTransition", @@ -259,6 +291,7 @@ class InvalidVPCPeeringConnectionStateTransitionError(EC2ClientError): class InvalidParameterValueError(EC2ClientError): + def __init__(self, parameter_value): super(InvalidParameterValueError, self).__init__( "InvalidParameterValue", @@ -267,6 +300,7 @@ class InvalidParameterValueError(EC2ClientError): class InvalidParameterValueErrorTagNull(EC2ClientError): + def __init__(self): super(InvalidParameterValueErrorTagNull, self).__init__( "InvalidParameterValue", @@ -274,6 +308,7 @@ class InvalidParameterValueErrorTagNull(EC2ClientError): class InvalidInternetGatewayIdError(EC2ClientError): + def __init__(self, internet_gateway_id): super(InvalidInternetGatewayIdError, self).__init__( "InvalidInternetGatewayID.NotFound", @@ -282,6 +317,7 @@ class InvalidInternetGatewayIdError(EC2ClientError): class GatewayNotAttachedError(EC2ClientError): + def __init__(self, internet_gateway_id, vpc_id): super(GatewayNotAttachedError, self).__init__( "Gateway.NotAttached", @@ -290,6 +326,7 @@ class GatewayNotAttachedError(EC2ClientError): class ResourceAlreadyAssociatedError(EC2ClientError): + def __init__(self, resource_id): super(ResourceAlreadyAssociatedError, self).__init__( "Resource.AlreadyAssociated", @@ -298,6 +335,7 @@ class ResourceAlreadyAssociatedError(EC2ClientError): class TagLimitExceeded(EC2ClientError): + def __init__(self): super(TagLimitExceeded, self).__init__( "TagLimitExceeded", @@ -305,6 +343,7 @@ class TagLimitExceeded(EC2ClientError): class InvalidID(EC2ClientError): + def __init__(self, resource_id): super(InvalidID, self).__init__( "InvalidID", @@ -313,6 +352,7 @@ class InvalidID(EC2ClientError): class InvalidCIDRSubnetError(EC2ClientError): + def __init__(self, cidr): super(InvalidCIDRSubnetError, self).__init__( "InvalidParameterValue", @@ -321,6 +361,7 @@ class InvalidCIDRSubnetError(EC2ClientError): class RulesPerSecurityGroupLimitExceededError(EC2ClientError): + def __init__(self): super(RulesPerSecurityGroupLimitExceededError, self).__init__( "RulesPerSecurityGroupLimitExceeded", diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 30769fd7e..2e6b5e5b6 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import boto import copy import itertools import re @@ -117,20 +116,24 @@ def validate_resource_ids(resource_ids): class InstanceState(object): + def __init__(self, name='pending', code=0): self.name = name self.code = code class StateReason(object): + def __init__(self, message="", code=""): self.message = message self.code = code class TaggedEC2Resource(object): + def get_tags(self, *args, **kwargs): - tags = self.ec2_backend.describe_tags(filters={'resource-id': [self.id]}) + tags = self.ec2_backend.describe_tags( + filters={'resource-id': [self.id]}) return tags def add_tag(self, key, value): @@ -155,8 +158,9 @@ class TaggedEC2Resource(object): class NetworkInterface(TaggedEC2Resource): + def __init__(self, ec2_backend, subnet, private_ip_address, device_index=0, - public_ip_auto_assign=True, group_ids=None): + public_ip_auto_assign=True, group_ids=None): self.ec2_backend = ec2_backend self.id = random_eni_id() self.device_index = device_index @@ -181,7 +185,8 @@ class NetworkInterface(TaggedEC2Resource): group = self.ec2_backend.get_security_group_from_id(group_id) if not group: # Create with specific group ID. - group = SecurityGroup(self.ec2_backend, group_id, group_id, group_id, vpc_id=subnet.vpc_id) + group = SecurityGroup( + self.ec2_backend, group_id, group_id, group_id, vpc_id=subnet.vpc_id) self.ec2_backend.groups[subnet.vpc_id][group_id] = group if group: self._group_set.append(group) @@ -231,7 +236,8 @@ class NetworkInterface(TaggedEC2Resource): if attribute_name == 'PrimaryPrivateIpAddress': return self.private_ip_address elif attribute_name == 'SecondaryPrivateIpAddresses': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SecondaryPrivateIpAddresses" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "SecondaryPrivateIpAddresses" ]"') raise UnformattedGetAttTemplateException() @property @@ -250,23 +256,27 @@ class NetworkInterface(TaggedEC2Resource): elif filter_name == 'group-id': return [group.id for group in self._group_set] - filter_value = super(NetworkInterface, self).get_filter_value(filter_name) + filter_value = super( + NetworkInterface, self).get_filter_value(filter_name) if filter_value is None: self.ec2_backend.raise_not_implemented_error( - "The filter '{0}' for DescribeNetworkInterfaces".format(filter_name) + "The filter '{0}' for DescribeNetworkInterfaces".format( + filter_name) ) return filter_value class NetworkInterfaceBackend(object): + def __init__(self): self.enis = {} super(NetworkInterfaceBackend, self).__init__() def create_network_interface(self, subnet, private_ip_address, group_ids=None, **kwargs): - eni = NetworkInterface(self, subnet, private_ip_address, group_ids=group_ids, **kwargs) + eni = NetworkInterface( + self, subnet, private_ip_address, group_ids=group_ids, **kwargs) self.enis[eni.id] = eni return eni @@ -289,7 +299,8 @@ class NetworkInterfaceBackend(object): for (_filter, _filter_value) in filters.items(): if _filter == 'network-interface-id': _filter = 'id' - enis = [eni for eni in enis if getattr(eni, _filter) in _filter_value] + enis = [eni for eni in enis if getattr( + eni, _filter) in _filter_value] elif _filter == 'group-id': original_enis = enis enis = [] @@ -299,7 +310,8 @@ class NetworkInterfaceBackend(object): enis.append(eni) break else: - self.raise_not_implemented_error("The filter '{0}' for DescribeNetworkInterfaces".format(_filter)) + self.raise_not_implemented_error( + "The filter '{0}' for DescribeNetworkInterfaces".format(_filter)) return enis def attach_network_interface(self, eni_id, instance_id, device_index): @@ -330,13 +342,15 @@ class NetworkInterfaceBackend(object): if eni_ids: enis = [eni for eni in enis if eni.id in eni_ids] if len(enis) != len(eni_ids): - invalid_id = list(set(eni_ids).difference(set([eni.id for eni in enis])))[0] + invalid_id = list(set(eni_ids).difference( + set([eni.id for eni in enis])))[0] raise InvalidNetworkInterfaceIdError(invalid_id) return generic_filter(filters, enis) class Instance(BotoInstance, TaggedEC2Resource): + def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs): super(Instance, self).__init__() self.ec2_backend = ec2_backend @@ -367,7 +381,8 @@ class Instance(BotoInstance, TaggedEC2Resource): self.virtualization_type = ami.virtualization_type if ami else 'paravirtual' self.architecture = ami.architecture if ami else 'x86_64' - # handle weird bug around user_data -- something grabs the repr(), so it must be clean + # handle weird bug around user_data -- something grabs the repr(), so + # it must be clean if isinstance(self.user_data, list) and len(self.user_data) > 0: if six.PY3 and isinstance(self.user_data[0], six.binary_type): # string will have a "b" prefix -- need to get rid of it @@ -393,7 +408,8 @@ class Instance(BotoInstance, TaggedEC2Resource): associate_public_ip=associate_public_ip) def setup_defaults(self): - # Default have an instance with root volume should you not wish to override with attach volume cmd. + # Default have an instance with root volume should you not wish to + # override with attach volume cmd. volume = self.ec2_backend.create_volume(8, 'us-east-1a') self.ec2_backend.attach_volume(volume.id, self.id, '/dev/sda1') @@ -429,7 +445,8 @@ class Instance(BotoInstance, TaggedEC2Resource): ec2_backend = ec2_backends[region_name] security_group_ids = properties.get('SecurityGroups', []) - group_names = [ec2_backend.get_security_group_from_id(group_id).name for group_id in security_group_ids] + group_names = [ec2_backend.get_security_group_from_id( + group_id).name for group_id in security_group_ids] reservation = ec2_backend.add_instances( image_id=properties['ImageId'], @@ -464,7 +481,8 @@ class Instance(BotoInstance, TaggedEC2Resource): self._state.name = "stopped" self._state.code = 80 - self._reason = "User initiated ({0})".format(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')) + self._reason = "User initiated ({0})".format( + datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')) self._state_reason = StateReason("Client.UserInitiatedShutdown: User initiated shutdown", "Client.UserInitiatedShutdown") @@ -480,7 +498,8 @@ class Instance(BotoInstance, TaggedEC2Resource): self._state.name = "terminated" self._state.code = 48 - self._reason = "User initiated ({0})".format(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')) + self._reason = "User initiated ({0})".format( + datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')) self._state_reason = StateReason("Client.UserInitiatedShutdown: User initiated shutdown", "Client.UserInitiatedShutdown") @@ -514,7 +533,8 @@ class Instance(BotoInstance, TaggedEC2Resource): 'AssociatePublicIpAddress': associate_public_ip} primary_nic = dict((k, v) for k, v in primary_nic.items() if v) - # If empty NIC spec but primary NIC values provided, create NIC from them. + # If empty NIC spec but primary NIC values provided, create NIC from + # them. if primary_nic and not nic_spec: nic_spec[0] = primary_nic nic_spec[0]['DeviceIndex'] = 0 @@ -544,10 +564,12 @@ class Instance(BotoInstance, TaggedEC2Resource): group_ids = [group_id] if group_id else [] use_nic = self.ec2_backend.create_network_interface(subnet, - nic.get('PrivateIpAddress'), - device_index=device_index, - public_ip_auto_assign=nic.get('AssociatePublicIpAddress', False), - group_ids=group_ids) + nic.get( + 'PrivateIpAddress'), + device_index=device_index, + public_ip_auto_assign=nic.get( + 'AssociatePublicIpAddress', False), + group_ids=group_ids) self.attach_eni(use_nic, device_index) @@ -559,7 +581,8 @@ class Instance(BotoInstance, TaggedEC2Resource): device_index = int(device_index) self.nics[device_index] = eni - eni.instance = self # This is used upon associate/disassociate public IP. + # This is used upon associate/disassociate public IP. + eni.instance = self eni.attachment_id = random_eni_attach_id() eni.device_index = device_index @@ -639,7 +662,8 @@ class InstanceBackend(object): def terminate_instances(self, instance_ids): terminated_instances = [] if not instance_ids: - raise EC2ClientError("InvalidParameterCombination", "No instances specified") + raise EC2ClientError( + "InvalidParameterCombination", "No instances specified") for instance in self.get_multi_instances_by_id(instance_ids): instance.terminate() terminated_instances.append(instance) @@ -716,16 +740,21 @@ class InstanceBackend(object): """ reservations = [] for reservation in self.all_reservations(make_copy=True): - reservation_instance_ids = [instance.id for instance in reservation.instances] - matching_reservation = any(instance_id in reservation_instance_ids for instance_id in instance_ids) + reservation_instance_ids = [ + instance.id for instance in reservation.instances] + matching_reservation = any( + instance_id in reservation_instance_ids for instance_id in instance_ids) if matching_reservation: # We need to make a copy of the reservation because we have to modify the # instances to limit to those requested - reservation.instances = [instance for instance in reservation.instances if instance.id in instance_ids] + reservation.instances = [ + instance for instance in reservation.instances if instance.id in instance_ids] reservations.append(reservation) - found_instance_ids = [instance.id for reservation in reservations for instance in reservation.instances] + found_instance_ids = [ + instance.id for reservation in reservations for instance in reservation.instances] if len(found_instance_ids) != len(instance_ids): - invalid_id = list(set(instance_ids).difference(set(found_instance_ids)))[0] + invalid_id = list(set(instance_ids).difference( + set(found_instance_ids)))[0] raise InvalidInstanceIdError(invalid_id) if filters is not None: reservations = filter_reservations(reservations, filters) @@ -735,9 +764,11 @@ class InstanceBackend(object): if make_copy: # Return copies so that other functions can modify them with changing # the originals - reservations = [copy.deepcopy(reservation) for reservation in self.reservations.values()] + reservations = [copy.deepcopy(reservation) + for reservation in self.reservations.values()] else: - reservations = [reservation for reservation in self.reservations.values()] + reservations = [ + reservation for reservation in self.reservations.values()] if filters is not None: reservations = filter_reservations(reservations, filters) return reservations @@ -848,16 +879,19 @@ class TagBackend(object): if tag_filter in self.VALID_TAG_FILTERS: if tag_filter == 'key': for value in filters[tag_filter]: - key_filters.append(re.compile(simple_aws_filter_to_re(value))) + key_filters.append(re.compile( + simple_aws_filter_to_re(value))) if tag_filter == 'resource-id': for value in filters[tag_filter]: - resource_id_filters.append(re.compile(simple_aws_filter_to_re(value))) + resource_id_filters.append( + re.compile(simple_aws_filter_to_re(value))) if tag_filter == 'resource-type': for value in filters[tag_filter]: resource_type_filters.append(value) if tag_filter == 'value': for value in filters[tag_filter]: - value_filters.append(re.compile(simple_aws_filter_to_re(value))) + value_filters.append(re.compile( + simple_aws_filter_to_re(value))) for resource_id, tags in self.tags.items(): for key, value in tags.items(): add_result = False @@ -907,8 +941,9 @@ class TagBackend(object): class Ami(TaggedEC2Resource): + def __init__(self, ec2_backend, ami_id, instance=None, source_ami=None, - name=None, description=None): + name=None, description=None): self.ec2_backend = ec2_backend self.id = ami_id self.state = "available" @@ -948,7 +983,8 @@ class Ami(TaggedEC2Resource): # AWS auto-creates these, we should reflect the same. volume = self.ec2_backend.create_volume(15, "us-east-1a") - self.ebs_snapshot = self.ec2_backend.create_snapshot(volume.id, "Auto-created snapshot for AMI %s" % self.id) + self.ebs_snapshot = self.ec2_backend.create_snapshot( + volume.id, "Auto-created snapshot for AMI %s" % self.id) @property def is_public(self): @@ -977,12 +1013,14 @@ class Ami(TaggedEC2Resource): filter_value = super(Ami, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeImages".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeImages".format(filter_name)) return filter_value class AmiBackend(object): + def __init__(self): self.amis = {} super(AmiBackend, self).__init__() @@ -991,14 +1029,17 @@ class AmiBackend(object): # TODO: check that instance exists and pull info from it. ami_id = random_ami_id() instance = self.get_instance(instance_id) - ami = Ami(self, ami_id, instance=instance, source_ami=None, name=name, description=description) + ami = Ami(self, ami_id, instance=instance, source_ami=None, + name=name, description=description) self.amis[ami_id] = ami return ami def copy_image(self, source_image_id, source_region, name=None, description=None): - source_ami = ec2_backends[source_region].describe_images(ami_ids=[source_image_id])[0] + source_ami = ec2_backends[source_region].describe_images( + ami_ids=[source_image_id])[0] ami_id = random_ami_id() - ami = Ami(self, ami_id, instance=None, source_ami=source_ami, name=name, description=description) + ami = Ami(self, ami_id, instance=None, source_ami=source_ami, + name=name, description=description) self.amis[ami_id] = ami return ami @@ -1074,12 +1115,14 @@ class AmiBackend(object): class Region(object): + def __init__(self, name, endpoint): self.name = name self.endpoint = endpoint class Zone(object): + def __init__(self, name, region_name): self.name = name self.region_name = region_name @@ -1122,6 +1165,7 @@ class RegionsAndZonesBackend(object): class SecurityRule(object): + def __init__(self, ip_protocol, from_port, to_port, ip_ranges, source_groups): self.ip_protocol = ip_protocol self.from_port = from_port @@ -1144,6 +1188,7 @@ class SecurityRule(object): class SecurityGroup(TaggedEC2Resource): + def __init__(self, ec2_backend, group_id, name, description, vpc_id=None): self.ec2_backend = ec2_backend self.id = group_id @@ -1189,19 +1234,22 @@ class SecurityGroup(TaggedEC2Resource): @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): - cls._delete_security_group_given_vpc_id(original_resource.name, original_resource.vpc_id, region_name) + cls._delete_security_group_given_vpc_id( + original_resource.name, original_resource.vpc_id, region_name) return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name) @classmethod def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] vpc_id = properties.get('VpcId') - cls._delete_security_group_given_vpc_id(resource_name, vpc_id, region_name) + cls._delete_security_group_given_vpc_id( + resource_name, vpc_id, region_name) @classmethod def _delete_security_group_given_vpc_id(cls, resource_name, vpc_id, region_name): ec2_backend = ec2_backends[region_name] - security_group = ec2_backend.get_security_group_from_name(resource_name, vpc_id) + security_group = ec2_backend.get_security_group_from_name( + resource_name, vpc_id) if security_group: security_group.delete(region_name) @@ -1304,13 +1352,14 @@ class SecurityGroupBackend(object): return group def describe_security_groups(self, group_ids=None, groupnames=None, filters=None): - all_groups = itertools.chain(*[x.values() for x in self.groups.values()]) + all_groups = itertools.chain(*[x.values() + for x in self.groups.values()]) groups = [] if group_ids or groupnames or filters: for group in all_groups: - if ((group_ids and not group.id in group_ids) or - (groupnames and not group.name in groupnames)): + if ((group_ids and group.id not in group_ids) or + (groupnames and group.name not in groupnames)): continue if filters and not group.matches_filters(filters): continue @@ -1322,7 +1371,8 @@ class SecurityGroupBackend(object): def _delete_security_group(self, vpc_id, group_id): if self.groups[vpc_id][group_id].enis: - raise DependencyViolationError("{0} is being utilized by {1}".format(group_id, 'ENIs')) + raise DependencyViolationError( + "{0} is being utilized by {1}".format(group_id, 'ENIs')) return self.groups[vpc_id].pop(group_id) def delete_security_group(self, name=None, group_id=None): @@ -1333,7 +1383,8 @@ class SecurityGroupBackend(object): return self._delete_security_group(vpc_id, group_id) raise InvalidSecurityGroupNotFoundError(group_id) elif name: - # Group Name. Has to be in standard EC2, VPC needs to be identified by group_id + # Group Name. Has to be in standard EC2, VPC needs to be + # identified by group_id group = self.get_security_group_from_name(name) if group: return self._delete_security_group(None, group.id) @@ -1341,7 +1392,8 @@ class SecurityGroupBackend(object): def get_security_group_from_id(self, group_id): # 2 levels of chaining necessary since it's a complex structure - all_groups = itertools.chain.from_iterable([x.values() for x in self.groups.values()]) + all_groups = itertools.chain.from_iterable( + [x.values() for x in self.groups.values()]) for group in all_groups: if group.id == group_id: return group @@ -1384,7 +1436,8 @@ class SecurityGroupBackend(object): source_groups = [] for source_group_name in source_group_names: - source_group = self.get_security_group_from_name(source_group_name, vpc_id) + source_group = self.get_security_group_from_name( + source_group_name, vpc_id) if source_group: source_groups.append(source_group) @@ -1394,7 +1447,8 @@ class SecurityGroupBackend(object): if source_group: source_groups.append(source_group) - security_rule = SecurityRule(ip_protocol, from_port, to_port, ip_ranges, source_groups) + security_rule = SecurityRule( + ip_protocol, from_port, to_port, ip_ranges, source_groups) group.add_ingress_rule(security_rule) def revoke_security_group_ingress(self, @@ -1411,7 +1465,8 @@ class SecurityGroupBackend(object): source_groups = [] for source_group_name in source_group_names: - source_group = self.get_security_group_from_name(source_group_name, vpc_id) + source_group = self.get_security_group_from_name( + source_group_name, vpc_id) if source_group: source_groups.append(source_group) @@ -1420,7 +1475,8 @@ class SecurityGroupBackend(object): if source_group: source_groups.append(source_group) - security_rule = SecurityRule(ip_protocol, from_port, to_port, ip_ranges, source_groups) + security_rule = SecurityRule( + ip_protocol, from_port, to_port, ip_ranges, source_groups) if security_rule in group.ingress_rules: group.ingress_rules.remove(security_rule) return security_rule @@ -1453,7 +1509,8 @@ class SecurityGroupBackend(object): source_groups = [] for source_group_name in source_group_names: - source_group = self.get_security_group_from_name(source_group_name, vpc_id) + source_group = self.get_security_group_from_name( + source_group_name, vpc_id) if source_group: source_groups.append(source_group) @@ -1463,7 +1520,8 @@ class SecurityGroupBackend(object): if source_group: source_groups.append(source_group) - security_rule = SecurityRule(ip_protocol, from_port, to_port, ip_ranges, source_groups) + security_rule = SecurityRule( + ip_protocol, from_port, to_port, ip_ranges, source_groups) group.add_egress_rule(security_rule) def revoke_security_group_egress(self, @@ -1480,7 +1538,8 @@ class SecurityGroupBackend(object): source_groups = [] for source_group_name in source_group_names: - source_group = self.get_security_group_from_name(source_group_name, vpc_id) + source_group = self.get_security_group_from_name( + source_group_name, vpc_id) if source_group: source_groups.append(source_group) @@ -1489,7 +1548,8 @@ class SecurityGroupBackend(object): if source_group: source_groups.append(source_group) - security_rule = SecurityRule(ip_protocol, from_port, to_port, ip_ranges, source_groups) + security_rule = SecurityRule( + ip_protocol, from_port, to_port, ip_ranges, source_groups) if security_rule in group.egress_rules: group.egress_rules.remove(security_rule) return security_rule @@ -1528,7 +1588,8 @@ class SecurityGroupIngress(object): from_port = properties.get("FromPort") source_security_group_id = properties.get("SourceSecurityGroupId") source_security_group_name = properties.get("SourceSecurityGroupName") - # source_security_owner_id = properties.get("SourceSecurityGroupOwnerId") # IGNORED AT THE MOMENT + # source_security_owner_id = + # properties.get("SourceSecurityGroupOwnerId") # IGNORED AT THE MOMENT to_port = properties.get("ToPort") assert group_id or group_name @@ -1549,9 +1610,11 @@ class SecurityGroupIngress(object): ip_ranges = [] if group_id: - security_group = ec2_backend.describe_security_groups(group_ids=[group_id])[0] + security_group = ec2_backend.describe_security_groups(group_ids=[group_id])[ + 0] else: - security_group = ec2_backend.describe_security_groups(groupnames=[group_name])[0] + security_group = ec2_backend.describe_security_groups( + groupnames=[group_name])[0] ec2_backend.authorize_security_group_ingress( group_name_or_id=security_group.id, @@ -1567,6 +1630,7 @@ class SecurityGroupIngress(object): class VolumeAttachment(object): + def __init__(self, volume, instance, device, status): self.volume = volume self.attach_time = utc_date_and_time() @@ -1591,6 +1655,7 @@ class VolumeAttachment(object): class Volume(TaggedEC2Resource): + def __init__(self, ec2_backend, volume_id, size, zone, snapshot_id=None, encrypted=False): self.id = volume_id self.size = size @@ -1657,12 +1722,14 @@ class Volume(TaggedEC2Resource): filter_value = super(Volume, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeVolumes".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeVolumes".format(filter_name)) return filter_value class Snapshot(TaggedEC2Resource): + def __init__(self, ec2_backend, snapshot_id, volume, description, encrypted=False): self.id = snapshot_id self.volume = volume @@ -1696,12 +1763,14 @@ class Snapshot(TaggedEC2Resource): filter_value = super(Snapshot, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeSnapshots".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeSnapshots".format(filter_name)) return filter_value class EBSBackend(object): + def __init__(self): self.volumes = {} self.attachments = {} @@ -1745,7 +1814,8 @@ class EBSBackend(object): if not volume or not instance: return False - volume.attachment = VolumeAttachment(volume, instance, device_path, 'attached') + volume.attachment = VolumeAttachment( + volume, instance, device_path, 'attached') # Modify instance to capture mount of block device. bdt = BlockDeviceType(volume_id=volume_id, status=volume.status, size=volume.size, attach_time=utc_date_and_time()) @@ -1767,7 +1837,8 @@ class EBSBackend(object): def create_snapshot(self, volume_id, description): snapshot_id = random_snapshot_id() volume = self.get_volume(volume_id) - snapshot = Snapshot(self, snapshot_id, volume, description, volume.encrypted) + snapshot = Snapshot(self, snapshot_id, volume, + description, volume.encrypted) self.snapshots[snapshot_id] = snapshot return snapshot @@ -1794,7 +1865,8 @@ class EBSBackend(object): def add_create_volume_permission(self, snapshot_id, user_id=None, group=None): if user_id: - self.raise_not_implemented_error("The UserId parameter for ModifySnapshotAttribute") + self.raise_not_implemented_error( + "The UserId parameter for ModifySnapshotAttribute") if group != 'all': raise InvalidAMIAttributeItemValueError("UserGroup", group) @@ -1804,7 +1876,8 @@ class EBSBackend(object): def remove_create_volume_permission(self, snapshot_id, user_id=None, group=None): if user_id: - self.raise_not_implemented_error("The UserId parameter for ModifySnapshotAttribute") + self.raise_not_implemented_error( + "The UserId parameter for ModifySnapshotAttribute") if group != 'all': raise InvalidAMIAttributeItemValueError("UserGroup", group) @@ -1814,6 +1887,7 @@ class EBSBackend(object): class VPC(TaggedEC2Resource): + def __init__(self, ec2_backend, vpc_id, cidr_block, is_default, instance_tenancy='default'): self.ec2_backend = ec2_backend self.id = vpc_id @@ -1862,19 +1936,22 @@ class VPC(TaggedEC2Resource): filter_value = super(VPC, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeVPCs".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeVPCs".format(filter_name)) return filter_value class VPCBackend(object): + def __init__(self): self.vpcs = {} super(VPCBackend, self).__init__() def create_vpc(self, cidr_block, instance_tenancy='default'): vpc_id = random_vpc_id() - vpc = VPC(self, vpc_id, cidr_block, len(self.vpcs) == 0, instance_tenancy) + vpc = VPC(self, vpc_id, cidr_block, len( + self.vpcs) == 0, instance_tenancy) self.vpcs[vpc_id] = vpc # AWS creates a default main route table and security group. @@ -1885,7 +1962,8 @@ class VPCBackend(object): default = self.get_security_group_from_name('default', vpc_id=vpc_id) if not default: - self.create_security_group('default', 'default VPC security group', vpc_id=vpc_id) + self.create_security_group( + 'default', 'default VPC security group', vpc_id=vpc_id) return vpc @@ -1945,6 +2023,7 @@ class VPCBackend(object): class VPCPeeringConnectionStatus(object): + def __init__(self, code='initiating-request', message=''): self.code = code self.message = message @@ -1967,6 +2046,7 @@ class VPCPeeringConnectionStatus(object): class VPCPeeringConnection(TaggedEC2Resource): + def __init__(self, vpc_pcx_id, vpc, peer_vpc): self.id = vpc_pcx_id self.vpc = vpc @@ -1991,6 +2071,7 @@ class VPCPeeringConnection(TaggedEC2Resource): class VPCPeeringConnectionBackend(object): + def __init__(self): self.vpc_pcxs = {} super(VPCPeeringConnectionBackend, self).__init__() @@ -2032,6 +2113,7 @@ class VPCPeeringConnectionBackend(object): class Subnet(TaggedEC2Resource): + def __init__(self, ec2_backend, subnet_id, vpc_id, cidr_block, availability_zone, default_for_az, map_public_ip_on_launch): self.ec2_backend = ec2_backend @@ -2101,18 +2183,21 @@ class Subnet(TaggedEC2Resource): filter_value = super(Subnet, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeSubnets".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeSubnets".format(filter_name)) return filter_value def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'AvailabilityZone': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"') raise UnformattedGetAttTemplateException() class SubnetBackend(object): + def __init__(self): # maps availability zone to dict of (subnet_id, subnet) self.subnets = defaultdict(dict) @@ -2126,7 +2211,7 @@ class SubnetBackend(object): def create_subnet(self, vpc_id, cidr_block, availability_zone): subnet_id = random_subnet_id() - vpc = self.get_vpc(vpc_id) # Validate VPC exists + self.get_vpc(vpc_id) # Validate VPC exists # if this is the first subnet for an availability zone, # consider it the default @@ -2166,6 +2251,7 @@ class SubnetBackend(object): class SubnetRouteTableAssociation(object): + def __init__(self, route_table_id, subnet_id): self.route_table_id = route_table_id self.subnet_id = subnet_id @@ -2186,17 +2272,21 @@ class SubnetRouteTableAssociation(object): class SubnetRouteTableAssociationBackend(object): + def __init__(self): self.subnet_associations = {} super(SubnetRouteTableAssociationBackend, self).__init__() def create_subnet_association(self, route_table_id, subnet_id): - subnet_association = SubnetRouteTableAssociation(route_table_id, subnet_id) - self.subnet_associations["{0}:{1}".format(route_table_id, subnet_id)] = subnet_association + subnet_association = SubnetRouteTableAssociation( + route_table_id, subnet_id) + self.subnet_associations["{0}:{1}".format( + route_table_id, subnet_id)] = subnet_association return subnet_association class RouteTable(TaggedEC2Resource): + def __init__(self, ec2_backend, route_table_id, vpc_id, main=False): self.ec2_backend = ec2_backend self.id = route_table_id @@ -2242,12 +2332,14 @@ class RouteTable(TaggedEC2Resource): filter_value = super(RouteTable, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeRouteTables".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeRouteTables".format(filter_name)) return filter_value class RouteTableBackend(object): + def __init__(self): self.route_tables = {} super(RouteTableBackend, self).__init__() @@ -2273,9 +2365,11 @@ class RouteTableBackend(object): route_tables = self.route_tables.values() if route_table_ids: - route_tables = [route_table for route_table in route_tables if route_table.id in route_table_ids] + route_tables = [ + route_table for route_table in route_tables if route_table.id in route_table_ids] if len(route_tables) != len(route_table_ids): - invalid_id = list(set(route_table_ids).difference(set([route_table.id for route_table in route_tables])))[0] + invalid_id = list(set(route_table_ids).difference( + set([route_table.id for route_table in route_tables])))[0] raise InvalidRouteTableIdError(invalid_id) return generic_filter(filters, route_tables) @@ -2292,7 +2386,8 @@ class RouteTableBackend(object): def associate_route_table(self, route_table_id, subnet_id): # Idempotent if association already exists. - route_tables_by_subnet = self.get_all_route_tables(filters={'association.subnet-id': [subnet_id]}) + route_tables_by_subnet = self.get_all_route_tables( + filters={'association.subnet-id': [subnet_id]}) if route_tables_by_subnet: for association_id, check_subnet_id in route_tables_by_subnet[0].associations.items(): if subnet_id == check_subnet_id: @@ -2318,7 +2413,8 @@ class RouteTableBackend(object): return association_id # Find route table which currently has the association, error if none. - route_tables_by_association_id = self.get_all_route_tables(filters={'association.route-table-association-id': [association_id]}) + route_tables_by_association_id = self.get_all_route_tables( + filters={'association.route-table-association-id': [association_id]}) if not route_tables_by_association_id: raise InvalidAssociationIdError(association_id) @@ -2329,6 +2425,7 @@ class RouteTableBackend(object): class Route(object): + def __init__(self, route_table, destination_cidr_block, local=False, gateway=None, instance=None, interface=None, vpc_pcx=None): self.id = generate_route_id(route_table.id, destination_cidr_block) @@ -2363,6 +2460,7 @@ class Route(object): class RouteBackend(object): + def __init__(self): super(RouteBackend, self).__init__() @@ -2372,7 +2470,8 @@ class RouteBackend(object): route_table = self.get_route_table(route_table_id) if interface_id: - self.raise_not_implemented_error("CreateRoute to NetworkInterfaceId") + self.raise_not_implemented_error( + "CreateRoute to NetworkInterfaceId") gateway = None if gateway_id: @@ -2383,21 +2482,23 @@ class RouteBackend(object): route = Route(route_table, destination_cidr_block, local=local, gateway=gateway, - instance=self.get_instance(instance_id) if instance_id else None, + instance=self.get_instance( + instance_id) if instance_id else None, interface=None, vpc_pcx=self.get_vpc_peering_connection(vpc_peering_connection_id) if vpc_peering_connection_id else None) route_table.routes[route.id] = route return route def replace_route(self, route_table_id, destination_cidr_block, - gateway_id=None, instance_id=None, interface_id=None, - vpc_peering_connection_id=None): + gateway_id=None, instance_id=None, interface_id=None, + vpc_peering_connection_id=None): route_table = self.get_route_table(route_table_id) route_id = generate_route_id(route_table.id, destination_cidr_block) route = route_table.routes[route_id] if interface_id: - self.raise_not_implemented_error("ReplaceRoute to NetworkInterfaceId") + self.raise_not_implemented_error( + "ReplaceRoute to NetworkInterfaceId") route.gateway = None if gateway_id: @@ -2406,9 +2507,11 @@ class RouteBackend(object): elif EC2_RESOURCE_TO_PREFIX['internet-gateway'] in gateway_id: route.gateway = self.get_internet_gateway(gateway_id) - route.instance = self.get_instance(instance_id) if instance_id else None + route.instance = self.get_instance( + instance_id) if instance_id else None route.interface = None - route.vpc_pcx = self.get_vpc_peering_connection(vpc_peering_connection_id) if vpc_peering_connection_id else None + route.vpc_pcx = self.get_vpc_peering_connection( + vpc_peering_connection_id) if vpc_peering_connection_id else None route_table.routes[route.id] = route return route @@ -2428,6 +2531,7 @@ class RouteBackend(object): class InternetGateway(TaggedEC2Resource): + def __init__(self, ec2_backend): self.ec2_backend = ec2_backend self.id = random_internet_gateway_id() @@ -2451,6 +2555,7 @@ class InternetGateway(TaggedEC2Resource): class InternetGatewayBackend(object): + def __init__(self): self.internet_gateways = {} super(InternetGatewayBackend, self).__init__() @@ -2505,6 +2610,7 @@ class InternetGatewayBackend(object): class VPCGatewayAttachment(object): + def __init__(self, gateway_id, vpc_id): self.gateway_id = gateway_id self.vpc_id = vpc_id @@ -2518,7 +2624,8 @@ class VPCGatewayAttachment(object): gateway_id=properties['InternetGatewayId'], vpc_id=properties['VpcId'], ) - ec2_backend.attach_internet_gateway(properties['InternetGatewayId'], properties['VpcId']) + ec2_backend.attach_internet_gateway( + properties['InternetGatewayId'], properties['VpcId']) return attachment @property @@ -2527,6 +2634,7 @@ class VPCGatewayAttachment(object): class VPCGatewayAttachmentBackend(object): + def __init__(self): self.gateway_attachments = {} super(VPCGatewayAttachmentBackend, self).__init__() @@ -2538,6 +2646,7 @@ class VPCGatewayAttachmentBackend(object): class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): + def __init__(self, ec2_backend, spot_request_id, price, image_id, type, valid_from, valid_until, launch_group, availability_zone_group, key_name, security_groups, user_data, instance_type, placement, @@ -2567,12 +2676,14 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): if security_groups: for group_name in security_groups: - group = self.ec2_backend.get_security_group_from_name(group_name) + group = self.ec2_backend.get_security_group_from_name( + group_name) if group: ls.groups.append(group) else: # If not security groups, add the default - default_group = self.ec2_backend.get_security_group_from_name("default") + default_group = self.ec2_backend.get_security_group_from_name( + "default") ls.groups.append(default_group) self.instance = self.launch_instance() @@ -2582,10 +2693,12 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): return self.state if filter_name == 'spot-instance-request-id': return self.id - filter_value = super(SpotInstanceRequest, self).get_filter_value(filter_name) + filter_value = super(SpotInstanceRequest, + self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeSpotInstanceRequests".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeSpotInstanceRequests".format(filter_name)) return filter_value @@ -2604,6 +2717,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): @six.add_metaclass(Model) class SpotRequestBackend(object): + def __init__(self): self.spot_instance_requests = {} super(SpotRequestBackend, self).__init__() @@ -2617,10 +2731,10 @@ class SpotRequestBackend(object): for _ in range(count): spot_request_id = random_spot_request_id() request = SpotInstanceRequest(self, - spot_request_id, price, image_id, type, valid_from, valid_until, - launch_group, availability_zone_group, key_name, security_groups, - user_data, instance_type, placement, kernel_id, ramdisk_id, - monitoring_enabled, subnet_id) + spot_request_id, price, image_id, type, valid_from, valid_until, + launch_group, availability_zone_group, key_name, security_groups, + user_data, instance_type, placement, kernel_id, ramdisk_id, + monitoring_enabled, subnet_id) self.spot_instance_requests[spot_request_id] = request requests.append(request) return requests @@ -2639,9 +2753,10 @@ class SpotRequestBackend(object): class SpotFleetLaunchSpec(object): + def __init__(self, ebs_optimized, group_set, iam_instance_profile, image_id, - instance_type, key_name, monitoring, spot_price, subnet_id, user_data, - weighted_capacity): + instance_type, key_name, monitoring, spot_price, subnet_id, user_data, + weighted_capacity): self.ebs_optimized = ebs_optimized self.group_set = group_set self.iam_instance_profile = iam_instance_profile @@ -2658,7 +2773,7 @@ class SpotFleetLaunchSpec(object): class SpotFleetRequest(TaggedEC2Resource): def __init__(self, ec2_backend, spot_fleet_request_id, spot_price, - target_capacity, iam_fleet_role, allocation_strategy, launch_specs): + target_capacity, iam_fleet_role, allocation_strategy, launch_specs): self.ec2_backend = ec2_backend self.id = spot_fleet_request_id @@ -2672,18 +2787,19 @@ class SpotFleetRequest(TaggedEC2Resource): self.launch_specs = [] for spec in launch_specs: self.launch_specs.append(SpotFleetLaunchSpec( - ebs_optimized=spec['ebs_optimized'], - group_set=[val for key, val in spec.items() if key.startswith("group_set")], - iam_instance_profile=spec.get('iam_instance_profile._arn'), - image_id=spec['image_id'], - instance_type=spec['instance_type'], - key_name=spec.get('key_name'), - monitoring=spec.get('monitoring._enabled'), - spot_price=spec.get('spot_price', self.spot_price), - subnet_id=spec['subnet_id'], - user_data=spec.get('user_data'), - weighted_capacity=spec['weighted_capacity'], - ) + ebs_optimized=spec['ebs_optimized'], + group_set=[val for key, val in spec.items( + ) if key.startswith("group_set")], + iam_instance_profile=spec.get('iam_instance_profile._arn'), + image_id=spec['image_id'], + instance_type=spec['instance_type'], + key_name=spec.get('key_name'), + monitoring=spec.get('monitoring._enabled'), + spot_price=spec.get('spot_price', self.spot_price), + subnet_id=spec['subnet_id'], + user_data=spec.get('user_data'), + weighted_capacity=spec['weighted_capacity'], + ) ) self.spot_requests = [] @@ -2695,7 +2811,8 @@ class SpotFleetRequest(TaggedEC2Resource): @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): - properties = cloudformation_json['Properties']['SpotFleetRequestConfigData'] + properties = cloudformation_json[ + 'Properties']['SpotFleetRequestConfigData'] ec2_backend = ec2_backends[region_name] spot_price = properties['SpotPrice'] @@ -2704,17 +2821,17 @@ class SpotFleetRequest(TaggedEC2Resource): allocation_strategy = properties['AllocationStrategy'] launch_specs = properties["LaunchSpecifications"] launch_specs = [ - dict([(camelcase_to_underscores(key), val) for key, val in launch_spec.items()]) + dict([(camelcase_to_underscores(key), val) + for key, val in launch_spec.items()]) for launch_spec in launch_specs ] spot_fleet_request = ec2_backend.request_spot_fleet(spot_price, - target_capacity, iam_fleet_role, allocation_strategy, launch_specs) + target_capacity, iam_fleet_role, allocation_strategy, launch_specs) return spot_fleet_request - def get_launch_spec_counts(self): weight_map = defaultdict(int) @@ -2722,39 +2839,42 @@ class SpotFleetRequest(TaggedEC2Resource): weight_so_far = 0 launch_spec_index = 0 while True: - launch_spec = self.launch_specs[launch_spec_index % len(self.launch_specs)] + launch_spec = self.launch_specs[ + launch_spec_index % len(self.launch_specs)] weight_map[launch_spec] += 1 weight_so_far += launch_spec.weighted_capacity if weight_so_far >= self.target_capacity: break launch_spec_index += 1 else: # lowestPrice - cheapest_spec = sorted(self.launch_specs, key=lambda spec: float(spec.spot_price))[0] + cheapest_spec = sorted( + self.launch_specs, key=lambda spec: float(spec.spot_price))[0] extra = 1 if self.target_capacity % cheapest_spec.weighted_capacity else 0 - weight_map[cheapest_spec] = int(self.target_capacity // cheapest_spec.weighted_capacity) + extra + weight_map[cheapest_spec] = int( + self.target_capacity // cheapest_spec.weighted_capacity) + extra return weight_map.items() def create_spot_requests(self): for launch_spec, count in self.get_launch_spec_counts(): requests = self.ec2_backend.request_spot_instances( - price=launch_spec.spot_price, - image_id=launch_spec.image_id, - count=count, - type="persistent", - valid_from=None, - valid_until=None, - launch_group=None, - availability_zone_group=None, - key_name=launch_spec.key_name, - security_groups=launch_spec.group_set, - user_data=launch_spec.user_data, - instance_type=launch_spec.instance_type, - placement=None, - kernel_id=None, - ramdisk_id=None, - monitoring_enabled=launch_spec.monitoring, - subnet_id=launch_spec.subnet_id, + price=launch_spec.spot_price, + image_id=launch_spec.image_id, + count=count, + type="persistent", + valid_from=None, + valid_until=None, + launch_group=None, + availability_zone_group=None, + key_name=launch_spec.key_name, + security_groups=launch_spec.group_set, + user_data=launch_spec.user_data, + instance_type=launch_spec.instance_type, + placement=None, + kernel_id=None, + ramdisk_id=None, + monitoring_enabled=launch_spec.monitoring, + subnet_id=launch_spec.subnet_id, ) self.spot_requests.extend(requests) return self.spot_requests @@ -2764,16 +2884,17 @@ class SpotFleetRequest(TaggedEC2Resource): class SpotFleetBackend(object): + def __init__(self): self.spot_fleet_requests = {} super(SpotFleetBackend, self).__init__() def request_spot_fleet(self, spot_price, target_capacity, iam_fleet_role, - allocation_strategy, launch_specs): + allocation_strategy, launch_specs): spot_fleet_request_id = random_spot_fleet_request_id() request = SpotFleetRequest(self, spot_fleet_request_id, spot_price, - target_capacity, iam_fleet_role, allocation_strategy, launch_specs) + target_capacity, iam_fleet_role, allocation_strategy, launch_specs) self.spot_fleet_requests[spot_fleet_request_id] = request return request @@ -2788,7 +2909,8 @@ class SpotFleetBackend(object): requests = self.spot_fleet_requests.values() if spot_fleet_request_ids: - requests = [request for request in requests if request.id in spot_fleet_request_ids] + requests = [ + request for request in requests if request.id in spot_fleet_request_ids] return requests @@ -2803,6 +2925,7 @@ class SpotFleetBackend(object): class ElasticAddress(object): + def __init__(self, domain): self.public_ip = random_ip() self.allocation_id = random_eip_allocation_id() if domain == "vpc" else None @@ -2894,8 +3017,10 @@ class ElasticAddressBackend(object): eips = self.address_by_allocation([allocation_id]) eip = eips[0] - new_instance_association = bool(instance and (not eip.instance or eip.instance.id == instance.id)) - new_eni_association = bool(eni and (not eip.eni or eni.id == eip.eni.id)) + new_instance_association = bool(instance and ( + not eip.instance or eip.instance.id == instance.id)) + new_eni_association = bool( + eni and (not eip.eni or eni.id == eip.eni.id)) if new_instance_association or new_eni_association or reassociate: eip.instance = instance @@ -2948,6 +3073,7 @@ class ElasticAddressBackend(object): class DHCPOptionsSet(TaggedEC2Resource): + def __init__(self, ec2_backend, domain_name_servers=None, domain_name=None, ntp_servers=None, netbios_name_servers=None, netbios_node_type=None): @@ -2983,10 +3109,12 @@ class DHCPOptionsSet(TaggedEC2Resource): values = [item for item in list(self._options.values()) if item] return itertools.chain(*values) - filter_value = super(DHCPOptionsSet, self).get_filter_value(filter_name) + filter_value = super( + DHCPOptionsSet, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeDhcpOptions".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeDhcpOptions".format(filter_name)) return filter_value @@ -2996,6 +3124,7 @@ class DHCPOptionsSet(TaggedEC2Resource): class DHCPOptionsSetBackend(object): + def __init__(self): self.dhcp_options_sets = {} super(DHCPOptionsSetBackend, self).__init__() @@ -3040,7 +3169,8 @@ class DHCPOptionsSetBackend(object): if options_id in self.dhcp_options_sets: if self.dhcp_options_sets[options_id].vpc: - raise DependencyViolationError("Cannot delete assigned DHCP options.") + raise DependencyViolationError( + "Cannot delete assigned DHCP options.") self.dhcp_options_sets.pop(options_id) else: raise InvalidDHCPOptionsIdError(options_id) @@ -3050,15 +3180,18 @@ class DHCPOptionsSetBackend(object): dhcp_options_sets = self.dhcp_options_sets.values() if dhcp_options_ids: - dhcp_options_sets = [dhcp_options_set for dhcp_options_set in dhcp_options_sets if dhcp_options_set.id in dhcp_options_ids] + dhcp_options_sets = [ + dhcp_options_set for dhcp_options_set in dhcp_options_sets if dhcp_options_set.id in dhcp_options_ids] if len(dhcp_options_sets) != len(dhcp_options_ids): - invalid_id = list(set(dhcp_options_ids).difference(set([dhcp_options_set.id for dhcp_options_set in dhcp_options_sets])))[0] + invalid_id = list(set(dhcp_options_ids).difference( + set([dhcp_options_set.id for dhcp_options_set in dhcp_options_sets])))[0] raise InvalidDHCPOptionsIdError(invalid_id) return generic_filter(filters, dhcp_options_sets) class VPNConnection(TaggedEC2Resource): + def __init__(self, ec2_backend, id, type, customer_gateway_id, vpn_gateway_id): self.ec2_backend = ec2_backend @@ -3074,6 +3207,7 @@ class VPNConnection(TaggedEC2Resource): class VPNConnectionBackend(object): + def __init__(self): self.vpn_connections = {} super(VPNConnectionBackend, self).__init__() @@ -3116,13 +3250,15 @@ class VPNConnectionBackend(object): vpn_connections = [vpn_connection for vpn_connection in vpn_connections if vpn_connection.id in vpn_connection_ids] if len(vpn_connections) != len(vpn_connection_ids): - invalid_id = list(set(vpn_connection_ids).difference(set([vpn_connection.id for vpn_connection in vpn_connections])))[0] + invalid_id = list(set(vpn_connection_ids).difference( + set([vpn_connection.id for vpn_connection in vpn_connections])))[0] raise InvalidVpnConnectionIdError(invalid_id) return generic_filter(filters, vpn_connections) class NetworkAclBackend(object): + def __init__(self): self.network_acls = {} super(NetworkAclBackend, self).__init__() @@ -3147,7 +3283,8 @@ class NetworkAclBackend(object): network_acls = [network_acl for network_acl in network_acls if network_acl.id in network_acl_ids] if len(network_acls) != len(network_acl_ids): - invalid_id = list(set(network_acl_ids).difference(set([network_acl.id for network_acl in network_acls])))[0] + invalid_id = list(set(network_acl_ids).difference( + set([network_acl.id for network_acl in network_acls])))[0] raise InvalidRouteTableIdError(invalid_id) return generic_filter(filters, network_acls) @@ -3177,7 +3314,7 @@ class NetworkAclBackend(object): # lookup existing association for subnet and delete it default_acl = next(value for key, value in self.network_acls.items() - if association_id in value.associations.keys()) + if association_id in value.associations.keys()) subnet_id = None for key, value in default_acl.associations.items(): @@ -3203,6 +3340,7 @@ class NetworkAclBackend(object): class NetworkAclAssociation(object): + def __init__(self, ec2_backend, new_association_id, subnet_id, network_acl_id): self.ec2_backend = ec2_backend @@ -3214,6 +3352,7 @@ class NetworkAclAssociation(object): class NetworkAcl(TaggedEC2Resource): + def __init__(self, ec2_backend, network_acl_id, vpc_id, default=False): self.ec2_backend = ec2_backend self.id = network_acl_id @@ -3235,12 +3374,14 @@ class NetworkAcl(TaggedEC2Resource): filter_value = super(NetworkAcl, self).get_filter_value(filter_name) if filter_value is None: - self.ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeNetworkAcls".format(filter_name)) + self.ec2_backend.raise_not_implemented_error( + "The filter '{0}' for DescribeNetworkAcls".format(filter_name)) return filter_value class NetworkAclEntry(TaggedEC2Resource): + def __init__(self, ec2_backend, network_acl_id, rule_number, protocol, rule_action, egress, cidr_block, icmp_code, icmp_type, port_range_from, @@ -3259,6 +3400,7 @@ class NetworkAclEntry(TaggedEC2Resource): class VpnGateway(TaggedEC2Resource): + def __init__(self, ec2_backend, id, type): self.ec2_backend = ec2_backend self.id = id @@ -3268,6 +3410,7 @@ class VpnGateway(TaggedEC2Resource): class VpnGatewayAttachment(object): + def __init__(self, vpc_id, state): self.vpc_id = vpc_id self.state = state @@ -3275,6 +3418,7 @@ class VpnGatewayAttachment(object): class VpnGatewayBackend(object): + def __init__(self): self.vpn_gateways = {} super(VpnGatewayBackend, self).__init__() @@ -3318,6 +3462,7 @@ class VpnGatewayBackend(object): class CustomerGateway(TaggedEC2Resource): + def __init__(self, ec2_backend, id, type, ip_address, bgp_asn): self.ec2_backend = ec2_backend self.id = id @@ -3329,13 +3474,15 @@ class CustomerGateway(TaggedEC2Resource): class CustomerGatewayBackend(object): + def __init__(self): self.customer_gateways = {} super(CustomerGatewayBackend, self).__init__() def create_customer_gateway(self, type='ipsec.1', ip_address=None, bgp_asn=None): customer_gateway_id = random_customer_gateway_id() - customer_gateway = CustomerGateway(self, customer_gateway_id, type, ip_address, bgp_asn) + customer_gateway = CustomerGateway( + self, customer_gateway_id, type, ip_address, bgp_asn) self.customer_gateways[customer_gateway_id] = customer_gateway return customer_gateway @@ -3344,7 +3491,8 @@ class CustomerGatewayBackend(object): return generic_filter(filters, customer_gateways) def get_customer_gateway(self, customer_gateway_id): - customer_gateway = self.customer_gateways.get(customer_gateway_id, None) + customer_gateway = self.customer_gateways.get( + customer_gateway_id, None) if not customer_gateway: raise InvalidCustomerGatewayIdError(customer_gateway_id) return customer_gateway @@ -3370,10 +3518,12 @@ class NatGateway(object): self._created_at = datetime.utcnow() self._backend = backend # NOTE: this is the core of NAT Gateways creation - self._eni = self._backend.create_network_interface(backend.get_subnet(self.subnet_id), self.private_ip) + self._eni = self._backend.create_network_interface( + backend.get_subnet(self.subnet_id), self.private_ip) # associate allocation with ENI - self._backend.associate_address(eni=self._eni, allocation_id=self.allocation_id) + self._backend.associate_address( + eni=self._eni, allocation_id=self.allocation_id) @property def vpc_id(self): @@ -3427,7 +3577,7 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, VPCPeeringConnectionBackend, RouteTableBackend, RouteBackend, InternetGatewayBackend, VPCGatewayAttachmentBackend, SpotFleetBackend, - SpotRequestBackend,ElasticAddressBackend, KeyPairBackend, + SpotRequestBackend, ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend, NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend, NatGatewayBackend): @@ -3463,7 +3613,8 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, self.__dict__ = {} self.__init__(region_name) - # Use this to generate a proper error template response when in a response handler. + # Use this to generate a proper error template response when in a response + # handler. def raise_error(self, code, message): raise EC2ClientError(code, message) @@ -3485,11 +3636,13 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, elif resource_prefix == EC2_RESOURCE_TO_PREFIX['instance']: self.get_instance_by_id(instance_id=resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['internet-gateway']: - self.describe_internet_gateways(internet_gateway_ids=[resource_id]) + self.describe_internet_gateways( + internet_gateway_ids=[resource_id]) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['network-acl']: self.get_all_network_acls() elif resource_prefix == EC2_RESOURCE_TO_PREFIX['network-interface']: - self.describe_network_interfaces(filters={'network-interface-id': resource_id}) + self.describe_network_interfaces( + filters={'network-interface-id': resource_id}) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['reserved-instance']: self.raise_not_implemented_error('DescribeReservedInstances') elif resource_prefix == EC2_RESOURCE_TO_PREFIX['route-table']: @@ -3499,7 +3652,8 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, elif resource_prefix == EC2_RESOURCE_TO_PREFIX['snapshot']: self.get_snapshot(snapshot_id=resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['spot-instance-request']: - self.describe_spot_instance_requests(filters={'spot-instance-request-id': resource_id}) + self.describe_spot_instance_requests( + filters={'spot-instance-request-id': resource_id}) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['subnet']: self.get_subnet(subnet_id=resource_id) elif resource_prefix == EC2_RESOURCE_TO_PREFIX['volume']: @@ -3514,6 +3668,7 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, self.get_vpn_gateway(vpn_gateway_id=resource_id) return True + ec2_backends = {} for region in RegionsAndZonesBackend.regions: ec2_backends[region.name] = EC2Backend(region.name) diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py index 2049998ad..449d25a45 100644 --- a/moto/ec2/responses/__init__.py +++ b/moto/ec2/responses/__init__.py @@ -66,6 +66,7 @@ class EC2Response( Windows, NatGateways, ): + @property def ec2_backend(self): from moto.ec2.models import ec2_backends diff --git a/moto/ec2/responses/amazon_dev_pay.py b/moto/ec2/responses/amazon_dev_pay.py index af10a8d68..14df3f004 100644 --- a/moto/ec2/responses/amazon_dev_pay.py +++ b/moto/ec2/responses/amazon_dev_pay.py @@ -3,5 +3,7 @@ from moto.core.responses import BaseResponse class AmazonDevPay(BaseResponse): + def confirm_product_instance(self): - raise NotImplementedError('AmazonDevPay.confirm_product_instance is not yet implemented') + raise NotImplementedError( + 'AmazonDevPay.confirm_product_instance is not yet implemented') diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py index b60452a3f..42bfba209 100755 --- a/moto/ec2/responses/amis.py +++ b/moto/ec2/responses/amis.py @@ -5,6 +5,7 @@ from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_queryst class AmisResponse(BaseResponse): + def create_image(self): name = self.querystring.get('Name')[0] if "Description" in self.querystring: @@ -14,17 +15,21 @@ class AmisResponse(BaseResponse): instance_ids = instance_ids_from_querystring(self.querystring) instance_id = instance_ids[0] if self.is_not_dryrun('CreateImage'): - image = self.ec2_backend.create_image(instance_id, name, description) + image = self.ec2_backend.create_image( + instance_id, name, description) template = self.response_template(CREATE_IMAGE_RESPONSE) return template.render(image=image) def copy_image(self): source_image_id = self.querystring.get('SourceImageId')[0] source_region = self.querystring.get('SourceRegion')[0] - name = self.querystring.get('Name')[0] if self.querystring.get('Name') else None - description = self.querystring.get('Description')[0] if self.querystring.get('Description') else None + name = self.querystring.get( + 'Name')[0] if self.querystring.get('Name') else None + description = self.querystring.get( + 'Description')[0] if self.querystring.get('Description') else None if self.is_not_dryrun('CopyImage'): - image = self.ec2_backend.copy_image(source_image_id, source_region, name, description) + image = self.ec2_backend.copy_image( + source_image_id, source_region, name, description) template = self.response_template(COPY_IMAGE_RESPONSE) return template.render(image=image) @@ -38,7 +43,8 @@ class AmisResponse(BaseResponse): def describe_images(self): ami_ids = image_ids_from_querystring(self.querystring) filters = filters_from_querystring(self.querystring) - images = self.ec2_backend.describe_images(ami_ids=ami_ids, filters=filters) + images = self.ec2_backend.describe_images( + ami_ids=ami_ids, filters=filters) template = self.response_template(DESCRIBE_IMAGES_RESPONSE) return template.render(images=images) @@ -56,18 +62,22 @@ class AmisResponse(BaseResponse): user_ids = sequence_from_querystring('UserId', self.querystring) if self.is_not_dryrun('ModifyImageAttribute'): if (operation_type == 'add'): - self.ec2_backend.add_launch_permission(ami_id, user_ids=user_ids, group=group) + self.ec2_backend.add_launch_permission( + ami_id, user_ids=user_ids, group=group) elif (operation_type == 'remove'): - self.ec2_backend.remove_launch_permission(ami_id, user_ids=user_ids, group=group) + self.ec2_backend.remove_launch_permission( + ami_id, user_ids=user_ids, group=group) return MODIFY_IMAGE_ATTRIBUTE_RESPONSE def register_image(self): if self.is_not_dryrun('RegisterImage'): - raise NotImplementedError('AMIs.register_image is not yet implemented') + raise NotImplementedError( + 'AMIs.register_image is not yet implemented') def reset_image_attribute(self): if self.is_not_dryrun('ResetImageAttribute'): - raise NotImplementedError('AMIs.reset_image_attribute is not yet implemented') + raise NotImplementedError( + 'AMIs.reset_image_attribute is not yet implemented') CREATE_IMAGE_RESPONSE = """ @@ -80,7 +90,8 @@ COPY_IMAGE_RESPONSE = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE diff --git a/moto/ec2/responses/availability_zones_and_regions.py b/moto/ec2/responses/availability_zones_and_regions.py index 50869e934..3d0a5ab05 100644 --- a/moto/ec2/responses/availability_zones_and_regions.py +++ b/moto/ec2/responses/availability_zones_and_regions.py @@ -3,6 +3,7 @@ from moto.core.responses import BaseResponse class AvailabilityZonesAndRegions(BaseResponse): + def describe_availability_zones(self): zones = self.ec2_backend.describe_availability_zones() template = self.response_template(DESCRIBE_ZONES_RESPONSE) @@ -13,6 +14,7 @@ class AvailabilityZonesAndRegions(BaseResponse): template = self.response_template(DESCRIBE_REGIONS_RESPONSE) return template.render(regions=regions) + DESCRIBE_REGIONS_RESPONSE = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE diff --git a/moto/ec2/responses/customer_gateways.py b/moto/ec2/responses/customer_gateways.py index 85f50fbcd..6da2ed2f8 100644 --- a/moto/ec2/responses/customer_gateways.py +++ b/moto/ec2/responses/customer_gateways.py @@ -10,13 +10,15 @@ class CustomerGateways(BaseResponse): type = self.querystring.get('Type', None)[0] ip_address = self.querystring.get('IpAddress', None)[0] bgp_asn = self.querystring.get('BgpAsn', None)[0] - customer_gateway = self.ec2_backend.create_customer_gateway(type, ip_address=ip_address, bgp_asn=bgp_asn) + customer_gateway = self.ec2_backend.create_customer_gateway( + type, ip_address=ip_address, bgp_asn=bgp_asn) template = self.response_template(CREATE_CUSTOMER_GATEWAY_RESPONSE) return template.render(customer_gateway=customer_gateway) def delete_customer_gateway(self): customer_gateway_id = self.querystring.get('CustomerGatewayId')[0] - delete_status = self.ec2_backend.delete_customer_gateway(customer_gateway_id) + delete_status = self.ec2_backend.delete_customer_gateway( + customer_gateway_id) template = self.response_template(DELETE_CUSTOMER_GATEWAY_RESPONSE) return template.render(customer_gateway=delete_status) diff --git a/moto/ec2/responses/dhcp_options.py b/moto/ec2/responses/dhcp_options.py index b9d1469b5..450ef1bf9 100644 --- a/moto/ec2/responses/dhcp_options.py +++ b/moto/ec2/responses/dhcp_options.py @@ -7,6 +7,7 @@ from moto.ec2.utils import ( class DHCPOptions(BaseResponse): + def associate_dhcp_options(self): dhcp_opt_id = self.querystring.get("DhcpOptionsId", [None])[0] vpc_id = self.querystring.get("VpcId", [None])[0] @@ -48,9 +49,11 @@ class DHCPOptions(BaseResponse): return template.render(delete_status=delete_status) def describe_dhcp_options(self): - dhcp_opt_ids = sequence_from_querystring("DhcpOptionsId", self.querystring) + dhcp_opt_ids = sequence_from_querystring( + "DhcpOptionsId", self.querystring) filters = filters_from_querystring(self.querystring) - dhcp_opts = self.ec2_backend.get_all_dhcp_options(dhcp_opt_ids, filters) + dhcp_opts = self.ec2_backend.get_all_dhcp_options( + dhcp_opt_ids, filters) template = self.response_template(DESCRIBE_DHCP_OPTIONS_RESPONSE) return template.render(dhcp_options=dhcp_opts) diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index ddbf30e68..0773ffbe2 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -10,13 +10,15 @@ class ElasticBlockStore(BaseResponse): instance_id = self.querystring.get('InstanceId')[0] device_path = self.querystring.get('Device')[0] if self.is_not_dryrun('AttachVolume'): - attachment = self.ec2_backend.attach_volume(volume_id, instance_id, device_path) + attachment = self.ec2_backend.attach_volume( + volume_id, instance_id, device_path) template = self.response_template(ATTACHED_VOLUME_RESPONSE) return template.render(attachment=attachment) def copy_snapshot(self): if self.is_not_dryrun('CopySnapshot'): - raise NotImplementedError('ElasticBlockStore.copy_snapshot is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.copy_snapshot is not yet implemented') def create_snapshot(self): description = self.querystring.get('Description', [None])[0] @@ -32,7 +34,8 @@ class ElasticBlockStore(BaseResponse): snapshot_id = self.querystring.get('SnapshotId', [None])[0] encrypted = self.querystring.get('Encrypted', ['false'])[0] if self.is_not_dryrun('CreateVolume'): - volume = self.ec2_backend.create_volume(size, zone, snapshot_id, encrypted) + volume = self.ec2_backend.create_volume( + size, zone, snapshot_id, encrypted) template = self.response_template(CREATE_VOLUME_RESPONSE) return template.render(volume=volume) @@ -50,51 +53,64 @@ class ElasticBlockStore(BaseResponse): def describe_snapshots(self): filters = filters_from_querystring(self.querystring) - # querystring for multiple snapshotids results in SnapshotId.1, SnapshotId.2 etc - snapshot_ids = ','.join([','.join(s[1]) for s in self.querystring.items() if 'SnapshotId' in s[0]]) + # querystring for multiple snapshotids results in SnapshotId.1, + # SnapshotId.2 etc + snapshot_ids = ','.join( + [','.join(s[1]) for s in self.querystring.items() if 'SnapshotId' in s[0]]) snapshots = self.ec2_backend.describe_snapshots(filters=filters) # Describe snapshots to handle filter on snapshot_ids - snapshots = [s for s in snapshots if s.id in snapshot_ids] if snapshot_ids else snapshots + snapshots = [ + s for s in snapshots if s.id in snapshot_ids] if snapshot_ids else snapshots template = self.response_template(DESCRIBE_SNAPSHOTS_RESPONSE) return template.render(snapshots=snapshots) def describe_volumes(self): filters = filters_from_querystring(self.querystring) - # querystring for multiple volumeids results in VolumeId.1, VolumeId.2 etc - volume_ids = ','.join([','.join(v[1]) for v in self.querystring.items() if 'VolumeId' in v[0]]) + # querystring for multiple volumeids results in VolumeId.1, VolumeId.2 + # etc + volume_ids = ','.join( + [','.join(v[1]) for v in self.querystring.items() if 'VolumeId' in v[0]]) volumes = self.ec2_backend.describe_volumes(filters=filters) # Describe volumes to handle filter on volume_ids - volumes = [v for v in volumes if v.id in volume_ids] if volume_ids else volumes + volumes = [ + v for v in volumes if v.id in volume_ids] if volume_ids else volumes template = self.response_template(DESCRIBE_VOLUMES_RESPONSE) return template.render(volumes=volumes) def describe_volume_attribute(self): - raise NotImplementedError('ElasticBlockStore.describe_volume_attribute is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.describe_volume_attribute is not yet implemented') def describe_volume_status(self): - raise NotImplementedError('ElasticBlockStore.describe_volume_status is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.describe_volume_status is not yet implemented') def detach_volume(self): volume_id = self.querystring.get('VolumeId')[0] instance_id = self.querystring.get('InstanceId')[0] device_path = self.querystring.get('Device')[0] if self.is_not_dryrun('DetachVolume'): - attachment = self.ec2_backend.detach_volume(volume_id, instance_id, device_path) + attachment = self.ec2_backend.detach_volume( + volume_id, instance_id, device_path) template = self.response_template(DETATCH_VOLUME_RESPONSE) return template.render(attachment=attachment) def enable_volume_io(self): if self.is_not_dryrun('EnableVolumeIO'): - raise NotImplementedError('ElasticBlockStore.enable_volume_io is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.enable_volume_io is not yet implemented') def import_volume(self): if self.is_not_dryrun('ImportVolume'): - raise NotImplementedError('ElasticBlockStore.import_volume is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.import_volume is not yet implemented') def describe_snapshot_attribute(self): snapshot_id = self.querystring.get('SnapshotId')[0] - groups = self.ec2_backend.get_create_volume_permission_groups(snapshot_id) - template = self.response_template(DESCRIBE_SNAPSHOT_ATTRIBUTES_RESPONSE) + groups = self.ec2_backend.get_create_volume_permission_groups( + snapshot_id) + template = self.response_template( + DESCRIBE_SNAPSHOT_ATTRIBUTES_RESPONSE) return template.render(snapshot_id=snapshot_id, groups=groups) def modify_snapshot_attribute(self): @@ -104,18 +120,22 @@ class ElasticBlockStore(BaseResponse): user_id = self.querystring.get('UserId.1', [None])[0] if self.is_not_dryrun('ModifySnapshotAttribute'): if (operation_type == 'add'): - self.ec2_backend.add_create_volume_permission(snapshot_id, user_id=user_id, group=group) + self.ec2_backend.add_create_volume_permission( + snapshot_id, user_id=user_id, group=group) elif (operation_type == 'remove'): - self.ec2_backend.remove_create_volume_permission(snapshot_id, user_id=user_id, group=group) + self.ec2_backend.remove_create_volume_permission( + snapshot_id, user_id=user_id, group=group) return MODIFY_SNAPSHOT_ATTRIBUTE_RESPONSE def modify_volume_attribute(self): if self.is_not_dryrun('ModifyVolumeAttribute'): - raise NotImplementedError('ElasticBlockStore.modify_volume_attribute is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.modify_volume_attribute is not yet implemented') def reset_snapshot_attribute(self): if self.is_not_dryrun('ResetSnapshotAttribute'): - raise NotImplementedError('ElasticBlockStore.reset_snapshot_attribute is not yet implemented') + raise NotImplementedError( + 'ElasticBlockStore.reset_snapshot_attribute is not yet implemented') CREATE_VOLUME_RESPONSE = """ @@ -272,4 +292,4 @@ MODIFY_SNAPSHOT_ATTRIBUTE_RESPONSE = """ 666d2944-9276-4d6a-be12-1f4ada972fd8 true -""" \ No newline at end of file +""" diff --git a/moto/ec2/responses/elastic_ip_addresses.py b/moto/ec2/responses/elastic_ip_addresses.py index 3ae75671f..a64a33bb5 100644 --- a/moto/ec2/responses/elastic_ip_addresses.py +++ b/moto/ec2/responses/elastic_ip_addresses.py @@ -4,6 +4,7 @@ from moto.ec2.utils import sequence_from_querystring class ElasticIPAddresses(BaseResponse): + def allocate_address(self): if "Domain" in self.querystring: domain = self.querystring.get('Domain')[0] @@ -18,11 +19,14 @@ class ElasticIPAddresses(BaseResponse): instance = eni = None if "InstanceId" in self.querystring: - instance = self.ec2_backend.get_instance(self.querystring['InstanceId'][0]) + instance = self.ec2_backend.get_instance( + self.querystring['InstanceId'][0]) elif "NetworkInterfaceId" in self.querystring: - eni = self.ec2_backend.get_network_interface(self.querystring['NetworkInterfaceId'][0]) + eni = self.ec2_backend.get_network_interface( + self.querystring['NetworkInterfaceId'][0]) else: - self.ec2_backend.raise_error("MissingParameter", "Invalid request, expect InstanceId/NetworkId parameter.") + self.ec2_backend.raise_error( + "MissingParameter", "Invalid request, expect InstanceId/NetworkId parameter.") reassociate = False if "AllowReassociation" in self.querystring: @@ -31,13 +35,17 @@ class ElasticIPAddresses(BaseResponse): if self.is_not_dryrun('AssociateAddress'): if instance or eni: if "PublicIp" in self.querystring: - eip = self.ec2_backend.associate_address(instance=instance, eni=eni, address=self.querystring['PublicIp'][0], reassociate=reassociate) + eip = self.ec2_backend.associate_address(instance=instance, eni=eni, address=self.querystring[ + 'PublicIp'][0], reassociate=reassociate) elif "AllocationId" in self.querystring: - eip = self.ec2_backend.associate_address(instance=instance, eni=eni, allocation_id=self.querystring['AllocationId'][0], reassociate=reassociate) + eip = self.ec2_backend.associate_address(instance=instance, eni=eni, allocation_id=self.querystring[ + 'AllocationId'][0], reassociate=reassociate) else: - self.ec2_backend.raise_error("MissingParameter", "Invalid request, expect PublicIp/AllocationId parameter.") + self.ec2_backend.raise_error( + "MissingParameter", "Invalid request, expect PublicIp/AllocationId parameter.") else: - self.ec2_backend.raise_error("MissingParameter", "Invalid request, expect either instance or ENI.") + self.ec2_backend.raise_error( + "MissingParameter", "Invalid request, expect either instance or ENI.") template = self.response_template(ASSOCIATE_ADDRESS_RESPONSE) return template.render(address=eip) @@ -46,17 +54,23 @@ class ElasticIPAddresses(BaseResponse): template = self.response_template(DESCRIBE_ADDRESS_RESPONSE) if "Filter.1.Name" in self.querystring: - filter_by = sequence_from_querystring("Filter.1.Name", self.querystring)[0] - filter_value = sequence_from_querystring("Filter.1.Value", self.querystring) + filter_by = sequence_from_querystring( + "Filter.1.Name", self.querystring)[0] + filter_value = sequence_from_querystring( + "Filter.1.Value", self.querystring) if filter_by == 'instance-id': - addresses = filter(lambda x: x.instance.id == filter_value[0], self.ec2_backend.describe_addresses()) + addresses = filter(lambda x: x.instance.id == filter_value[ + 0], self.ec2_backend.describe_addresses()) else: - raise NotImplementedError("Filtering not supported in describe_address.") + raise NotImplementedError( + "Filtering not supported in describe_address.") elif "PublicIp.1" in self.querystring: - public_ips = sequence_from_querystring("PublicIp", self.querystring) + public_ips = sequence_from_querystring( + "PublicIp", self.querystring) addresses = self.ec2_backend.address_by_ip(public_ips) elif "AllocationId.1" in self.querystring: - allocation_ids = sequence_from_querystring("AllocationId", self.querystring) + allocation_ids = sequence_from_querystring( + "AllocationId", self.querystring) addresses = self.ec2_backend.address_by_allocation(allocation_ids) else: addresses = self.ec2_backend.describe_addresses() @@ -65,22 +79,28 @@ class ElasticIPAddresses(BaseResponse): def disassociate_address(self): if self.is_not_dryrun('DisAssociateAddress'): if "PublicIp" in self.querystring: - self.ec2_backend.disassociate_address(address=self.querystring['PublicIp'][0]) + self.ec2_backend.disassociate_address( + address=self.querystring['PublicIp'][0]) elif "AssociationId" in self.querystring: - self.ec2_backend.disassociate_address(association_id=self.querystring['AssociationId'][0]) + self.ec2_backend.disassociate_address( + association_id=self.querystring['AssociationId'][0]) else: - self.ec2_backend.raise_error("MissingParameter", "Invalid request, expect PublicIp/AssociationId parameter.") + self.ec2_backend.raise_error( + "MissingParameter", "Invalid request, expect PublicIp/AssociationId parameter.") return self.response_template(DISASSOCIATE_ADDRESS_RESPONSE).render() def release_address(self): if self.is_not_dryrun('ReleaseAddress'): if "PublicIp" in self.querystring: - self.ec2_backend.release_address(address=self.querystring['PublicIp'][0]) + self.ec2_backend.release_address( + address=self.querystring['PublicIp'][0]) elif "AllocationId" in self.querystring: - self.ec2_backend.release_address(allocation_id=self.querystring['AllocationId'][0]) + self.ec2_backend.release_address( + allocation_id=self.querystring['AllocationId'][0]) else: - self.ec2_backend.raise_error("MissingParameter", "Invalid request, expect PublicIp/AllocationId parameter.") + self.ec2_backend.raise_error( + "MissingParameter", "Invalid request, expect PublicIp/AllocationId parameter.") return self.response_template(RELEASE_ADDRESS_RESPONSE).render() diff --git a/moto/ec2/responses/elastic_network_interfaces.py b/moto/ec2/responses/elastic_network_interfaces.py index c1c7383cb..cbe76e306 100644 --- a/moto/ec2/responses/elastic_network_interfaces.py +++ b/moto/ec2/responses/elastic_network_interfaces.py @@ -4,28 +4,35 @@ from moto.ec2.utils import sequence_from_querystring, filters_from_querystring class ElasticNetworkInterfaces(BaseResponse): + def create_network_interface(self): subnet_id = self.querystring.get('SubnetId')[0] - private_ip_address = self.querystring.get('PrivateIpAddress', [None])[0] + private_ip_address = self.querystring.get( + 'PrivateIpAddress', [None])[0] groups = sequence_from_querystring('SecurityGroupId', self.querystring) subnet = self.ec2_backend.get_subnet(subnet_id) if self.is_not_dryrun('CreateNetworkInterface'): - eni = self.ec2_backend.create_network_interface(subnet, private_ip_address, groups) - template = self.response_template(CREATE_NETWORK_INTERFACE_RESPONSE) + eni = self.ec2_backend.create_network_interface( + subnet, private_ip_address, groups) + template = self.response_template( + CREATE_NETWORK_INTERFACE_RESPONSE) return template.render(eni=eni) def delete_network_interface(self): eni_id = self.querystring.get('NetworkInterfaceId')[0] if self.is_not_dryrun('DeleteNetworkInterface'): self.ec2_backend.delete_network_interface(eni_id) - template = self.response_template(DELETE_NETWORK_INTERFACE_RESPONSE) + template = self.response_template( + DELETE_NETWORK_INTERFACE_RESPONSE) return template.render() def describe_network_interface_attribute(self): - raise NotImplementedError('ElasticNetworkInterfaces(AmazonVPC).describe_network_interface_attribute is not yet implemented') + raise NotImplementedError( + 'ElasticNetworkInterfaces(AmazonVPC).describe_network_interface_attribute is not yet implemented') def describe_network_interfaces(self): - eni_ids = sequence_from_querystring('NetworkInterfaceId', self.querystring) + eni_ids = sequence_from_querystring( + 'NetworkInterfaceId', self.querystring) filters = filters_from_querystring(self.querystring) enis = self.ec2_backend.get_all_network_interfaces(eni_ids, filters) template = self.response_template(DESCRIBE_NETWORK_INTERFACES_RESPONSE) @@ -36,15 +43,18 @@ class ElasticNetworkInterfaces(BaseResponse): instance_id = self.querystring.get('InstanceId')[0] device_index = self.querystring.get('DeviceIndex')[0] if self.is_not_dryrun('AttachNetworkInterface'): - attachment_id = self.ec2_backend.attach_network_interface(eni_id, instance_id, device_index) - template = self.response_template(ATTACH_NETWORK_INTERFACE_RESPONSE) + attachment_id = self.ec2_backend.attach_network_interface( + eni_id, instance_id, device_index) + template = self.response_template( + ATTACH_NETWORK_INTERFACE_RESPONSE) return template.render(attachment_id=attachment_id) def detach_network_interface(self): attachment_id = self.querystring.get('AttachmentId')[0] if self.is_not_dryrun('DetachNetworkInterface'): self.ec2_backend.detach_network_interface(attachment_id) - template = self.response_template(DETACH_NETWORK_INTERFACE_RESPONSE) + template = self.response_template( + DETACH_NETWORK_INTERFACE_RESPONSE) return template.render() def modify_network_interface_attribute(self): @@ -52,12 +62,15 @@ class ElasticNetworkInterfaces(BaseResponse): eni_id = self.querystring.get('NetworkInterfaceId')[0] group_id = self.querystring.get('SecurityGroupId.1')[0] if self.is_not_dryrun('ModifyNetworkInterface'): - self.ec2_backend.modify_network_interface_attribute(eni_id, group_id) + self.ec2_backend.modify_network_interface_attribute( + eni_id, group_id) return MODIFY_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE def reset_network_interface_attribute(self): if self.is_not_dryrun('ResetNetworkInterface'): - raise NotImplementedError('ElasticNetworkInterfaces(AmazonVPC).reset_network_interface_attribute is not yet implemented') + raise NotImplementedError( + 'ElasticNetworkInterfaces(AmazonVPC).reset_network_interface_attribute is not yet implemented') + CREATE_NETWORK_INTERFACE_RESPONSE = """ diff --git a/moto/ec2/responses/general.py b/moto/ec2/responses/general.py index 9fce05ccf..bd95c1975 100644 --- a/moto/ec2/responses/general.py +++ b/moto/ec2/responses/general.py @@ -4,6 +4,7 @@ from moto.ec2.utils import instance_ids_from_querystring class General(BaseResponse): + def get_console_output(self): self.instance_ids = instance_ids_from_querystring(self.querystring) instance_id = self.instance_ids[0] diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index 3c5a087d9..4da7b880f 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -5,14 +5,18 @@ from moto.core.utils import camelcase_to_underscores from moto.ec2.utils import instance_ids_from_querystring, filters_from_querystring, \ dict_from_querystring, optional_from_querystring + class InstanceResponse(BaseResponse): + def describe_instances(self): filter_dict = filters_from_querystring(self.querystring) instance_ids = instance_ids_from_querystring(self.querystring) if instance_ids: - reservations = self.ec2_backend.get_reservations_by_instance_ids(instance_ids, filters=filter_dict) + reservations = self.ec2_backend.get_reservations_by_instance_ids( + instance_ids, filters=filter_dict) else: - reservations = self.ec2_backend.all_reservations(make_copy=True, filters=filter_dict) + reservations = self.ec2_backend.all_reservations( + make_copy=True, filters=filter_dict) template = self.response_template(EC2_DESCRIBE_INSTANCES) return template.render(reservations=reservations) @@ -25,10 +29,12 @@ class InstanceResponse(BaseResponse): security_group_ids = self._get_multi_param('SecurityGroupId') nics = dict_from_querystring("NetworkInterface", self.querystring) instance_type = self.querystring.get("InstanceType", ["m1.small"])[0] - placement = self.querystring.get("Placement.AvailabilityZone", [None])[0] + placement = self.querystring.get( + "Placement.AvailabilityZone", [None])[0] subnet_id = self.querystring.get("SubnetId", [None])[0] private_ip = self.querystring.get("PrivateIpAddress", [None])[0] - associate_public_ip = self.querystring.get("AssociatePublicIpAddress", [None])[0] + associate_public_ip = self.querystring.get( + "AssociatePublicIpAddress", [None])[0] key_name = self.querystring.get("KeyName", [None])[0] if self.is_not_dryrun('RunInstance'): @@ -72,10 +78,11 @@ class InstanceResponse(BaseResponse): def describe_instance_status(self): instance_ids = instance_ids_from_querystring(self.querystring) include_all_instances = optional_from_querystring('IncludeAllInstances', - self.querystring) == 'true' + self.querystring) == 'true' if instance_ids: - instances = self.ec2_backend.get_multi_instances_by_id(instance_ids) + instances = self.ec2_backend.get_multi_instances_by_id( + instance_ids) elif include_all_instances: instances = self.ec2_backend.all_instances() else: @@ -85,7 +92,8 @@ class InstanceResponse(BaseResponse): return template.render(instances=instances) def describe_instance_types(self): - instance_types = [InstanceType(name='t1.micro', cores=1, memory=644874240, disk=0)] + instance_types = [InstanceType( + name='t1.micro', cores=1, memory=644874240, disk=0)] template = self.response_template(EC2_DESCRIBE_INSTANCE_TYPES) return template.render(instance_types=instance_types) @@ -96,10 +104,12 @@ class InstanceResponse(BaseResponse): key = camelcase_to_underscores(attribute) instance_ids = instance_ids_from_querystring(self.querystring) instance_id = instance_ids[0] - instance, value = self.ec2_backend.describe_instance_attribute(instance_id, key) + instance, value = self.ec2_backend.describe_instance_attribute( + instance_id, key) if key == "group_set": - template = self.response_template(EC2_DESCRIBE_INSTANCE_GROUPSET_ATTRIBUTE) + template = self.response_template( + EC2_DESCRIBE_INSTANCE_GROUPSET_ATTRIBUTE) else: template = self.response_template(EC2_DESCRIBE_INSTANCE_ATTRIBUTE) @@ -152,7 +162,8 @@ class InstanceResponse(BaseResponse): instance = self.ec2_backend.get_instance(instance_id) if self.is_not_dryrun('ModifyInstanceAttribute'): - block_device_type = instance.block_device_mapping[device_name_value] + block_device_type = instance.block_device_mapping[ + device_name_value] block_device_type.delete_on_termination = del_on_term_value # +1 for the next device @@ -171,24 +182,27 @@ class InstanceResponse(BaseResponse): if not attribute_key: return - if self.is_not_dryrun('Modify'+attribute_key.split(".")[0]): + if self.is_not_dryrun('Modify' + attribute_key.split(".")[0]): value = self.querystring.get(attribute_key)[0] - normalized_attribute = camelcase_to_underscores(attribute_key.split(".")[0]) + normalized_attribute = camelcase_to_underscores( + attribute_key.split(".")[0]) instance_ids = instance_ids_from_querystring(self.querystring) instance_id = instance_ids[0] - self.ec2_backend.modify_instance_attribute(instance_id, normalized_attribute, value) + self.ec2_backend.modify_instance_attribute( + instance_id, normalized_attribute, value) return EC2_MODIFY_INSTANCE_ATTRIBUTE def _security_grp_instance_attribute_handler(self): new_security_grp_list = [] for key, value in self.querystring.items(): - if 'GroupId.' in key: + if 'GroupId.' in key: new_security_grp_list.append(self.querystring.get(key)[0]) instance_ids = instance_ids_from_querystring(self.querystring) instance_id = instance_ids[0] if self.is_not_dryrun('ModifyInstanceSecurityGroups'): - self.ec2_backend.modify_instance_security_groups(instance_id, new_security_grp_list) + self.ec2_backend.modify_instance_security_groups( + instance_id, new_security_grp_list) return EC2_MODIFY_INSTANCE_ATTRIBUTE @@ -630,4 +644,4 @@ EC2_DESCRIBE_INSTANCE_TYPES = """ {% endfor %} -""" \ No newline at end of file +""" diff --git a/moto/ec2/responses/internet_gateways.py b/moto/ec2/responses/internet_gateways.py index 5b7a824f0..4a3da0b34 100644 --- a/moto/ec2/responses/internet_gateways.py +++ b/moto/ec2/responses/internet_gateways.py @@ -7,6 +7,7 @@ from moto.ec2.utils import ( class InternetGateways(BaseResponse): + def attach_internet_gateway(self): igw_id = self.querystring.get("InternetGatewayId", [None])[0] vpc_id = self.querystring.get("VpcId", [None])[0] @@ -33,9 +34,11 @@ class InternetGateways(BaseResponse): if "InternetGatewayId.1" in self.querystring: igw_ids = sequence_from_querystring( "InternetGatewayId", self.querystring) - igws = self.ec2_backend.describe_internet_gateways(igw_ids, filters=filter_dict) + igws = self.ec2_backend.describe_internet_gateways( + igw_ids, filters=filter_dict) else: - igws = self.ec2_backend.describe_internet_gateways(filters=filter_dict) + igws = self.ec2_backend.describe_internet_gateways( + filters=filter_dict) template = self.response_template(DESCRIBE_INTERNET_GATEWAYS_RESPONSE) return template.render(internet_gateways=igws) diff --git a/moto/ec2/responses/ip_addresses.py b/moto/ec2/responses/ip_addresses.py index 995719202..fab5cbddc 100644 --- a/moto/ec2/responses/ip_addresses.py +++ b/moto/ec2/responses/ip_addresses.py @@ -4,10 +4,13 @@ from moto.core.responses import BaseResponse class IPAddresses(BaseResponse): + def assign_private_ip_addresses(self): if self.is_not_dryrun('AssignPrivateIPAddress'): - raise NotImplementedError('IPAddresses.assign_private_ip_addresses is not yet implemented') + raise NotImplementedError( + 'IPAddresses.assign_private_ip_addresses is not yet implemented') def unassign_private_ip_addresses(self): if self.is_not_dryrun('UnAssignPrivateIPAddress'): - raise NotImplementedError('IPAddresses.unassign_private_ip_addresses is not yet implemented') + raise NotImplementedError( + 'IPAddresses.unassign_private_ip_addresses is not yet implemented') diff --git a/moto/ec2/responses/key_pairs.py b/moto/ec2/responses/key_pairs.py index 72f8715ec..936df2cd3 100644 --- a/moto/ec2/responses/key_pairs.py +++ b/moto/ec2/responses/key_pairs.py @@ -16,14 +16,16 @@ class KeyPairs(BaseResponse): def delete_key_pair(self): name = self.querystring.get('KeyName')[0] if self.is_not_dryrun('DeleteKeyPair'): - success = six.text_type(self.ec2_backend.delete_key_pair(name)).lower() + success = six.text_type( + self.ec2_backend.delete_key_pair(name)).lower() return self.response_template(DELETE_KEY_PAIR_RESPONSE).render(success=success) def describe_key_pairs(self): names = keypair_names_from_querystring(self.querystring) filters = filters_from_querystring(self.querystring) if len(filters) > 0: - raise NotImplementedError('Using filters in KeyPairs.describe_key_pairs is not yet implemented') + raise NotImplementedError( + 'Using filters in KeyPairs.describe_key_pairs is not yet implemented') keypairs = self.ec2_backend.describe_key_pairs(names) template = self.response_template(DESCRIBE_KEY_PAIRS_RESPONSE) diff --git a/moto/ec2/responses/monitoring.py b/moto/ec2/responses/monitoring.py index 3d40a1479..2024abe7e 100644 --- a/moto/ec2/responses/monitoring.py +++ b/moto/ec2/responses/monitoring.py @@ -3,10 +3,13 @@ from moto.core.responses import BaseResponse class Monitoring(BaseResponse): + def monitor_instances(self): if self.is_not_dryrun('MonitorInstances'): - raise NotImplementedError('Monitoring.monitor_instances is not yet implemented') + raise NotImplementedError( + 'Monitoring.monitor_instances is not yet implemented') def unmonitor_instances(self): if self.is_not_dryrun('UnMonitorInstances'): - raise NotImplementedError('Monitoring.unmonitor_instances is not yet implemented') + raise NotImplementedError( + 'Monitoring.unmonitor_instances is not yet implemented') diff --git a/moto/ec2/responses/nat_gateways.py b/moto/ec2/responses/nat_gateways.py index 98d383d47..ce9479e82 100644 --- a/moto/ec2/responses/nat_gateways.py +++ b/moto/ec2/responses/nat_gateways.py @@ -8,7 +8,8 @@ class NatGateways(BaseResponse): def create_nat_gateway(self): subnet_id = self._get_param('SubnetId') allocation_id = self._get_param('AllocationId') - nat_gateway = self.ec2_backend.create_nat_gateway(subnet_id=subnet_id, allocation_id=allocation_id) + nat_gateway = self.ec2_backend.create_nat_gateway( + subnet_id=subnet_id, allocation_id=allocation_id) template = self.response_template(CREATE_NAT_GATEWAY) return template.render(nat_gateway=nat_gateway) diff --git a/moto/ec2/responses/network_acls.py b/moto/ec2/responses/network_acls.py index 8093e18c8..bf9833d13 100644 --- a/moto/ec2/responses/network_acls.py +++ b/moto/ec2/responses/network_acls.py @@ -45,7 +45,8 @@ class NetworkACLs(BaseResponse): def describe_network_acls(self): network_acl_ids = network_acl_ids_from_querystring(self.querystring) filters = filters_from_querystring(self.querystring) - network_acls = self.ec2_backend.get_all_network_acls(network_acl_ids, filters) + network_acls = self.ec2_backend.get_all_network_acls( + network_acl_ids, filters) template = self.response_template(DESCRIBE_NETWORK_ACL_RESPONSE) return template.render(network_acls=network_acls) diff --git a/moto/ec2/responses/placement_groups.py b/moto/ec2/responses/placement_groups.py index 88926490f..06930f700 100644 --- a/moto/ec2/responses/placement_groups.py +++ b/moto/ec2/responses/placement_groups.py @@ -3,13 +3,17 @@ from moto.core.responses import BaseResponse class PlacementGroups(BaseResponse): + def create_placement_group(self): if self.is_not_dryrun('CreatePlacementGroup'): - raise NotImplementedError('PlacementGroups.create_placement_group is not yet implemented') + raise NotImplementedError( + 'PlacementGroups.create_placement_group is not yet implemented') def delete_placement_group(self): if self.is_not_dryrun('DeletePlacementGroup'): - raise NotImplementedError('PlacementGroups.delete_placement_group is not yet implemented') + raise NotImplementedError( + 'PlacementGroups.delete_placement_group is not yet implemented') def describe_placement_groups(self): - raise NotImplementedError('PlacementGroups.describe_placement_groups is not yet implemented') + raise NotImplementedError( + 'PlacementGroups.describe_placement_groups is not yet implemented') diff --git a/moto/ec2/responses/reserved_instances.py b/moto/ec2/responses/reserved_instances.py index be27260c8..07bd6661e 100644 --- a/moto/ec2/responses/reserved_instances.py +++ b/moto/ec2/responses/reserved_instances.py @@ -3,23 +3,30 @@ from moto.core.responses import BaseResponse class ReservedInstances(BaseResponse): + def cancel_reserved_instances_listing(self): if self.is_not_dryrun('CancelReservedInstances'): - raise NotImplementedError('ReservedInstances.cancel_reserved_instances_listing is not yet implemented') + raise NotImplementedError( + 'ReservedInstances.cancel_reserved_instances_listing is not yet implemented') def create_reserved_instances_listing(self): if self.is_not_dryrun('CreateReservedInstances'): - raise NotImplementedError('ReservedInstances.create_reserved_instances_listing is not yet implemented') + raise NotImplementedError( + 'ReservedInstances.create_reserved_instances_listing is not yet implemented') def describe_reserved_instances(self): - raise NotImplementedError('ReservedInstances.describe_reserved_instances is not yet implemented') + raise NotImplementedError( + 'ReservedInstances.describe_reserved_instances is not yet implemented') def describe_reserved_instances_listings(self): - raise NotImplementedError('ReservedInstances.describe_reserved_instances_listings is not yet implemented') + raise NotImplementedError( + 'ReservedInstances.describe_reserved_instances_listings is not yet implemented') def describe_reserved_instances_offerings(self): - raise NotImplementedError('ReservedInstances.describe_reserved_instances_offerings is not yet implemented') + raise NotImplementedError( + 'ReservedInstances.describe_reserved_instances_offerings is not yet implemented') def purchase_reserved_instances_offering(self): if self.is_not_dryrun('PurchaseReservedInstances'): - raise NotImplementedError('ReservedInstances.purchase_reserved_instances_offering is not yet implemented') + raise NotImplementedError( + 'ReservedInstances.purchase_reserved_instances_offering is not yet implemented') diff --git a/moto/ec2/responses/route_tables.py b/moto/ec2/responses/route_tables.py index 04fdf1d25..6f68a6553 100644 --- a/moto/ec2/responses/route_tables.py +++ b/moto/ec2/responses/route_tables.py @@ -8,24 +8,28 @@ class RouteTables(BaseResponse): def associate_route_table(self): route_table_id = self.querystring.get('RouteTableId')[0] subnet_id = self.querystring.get('SubnetId')[0] - association_id = self.ec2_backend.associate_route_table(route_table_id, subnet_id) + association_id = self.ec2_backend.associate_route_table( + route_table_id, subnet_id) template = self.response_template(ASSOCIATE_ROUTE_TABLE_RESPONSE) return template.render(association_id=association_id) def create_route(self): route_table_id = self.querystring.get('RouteTableId')[0] - destination_cidr_block = self.querystring.get('DestinationCidrBlock')[0] + destination_cidr_block = self.querystring.get( + 'DestinationCidrBlock')[0] gateway_id = optional_from_querystring('GatewayId', self.querystring) instance_id = optional_from_querystring('InstanceId', self.querystring) - interface_id = optional_from_querystring('NetworkInterfaceId', self.querystring) - pcx_id = optional_from_querystring('VpcPeeringConnectionId', self.querystring) + interface_id = optional_from_querystring( + 'NetworkInterfaceId', self.querystring) + pcx_id = optional_from_querystring( + 'VpcPeeringConnectionId', self.querystring) self.ec2_backend.create_route(route_table_id, destination_cidr_block, - gateway_id=gateway_id, - instance_id=instance_id, - interface_id=interface_id, - vpc_peering_connection_id=pcx_id) + gateway_id=gateway_id, + instance_id=instance_id, + interface_id=interface_id, + vpc_peering_connection_id=pcx_id) template = self.response_template(CREATE_ROUTE_RESPONSE) return template.render() @@ -38,7 +42,8 @@ class RouteTables(BaseResponse): def delete_route(self): route_table_id = self.querystring.get('RouteTableId')[0] - destination_cidr_block = self.querystring.get('DestinationCidrBlock')[0] + destination_cidr_block = self.querystring.get( + 'DestinationCidrBlock')[0] self.ec2_backend.delete_route(route_table_id, destination_cidr_block) template = self.response_template(DELETE_ROUTE_RESPONSE) return template.render() @@ -52,7 +57,8 @@ class RouteTables(BaseResponse): def describe_route_tables(self): route_table_ids = route_table_ids_from_querystring(self.querystring) filters = filters_from_querystring(self.querystring) - route_tables = self.ec2_backend.get_all_route_tables(route_table_ids, filters) + route_tables = self.ec2_backend.get_all_route_tables( + route_table_ids, filters) template = self.response_template(DESCRIBE_ROUTE_TABLES_RESPONSE) return template.render(route_tables=route_tables) @@ -64,18 +70,21 @@ class RouteTables(BaseResponse): def replace_route(self): route_table_id = self.querystring.get('RouteTableId')[0] - destination_cidr_block = self.querystring.get('DestinationCidrBlock')[0] + destination_cidr_block = self.querystring.get( + 'DestinationCidrBlock')[0] gateway_id = optional_from_querystring('GatewayId', self.querystring) instance_id = optional_from_querystring('InstanceId', self.querystring) - interface_id = optional_from_querystring('NetworkInterfaceId', self.querystring) - pcx_id = optional_from_querystring('VpcPeeringConnectionId', self.querystring) + interface_id = optional_from_querystring( + 'NetworkInterfaceId', self.querystring) + pcx_id = optional_from_querystring( + 'VpcPeeringConnectionId', self.querystring) self.ec2_backend.replace_route(route_table_id, destination_cidr_block, - gateway_id=gateway_id, - instance_id=instance_id, - interface_id=interface_id, - vpc_peering_connection_id=pcx_id) + gateway_id=gateway_id, + instance_id=instance_id, + interface_id=interface_id, + vpc_peering_connection_id=pcx_id) template = self.response_template(REPLACE_ROUTE_RESPONSE) return template.render() @@ -83,8 +92,10 @@ class RouteTables(BaseResponse): def replace_route_table_association(self): route_table_id = self.querystring.get('RouteTableId')[0] association_id = self.querystring.get('AssociationId')[0] - new_association_id = self.ec2_backend.replace_route_table_association(association_id, route_table_id) - template = self.response_template(REPLACE_ROUTE_TABLE_ASSOCIATION_RESPONSE) + new_association_id = self.ec2_backend.replace_route_table_association( + association_id, route_table_id) + template = self.response_template( + REPLACE_ROUTE_TABLE_ASSOCIATION_RESPONSE) return template.render(association_id=new_association_id) diff --git a/moto/ec2/responses/security_groups.py b/moto/ec2/responses/security_groups.py index 3451dc1ef..6f485fa31 100644 --- a/moto/ec2/responses/security_groups.py +++ b/moto/ec2/responses/security_groups.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import collections - from moto.core.responses import BaseResponse from moto.ec2.utils import filters_from_querystring @@ -55,10 +53,11 @@ def process_rules_from_querystring(querystring): source_groups.append(group_dict['GroupName'][0]) yield (group_name_or_id, ip_protocol, from_port, to_port, ip_ranges, - source_groups, source_group_ids) + source_groups, source_group_ids) class SecurityGroups(BaseResponse): + def authorize_security_group_egress(self): if self.is_not_dryrun('GrantSecurityGroupEgress'): for args in process_rules_from_querystring(self.querystring): @@ -77,12 +76,15 @@ class SecurityGroups(BaseResponse): vpc_id = self.querystring.get("VpcId", [None])[0] if self.is_not_dryrun('CreateSecurityGroup'): - group = self.ec2_backend.create_security_group(name, description, vpc_id=vpc_id) + group = self.ec2_backend.create_security_group( + name, description, vpc_id=vpc_id) template = self.response_template(CREATE_SECURITY_GROUP_RESPONSE) return template.render(group=group) def delete_security_group(self): - # TODO this should raise an error if there are instances in the group. See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DeleteSecurityGroup.html + # TODO this should raise an error if there are instances in the group. + # See + # http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DeleteSecurityGroup.html name = self.querystring.get('GroupName') sg_id = self.querystring.get('GroupId') diff --git a/moto/ec2/responses/spot_fleets.py b/moto/ec2/responses/spot_fleets.py index 3004cc0bb..e39d9b178 100644 --- a/moto/ec2/responses/spot_fleets.py +++ b/moto/ec2/responses/spot_fleets.py @@ -7,21 +7,25 @@ class SpotFleets(BaseResponse): def cancel_spot_fleet_requests(self): spot_fleet_request_ids = self._get_multi_param("SpotFleetRequestId.") terminate_instances = self._get_param("TerminateInstances") - spot_fleets = self.ec2_backend.cancel_spot_fleet_requests(spot_fleet_request_ids, terminate_instances) + spot_fleets = self.ec2_backend.cancel_spot_fleet_requests( + spot_fleet_request_ids, terminate_instances) template = self.response_template(CANCEL_SPOT_FLEETS_TEMPLATE) return template.render(spot_fleets=spot_fleets) def describe_spot_fleet_instances(self): spot_fleet_request_id = self._get_param("SpotFleetRequestId") - spot_requests = self.ec2_backend.describe_spot_fleet_instances(spot_fleet_request_id) - template = self.response_template(DESCRIBE_SPOT_FLEET_INSTANCES_TEMPLATE) + spot_requests = self.ec2_backend.describe_spot_fleet_instances( + spot_fleet_request_id) + template = self.response_template( + DESCRIBE_SPOT_FLEET_INSTANCES_TEMPLATE) return template.render(spot_request_id=spot_fleet_request_id, spot_requests=spot_requests) def describe_spot_fleet_requests(self): spot_fleet_request_ids = self._get_multi_param("SpotFleetRequestId.") - requests = self.ec2_backend.describe_spot_fleet_requests(spot_fleet_request_ids) + requests = self.ec2_backend.describe_spot_fleet_requests( + spot_fleet_request_ids) template = self.response_template(DESCRIBE_SPOT_FLEET_TEMPLATE) return template.render(requests=requests) @@ -32,7 +36,8 @@ class SpotFleets(BaseResponse): iam_fleet_role = spot_config['iam_fleet_role'] allocation_strategy = spot_config['allocation_strategy'] - launch_specs = self._get_list_prefix("SpotFleetRequestConfig.LaunchSpecifications") + launch_specs = self._get_list_prefix( + "SpotFleetRequestConfig.LaunchSpecifications") request = self.ec2_backend.request_spot_fleet( spot_price=spot_price, @@ -45,6 +50,7 @@ class SpotFleets(BaseResponse): template = self.response_template(REQUEST_SPOT_FLEET_TEMPLATE) return template.render(request=request) + REQUEST_SPOT_FLEET_TEMPLATE = """ 60262cc5-2bd4-4c8d-98ed-example {{ request.id }} diff --git a/moto/ec2/responses/spot_instances.py b/moto/ec2/responses/spot_instances.py index 96e5a1ba4..b0e80a320 100644 --- a/moto/ec2/responses/spot_instances.py +++ b/moto/ec2/responses/spot_instances.py @@ -8,29 +8,35 @@ class SpotInstances(BaseResponse): def cancel_spot_instance_requests(self): request_ids = self._get_multi_param('SpotInstanceRequestId') if self.is_not_dryrun('CancelSpotInstance'): - requests = self.ec2_backend.cancel_spot_instance_requests(request_ids) + requests = self.ec2_backend.cancel_spot_instance_requests( + request_ids) template = self.response_template(CANCEL_SPOT_INSTANCES_TEMPLATE) return template.render(requests=requests) def create_spot_datafeed_subscription(self): if self.is_not_dryrun('CreateSpotDatafeedSubscription'): - raise NotImplementedError('SpotInstances.create_spot_datafeed_subscription is not yet implemented') + raise NotImplementedError( + 'SpotInstances.create_spot_datafeed_subscription is not yet implemented') def delete_spot_datafeed_subscription(self): if self.is_not_dryrun('DeleteSpotDatafeedSubscription'): - raise NotImplementedError('SpotInstances.delete_spot_datafeed_subscription is not yet implemented') + raise NotImplementedError( + 'SpotInstances.delete_spot_datafeed_subscription is not yet implemented') def describe_spot_datafeed_subscription(self): - raise NotImplementedError('SpotInstances.describe_spot_datafeed_subscription is not yet implemented') + raise NotImplementedError( + 'SpotInstances.describe_spot_datafeed_subscription is not yet implemented') def describe_spot_instance_requests(self): filters = filters_from_querystring(self.querystring) - requests = self.ec2_backend.describe_spot_instance_requests(filters=filters) + requests = self.ec2_backend.describe_spot_instance_requests( + filters=filters) template = self.response_template(DESCRIBE_SPOT_INSTANCES_TEMPLATE) return template.render(requests=requests) def describe_spot_price_history(self): - raise NotImplementedError('SpotInstances.describe_spot_price_history is not yet implemented') + raise NotImplementedError( + 'SpotInstances.describe_spot_price_history is not yet implemented') def request_spot_instances(self): price = self._get_param('SpotPrice') @@ -42,13 +48,17 @@ class SpotInstances(BaseResponse): launch_group = self._get_param('LaunchGroup') availability_zone_group = self._get_param('AvailabilityZoneGroup') key_name = self._get_param('LaunchSpecification.KeyName') - security_groups = self._get_multi_param('LaunchSpecification.SecurityGroup') + security_groups = self._get_multi_param( + 'LaunchSpecification.SecurityGroup') user_data = self._get_param('LaunchSpecification.UserData') - instance_type = self._get_param('LaunchSpecification.InstanceType', 'm1.small') - placement = self._get_param('LaunchSpecification.Placement.AvailabilityZone') + instance_type = self._get_param( + 'LaunchSpecification.InstanceType', 'm1.small') + placement = self._get_param( + 'LaunchSpecification.Placement.AvailabilityZone') kernel_id = self._get_param('LaunchSpecification.KernelId') ramdisk_id = self._get_param('LaunchSpecification.RamdiskId') - monitoring_enabled = self._get_param('LaunchSpecification.Monitoring.Enabled') + monitoring_enabled = self._get_param( + 'LaunchSpecification.Monitoring.Enabled') subnet_id = self._get_param('LaunchSpecification.SubnetId') if self.is_not_dryrun('RequestSpotInstance'): diff --git a/moto/ec2/responses/subnets.py b/moto/ec2/responses/subnets.py index 9486a3ca1..67fd09a14 100644 --- a/moto/ec2/responses/subnets.py +++ b/moto/ec2/responses/subnets.py @@ -5,13 +5,15 @@ from moto.ec2.utils import filters_from_querystring class Subnets(BaseResponse): + def create_subnet(self): vpc_id = self.querystring.get('VpcId')[0] cidr_block = self.querystring.get('CidrBlock')[0] if 'AvailabilityZone' in self.querystring: availability_zone = self.querystring['AvailabilityZone'][0] else: - zone = random.choice(self.ec2_backend.describe_availability_zones()) + zone = random.choice( + self.ec2_backend.describe_availability_zones()) availability_zone = zone.name subnet = self.ec2_backend.create_subnet( vpc_id, diff --git a/moto/ec2/responses/tags.py b/moto/ec2/responses/tags.py index 8c2c43ba7..a747067fb 100644 --- a/moto/ec2/responses/tags.py +++ b/moto/ec2/responses/tags.py @@ -8,7 +8,8 @@ from moto.ec2.utils import sequence_from_querystring, tags_from_query_string, fi class TagResponse(BaseResponse): def create_tags(self): - resource_ids = sequence_from_querystring('ResourceId', self.querystring) + resource_ids = sequence_from_querystring( + 'ResourceId', self.querystring) validate_resource_ids(resource_ids) self.ec2_backend.do_resources_exist(resource_ids) tags = tags_from_query_string(self.querystring) @@ -17,7 +18,8 @@ class TagResponse(BaseResponse): return CREATE_RESPONSE def delete_tags(self): - resource_ids = sequence_from_querystring('ResourceId', self.querystring) + resource_ids = sequence_from_querystring( + 'ResourceId', self.querystring) validate_resource_ids(resource_ids) tags = tags_from_query_string(self.querystring) if self.is_not_dryrun('DeleteTags'): diff --git a/moto/ec2/responses/virtual_private_gateways.py b/moto/ec2/responses/virtual_private_gateways.py index e167437d5..2a677d36c 100644 --- a/moto/ec2/responses/virtual_private_gateways.py +++ b/moto/ec2/responses/virtual_private_gateways.py @@ -4,6 +4,7 @@ from moto.ec2.utils import filters_from_querystring class VirtualPrivateGateways(BaseResponse): + def attach_vpn_gateway(self): vpn_gateway_id = self.querystring.get('VpnGatewayId')[0] vpc_id = self.querystring.get('VpcId')[0] @@ -42,6 +43,7 @@ class VirtualPrivateGateways(BaseResponse): template = self.response_template(DETACH_VPN_GATEWAY_RESPONSE) return template.render(attachment=attachment) + CREATE_VPN_GATEWAY_RESPONSE = """ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE diff --git a/moto/ec2/responses/vm_export.py b/moto/ec2/responses/vm_export.py index 98c3dd3ea..6fdf59ba3 100644 --- a/moto/ec2/responses/vm_export.py +++ b/moto/ec2/responses/vm_export.py @@ -3,11 +3,15 @@ from moto.core.responses import BaseResponse class VMExport(BaseResponse): + def cancel_export_task(self): - raise NotImplementedError('VMExport.cancel_export_task is not yet implemented') + raise NotImplementedError( + 'VMExport.cancel_export_task is not yet implemented') def create_instance_export_task(self): - raise NotImplementedError('VMExport.create_instance_export_task is not yet implemented') + raise NotImplementedError( + 'VMExport.create_instance_export_task is not yet implemented') def describe_export_tasks(self): - raise NotImplementedError('VMExport.describe_export_tasks is not yet implemented') + raise NotImplementedError( + 'VMExport.describe_export_tasks is not yet implemented') diff --git a/moto/ec2/responses/vm_import.py b/moto/ec2/responses/vm_import.py index ea88bdc98..8c2ba138c 100644 --- a/moto/ec2/responses/vm_import.py +++ b/moto/ec2/responses/vm_import.py @@ -3,14 +3,19 @@ from moto.core.responses import BaseResponse class VMImport(BaseResponse): + def cancel_conversion_task(self): - raise NotImplementedError('VMImport.cancel_conversion_task is not yet implemented') + raise NotImplementedError( + 'VMImport.cancel_conversion_task is not yet implemented') def describe_conversion_tasks(self): - raise NotImplementedError('VMImport.describe_conversion_tasks is not yet implemented') + raise NotImplementedError( + 'VMImport.describe_conversion_tasks is not yet implemented') def import_instance(self): - raise NotImplementedError('VMImport.import_instance is not yet implemented') + raise NotImplementedError( + 'VMImport.import_instance is not yet implemented') def import_volume(self): - raise NotImplementedError('VMImport.import_volume is not yet implemented') + raise NotImplementedError( + 'VMImport.import_volume is not yet implemented') diff --git a/moto/ec2/responses/vpc_peering_connections.py b/moto/ec2/responses/vpc_peering_connections.py index 704dd7a3e..f6bff4310 100644 --- a/moto/ec2/responses/vpc_peering_connections.py +++ b/moto/ec2/responses/vpc_peering_connections.py @@ -3,34 +3,41 @@ from moto.core.responses import BaseResponse class VPCPeeringConnections(BaseResponse): + def create_vpc_peering_connection(self): vpc = self.ec2_backend.get_vpc(self.querystring.get('VpcId')[0]) - peer_vpc = self.ec2_backend.get_vpc(self.querystring.get('PeerVpcId')[0]) + peer_vpc = self.ec2_backend.get_vpc( + self.querystring.get('PeerVpcId')[0]) vpc_pcx = self.ec2_backend.create_vpc_peering_connection(vpc, peer_vpc) - template = self.response_template(CREATE_VPC_PEERING_CONNECTION_RESPONSE) + template = self.response_template( + CREATE_VPC_PEERING_CONNECTION_RESPONSE) return template.render(vpc_pcx=vpc_pcx) def delete_vpc_peering_connection(self): vpc_pcx_id = self.querystring.get('VpcPeeringConnectionId')[0] vpc_pcx = self.ec2_backend.delete_vpc_peering_connection(vpc_pcx_id) - template = self.response_template(DELETE_VPC_PEERING_CONNECTION_RESPONSE) + template = self.response_template( + DELETE_VPC_PEERING_CONNECTION_RESPONSE) return template.render(vpc_pcx=vpc_pcx) def describe_vpc_peering_connections(self): vpc_pcxs = self.ec2_backend.get_all_vpc_peering_connections() - template = self.response_template(DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE) + template = self.response_template( + DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE) return template.render(vpc_pcxs=vpc_pcxs) def accept_vpc_peering_connection(self): vpc_pcx_id = self.querystring.get('VpcPeeringConnectionId')[0] vpc_pcx = self.ec2_backend.accept_vpc_peering_connection(vpc_pcx_id) - template = self.response_template(ACCEPT_VPC_PEERING_CONNECTION_RESPONSE) + template = self.response_template( + ACCEPT_VPC_PEERING_CONNECTION_RESPONSE) return template.render(vpc_pcx=vpc_pcx) def reject_vpc_peering_connection(self): vpc_pcx_id = self.querystring.get('VpcPeeringConnectionId')[0] self.ec2_backend.reject_vpc_peering_connection(vpc_pcx_id) - template = self.response_template(REJECT_VPC_PEERING_CONNECTION_RESPONSE) + template = self.response_template( + REJECT_VPC_PEERING_CONNECTION_RESPONSE) return template.render() diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 3d2a99894..129f91a3b 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -5,9 +5,11 @@ from moto.ec2.utils import filters_from_querystring, vpc_ids_from_querystring class VPCs(BaseResponse): + def create_vpc(self): cidr_block = self.querystring.get('CidrBlock')[0] - instance_tenancy = self.querystring.get('InstanceTenancy', ['default'])[0] + instance_tenancy = self.querystring.get( + 'InstanceTenancy', ['default'])[0] vpc = self.ec2_backend.create_vpc(cidr_block, instance_tenancy) template = self.response_template(CREATE_VPC_RESPONSE) return template.render(vpc=vpc) @@ -40,7 +42,8 @@ class VPCs(BaseResponse): if self.querystring.get('%s.Value' % attribute): attr_name = camelcase_to_underscores(attribute) attr_value = self.querystring.get('%s.Value' % attribute)[0] - self.ec2_backend.modify_vpc_attribute(vpc_id, attr_name, attr_value) + self.ec2_backend.modify_vpc_attribute( + vpc_id, attr_name, attr_value) return MODIFY_VPC_ATTRIBUTE_RESPONSE diff --git a/moto/ec2/responses/vpn_connections.py b/moto/ec2/responses/vpn_connections.py index 7825e7ebb..2a4a7ef99 100644 --- a/moto/ec2/responses/vpn_connections.py +++ b/moto/ec2/responses/vpn_connections.py @@ -4,23 +4,27 @@ from moto.ec2.utils import filters_from_querystring, sequence_from_querystring class VPNConnections(BaseResponse): + def create_vpn_connection(self): type = self.querystring.get("Type", [None])[0] cgw_id = self.querystring.get("CustomerGatewayId", [None])[0] vgw_id = self.querystring.get("VPNGatewayId", [None])[0] static_routes = self.querystring.get("StaticRoutesOnly", [None])[0] - vpn_connection = self.ec2_backend.create_vpn_connection(type, cgw_id, vgw_id, static_routes_only=static_routes) + vpn_connection = self.ec2_backend.create_vpn_connection( + type, cgw_id, vgw_id, static_routes_only=static_routes) template = self.response_template(CREATE_VPN_CONNECTION_RESPONSE) return template.render(vpn_connection=vpn_connection) def delete_vpn_connection(self): vpn_connection_id = self.querystring.get('VpnConnectionId')[0] - vpn_connection = self.ec2_backend.delete_vpn_connection(vpn_connection_id) + vpn_connection = self.ec2_backend.delete_vpn_connection( + vpn_connection_id) template = self.response_template(DELETE_VPN_CONNECTION_RESPONSE) return template.render(vpn_connection=vpn_connection) def describe_vpn_connections(self): - vpn_connection_ids = sequence_from_querystring('VpnConnectionId', self.querystring) + vpn_connection_ids = sequence_from_querystring( + 'VpnConnectionId', self.querystring) filters = filters_from_querystring(self.querystring) vpn_connections = self.ec2_backend.get_all_vpn_connections( vpn_connection_ids=vpn_connection_ids, filters=filters) diff --git a/moto/ec2/responses/windows.py b/moto/ec2/responses/windows.py index 0a5e31a0e..13dfa9b67 100644 --- a/moto/ec2/responses/windows.py +++ b/moto/ec2/responses/windows.py @@ -3,14 +3,19 @@ from moto.core.responses import BaseResponse class Windows(BaseResponse): + def bundle_instance(self): - raise NotImplementedError('Windows.bundle_instance is not yet implemented') + raise NotImplementedError( + 'Windows.bundle_instance is not yet implemented') def cancel_bundle_task(self): - raise NotImplementedError('Windows.cancel_bundle_task is not yet implemented') + raise NotImplementedError( + 'Windows.cancel_bundle_task is not yet implemented') def describe_bundle_tasks(self): - raise NotImplementedError('Windows.describe_bundle_tasks is not yet implemented') + raise NotImplementedError( + 'Windows.describe_bundle_tasks is not yet implemented') def get_password_data(self): - raise NotImplementedError('Windows.get_password_data is not yet implemented') + raise NotImplementedError( + 'Windows.get_password_data is not yet implemented') diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 4d0f75254..8cba650a6 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -32,13 +32,15 @@ EC2_RESOURCE_TO_PREFIX = { 'vpn-gateway': 'vgw'} -EC2_PREFIX_TO_RESOURCE = dict((v, k) for (k, v) in EC2_RESOURCE_TO_PREFIX.items()) +EC2_PREFIX_TO_RESOURCE = dict((v, k) + for (k, v) in EC2_RESOURCE_TO_PREFIX.items()) def random_id(prefix='', size=8): chars = list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f'] - resource_id = ''.join(six.text_type(random.choice(chars)) for x in range(size)) + resource_id = ''.join(six.text_type(random.choice(chars)) + for x in range(size)) return '{0}-{1}'.format(prefix, resource_id) @@ -228,7 +230,8 @@ def tags_from_query_string(querystring_dict): tag_key = querystring_dict.get("Tag.{0}.Key".format(tag_index))[0] tag_value_key = "Tag.{0}.Value".format(tag_index) if tag_value_key in querystring_dict: - response_values[tag_key] = querystring_dict.get(tag_value_key)[0] + response_values[tag_key] = querystring_dict.get(tag_value_key)[ + 0] else: response_values[tag_key] = None return response_values @@ -262,7 +265,8 @@ def dhcp_configuration_from_querystring(querystring, option=u'DhcpConfiguration' key_index = key.split(".")[1] value_index = 1 while True: - value_key = u'{0}.{1}.Value.{2}'.format(option, key_index, value_index) + value_key = u'{0}.{1}.Value.{2}'.format( + option, key_index, value_index) if value_key in querystring: values.extend(querystring[value_key]) else: @@ -337,16 +341,20 @@ def get_obj_tag(obj, filter_name): tags = dict((tag['key'], tag['value']) for tag in obj.get_tags()) return tags.get(tag_name) + def get_obj_tag_names(obj): tags = set((tag['key'] for tag in obj.get_tags())) return tags + def get_obj_tag_values(obj): tags = set((tag['value'] for tag in obj.get_tags())) return tags + def tag_filter_matches(obj, filter_name, filter_values): - regex_filters = [re.compile(simple_aws_filter_to_re(f)) for f in filter_values] + regex_filters = [re.compile(simple_aws_filter_to_re(f)) + for f in filter_values] if filter_name == 'tag-key': tag_values = get_obj_tag_names(obj) elif filter_name == 'tag-value': @@ -400,7 +408,7 @@ def instance_value_in_filter_values(instance_value, filter_values): if not set(filter_values).intersection(set(instance_value)): return False elif instance_value not in filter_values: - return False + return False return True @@ -464,7 +472,8 @@ def is_filter_matching(obj, filter, filter_value): def generic_filter(filters, objects): if filters: for (_filter, _filter_value) in filters.items(): - objects = [obj for obj in objects if is_filter_matching(obj, _filter, _filter_value)] + objects = [obj for obj in objects if is_filter_matching( + obj, _filter, _filter_value)] return objects @@ -480,8 +489,10 @@ def simple_aws_filter_to_re(filter_string): def random_key_pair(): def random_hex(): return chr(random.choice(list(range(48, 58)) + list(range(97, 102)))) + def random_fingerprint(): - return ':'.join([random_hex()+random_hex() for i in range(20)]) + return ':'.join([random_hex() + random_hex() for i in range(20)]) + def random_material(): return ''.join([ chr(random.choice(list(range(65, 91)) + list(range(48, 58)) + @@ -489,7 +500,7 @@ def random_key_pair(): for i in range(1000) ]) material = "---- BEGIN RSA PRIVATE KEY ----" + random_material() + \ - "-----END RSA PRIVATE KEY-----" + "-----END RSA PRIVATE KEY-----" return { 'fingerprint': random_fingerprint(), 'material': material @@ -500,9 +511,11 @@ def get_prefix(resource_id): resource_id_prefix, separator, after = resource_id.partition('-') if resource_id_prefix == EC2_RESOURCE_TO_PREFIX['network-interface']: if after.startswith('attach'): - resource_id_prefix = EC2_RESOURCE_TO_PREFIX['network-interface-attachment'] + resource_id_prefix = EC2_RESOURCE_TO_PREFIX[ + 'network-interface-attachment'] if resource_id_prefix not in EC2_RESOURCE_TO_PREFIX.values(): - uuid4hex = re.compile('[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I) + uuid4hex = re.compile( + '[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I) if uuid4hex.match(resource_id) is not None: resource_id_prefix = EC2_RESOURCE_TO_PREFIX['reserved-instance'] else: @@ -539,20 +552,20 @@ def generate_instance_identity_document(instance): """ document = { - 'devPayProductCodes': None, - 'availabilityZone': instance.placement['AvailabilityZone'], - 'privateIp': instance.private_ip_address, - 'version': '2010-8-31', - 'region': instance.placement['AvailabilityZone'][:-1], - 'instanceId': instance.id, - 'billingProducts': None, - 'instanceType': instance.instance_type, - 'accountId': '012345678910', - 'pendingTime': '2015-11-19T16:32:11Z', - 'imageId': instance.image_id, - 'kernelId': instance.kernel_id, - 'ramdiskId': instance.ramdisk_id, - 'architecture': instance.architecture, - } + 'devPayProductCodes': None, + 'availabilityZone': instance.placement['AvailabilityZone'], + 'privateIp': instance.private_ip_address, + 'version': '2010-8-31', + 'region': instance.placement['AvailabilityZone'][:-1], + 'instanceId': instance.id, + 'billingProducts': None, + 'instanceType': instance.instance_type, + 'accountId': '012345678910', + 'pendingTime': '2015-11-19T16:32:11Z', + 'imageId': instance.image_id, + 'kernelId': instance.kernel_id, + 'ramdiskId': instance.ramdisk_id, + 'architecture': instance.architecture, + } return document diff --git a/moto/ecs/__init__.py b/moto/ecs/__init__.py index 6864355ad..8fb3dd41e 100644 --- a/moto/ecs/__init__.py +++ b/moto/ecs/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import ecs_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator ecs_backend = ecs_backends['us-east-1'] mock_ecs = base_decorator(ecs_backends) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 3ce7be8b5..5a046c376 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -8,6 +8,7 @@ from copy import copy class BaseObject(object): + def camelCase(self, key): words = [] for i, word in enumerate(key.split('_')): @@ -31,9 +32,11 @@ class BaseObject(object): class Cluster(BaseObject): + def __init__(self, cluster_name): self.active_services_count = 0 - self.arn = 'arn:aws:ecs:us-east-1:012345678910:cluster/{0}'.format(cluster_name) + self.arn = 'arn:aws:ecs:us-east-1:012345678910:cluster/{0}'.format( + cluster_name) self.name = cluster_name self.pending_tasks_count = 0 self.registered_container_instances_count = 0 @@ -58,9 +61,12 @@ class Cluster(BaseObject): ecs_backend = ecs_backends[region_name] return ecs_backend.create_cluster( - # ClusterName is optional in CloudFormation, thus create a random name if necessary - cluster_name=properties.get('ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))), + # ClusterName is optional in CloudFormation, thus create a random + # name if necessary + cluster_name=properties.get( + 'ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))), ) + @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] @@ -69,8 +75,10 @@ class Cluster(BaseObject): ecs_backend = ecs_backends[region_name] ecs_backend.delete_cluster(original_resource.arn) return ecs_backend.create_cluster( - # ClusterName is optional in CloudFormation, thus create a random name if necessary - cluster_name=properties.get('ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))), + # ClusterName is optional in CloudFormation, thus create a + # random name if necessary + cluster_name=properties.get( + 'ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))), ) else: # no-op when nothing changed between old and new resources @@ -78,9 +86,11 @@ class Cluster(BaseObject): class TaskDefinition(BaseObject): + def __init__(self, family, revision, container_definitions, volumes=None): self.family = family - self.arn = 'arn:aws:ecs:us-east-1:012345678910:task-definition/{0}:{1}'.format(family, revision) + self.arn = 'arn:aws:ecs:us-east-1:012345678910:task-definition/{0}:{1}'.format( + family, revision) self.container_definitions = container_definitions if volumes is None: self.volumes = [] @@ -98,7 +108,8 @@ class TaskDefinition(BaseObject): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] - family = properties.get('Family', 'task-definition-{0}'.format(int(random() * 10 ** 6))) + family = properties.get( + 'Family', 'task-definition-{0}'.format(int(random() * 10 ** 6))) container_definitions = properties['ContainerDefinitions'] volumes = properties['Volumes'] @@ -110,14 +121,16 @@ class TaskDefinition(BaseObject): def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] - family = properties.get('Family', 'task-definition-{0}'.format(int(random() * 10 ** 6))) + family = properties.get( + 'Family', 'task-definition-{0}'.format(int(random() * 10 ** 6))) container_definitions = properties['ContainerDefinitions'] volumes = properties['Volumes'] if (original_resource.family != family or - original_resource.container_definitions != container_definitions or - original_resource.volumes != volumes - # currently TaskRoleArn isn't stored at TaskDefinition instances - ): + original_resource.container_definitions != container_definitions or + original_resource.volumes != volumes): + # currently TaskRoleArn isn't stored at TaskDefinition + # instances + ecs_backend = ecs_backends[region_name] ecs_backend.deregister_task_definition(original_resource.arn) return ecs_backend.register_task_definition( @@ -126,10 +139,13 @@ class TaskDefinition(BaseObject): # no-op when nothing changed between old and new resources return original_resource + class Task(BaseObject): + def __init__(self, cluster, task_definition, container_instance_arn, overrides={}, started_by=''): self.cluster_arn = cluster.arn - self.task_arn = 'arn:aws:ecs:us-east-1:012345678910:task/{0}'.format(str(uuid.uuid1())) + self.task_arn = 'arn:aws:ecs:us-east-1:012345678910:task/{0}'.format( + str(uuid.uuid1())) self.container_instance_arn = container_instance_arn self.last_status = 'RUNNING' self.desired_status = 'RUNNING' @@ -146,9 +162,11 @@ class Task(BaseObject): class Service(BaseObject): + def __init__(self, cluster, service_name, task_definition, desired_count): self.cluster_arn = cluster.arn - self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format(service_name) + self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format( + service_name) self.name = service_name self.status = 'ACTIVE' self.running_count = 0 @@ -209,7 +227,8 @@ class Service(BaseObject): # TODO: LoadBalancers # TODO: Role ecs_backend.delete_service(cluster_name, service_name) - new_service_name = '{0}Service{1}'.format(cluster_name, int(random() * 10 ** 6)) + new_service_name = '{0}Service{1}'.format( + cluster_name, int(random() * 10 ** 6)) return ecs_backend.create_service( cluster_name, new_service_name, task_definition, desired_count) else: @@ -217,20 +236,22 @@ class Service(BaseObject): class ContainerInstance(BaseObject): + def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id self.status = 'ACTIVE' self.registeredResources = [] self.agentConnected = True - self.containerInstanceArn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format(str(uuid.uuid1())) + self.containerInstanceArn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( + str(uuid.uuid1())) self.pendingTaskCount = 0 self.remainingResources = [] self.runningTaskCount = 0 self.versionInfo = { - 'agentVersion': "1.0.0", - 'agentHash': '4023248', - 'dockerVersion': 'DockerVersion: 1.5.0' - } + 'agentVersion': "1.0.0", + 'agentHash': '4023248', + 'dockerVersion': 'DockerVersion: 1.5.0' + } @property def response_object(self): @@ -240,9 +261,11 @@ class ContainerInstance(BaseObject): class ContainerInstanceFailure(BaseObject): + def __init__(self, reason, container_instance_id): self.reason = reason - self.arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format(container_instance_id) + self.arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( + container_instance_id) @property def response_object(self): @@ -253,6 +276,7 @@ class ContainerInstanceFailure(BaseObject): class EC2ContainerServiceBackend(BaseBackend): + def __init__(self): self.clusters = {} self.task_definitions = {} @@ -261,19 +285,21 @@ class EC2ContainerServiceBackend(BaseBackend): self.container_instances = {} def describe_task_definition(self, task_definition_str): - task_definition_components = task_definition_str.split(':') - if len(task_definition_components) == 2: - family, revision = task_definition_components + task_definition_name = task_definition_str.split('/')[-1] + if ':' in task_definition_name: + family, revision = task_definition_name.split(':') revision = int(revision) else: - family = task_definition_components[0] - revision = -1 + family = task_definition_name + revision = len(self.task_definitions.get(family, [])) + if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): return self.task_definitions[family][revision - 1] elif family in self.task_definitions and revision == -1: return self.task_definitions[family][revision] else: - raise Exception("{0} is not a task_definition".format(task_definition_str)) + raise Exception( + "{0} is not a task_definition".format(task_definition_name)) def create_cluster(self, cluster_name): cluster = Cluster(cluster_name) @@ -295,9 +321,11 @@ class EC2ContainerServiceBackend(BaseBackend): for cluster in list_clusters_name: cluster_name = cluster.split('/')[-1] if cluster_name in self.clusters: - list_clusters.append(self.clusters[cluster_name].response_object) + list_clusters.append( + self.clusters[cluster_name].response_object) else: - raise Exception("{0} is not a cluster".format(cluster_name)) + raise Exception( + "{0} is not a cluster".format(cluster_name)) return list_clusters def delete_cluster(self, cluster_str): @@ -313,7 +341,8 @@ class EC2ContainerServiceBackend(BaseBackend): else: self.task_definitions[family] = [] revision = 1 - task_definition = TaskDefinition(family, revision, container_definitions, volumes) + task_definition = TaskDefinition( + family, revision, container_definitions, volumes) self.task_definitions[family].append(task_definition) return task_definition @@ -324,23 +353,10 @@ class EC2ContainerServiceBackend(BaseBackend): """ task_arns = [] for task_definition_list in self.task_definitions.values(): - task_arns.extend([task_definition.arn for task_definition in task_definition_list]) + task_arns.extend( + [task_definition.arn for task_definition in task_definition_list]) return task_arns - def describe_task_definition(self, task_definition_str): - task_definition_name = task_definition_str.split('/')[-1] - if ':' in task_definition_name: - family, revision = task_definition_name.split(':') - revision = int(revision) - else: - family = task_definition_name - revision = len(self.task_definitions.get(family, [])) - - if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): - return self.task_definitions[family][revision-1] - else: - raise Exception("{0} is not a task_definition".format(task_definition_name)) - def deregister_task_definition(self, task_definition_str): task_definition_name = task_definition_str.split('/')[-1] family, revision = task_definition_name.split(':') @@ -348,7 +364,8 @@ class EC2ContainerServiceBackend(BaseBackend): if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): return self.task_definitions[family].pop(revision - 1) else: - raise Exception("{0} is not a task_definition".format(task_definition_name)) + raise Exception( + "{0} is not a task_definition".format(task_definition_name)) def run_task(self, cluster_str, task_definition_str, count, overrides, started_by): cluster_name = cluster_str.split('/')[-1] @@ -360,14 +377,17 @@ class EC2ContainerServiceBackend(BaseBackend): if cluster_name not in self.tasks: self.tasks[cluster_name] = {} tasks = [] - container_instances = list(self.container_instances.get(cluster_name, {}).keys()) + container_instances = list( + self.container_instances.get(cluster_name, {}).keys()) if not container_instances: - raise Exception("No instances found in cluster {}".format(cluster_name)) + raise Exception( + "No instances found in cluster {}".format(cluster_name)) for _ in range(count or 1): container_instance_arn = self.container_instances[cluster_name][ container_instances[randint(0, len(container_instances) - 1)] ].containerInstanceArn - task = Task(cluster, task_definition, container_instance_arn, overrides or {}, started_by or '') + task = Task(cluster, task_definition, container_instance_arn, + overrides or {}, started_by or '') tasks.append(task) self.tasks[cluster_name][task.task_arn] = task return tasks @@ -385,13 +405,15 @@ class EC2ContainerServiceBackend(BaseBackend): if not container_instances: raise Exception("No container instance list provided") - container_instance_ids = [x.split('/')[-1] for x in container_instances] + container_instance_ids = [x.split('/')[-1] + for x in container_instances] for container_instance_id in container_instance_ids: container_instance_arn = self.container_instances[cluster_name][ container_instance_id ].containerInstanceArn - task = Task(cluster, task_definition, container_instance_arn, overrides or {}, started_by or '') + task = Task(cluster, task_definition, container_instance_arn, + overrides or {}, started_by or '') tasks.append(task) self.tasks[cluster_name][task.task_arn] = task return tasks @@ -418,17 +440,18 @@ class EC2ContainerServiceBackend(BaseBackend): filtered_tasks.append(task) if cluster_str: cluster_name = cluster_str.split('/')[-1] - if cluster_name in self.clusters: - cluster = self.clusters[cluster_name] - else: + if cluster_name not in self.clusters: raise Exception("{0} is not a cluster".format(cluster_name)) - filtered_tasks = list(filter(lambda t: cluster_name in t.cluster_arn, filtered_tasks)) + filtered_tasks = list( + filter(lambda t: cluster_name in t.cluster_arn, filtered_tasks)) if container_instance: - filtered_tasks = list(filter(lambda t: container_instance in t.container_instance_arn, filtered_tasks)) + filtered_tasks = list(filter( + lambda t: container_instance in t.container_instance_arn, filtered_tasks)) if started_by: - filtered_tasks = list(filter(lambda t: started_by == t.started_by, filtered_tasks)) + filtered_tasks = list( + filter(lambda t: started_by == t.started_by, filtered_tasks)) return [t.task_arn for t in filtered_tasks] def stop_task(self, cluster_str, task_str, reason): @@ -441,14 +464,16 @@ class EC2ContainerServiceBackend(BaseBackend): task_id = task_str.split('/')[-1] tasks = self.tasks.get(cluster_name, None) if not tasks: - raise Exception("Cluster {} has no registered tasks".format(cluster_name)) + raise Exception( + "Cluster {} has no registered tasks".format(cluster_name)) for task in tasks.keys(): if task.endswith(task_id): tasks[task].last_status = 'STOPPED' tasks[task].desired_status = 'STOPPED' tasks[task].stopped_reason = reason return tasks[task] - raise Exception("Could not find task {} on cluster {}".format(task_str, cluster_name)) + raise Exception("Could not find task {} on cluster {}".format( + task_str, cluster_name)) def create_service(self, cluster_str, service_name, task_definition_str, desired_count): cluster_name = cluster_str.split('/')[-1] @@ -458,7 +483,8 @@ class EC2ContainerServiceBackend(BaseBackend): raise Exception("{0} is not a cluster".format(cluster_name)) task_definition = self.describe_task_definition(task_definition_str) desired_count = desired_count if desired_count is not None else 0 - service = Service(cluster, service_name, task_definition, desired_count) + service = Service(cluster, service_name, + task_definition, desired_count) cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) self.services[cluster_service_pair] = service return service @@ -476,7 +502,8 @@ class EC2ContainerServiceBackend(BaseBackend): result = [] for existing_service_name, existing_service_obj in sorted(self.services.items()): for requested_name_or_arn in service_names_or_arns: - cluster_service_pair = '{0}:{1}'.format(cluster_name, requested_name_or_arn) + cluster_service_pair = '{0}:{1}'.format( + cluster_name, requested_name_or_arn) if cluster_service_pair == existing_service_name or existing_service_obj.arn == requested_name_or_arn: result.append(existing_service_obj) return result @@ -486,13 +513,16 @@ class EC2ContainerServiceBackend(BaseBackend): cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) if cluster_service_pair in self.services: if task_definition_str is not None: - task_definition = self.describe_task_definition(task_definition_str) - self.services[cluster_service_pair].task_definition = task_definition_str + self.describe_task_definition(task_definition_str) + self.services[ + cluster_service_pair].task_definition = task_definition_str if desired_count is not None: - self.services[cluster_service_pair].desired_count = desired_count + self.services[ + cluster_service_pair].desired_count = desired_count return self.services[cluster_service_pair] else: - raise Exception("cluster {0} or service {1} does not exist".format(cluster_name, service_name)) + raise Exception("cluster {0} or service {1} does not exist".format( + cluster_name, service_name)) def delete_service(self, cluster_name, service_name): cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) @@ -503,7 +533,8 @@ class EC2ContainerServiceBackend(BaseBackend): else: return self.services.pop(cluster_service_pair) else: - raise Exception("cluster {0} or service {1} does not exist".format(cluster_name, service_name)) + raise Exception("cluster {0} or service {1} does not exist".format( + cluster_name, service_name)) def register_container_instance(self, cluster_str, ec2_instance_id): cluster_name = cluster_str.split('/')[-1] @@ -512,14 +543,18 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = ContainerInstance(ec2_instance_id) if not self.container_instances.get(cluster_name): self.container_instances[cluster_name] = {} - container_instance_id = container_instance.containerInstanceArn.split('/')[-1] - self.container_instances[cluster_name][container_instance_id] = container_instance + container_instance_id = container_instance.containerInstanceArn.split( + '/')[-1] + self.container_instances[cluster_name][ + container_instance_id] = container_instance return container_instance def list_container_instances(self, cluster_str): cluster_name = cluster_str.split('/')[-1] - container_instances_values = self.container_instances.get(cluster_name, {}).values() - container_instances = [ci.containerInstanceArn for ci in container_instances_values] + container_instances_values = self.container_instances.get( + cluster_name, {}).values() + container_instances = [ + ci.containerInstanceArn for ci in container_instances_values] return sorted(container_instances) def describe_container_instances(self, cluster_str, list_container_instance_ids): @@ -529,11 +564,13 @@ class EC2ContainerServiceBackend(BaseBackend): failures = [] container_instance_objects = [] for container_instance_id in list_container_instance_ids: - container_instance = self.container_instances[cluster_name].get(container_instance_id, None) + container_instance = self.container_instances[ + cluster_name].get(container_instance_id, None) if container_instance is not None: container_instance_objects.append(container_instance) else: - failures.append(ContainerInstanceFailure('MISSING', container_instance_id)) + failures.append(ContainerInstanceFailure( + 'MISSING', container_instance_id)) return container_instance_objects, failures diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index a8c0dddac..b28ec6a4e 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals import json -import uuid from moto.core.responses import BaseResponse from .models import ecs_backends class EC2ContainerServiceResponse(BaseResponse): + @property def ecs_backend(self): return ecs_backends[self.region] @@ -34,8 +34,7 @@ class EC2ContainerServiceResponse(BaseResponse): cluster_arns = self.ecs_backend.list_clusters() return json.dumps({ 'clusterArns': cluster_arns - #, - #'nextToken': str(uuid.uuid1()) + # 'nextToken': str(uuid.uuid1()) }) def describe_clusters(self): @@ -57,7 +56,8 @@ class EC2ContainerServiceResponse(BaseResponse): family = self._get_param('family') container_definitions = self._get_param('containerDefinitions') volumes = self._get_param('volumes') - task_definition = self.ecs_backend.register_task_definition(family, container_definitions, volumes) + task_definition = self.ecs_backend.register_task_definition( + family, container_definitions, volumes) return json.dumps({ 'taskDefinition': task_definition.response_object }) @@ -66,43 +66,7 @@ class EC2ContainerServiceResponse(BaseResponse): task_definition_arns = self.ecs_backend.list_task_definitions() return json.dumps({ 'taskDefinitionArns': task_definition_arns - #, - #'nextToken': str(uuid.uuid1()) - }) - - def describe_task_definition(self): - task_definition_str = self._get_param('taskDefinition') - task_definition = self.ecs_backend.describe_task_definition(task_definition_str) - return json.dumps({ - 'taskDefinition': task_definition.response_object - }) - - def deregister_task_definition(self): - task_definition_str = self._get_param('taskDefinition') - task_definition = self.ecs_backend.deregister_task_definition(task_definition_str) - return json.dumps({ - 'taskDefinition': task_definition.response_object - }) - - def run_task(self): - cluster_str = self._get_param('cluster') - overrides = self._get_param('overrides') - task_definition_str = self._get_param('taskDefinition') - count = self._get_int_param('count') - started_by = self._get_param('startedBy') - tasks = self.ecs_backend.run_task(cluster_str, task_definition_str, count, overrides, started_by) - return json.dumps({ - 'tasks': [task.response_object for task in tasks], - 'failures': [] - }) - - def describe_tasks(self): - cluster = self._get_param('cluster') - tasks = self._get_param('tasks') - data = self.ecs_backend.describe_tasks(cluster, tasks) - return json.dumps({ - 'tasks': [task.response_object for task in data], - 'failures': [] + # 'nextToken': str(uuid.uuid1()) }) def describe_task_definition(self): @@ -113,17 +77,48 @@ class EC2ContainerServiceResponse(BaseResponse): 'failures': [] }) + def deregister_task_definition(self): + task_definition_str = self._get_param('taskDefinition') + task_definition = self.ecs_backend.deregister_task_definition( + task_definition_str) + return json.dumps({ + 'taskDefinition': task_definition.response_object + }) + + def run_task(self): + cluster_str = self._get_param('cluster') + overrides = self._get_param('overrides') + task_definition_str = self._get_param('taskDefinition') + count = self._get_int_param('count') + started_by = self._get_param('startedBy') + tasks = self.ecs_backend.run_task( + cluster_str, task_definition_str, count, overrides, started_by) + return json.dumps({ + 'tasks': [task.response_object for task in tasks], + 'failures': [] + }) + + def describe_tasks(self): + cluster = self._get_param('cluster') + tasks = self._get_param('tasks') + data = self.ecs_backend.describe_tasks(cluster, tasks) + return json.dumps({ + 'tasks': [task.response_object for task in data], + 'failures': [] + }) + def start_task(self): cluster_str = self._get_param('cluster') overrides = self._get_param('overrides') task_definition_str = self._get_param('taskDefinition') container_instances = self._get_param('containerInstances') started_by = self._get_param('startedBy') - tasks = self.ecs_backend.start_task(cluster_str, task_definition_str, container_instances, overrides, started_by) + tasks = self.ecs_backend.start_task( + cluster_str, task_definition_str, container_instances, overrides, started_by) return json.dumps({ 'tasks': [task.response_object for task in tasks], 'failures': [] - }) + }) def list_tasks(self): cluster_str = self._get_param('cluster') @@ -132,11 +127,11 @@ class EC2ContainerServiceResponse(BaseResponse): started_by = self._get_param('startedBy') service_name = self._get_param('serviceName') desiredStatus = self._get_param('desiredStatus') - task_arns = self.ecs_backend.list_tasks(cluster_str, container_instance, family, started_by, service_name, desiredStatus) + task_arns = self.ecs_backend.list_tasks( + cluster_str, container_instance, family, started_by, service_name, desiredStatus) return json.dumps({ 'taskArns': task_arns - }) - + }) def stop_task(self): cluster_str = self._get_param('cluster') @@ -145,15 +140,15 @@ class EC2ContainerServiceResponse(BaseResponse): task = self.ecs_backend.stop_task(cluster_str, task, reason) return json.dumps({ 'task': task.response_object - }) - + }) def create_service(self): cluster_str = self._get_param('cluster') service_name = self._get_param('serviceName') task_definition_str = self._get_param('taskDefinition') desired_count = self._get_int_param('desiredCount') - service = self.ecs_backend.create_service(cluster_str, service_name, task_definition_str, desired_count) + service = self.ecs_backend.create_service( + cluster_str, service_name, task_definition_str, desired_count) return json.dumps({ 'service': service.response_object }) @@ -170,7 +165,8 @@ class EC2ContainerServiceResponse(BaseResponse): def describe_services(self): cluster_str = self._get_param('cluster') service_names = self._get_param('services') - services = self.ecs_backend.describe_services(cluster_str, service_names) + services = self.ecs_backend.describe_services( + cluster_str, service_names) return json.dumps({ 'services': [service.response_object for service in services], 'failures': [] @@ -181,7 +177,8 @@ class EC2ContainerServiceResponse(BaseResponse): service_name = self._get_param('service') task_definition = self._get_param('taskDefinition') desired_count = self._get_int_param('desiredCount') - service = self.ecs_backend.update_service(cluster_str, service_name, task_definition, desired_count) + service = self.ecs_backend.update_service( + cluster_str, service_name, task_definition, desired_count) return json.dumps({ 'service': service.response_object }) @@ -196,17 +193,20 @@ class EC2ContainerServiceResponse(BaseResponse): def register_container_instance(self): cluster_str = self._get_param('cluster') - instance_identity_document_str = self._get_param('instanceIdentityDocument') + instance_identity_document_str = self._get_param( + 'instanceIdentityDocument') instance_identity_document = json.loads(instance_identity_document_str) ec2_instance_id = instance_identity_document["instanceId"] - container_instance = self.ecs_backend.register_container_instance(cluster_str, ec2_instance_id) + container_instance = self.ecs_backend.register_container_instance( + cluster_str, ec2_instance_id) return json.dumps({ - 'containerInstance' : container_instance.response_object + 'containerInstance': container_instance.response_object }) def list_container_instances(self): cluster_str = self._get_param('cluster') - container_instance_arns = self.ecs_backend.list_container_instances(cluster_str) + container_instance_arns = self.ecs_backend.list_container_instances( + cluster_str) return json.dumps({ 'containerInstanceArns': container_instance_arns }) @@ -214,8 +214,9 @@ class EC2ContainerServiceResponse(BaseResponse): def describe_container_instances(self): cluster_str = self._get_param('cluster') list_container_instance_arns = self._get_param('containerInstances') - container_instances, failures = self.ecs_backend.describe_container_instances(cluster_str, list_container_instance_arns) + container_instances, failures = self.ecs_backend.describe_container_instances( + cluster_str, list_container_instance_arns) return json.dumps({ - 'failures': [ci.response_object for ci in failures], - 'containerInstances': [ci.response_object for ci in container_instances] + 'failures': [ci.response_object for ci in failures], + 'containerInstances': [ci.response_object for ci in container_instances] }) diff --git a/moto/elb/__init__.py b/moto/elb/__init__.py index a8e8dab8d..e25f2d486 100644 --- a/moto/elb/__init__.py +++ b/moto/elb/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import elb_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator elb_backend = elb_backends['us-east-1'] mock_elb = base_decorator(elb_backends) diff --git a/moto/elb/exceptions.py b/moto/elb/exceptions.py index 338f3c95b..897bd6dd1 100644 --- a/moto/elb/exceptions.py +++ b/moto/elb/exceptions.py @@ -7,6 +7,7 @@ class ELBClientError(RESTError): class DuplicateTagKeysError(ELBClientError): + def __init__(self, cidr): super(DuplicateTagKeysError, self).__init__( "DuplicateTagKeys", @@ -15,6 +16,7 @@ class DuplicateTagKeysError(ELBClientError): class LoadBalancerNotFoundError(ELBClientError): + def __init__(self, cidr): super(LoadBalancerNotFoundError, self).__init__( "LoadBalancerNotFound", @@ -23,6 +25,7 @@ class LoadBalancerNotFoundError(ELBClientError): class TooManyTagsError(ELBClientError): + def __init__(self): super(TooManyTagsError, self).__init__( "LoadBalancerNotFound", @@ -30,6 +33,7 @@ class TooManyTagsError(ELBClientError): class BadHealthCheckDefinition(ELBClientError): + def __init__(self): super(BadHealthCheckDefinition, self).__init__( "ValidationError", @@ -37,9 +41,9 @@ class BadHealthCheckDefinition(ELBClientError): class DuplicateLoadBalancerName(ELBClientError): + def __init__(self, name): super(DuplicateLoadBalancerName, self).__init__( "DuplicateLoadBalancerName", "The specified load balancer name already exists for this account: {0}" .format(name)) - diff --git a/moto/elb/models.py b/moto/elb/models.py index 055b08e4d..11559c2e7 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import boto.ec2.elb from boto.ec2.elb.attributes import ( LbAttributes, ConnectionSettingAttribute, @@ -22,8 +21,8 @@ from .exceptions import ( ) - class FakeHealthCheck(object): + def __init__(self, timeout, healthy_threshold, unhealthy_threshold, interval, target): self.timeout = timeout @@ -36,6 +35,7 @@ class FakeHealthCheck(object): class FakeListener(object): + def __init__(self, load_balancer_port, instance_port, protocol, ssl_certificate_id): self.load_balancer_port = load_balancer_port self.instance_port = instance_port @@ -48,6 +48,7 @@ class FakeListener(object): class FakeBackend(object): + def __init__(self, instance_port): self.instance_port = instance_port self.policy_names = [] @@ -57,6 +58,7 @@ class FakeBackend(object): class FakeLoadBalancer(object): + def __init__(self, name, zones, ports, scheme='internet-facing', vpc_id=None, subnets=None): self.name = name self.health_check = None @@ -78,16 +80,20 @@ class FakeLoadBalancer(object): for port in ports: listener = FakeListener( protocol=(port.get('protocol') or port['Protocol']), - load_balancer_port=(port.get('load_balancer_port') or port['LoadBalancerPort']), - instance_port=(port.get('instance_port') or port['InstancePort']), - ssl_certificate_id=port.get('sslcertificate_id', port.get('SSLCertificateId')), + load_balancer_port=( + port.get('load_balancer_port') or port['LoadBalancerPort']), + instance_port=( + port.get('instance_port') or port['InstancePort']), + ssl_certificate_id=port.get( + 'sslcertificate_id', port.get('SSLCertificateId')), ) self.listeners.append(listener) # it is unclear per the AWS documentation as to when or how backend # information gets set, so let's guess and set it here *shrug* backend = FakeBackend( - instance_port=(port.get('instance_port') or port['InstancePort']), + instance_port=( + port.get('instance_port') or port['InstancePort']), ) self.backends.append(backend) @@ -120,7 +126,8 @@ class FakeLoadBalancer(object): port_policies[port] = policies_for_port for port, policies in port_policies.items(): - elb_backend.set_load_balancer_policies_of_backend_server(new_elb.name, port, list(policies)) + elb_backend.set_load_balancer_policies_of_backend_server( + new_elb.name, port, list(policies)) health_check = properties.get('HealthCheck') if health_check: @@ -137,7 +144,8 @@ class FakeLoadBalancer(object): @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): - cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name) + cls.delete_from_cloudformation_json( + original_resource.name, cloudformation_json, region_name) return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name) @classmethod @@ -155,15 +163,19 @@ class FakeLoadBalancer(object): def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'CanonicalHostedZoneName': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneName" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneName" ]"') elif attribute_name == 'CanonicalHostedZoneNameID': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneNameID" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneNameID" ]"') elif attribute_name == 'DNSName': return self.dns_name elif attribute_name == 'SourceSecurityGroup.GroupName': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.GroupName" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.GroupName" ]"') elif attribute_name == 'SourceSecurityGroup.OwnerAlias': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.OwnerAlias" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.OwnerAlias" ]"') raise UnformattedGetAttTemplateException() @classmethod @@ -224,7 +236,8 @@ class ELBBackend(BaseBackend): vpc_id = subnet.vpc_id if name in self.load_balancers: raise DuplicateLoadBalancerName(name) - new_load_balancer = FakeLoadBalancer(name=name, zones=zones, ports=ports, scheme=scheme, subnets=subnets, vpc_id=vpc_id) + new_load_balancer = FakeLoadBalancer( + name=name, zones=zones, ports=ports, scheme=scheme, subnets=subnets, vpc_id=vpc_id) self.load_balancers[name] = new_load_balancer return new_load_balancer @@ -240,14 +253,16 @@ class ELBBackend(BaseBackend): if lb_port == listener.load_balancer_port: break else: - balancer.listeners.append(FakeListener(lb_port, instance_port, protocol, ssl_certificate_id)) + balancer.listeners.append(FakeListener( + lb_port, instance_port, protocol, ssl_certificate_id)) return balancer def describe_load_balancers(self, names): balancers = self.load_balancers.values() if names: - matched_balancers = [balancer for balancer in balancers if balancer.name in names] + matched_balancers = [ + balancer for balancer in balancers if balancer.name in names] if len(names) != len(matched_balancers): missing_elb = list(set(names) - set(matched_balancers))[0] raise LoadBalancerNotFoundError(missing_elb) @@ -288,7 +303,8 @@ class ELBBackend(BaseBackend): if balancer: for idx, listener in enumerate(balancer.listeners): if lb_port == listener.load_balancer_port: - balancer.listeners[idx].ssl_certificate_id = ssl_certificate_id + balancer.listeners[ + idx].ssl_certificate_id = ssl_certificate_id return balancer @@ -299,7 +315,8 @@ class ELBBackend(BaseBackend): def deregister_instances(self, load_balancer_name, instance_ids): load_balancer = self.get_load_balancer(load_balancer_name) - new_instance_ids = [instance_id for instance_id in load_balancer.instance_ids if instance_id not in instance_ids] + new_instance_ids = [ + instance_id for instance_id in load_balancer.instance_ids if instance_id not in instance_ids] load_balancer.instance_ids = new_instance_ids return load_balancer @@ -342,7 +359,8 @@ class ELBBackend(BaseBackend): def set_load_balancer_policies_of_backend_server(self, load_balancer_name, instance_port, policies): load_balancer = self.get_load_balancer(load_balancer_name) - backend = [b for b in load_balancer.backends if int(b.instance_port) == instance_port][0] + backend = [b for b in load_balancer.backends if int( + b.instance_port) == instance_port][0] backend_idx = load_balancer.backends.index(backend) backend.policy_names = policies load_balancer.backends[backend_idx] = backend @@ -350,7 +368,8 @@ class ELBBackend(BaseBackend): def set_load_balancer_policies_of_listener(self, load_balancer_name, load_balancer_port, policies): load_balancer = self.get_load_balancer(load_balancer_name) - listener = [l for l in load_balancer.listeners if int(l.load_balancer_port) == load_balancer_port][0] + listener = [l for l in load_balancer.listeners if int( + l.load_balancer_port) == load_balancer_port][0] listener_idx = load_balancer.listeners.index(listener) listener.policy_names = policies load_balancer.listeners[listener_idx] = listener diff --git a/moto/elb/responses.py b/moto/elb/responses.py index cba98e4e0..e90de260e 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -43,9 +43,11 @@ class ELBResponse(BaseResponse): load_balancer_name = self._get_param('LoadBalancerName') ports = self._get_list_prefix("Listeners.member") - self.elb_backend.create_load_balancer_listeners(name=load_balancer_name, ports=ports) + self.elb_backend.create_load_balancer_listeners( + name=load_balancer_name, ports=ports) - template = self.response_template(CREATE_LOAD_BALANCER_LISTENERS_TEMPLATE) + template = self.response_template( + CREATE_LOAD_BALANCER_LISTENERS_TEMPLATE) return template.render() def describe_load_balancers(self): @@ -59,7 +61,8 @@ class ELBResponse(BaseResponse): ports = self._get_multi_param("LoadBalancerPorts.member") ports = [int(port) for port in ports] - self.elb_backend.delete_load_balancer_listeners(load_balancer_name, ports) + self.elb_backend.delete_load_balancer_listeners( + load_balancer_name, ports) template = self.response_template(DELETE_LOAD_BALANCER_LISTENERS) return template.render() @@ -74,7 +77,8 @@ class ELBResponse(BaseResponse): load_balancer_name=self._get_param('LoadBalancerName'), timeout=self._get_param('HealthCheck.Timeout'), healthy_threshold=self._get_param('HealthCheck.HealthyThreshold'), - unhealthy_threshold=self._get_param('HealthCheck.UnhealthyThreshold'), + unhealthy_threshold=self._get_param( + 'HealthCheck.UnhealthyThreshold'), interval=self._get_param('HealthCheck.Interval'), target=self._get_param('HealthCheck.Target'), ) @@ -83,9 +87,11 @@ class ELBResponse(BaseResponse): def register_instances_with_load_balancer(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [value[0] for key, value in self.querystring.items() if "Instances.member" in key] + instance_ids = [value[0] for key, value in self.querystring.items( + ) if "Instances.member" in key] template = self.response_template(REGISTER_INSTANCES_TEMPLATE) - load_balancer = self.elb_backend.register_instances(load_balancer_name, instance_ids) + load_balancer = self.elb_backend.register_instances( + load_balancer_name, instance_ids) return template.render(load_balancer=load_balancer) def set_load_balancer_listener_sslcertificate(self): @@ -93,16 +99,19 @@ class ELBResponse(BaseResponse): ssl_certificate_id = self.querystring['SSLCertificateId'][0] lb_port = self.querystring['LoadBalancerPort'][0] - self.elb_backend.set_load_balancer_listener_sslcertificate(load_balancer_name, lb_port, ssl_certificate_id) + self.elb_backend.set_load_balancer_listener_sslcertificate( + load_balancer_name, lb_port, ssl_certificate_id) template = self.response_template(SET_LOAD_BALANCER_SSL_CERTIFICATE) return template.render() def deregister_instances_from_load_balancer(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [value[0] for key, value in self.querystring.items() if "Instances.member" in key] + instance_ids = [value[0] for key, value in self.querystring.items( + ) if "Instances.member" in key] template = self.response_template(DEREGISTER_INSTANCES_TEMPLATE) - load_balancer = self.elb_backend.deregister_instances(load_balancer_name, instance_ids) + load_balancer = self.elb_backend.deregister_instances( + load_balancer_name, instance_ids) return template.render(load_balancer=load_balancer) def describe_load_balancer_attributes(self): @@ -115,11 +124,13 @@ class ELBResponse(BaseResponse): load_balancer_name = self._get_param('LoadBalancerName') load_balancer = self.elb_backend.get_load_balancer(load_balancer_name) - cross_zone = self._get_dict_param("LoadBalancerAttributes.CrossZoneLoadBalancing.") + cross_zone = self._get_dict_param( + "LoadBalancerAttributes.CrossZoneLoadBalancing.") if cross_zone: attribute = CrossZoneLoadBalancingAttribute() attribute.enabled = cross_zone["enabled"] == "true" - self.elb_backend.set_cross_zone_load_balancing_attribute(load_balancer_name, attribute) + self.elb_backend.set_cross_zone_load_balancing_attribute( + load_balancer_name, attribute) access_log = self._get_dict_param("LoadBalancerAttributes.AccessLog.") if access_log: @@ -128,20 +139,25 @@ class ELBResponse(BaseResponse): attribute.s3_bucket_name = access_log['s3_bucket_name'] attribute.s3_bucket_prefix = access_log['s3_bucket_prefix'] attribute.emit_interval = access_log["emit_interval"] - self.elb_backend.set_access_log_attribute(load_balancer_name, attribute) + self.elb_backend.set_access_log_attribute( + load_balancer_name, attribute) - connection_draining = self._get_dict_param("LoadBalancerAttributes.ConnectionDraining.") + connection_draining = self._get_dict_param( + "LoadBalancerAttributes.ConnectionDraining.") if connection_draining: attribute = ConnectionDrainingAttribute() attribute.enabled = connection_draining["enabled"] == "true" attribute.timeout = connection_draining["timeout"] - self.elb_backend.set_connection_draining_attribute(load_balancer_name, attribute) + self.elb_backend.set_connection_draining_attribute( + load_balancer_name, attribute) - connection_settings = self._get_dict_param("LoadBalancerAttributes.ConnectionSettings.") + connection_settings = self._get_dict_param( + "LoadBalancerAttributes.ConnectionSettings.") if connection_settings: attribute = ConnectionSettingAttribute() attribute.idle_timeout = connection_settings["idle_timeout"] - self.elb_backend.set_connection_settings_attribute(load_balancer_name, attribute) + self.elb_backend.set_connection_settings_attribute( + load_balancer_name, attribute) template = self.response_template(MODIFY_ATTRIBUTES_TEMPLATE) return template.render(attributes=load_balancer.attributes) @@ -153,7 +169,8 @@ class ELBResponse(BaseResponse): policy_name = self._get_param("PolicyName") other_policy.policy_name = policy_name - self.elb_backend.create_lb_other_policy(load_balancer_name, other_policy) + self.elb_backend.create_lb_other_policy( + load_balancer_name, other_policy) template = self.response_template(CREATE_LOAD_BALANCER_POLICY_TEMPLATE) return template.render() @@ -165,7 +182,8 @@ class ELBResponse(BaseResponse): policy.policy_name = self._get_param("PolicyName") policy.cookie_name = self._get_param("CookieName") - self.elb_backend.create_app_cookie_stickiness_policy(load_balancer_name, policy) + self.elb_backend.create_app_cookie_stickiness_policy( + load_balancer_name, policy) template = self.response_template(CREATE_LOAD_BALANCER_POLICY_TEMPLATE) return template.render() @@ -181,7 +199,8 @@ class ELBResponse(BaseResponse): else: policy.cookie_expiration_period = None - self.elb_backend.create_lb_cookie_stickiness_policy(load_balancer_name, policy) + self.elb_backend.create_lb_cookie_stickiness_policy( + load_balancer_name, policy) template = self.response_template(CREATE_LOAD_BALANCER_POLICY_TEMPLATE) return template.render() @@ -191,13 +210,16 @@ class ELBResponse(BaseResponse): load_balancer = self.elb_backend.get_load_balancer(load_balancer_name) load_balancer_port = int(self._get_param('LoadBalancerPort')) - mb_listener = [l for l in load_balancer.listeners if int(l.load_balancer_port) == load_balancer_port] + mb_listener = [l for l in load_balancer.listeners if int( + l.load_balancer_port) == load_balancer_port] if mb_listener: policies = self._get_multi_param("PolicyNames.member") - self.elb_backend.set_load_balancer_policies_of_listener(load_balancer_name, load_balancer_port, policies) + self.elb_backend.set_load_balancer_policies_of_listener( + load_balancer_name, load_balancer_port, policies) # else: explode? - template = self.response_template(SET_LOAD_BALANCER_POLICIES_OF_LISTENER_TEMPLATE) + template = self.response_template( + SET_LOAD_BALANCER_POLICIES_OF_LISTENER_TEMPLATE) return template.render() def set_load_balancer_policies_for_backend_server(self): @@ -205,20 +227,25 @@ class ELBResponse(BaseResponse): load_balancer = self.elb_backend.get_load_balancer(load_balancer_name) instance_port = int(self.querystring.get('InstancePort')[0]) - mb_backend = [b for b in load_balancer.backends if int(b.instance_port) == instance_port] + mb_backend = [b for b in load_balancer.backends if int( + b.instance_port) == instance_port] if mb_backend: policies = self._get_multi_param('PolicyNames.member') - self.elb_backend.set_load_balancer_policies_of_backend_server(load_balancer_name, instance_port, policies) + self.elb_backend.set_load_balancer_policies_of_backend_server( + load_balancer_name, instance_port, policies) # else: explode? - template = self.response_template(SET_LOAD_BALANCER_POLICIES_FOR_BACKEND_SERVER_TEMPLATE) + template = self.response_template( + SET_LOAD_BALANCER_POLICIES_FOR_BACKEND_SERVER_TEMPLATE) return template.render() def describe_instance_health(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [value[0] for key, value in self.querystring.items() if "Instances.member" in key] + instance_ids = [value[0] for key, value in self.querystring.items( + ) if "Instances.member" in key] if len(instance_ids) == 0: - instance_ids = self.elb_backend.get_load_balancer(load_balancer_name).instance_ids + instance_ids = self.elb_backend.get_load_balancer( + load_balancer_name).instance_ids template = self.response_template(DESCRIBE_INSTANCE_HEALTH_TEMPLATE) return template.render(instance_ids=instance_ids) @@ -226,7 +253,6 @@ class ELBResponse(BaseResponse): for key, value in self.querystring.items(): if "LoadBalancerNames.member" in key: - number = key.split('.')[2] load_balancer_name = value[0] elb = self.elb_backend.get_load_balancer(load_balancer_name) if not elb: @@ -241,7 +267,8 @@ class ELBResponse(BaseResponse): for key, value in self.querystring.items(): if "LoadBalancerNames.member" in key: number = key.split('.')[2] - load_balancer_name = self._get_param('LoadBalancerNames.member.{0}'.format(number)) + load_balancer_name = self._get_param( + 'LoadBalancerNames.member.{0}'.format(number)) elb = self.elb_backend.get_load_balancer(load_balancer_name) if not elb: raise LoadBalancerNotFoundError(load_balancer_name) @@ -260,7 +287,8 @@ class ELBResponse(BaseResponse): for key, value in self.querystring.items(): if "LoadBalancerNames.member" in key: number = key.split('.')[2] - load_balancer_name = self._get_param('LoadBalancerNames.member.{0}'.format(number)) + load_balancer_name = self._get_param( + 'LoadBalancerNames.member.{0}'.format(number)) elb = self.elb_backend.get_load_balancer(load_balancer_name) if not elb: raise LoadBalancerNotFoundError(load_balancer_name) @@ -284,7 +312,7 @@ class ELBResponse(BaseResponse): for i in tag_keys: counts[i] = tag_keys.count(i) - counts = sorted(counts.items(), key=lambda i:i[1], reverse=True) + counts = sorted(counts.items(), key=lambda i: i[1], reverse=True) if counts and counts[0][1] > 1: # We have dupes... diff --git a/moto/emr/__init__.py b/moto/emr/__init__.py index fc6b4d4ab..b4223f2cb 100644 --- a/moto/emr/__init__.py +++ b/moto/emr/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import emr_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator emr_backend = emr_backends['us-east-1'] mock_emr = base_decorator(emr_backends) diff --git a/moto/emr/models.py b/moto/emr/models.py index 155e4a898..94bc45ecc 100644 --- a/moto/emr/models.py +++ b/moto/emr/models.py @@ -11,6 +11,7 @@ from .utils import random_instance_group_id, random_cluster_id, random_step_id class FakeApplication(object): + def __init__(self, name, version, args=None, additional_info=None): self.additional_info = additional_info or {} self.args = args or [] @@ -19,6 +20,7 @@ class FakeApplication(object): class FakeBootstrapAction(object): + def __init__(self, args, name, script_path): self.args = args or [] self.name = name @@ -26,6 +28,7 @@ class FakeBootstrapAction(object): class FakeInstanceGroup(object): + def __init__(self, instance_count, instance_role, instance_type, market='ON_DEMAND', name=None, id=None, bid_price=None): self.id = id or random_instance_group_id() @@ -55,6 +58,7 @@ class FakeInstanceGroup(object): class FakeStep(object): + def __init__(self, state, name='', @@ -78,6 +82,7 @@ class FakeStep(object): class FakeCluster(object): + def __init__(self, emr_backend, name, @@ -135,17 +140,24 @@ class FakeCluster(object): 'instance_type': instance_attrs['slave_instance_type'], 'market': 'ON_DEMAND', 'name': 'slave'}]) - self.additional_master_security_groups = instance_attrs.get('additional_master_security_groups') - self.additional_slave_security_groups = instance_attrs.get('additional_slave_security_groups') + self.additional_master_security_groups = instance_attrs.get( + 'additional_master_security_groups') + self.additional_slave_security_groups = instance_attrs.get( + 'additional_slave_security_groups') self.availability_zone = instance_attrs.get('availability_zone') self.ec2_key_name = instance_attrs.get('ec2_key_name') self.ec2_subnet_id = instance_attrs.get('ec2_subnet_id') self.hadoop_version = instance_attrs.get('hadoop_version') - self.keep_job_flow_alive_when_no_steps = instance_attrs.get('keep_job_flow_alive_when_no_steps') - self.master_security_group = instance_attrs.get('emr_managed_master_security_group') - self.service_access_security_group = instance_attrs.get('service_access_security_group') - self.slave_security_group = instance_attrs.get('emr_managed_slave_security_group') - self.termination_protected = instance_attrs.get('termination_protected') + self.keep_job_flow_alive_when_no_steps = instance_attrs.get( + 'keep_job_flow_alive_when_no_steps') + self.master_security_group = instance_attrs.get( + 'emr_managed_master_security_group') + self.service_access_security_group = instance_attrs.get( + 'service_access_security_group') + self.slave_security_group = instance_attrs.get( + 'emr_managed_slave_security_group') + self.termination_protected = instance_attrs.get( + 'termination_protected') self.release_label = release_label self.requested_ami_version = requested_ami_version @@ -286,7 +298,8 @@ class ElasticMapReduceBackend(BaseBackend): clusters = self.clusters.values() within_two_month = datetime.now(pytz.utc) - timedelta(days=60) - clusters = [c for c in clusters if c.creation_datetime >= within_two_month] + clusters = [ + c for c in clusters if c.creation_datetime >= within_two_month] if job_flow_ids: clusters = [c for c in clusters if c.id in job_flow_ids] @@ -294,10 +307,12 @@ class ElasticMapReduceBackend(BaseBackend): clusters = [c for c in clusters if c.state in job_flow_states] if created_after: created_after = dtparse(created_after) - clusters = [c for c in clusters if c.creation_datetime > created_after] + clusters = [ + c for c in clusters if c.creation_datetime > created_after] if created_before: created_before = dtparse(created_before) - clusters = [c for c in clusters if c.creation_datetime < created_before] + clusters = [ + c for c in clusters if c.creation_datetime < created_before] # Amazon EMR can return a maximum of 512 job flow descriptions return sorted(clusters, key=lambda x: x.id)[:512] @@ -322,7 +337,8 @@ class ElasticMapReduceBackend(BaseBackend): max_items = 50 actions = self.clusters[cluster_id].bootstrap_actions start_idx = 0 if marker is None else int(marker) - marker = None if len(actions) <= start_idx + max_items else str(start_idx + max_items) + marker = None if len(actions) <= start_idx + \ + max_items else str(start_idx + max_items) return actions[start_idx:start_idx + max_items], marker def list_clusters(self, cluster_states=None, created_after=None, @@ -333,13 +349,16 @@ class ElasticMapReduceBackend(BaseBackend): clusters = [c for c in clusters if c.state in cluster_states] if created_after: created_after = dtparse(created_after) - clusters = [c for c in clusters if c.creation_datetime > created_after] + clusters = [ + c for c in clusters if c.creation_datetime > created_after] if created_before: created_before = dtparse(created_before) - clusters = [c for c in clusters if c.creation_datetime < created_before] + clusters = [ + c for c in clusters if c.creation_datetime < created_before] clusters = sorted(clusters, key=lambda x: x.id) start_idx = 0 if marker is None else int(marker) - marker = None if len(clusters) <= start_idx + max_items else str(start_idx + max_items) + marker = None if len(clusters) <= start_idx + \ + max_items else str(start_idx + max_items) return clusters[start_idx:start_idx + max_items], marker def list_instance_groups(self, cluster_id, marker=None): @@ -347,7 +366,8 @@ class ElasticMapReduceBackend(BaseBackend): groups = sorted(self.clusters[cluster_id].instance_groups, key=lambda x: x.id) start_idx = 0 if marker is None else int(marker) - marker = None if len(groups) <= start_idx + max_items else str(start_idx + max_items) + marker = None if len(groups) <= start_idx + \ + max_items else str(start_idx + max_items) return groups[start_idx:start_idx + max_items], marker def list_steps(self, cluster_id, marker=None, step_ids=None, step_states=None): @@ -358,7 +378,8 @@ class ElasticMapReduceBackend(BaseBackend): if step_states: steps = [s for s in steps if s.state in step_states] start_idx = 0 if marker is None else int(marker) - marker = None if len(steps) <= start_idx + max_items else str(start_idx + max_items) + marker = None if len(steps) <= start_idx + \ + max_items else str(start_idx + max_items) return steps[start_idx:start_idx + max_items], marker def modify_instance_groups(self, instance_groups): diff --git a/moto/emr/responses.py b/moto/emr/responses.py index 3869c33ff..91dc8cc11 100644 --- a/moto/emr/responses.py +++ b/moto/emr/responses.py @@ -29,7 +29,8 @@ def generate_boto3_response(operation): {'x-amzn-requestid': '2690d7eb-ed86-11dd-9877-6fad448a8419', 'date': datetime.now(pytz.utc).strftime('%a, %d %b %Y %H:%M:%S %Z'), 'content-type': 'application/x-amz-json-1.1'}) - resp = xml_to_json_response(self.aws_service_spec, operation, rendered) + resp = xml_to_json_response( + self.aws_service_spec, operation, rendered) return '' if resp is None else json.dumps(resp) return rendered return f @@ -63,14 +64,16 @@ class ElasticMapReduceResponse(BaseResponse): instance_groups = self._get_list_prefix('InstanceGroups.member') for item in instance_groups: item['instance_count'] = int(item['instance_count']) - instance_groups = self.backend.add_instance_groups(jobflow_id, instance_groups) + instance_groups = self.backend.add_instance_groups( + jobflow_id, instance_groups) template = self.response_template(ADD_INSTANCE_GROUPS_TEMPLATE) return template.render(instance_groups=instance_groups) @generate_boto3_response('AddJobFlowSteps') def add_job_flow_steps(self): job_flow_id = self._get_param('JobFlowId') - steps = self.backend.add_job_flow_steps(job_flow_id, steps_from_query_string(self._get_list_prefix('Steps.member'))) + steps = self.backend.add_job_flow_steps( + job_flow_id, steps_from_query_string(self._get_list_prefix('Steps.member'))) template = self.response_template(ADD_JOB_FLOW_STEPS_TEMPLATE) return template.render(steps=steps) @@ -104,7 +107,8 @@ class ElasticMapReduceResponse(BaseResponse): created_before = self._get_param('CreatedBefore') job_flow_ids = self._get_multi_param("JobFlowIds.member") job_flow_states = self._get_multi_param('JobFlowStates.member') - clusters = self.backend.describe_job_flows(job_flow_ids, job_flow_states, created_after, created_before) + clusters = self.backend.describe_job_flows( + job_flow_ids, job_flow_states, created_after, created_before) template = self.response_template(DESCRIBE_JOB_FLOWS_TEMPLATE) return template.render(clusters=clusters) @@ -123,7 +127,8 @@ class ElasticMapReduceResponse(BaseResponse): def list_bootstrap_actions(self): cluster_id = self._get_param('ClusterId') marker = self._get_param('Marker') - bootstrap_actions, marker = self.backend.list_bootstrap_actions(cluster_id, marker) + bootstrap_actions, marker = self.backend.list_bootstrap_actions( + cluster_id, marker) template = self.response_template(LIST_BOOTSTRAP_ACTIONS_TEMPLATE) return template.render(bootstrap_actions=bootstrap_actions, marker=marker) @@ -133,7 +138,8 @@ class ElasticMapReduceResponse(BaseResponse): created_after = self._get_param('CreatedAfter') created_before = self._get_param('CreatedBefore') marker = self._get_param('Marker') - clusters, marker = self.backend.list_clusters(cluster_states, created_after, created_before, marker) + clusters, marker = self.backend.list_clusters( + cluster_states, created_after, created_before, marker) template = self.response_template(LIST_CLUSTERS_TEMPLATE) return template.render(clusters=clusters, marker=marker) @@ -141,7 +147,8 @@ class ElasticMapReduceResponse(BaseResponse): def list_instance_groups(self): cluster_id = self._get_param('ClusterId') marker = self._get_param('Marker') - instance_groups, marker = self.backend.list_instance_groups(cluster_id, marker=marker) + instance_groups, marker = self.backend.list_instance_groups( + cluster_id, marker=marker) template = self.response_template(LIST_INSTANCE_GROUPS_TEMPLATE) return template.render(instance_groups=instance_groups, marker=marker) @@ -154,7 +161,8 @@ class ElasticMapReduceResponse(BaseResponse): marker = self._get_param('Marker') step_ids = self._get_multi_param('StepIds.member') step_states = self._get_multi_param('StepStates.member') - steps, marker = self.backend.list_steps(cluster_id, marker=marker, step_ids=step_ids, step_states=step_states) + steps, marker = self.backend.list_steps( + cluster_id, marker=marker, step_ids=step_ids, step_states=step_states) template = self.response_template(LIST_STEPS_TEMPLATE) return template.render(steps=steps, marker=marker) @@ -178,19 +186,27 @@ class ElasticMapReduceResponse(BaseResponse): @generate_boto3_response('RunJobFlow') def run_job_flow(self): instance_attrs = dict( - master_instance_type=self._get_param('Instances.MasterInstanceType'), + master_instance_type=self._get_param( + 'Instances.MasterInstanceType'), slave_instance_type=self._get_param('Instances.SlaveInstanceType'), instance_count=self._get_int_param('Instances.InstanceCount', 1), ec2_key_name=self._get_param('Instances.Ec2KeyName'), ec2_subnet_id=self._get_param('Instances.Ec2SubnetId'), hadoop_version=self._get_param('Instances.HadoopVersion'), - availability_zone=self._get_param('Instances.Placement.AvailabilityZone', self.backend.region_name + 'a'), - keep_job_flow_alive_when_no_steps=self._get_bool_param('Instances.KeepJobFlowAliveWhenNoSteps', False), - termination_protected=self._get_bool_param('Instances.TerminationProtected', False), - emr_managed_master_security_group=self._get_param('Instances.EmrManagedMasterSecurityGroup'), - emr_managed_slave_security_group=self._get_param('Instances.EmrManagedSlaveSecurityGroup'), - service_access_security_group=self._get_param('Instances.ServiceAccessSecurityGroup'), - additional_master_security_groups=self._get_multi_param('Instances.AdditionalMasterSecurityGroups.member.'), + availability_zone=self._get_param( + 'Instances.Placement.AvailabilityZone', self.backend.region_name + 'a'), + keep_job_flow_alive_when_no_steps=self._get_bool_param( + 'Instances.KeepJobFlowAliveWhenNoSteps', False), + termination_protected=self._get_bool_param( + 'Instances.TerminationProtected', False), + emr_managed_master_security_group=self._get_param( + 'Instances.EmrManagedMasterSecurityGroup'), + emr_managed_slave_security_group=self._get_param( + 'Instances.EmrManagedSlaveSecurityGroup'), + service_access_security_group=self._get_param( + 'Instances.ServiceAccessSecurityGroup'), + additional_master_security_groups=self._get_multi_param( + 'Instances.AdditionalMasterSecurityGroups.member.'), additional_slave_security_groups=self._get_multi_param('Instances.AdditionalSlaveSecurityGroups.member.')) kwargs = dict( @@ -198,8 +214,10 @@ class ElasticMapReduceResponse(BaseResponse): log_uri=self._get_param('LogUri'), job_flow_role=self._get_param('JobFlowRole'), service_role=self._get_param('ServiceRole'), - steps=steps_from_query_string(self._get_list_prefix('Steps.member')), - visible_to_all_users=self._get_bool_param('VisibleToAllUsers', False), + steps=steps_from_query_string( + self._get_list_prefix('Steps.member')), + visible_to_all_users=self._get_bool_param( + 'VisibleToAllUsers', False), instance_attrs=instance_attrs, ) @@ -225,7 +243,8 @@ class ElasticMapReduceResponse(BaseResponse): if key.startswith('properties.'): config.pop(key) config['properties'] = {} - map_items = self._get_map_prefix('Configurations.member.{0}.Properties.entry'.format(idx)) + map_items = self._get_map_prefix( + 'Configurations.member.{0}.Properties.entry'.format(idx)) config['properties'] = map_items kwargs['configurations'] = configurations @@ -239,7 +258,8 @@ class ElasticMapReduceResponse(BaseResponse): 'Only one AMI version and release label may be specified. ' 'Provided AMI: {0}, release label: {1}.').format( ami_version, release_label) - raise EmrError(error_type="ValidationException", message=message, template='single_error') + raise EmrError(error_type="ValidationException", + message=message, template='single_error') else: if ami_version: kwargs['requested_ami_version'] = ami_version @@ -256,7 +276,8 @@ class ElasticMapReduceResponse(BaseResponse): self.backend.add_applications( cluster.id, [{'Name': 'Hadoop', 'Version': '0.18'}]) - instance_groups = self._get_list_prefix('Instances.InstanceGroups.member') + instance_groups = self._get_list_prefix( + 'Instances.InstanceGroups.member') if instance_groups: for ig in instance_groups: ig['instance_count'] = int(ig['instance_count']) @@ -274,7 +295,8 @@ class ElasticMapReduceResponse(BaseResponse): def set_termination_protection(self): termination_protection = self._get_param('TerminationProtected') job_ids = self._get_multi_param('JobFlowIds.member') - self.backend.set_termination_protection(job_ids, termination_protection) + self.backend.set_termination_protection( + job_ids, termination_protection) template = self.response_template(SET_TERMINATION_PROTECTION_TEMPLATE) return template.render() diff --git a/moto/emr/utils.py b/moto/emr/utils.py index 328fdd783..4f12522cf 100644 --- a/moto/emr/utils.py +++ b/moto/emr/utils.py @@ -32,7 +32,8 @@ def tags_from_query_string(querystring_dict): tag_key = querystring_dict.get("Tags.{0}.Key".format(tag_index))[0] tag_value_key = "Tags.{0}.Value".format(tag_index) if tag_value_key in querystring_dict: - response_values[tag_key] = querystring_dict.get(tag_value_key)[0] + response_values[tag_key] = querystring_dict.get(tag_value_key)[ + 0] else: response_values[tag_key] = None return response_values @@ -42,7 +43,8 @@ def steps_from_query_string(querystring_dict): steps = [] for step in querystring_dict: step['jar'] = step.pop('hadoop_jar_step._jar') - step['properties'] = dict((o['Key'], o['Value']) for o in step.get('properties', [])) + step['properties'] = dict((o['Key'], o['Value']) + for o in step.get('properties', [])) step['args'] = [] idx = 1 keyfmt = 'hadoop_jar_step._args.member.{0}' diff --git a/moto/events/models.py b/moto/events/models.py index 94cca5ee7..3cf2c3d7a 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -53,7 +53,8 @@ class EventsBackend(BaseBackend): def __init__(self): self.rules = {} - # This array tracks the order in which the rules have been added, since 2.6 doesn't have OrderedDicts. + # This array tracks the order in which the rules have been added, since + # 2.6 doesn't have OrderedDicts. self.rules_order = [] self.next_tokens = {} @@ -106,7 +107,8 @@ class EventsBackend(BaseBackend): matching_rules = [] return_obj = {} - start_index, end_index, new_next_token = self._process_token_and_limits(len(self.rules), next_token, limit) + start_index, end_index, new_next_token = self._process_token_and_limits( + len(self.rules), next_token, limit) for i in range(start_index, end_index): rule = self._get_rule_by_index(i) @@ -130,7 +132,8 @@ class EventsBackend(BaseBackend): matching_rules = [] return_obj = {} - start_index, end_index, new_next_token = self._process_token_and_limits(len(self.rules), next_token, limit) + start_index, end_index, new_next_token = self._process_token_and_limits( + len(self.rules), next_token, limit) for i in range(start_index, end_index): rule = self._get_rule_by_index(i) @@ -144,10 +147,12 @@ class EventsBackend(BaseBackend): return return_obj def list_targets_by_rule(self, rule, next_token=None, limit=None): - # We'll let a KeyError exception be thrown for response to handle if rule doesn't exist. + # We'll let a KeyError exception be thrown for response to handle if + # rule doesn't exist. rule = self.rules[rule] - start_index, end_index, new_next_token = self._process_token_and_limits(len(rule.targets), next_token, limit) + start_index, end_index, new_next_token = self._process_token_and_limits( + len(rule.targets), next_token, limit) returned_targets = [] return_obj = {} @@ -188,4 +193,5 @@ class EventsBackend(BaseBackend): def test_event_pattern(self): raise NotImplementedError() + events_backend = EventsBackend() diff --git a/moto/events/responses.py b/moto/events/responses.py index 75e703706..d03befe12 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -87,7 +87,8 @@ class EventsHandler(BaseResponse): if not target_arn: return self.error('ValidationException', 'Parameter TargetArn is required.') - rule_names = events_backend.list_rule_names_by_target(target_arn, next_token, limit) + rule_names = events_backend.list_rule_names_by_target( + target_arn, next_token, limit) return json.dumps(rule_names), self.response_headers @@ -118,7 +119,8 @@ class EventsHandler(BaseResponse): return self.error('ValidationException', 'Parameter Rule is required.') try: - targets = events_backend.list_targets_by_rule(rule_name, next_token, limit) + targets = events_backend.list_targets_by_rule( + rule_name, next_token, limit) except KeyError: return self.error('ResourceNotFoundException', 'Rule ' + rule_name + ' does not exist.') @@ -140,7 +142,8 @@ class EventsHandler(BaseResponse): try: json.loads(event_pattern) except ValueError: - # Not quite as informative as the real error, but it'll work for now. + # Not quite as informative as the real error, but it'll work + # for now. return self.error('InvalidEventPatternException', 'Event pattern is not valid.') if sched_exp: diff --git a/moto/glacier/__init__.py b/moto/glacier/__init__.py index 49b3375e1..1570fa7d4 100644 --- a/moto/glacier/__init__.py +++ b/moto/glacier/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import glacier_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator glacier_backend = glacier_backends['us-east-1'] mock_glacier = base_decorator(glacier_backends) diff --git a/moto/glacier/models.py b/moto/glacier/models.py index 836e84d37..8e3286887 100644 --- a/moto/glacier/models.py +++ b/moto/glacier/models.py @@ -36,6 +36,7 @@ class ArchiveJob(object): class Vault(object): + def __init__(self, vault_name, region): self.vault_name = vault_name self.region = region diff --git a/moto/glacier/responses.py b/moto/glacier/responses.py index eac9b94c6..cda859b29 100644 --- a/moto/glacier/responses.py +++ b/moto/glacier/responses.py @@ -128,7 +128,8 @@ class GlacierResponse(_TemplateEnvironmentMixin): archive_id = json_body['ArchiveId'] job_id = self.backend.initiate_job(vault_name, archive_id) headers['x-amz-job-id'] = job_id - headers['Location'] = "/{0}/vaults/{1}/jobs/{2}".format(account_id, vault_name, job_id) + headers[ + 'Location'] = "/{0}/vaults/{1}/jobs/{2}".format(account_id, vault_name, job_id) return 202, headers, "" @classmethod diff --git a/moto/iam/__init__.py b/moto/iam/__init__.py index c5110b35d..1dda654ce 100644 --- a/moto/iam/__init__.py +++ b/moto/iam/__init__.py @@ -3,4 +3,4 @@ from .models import iam_backend iam_backends = {"global": iam_backend} mock_iam = iam_backend.decorator -mock_iam_deprecated = iam_backend.deprecated_decorator \ No newline at end of file +mock_iam_deprecated = iam_backend.deprecated_decorator diff --git a/moto/iam/models.py b/moto/iam/models.py index d27722f33..91c4a14d7 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -97,6 +97,7 @@ class Role(object): class InstanceProfile(object): + def __init__(self, instance_profile_id, name, path, roles): self.id = instance_profile_id self.name = name @@ -126,6 +127,7 @@ class InstanceProfile(object): class Certificate(object): + def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None): self.cert_name = cert_name self.cert_body = cert_body @@ -139,6 +141,7 @@ class Certificate(object): class AccessKey(object): + def __init__(self, user_name): self.user_name = user_name self.access_key_id = random_access_key() @@ -157,6 +160,7 @@ class AccessKey(object): class Group(object): + def __init__(self, name, path='/'): self.name = name self.id = random_resource_id() @@ -176,6 +180,7 @@ class Group(object): class User(object): + def __init__(self, name, path=None): self.name = name self.id = random_resource_id() @@ -184,7 +189,8 @@ class User(object): datetime.utcnow(), "%Y-%m-%d-%H-%M-%S" ) - self.arn = 'arn:aws:iam::123456789012:user{0}{1}'.format(self.path, name) + self.arn = 'arn:aws:iam::123456789012:user{0}{1}'.format( + self.path, name) self.policies = {} self.access_keys = [] self.password = None @@ -194,7 +200,8 @@ class User(object): try: policy_json = self.policies[policy_name] except KeyError: - raise IAMNotFoundException("Policy {0} not found".format(policy_name)) + raise IAMNotFoundException( + "Policy {0} not found".format(policy_name)) return { 'policy_name': policy_name, @@ -207,7 +214,8 @@ class User(object): def delete_policy(self, policy_name): if policy_name not in self.policies: - raise IAMNotFoundException("Policy {0} not found".format(policy_name)) + raise IAMNotFoundException( + "Policy {0} not found".format(policy_name)) del self.policies[policy_name] @@ -225,7 +233,8 @@ class User(object): self.access_keys.remove(key) break else: - raise IAMNotFoundException("Key {0} not found".format(access_key_id)) + raise IAMNotFoundException( + "Key {0} not found".format(access_key_id)) def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -261,16 +270,18 @@ class User(object): access_key_2_last_rotated = date_created.strftime(date_format) return '{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A'.format(self.name, - self.arn, - date_created.strftime(date_format), - password_enabled, - password_last_used, - date_created.strftime(date_format), - access_key_1_active, - access_key_1_last_rotated, - access_key_2_active, - access_key_2_last_rotated - ) + self.arn, + date_created.strftime( + date_format), + password_enabled, + password_last_used, + date_created.strftime( + date_format), + access_key_1_active, + access_key_1_last_rotated, + access_key_2_active, + access_key_2_last_rotated + ) # predefine AWS managed policies @@ -439,7 +450,8 @@ class IAMBackend(BaseBackend): if scope == 'AWS': policies = [p for p in policies if isinstance(p, AWSManagedPolicy)] elif scope == 'Local': - policies = [p for p in policies if not isinstance(p, AWSManagedPolicy)] + policies = [p for p in policies if not isinstance( + p, AWSManagedPolicy)] if path_prefix: policies = [p for p in policies if p.path.startswith(path_prefix)] @@ -492,7 +504,8 @@ class IAMBackend(BaseBackend): instance_profile_id = random_resource_id() roles = [iam_backend.get_role_by_id(role_id) for role_id in role_ids] - instance_profile = InstanceProfile(instance_profile_id, name, path, roles) + instance_profile = InstanceProfile( + instance_profile_id, name, path, roles) self.instance_profiles[instance_profile_id] = instance_profile return instance_profile @@ -501,7 +514,8 @@ class IAMBackend(BaseBackend): if profile.name == profile_name: return profile - raise IAMNotFoundException("Instance profile {0} not found".format(profile_name)) + raise IAMNotFoundException( + "Instance profile {0} not found".format(profile_name)) def get_instance_profiles(self): return self.instance_profiles.values() @@ -546,7 +560,8 @@ class IAMBackend(BaseBackend): def create_group(self, group_name, path='/'): if group_name in self.groups: - raise IAMConflictException("Group {0} already exists".format(group_name)) + raise IAMConflictException( + "Group {0} already exists".format(group_name)) group = Group(group_name, path) self.groups[group_name] = group @@ -557,7 +572,8 @@ class IAMBackend(BaseBackend): try: group = self.groups[group_name] except KeyError: - raise IAMNotFoundException("Group {0} not found".format(group_name)) + raise IAMNotFoundException( + "Group {0} not found".format(group_name)) return group @@ -575,7 +591,8 @@ class IAMBackend(BaseBackend): def create_user(self, user_name, path='/'): if user_name in self.users: - raise IAMConflictException("EntityAlreadyExists", "User {0} already exists".format(user_name)) + raise IAMConflictException( + "EntityAlreadyExists", "User {0} already exists".format(user_name)) user = User(user_name, path) self.users[user_name] = user @@ -595,7 +612,8 @@ class IAMBackend(BaseBackend): try: users = self.users.values() except KeyError: - raise IAMNotFoundException("Users {0}, {1}, {2} not found".format(path_prefix, marker, max_items)) + raise IAMNotFoundException( + "Users {0}, {1}, {2} not found".format(path_prefix, marker, max_items)) return users @@ -603,13 +621,15 @@ class IAMBackend(BaseBackend): # This does not currently deal with PasswordPolicyViolation. user = self.get_user(user_name) if user.password: - raise IAMConflictException("User {0} already has password".format(user_name)) + raise IAMConflictException( + "User {0} already has password".format(user_name)) user.password = password def delete_login_profile(self, user_name): user = self.get_user(user_name) if not user.password: - raise IAMNotFoundException("Login profile for {0} not found".format(user_name)) + raise IAMNotFoundException( + "Login profile for {0} not found".format(user_name)) user.password = None def add_user_to_group(self, group_name, user_name): @@ -623,7 +643,8 @@ class IAMBackend(BaseBackend): try: group.users.remove(user) except ValueError: - raise IAMNotFoundException("User {0} not in group {1}".format(user_name, group_name)) + raise IAMNotFoundException( + "User {0} not in group {1}".format(user_name, group_name)) def get_user_policy(self, user_name, policy_name): user = self.get_user(user_name) @@ -672,4 +693,5 @@ class IAMBackend(BaseBackend): report += self.users[user].to_csv() return base64.b64encode(report.encode('ascii')).decode('ascii') + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 223691e1e..9bddd21df 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -18,7 +18,8 @@ class IamResponse(BaseResponse): path = self._get_param('Path') policy_document = self._get_param('PolicyDocument') policy_name = self._get_param('PolicyName') - policy = iam_backend.create_policy(description, path, policy_document, policy_name) + policy = iam_backend.create_policy( + description, path, policy_document, policy_name) template = self.response_template(CREATE_POLICY_TEMPLATE) return template.render(policy=policy) @@ -27,7 +28,8 @@ class IamResponse(BaseResponse): max_items = self._get_int_param('MaxItems', 100) path_prefix = self._get_param('PathPrefix', '/') role_name = self._get_param('RoleName') - policies, marker = iam_backend.list_attached_role_policies(role_name, marker=marker, max_items=max_items, path_prefix=path_prefix) + policies, marker = iam_backend.list_attached_role_policies( + role_name, marker=marker, max_items=max_items, path_prefix=path_prefix) template = self.response_template(LIST_ATTACHED_ROLE_POLICIES_TEMPLATE) return template.render(policies=policies, marker=marker) @@ -37,16 +39,19 @@ class IamResponse(BaseResponse): only_attached = self._get_bool_param('OnlyAttached', False) path_prefix = self._get_param('PathPrefix', '/') scope = self._get_param('Scope', 'All') - policies, marker = iam_backend.list_policies(marker, max_items, only_attached, path_prefix, scope) + policies, marker = iam_backend.list_policies( + marker, max_items, only_attached, path_prefix, scope) template = self.response_template(LIST_POLICIES_TEMPLATE) return template.render(policies=policies, marker=marker) def create_role(self): role_name = self._get_param('RoleName') path = self._get_param('Path') - assume_role_policy_document = self._get_param('AssumeRolePolicyDocument') + assume_role_policy_document = self._get_param( + 'AssumeRolePolicyDocument') - role = iam_backend.create_role(role_name, assume_role_policy_document, path) + role = iam_backend.create_role( + role_name, assume_role_policy_document, path) template = self.response_template(CREATE_ROLE_TEMPLATE) return template.render(role=role) @@ -74,7 +79,8 @@ class IamResponse(BaseResponse): def get_role_policy(self): role_name = self._get_param('RoleName') policy_name = self._get_param('PolicyName') - policy_name, policy_document = iam_backend.get_role_policy(role_name, policy_name) + policy_name, policy_document = iam_backend.get_role_policy( + role_name, policy_name) template = self.response_template(GET_ROLE_POLICY_TEMPLATE) return template.render(role_name=role_name, policy_name=policy_name, @@ -91,7 +97,8 @@ class IamResponse(BaseResponse): profile_name = self._get_param('InstanceProfileName') path = self._get_param('Path') - profile = iam_backend.create_instance_profile(profile_name, path, role_ids=[]) + profile = iam_backend.create_instance_profile( + profile_name, path, role_ids=[]) template = self.response_template(CREATE_INSTANCE_PROFILE_TEMPLATE) return template.render(profile=profile) @@ -107,7 +114,8 @@ class IamResponse(BaseResponse): role_name = self._get_param('RoleName') iam_backend.add_role_to_instance_profile(profile_name, role_name) - template = self.response_template(ADD_ROLE_TO_INSTANCE_PROFILE_TEMPLATE) + template = self.response_template( + ADD_ROLE_TO_INSTANCE_PROFILE_TEMPLATE) return template.render() def remove_role_from_instance_profile(self): @@ -115,7 +123,8 @@ class IamResponse(BaseResponse): role_name = self._get_param('RoleName') iam_backend.remove_role_from_instance_profile(profile_name, role_name) - template = self.response_template(REMOVE_ROLE_FROM_INSTANCE_PROFILE_TEMPLATE) + template = self.response_template( + REMOVE_ROLE_FROM_INSTANCE_PROFILE_TEMPLATE) return template.render() def list_roles(self): @@ -132,9 +141,11 @@ class IamResponse(BaseResponse): def list_instance_profiles_for_role(self): role_name = self._get_param('RoleName') - profiles = iam_backend.get_instance_profiles_for_role(role_name=role_name) + profiles = iam_backend.get_instance_profiles_for_role( + role_name=role_name) - template = self.response_template(LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE) + template = self.response_template( + LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE) return template.render(instance_profiles=profiles) def upload_server_certificate(self): @@ -144,7 +155,8 @@ class IamResponse(BaseResponse): private_key = self._get_param('PrivateKey') cert_chain = self._get_param('CertificateName') - cert = iam_backend.upload_server_cert(cert_name, cert_body, private_key, cert_chain=cert_chain, path=path) + cert = iam_backend.upload_server_cert( + cert_name, cert_body, private_key, cert_chain=cert_chain, path=path) template = self.response_template(UPLOAD_CERT_TEMPLATE) return template.render(certificate=cert) diff --git a/moto/instance_metadata/__init__.py b/moto/instance_metadata/__init__.py index 9197bcf7c..d1a674982 100644 --- a/moto/instance_metadata/__init__.py +++ b/moto/instance_metadata/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .models import instance_metadata_backend -instance_metadata_backends = {"global": instance_metadata_backend} \ No newline at end of file +instance_metadata_backends = {"global": instance_metadata_backend} diff --git a/moto/instance_metadata/models.py b/moto/instance_metadata/models.py index b86f86376..8f8d84154 100644 --- a/moto/instance_metadata/models.py +++ b/moto/instance_metadata/models.py @@ -4,4 +4,5 @@ from moto.core.models import BaseBackend class InstanceMetadataBackend(BaseBackend): pass + instance_metadata_backend = InstanceMetadataBackend() diff --git a/moto/instance_metadata/responses.py b/moto/instance_metadata/responses.py index b2de66e7b..2ea9aa9a8 100644 --- a/moto/instance_metadata/responses.py +++ b/moto/instance_metadata/responses.py @@ -7,6 +7,7 @@ from moto.core.responses import BaseResponse class InstanceMetadataResponse(BaseResponse): + def metadata_response(self, request, full_url, headers): """ Mock response for localhost metadata @@ -43,5 +44,6 @@ class InstanceMetadataResponse(BaseResponse): elif path == 'iam/security-credentials/default-role': result = json.dumps(credentials) else: - raise NotImplementedError("The {0} metadata path has not been implemented".format(path)) + raise NotImplementedError( + "The {0} metadata path has not been implemented".format(path)) return 200, headers, result diff --git a/moto/kinesis/__init__.py b/moto/kinesis/__init__.py index c3f06d5b1..7d9767a9f 100644 --- a/moto/kinesis/__init__.py +++ b/moto/kinesis/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import kinesis_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator kinesis_backend = kinesis_backends['us-east-1'] mock_kinesis = base_decorator(kinesis_backends) diff --git a/moto/kinesis/exceptions.py b/moto/kinesis/exceptions.py index 0fcb3652a..e2fe02775 100644 --- a/moto/kinesis/exceptions.py +++ b/moto/kinesis/exceptions.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import BadRequest class ResourceNotFoundError(BadRequest): + def __init__(self, message): super(ResourceNotFoundError, self).__init__() self.description = json.dumps({ @@ -14,6 +15,7 @@ class ResourceNotFoundError(BadRequest): class ResourceInUseError(BadRequest): + def __init__(self, message): super(ResourceNotFoundError, self).__init__() self.description = json.dumps({ @@ -23,18 +25,21 @@ class ResourceInUseError(BadRequest): class StreamNotFoundError(ResourceNotFoundError): + def __init__(self, stream_name): super(StreamNotFoundError, self).__init__( 'Stream {0} under account 123456789012 not found.'.format(stream_name)) class ShardNotFoundError(ResourceNotFoundError): + def __init__(self, shard_id): super(ShardNotFoundError, self).__init__( 'Shard {0} under account 123456789012 not found.'.format(shard_id)) class InvalidArgumentError(BadRequest): + def __init__(self, message): super(InvalidArgumentError, self).__init__() self.description = json.dumps({ diff --git a/moto/kinesis/models.py b/moto/kinesis/models.py index e0e20da3f..5d80426ae 100644 --- a/moto/kinesis/models.py +++ b/moto/kinesis/models.py @@ -18,6 +18,7 @@ from .utils import compose_shard_iterator, compose_new_shard_iterator, decompose class Record(object): + def __init__(self, partition_key, data, sequence_number, explicit_hash_key): self.partition_key = partition_key self.data = data @@ -33,6 +34,7 @@ class Record(object): class Shard(object): + def __init__(self, shard_id, starting_hash, ending_hash): self._shard_id = shard_id self.starting_hash = starting_hash @@ -64,7 +66,8 @@ class Shard(object): else: last_sequence_number = 0 sequence_number = last_sequence_number + 1 - self.records[sequence_number] = Record(partition_key, data, sequence_number, explicit_hash_key) + self.records[sequence_number] = Record( + partition_key, data, sequence_number, explicit_hash_key) return sequence_number def get_min_sequence_number(self): @@ -107,8 +110,10 @@ class Stream(object): izip_longest = itertools.izip_longest for index, start, end in izip_longest(range(shard_count), - range(0,2**128,2**128//shard_count), - range(2**128//shard_count,2**128,2**128//shard_count), + range(0, 2**128, 2 ** + 128 // shard_count), + range(2**128 // shard_count, 2 ** + 128, 2**128 // shard_count), fillvalue=2**128): shard = Shard(index, start, end) self.shards[shard.shard_id] = shard @@ -152,7 +157,8 @@ class Stream(object): def put_record(self, partition_key, explicit_hash_key, sequence_number_for_ordering, data): shard = self.get_shard_for_key(partition_key, explicit_hash_key) - sequence_number = shard.put_record(partition_key, data, explicit_hash_key) + sequence_number = shard.put_record( + partition_key, data, explicit_hash_key) return sequence_number, shard.shard_id def to_json(self): @@ -168,12 +174,14 @@ class Stream(object): class FirehoseRecord(object): + def __init__(self, record_data): self.record_id = 12345678 self.record_data = record_data class DeliveryStream(object): + def __init__(self, stream_name, **stream_kwargs): self.name = stream_name self.redshift_username = stream_kwargs.get('redshift_username') @@ -185,14 +193,18 @@ class DeliveryStream(object): self.s3_role_arn = stream_kwargs.get('s3_role_arn') self.s3_bucket_arn = stream_kwargs.get('s3_bucket_arn') self.s3_prefix = stream_kwargs.get('s3_prefix') - self.s3_compression_format = stream_kwargs.get('s3_compression_format', 'UNCOMPRESSED') + self.s3_compression_format = stream_kwargs.get( + 's3_compression_format', 'UNCOMPRESSED') self.s3_buffering_hings = stream_kwargs.get('s3_buffering_hings') self.redshift_s3_role_arn = stream_kwargs.get('redshift_s3_role_arn') - self.redshift_s3_bucket_arn = stream_kwargs.get('redshift_s3_bucket_arn') + self.redshift_s3_bucket_arn = stream_kwargs.get( + 'redshift_s3_bucket_arn') self.redshift_s3_prefix = stream_kwargs.get('redshift_s3_prefix') - self.redshift_s3_compression_format = stream_kwargs.get('redshift_s3_compression_format', 'UNCOMPRESSED') - self.redshift_s3_buffering_hings = stream_kwargs.get('redshift_s3_buffering_hings') + self.redshift_s3_compression_format = stream_kwargs.get( + 'redshift_s3_compression_format', 'UNCOMPRESSED') + self.redshift_s3_buffering_hings = stream_kwargs.get( + 'redshift_s3_buffering_hings') self.records = [] self.status = 'ACTIVE' @@ -231,9 +243,8 @@ class DeliveryStream(object): }, "Username": self.redshift_username, }, - } - ] - + } + ] def to_dict(self): return { @@ -261,10 +272,9 @@ class KinesisBackend(BaseBackend): self.streams = {} self.delivery_streams = {} - def create_stream(self, stream_name, shard_count, region): if stream_name in self.streams: - raise ResourceInUseError(stream_name) + raise ResourceInUseError(stream_name) stream = Stream(stream_name, shard_count, region) self.streams[stream_name] = stream return stream @@ -302,7 +312,8 @@ class KinesisBackend(BaseBackend): records, last_sequence_id = shard.get_records(last_sequence_id, limit) - next_shard_iterator = compose_shard_iterator(stream_name, shard, last_sequence_id) + next_shard_iterator = compose_shard_iterator( + stream_name, shard, last_sequence_id) return next_shard_iterator, records @@ -320,7 +331,7 @@ class KinesisBackend(BaseBackend): response = { "FailedRecordCount": 0, - "Records" : [] + "Records": [] } for record in records: @@ -342,7 +353,7 @@ class KinesisBackend(BaseBackend): stream = self.describe_stream(stream_name) if shard_to_split not in stream.shards: - raise ResourceNotFoundError(shard_to_split) + raise ResourceNotFoundError(shard_to_split) if not re.match(r'0|([1-9]\d{0,38})', new_starting_hash_key): raise InvalidArgumentError(new_starting_hash_key) @@ -350,10 +361,12 @@ class KinesisBackend(BaseBackend): shard = stream.shards[shard_to_split] - last_id = sorted(stream.shards.values(), key=attrgetter('_shard_id'))[-1]._shard_id + last_id = sorted(stream.shards.values(), + key=attrgetter('_shard_id'))[-1]._shard_id if shard.starting_hash < new_starting_hash_key < shard.ending_hash: - new_shard = Shard(last_id+1, new_starting_hash_key, shard.ending_hash) + new_shard = Shard( + last_id + 1, new_starting_hash_key, shard.ending_hash) shard.ending_hash = new_starting_hash_key stream.shards[new_shard.shard_id] = new_shard else: @@ -372,10 +385,10 @@ class KinesisBackend(BaseBackend): stream = self.describe_stream(stream_name) if shard_to_merge not in stream.shards: - raise ResourceNotFoundError(shard_to_merge) + raise ResourceNotFoundError(shard_to_merge) if adjacent_shard_to_merge not in stream.shards: - raise ResourceNotFoundError(adjacent_shard_to_merge) + raise ResourceNotFoundError(adjacent_shard_to_merge) shard1 = stream.shards[shard_to_merge] shard2 = stream.shards[adjacent_shard_to_merge] @@ -390,9 +403,11 @@ class KinesisBackend(BaseBackend): del stream.shards[shard2.shard_id] for index in shard2.records: record = shard2.records[index] - shard1.put_record(record.partition_key, record.data, record.explicit_hash_key) + shard1.put_record(record.partition_key, + record.data, record.explicit_hash_key) ''' Firehose ''' + def create_delivery_stream(self, stream_name, **stream_kwargs): stream = DeliveryStream(stream_name, **stream_kwargs) self.delivery_streams[stream_name] = stream @@ -416,19 +431,19 @@ class KinesisBackend(BaseBackend): return record def list_tags_for_stream(self, stream_name, exclusive_start_tag_key=None, limit=None): - stream = self.describe_stream(stream_name) + stream = self.describe_stream(stream_name) tags = [] result = { 'HasMoreTags': False, 'Tags': tags } - for key, val in sorted(stream.tags.items(), key=lambda x:x[0]): - if limit and len(res) >= limit: - result['HasMoreTags'] = True - break - if exclusive_start_tag_key and key < exexclusive_start_tag_key: - continue + for key, val in sorted(stream.tags.items(), key=lambda x: x[0]): + if limit and len(tags) >= limit: + result['HasMoreTags'] = True + break + if exclusive_start_tag_key and key < exclusive_start_tag_key: + continue tags.append({ 'Key': key, @@ -438,14 +453,14 @@ class KinesisBackend(BaseBackend): return result def add_tags_to_stream(self, stream_name, tags): - stream = self.describe_stream(stream_name) + stream = self.describe_stream(stream_name) stream.tags.update(tags) def remove_tags_from_stream(self, stream_name, tag_keys): - stream = self.describe_stream(stream_name) + stream = self.describe_stream(stream_name) for key in tag_keys: if key in stream.tags: - del stream.tags[key] + del stream.tags[key] kinesis_backends = {} diff --git a/moto/kinesis/responses.py b/moto/kinesis/responses.py index 29f6c07ff..8bc81925f 100644 --- a/moto/kinesis/responses.py +++ b/moto/kinesis/responses.py @@ -4,7 +4,6 @@ import json from moto.core.responses import BaseResponse from .models import kinesis_backends -from werkzeug.exceptions import BadRequest class KinesisResponse(BaseResponse): @@ -25,7 +24,8 @@ class KinesisResponse(BaseResponse): def create_stream(self): stream_name = self.parameters.get('StreamName') shard_count = self.parameters.get('ShardCount') - self.kinesis_backend.create_stream(stream_name, shard_count, self.region) + self.kinesis_backend.create_stream( + stream_name, shard_count, self.region) return "" def describe_stream(self): @@ -50,7 +50,8 @@ class KinesisResponse(BaseResponse): stream_name = self.parameters.get("StreamName") shard_id = self.parameters.get("ShardId") shard_iterator_type = self.parameters.get("ShardIteratorType") - starting_sequence_number = self.parameters.get("StartingSequenceNumber") + starting_sequence_number = self.parameters.get( + "StartingSequenceNumber") shard_iterator = self.kinesis_backend.get_shard_iterator( stream_name, shard_id, shard_iterator_type, starting_sequence_number, @@ -64,7 +65,8 @@ class KinesisResponse(BaseResponse): shard_iterator = self.parameters.get("ShardIterator") limit = self.parameters.get("Limit") - next_shard_iterator, records = self.kinesis_backend.get_records(shard_iterator, limit) + next_shard_iterator, records = self.kinesis_backend.get_records( + shard_iterator, limit) return json.dumps({ "NextShardIterator": next_shard_iterator, @@ -77,7 +79,8 @@ class KinesisResponse(BaseResponse): stream_name = self.parameters.get("StreamName") partition_key = self.parameters.get("PartitionKey") explicit_hash_key = self.parameters.get("ExplicitHashKey") - sequence_number_for_ordering = self.parameters.get("SequenceNumberForOrdering") + sequence_number_for_ordering = self.parameters.get( + "SequenceNumberForOrdering") data = self.parameters.get("Data") sequence_number, shard_id = self.kinesis_backend.put_record( @@ -105,7 +108,7 @@ class KinesisResponse(BaseResponse): stream_name = self.parameters.get("StreamName") shard_to_split = self.parameters.get("ShardToSplit") new_starting_hash_key = self.parameters.get("NewStartingHashKey") - response = self.kinesis_backend.split_shard( + self.kinesis_backend.split_shard( stream_name, shard_to_split, new_starting_hash_key ) return "" @@ -114,15 +117,17 @@ class KinesisResponse(BaseResponse): stream_name = self.parameters.get("StreamName") shard_to_merge = self.parameters.get("ShardToMerge") adjacent_shard_to_merge = self.parameters.get("AdjacentShardToMerge") - response = self.kinesis_backend.merge_shards( + self.kinesis_backend.merge_shards( stream_name, shard_to_merge, adjacent_shard_to_merge ) return "" ''' Firehose ''' + def create_delivery_stream(self): stream_name = self.parameters['DeliveryStreamName'] - redshift_config = self.parameters.get('RedshiftDestinationConfiguration') + redshift_config = self.parameters.get( + 'RedshiftDestinationConfiguration') if redshift_config: redshift_s3_config = redshift_config['S3Configuration'] @@ -149,7 +154,8 @@ class KinesisResponse(BaseResponse): 's3_compression_format': s3_config.get('CompressionFormat'), 's3_buffering_hings': s3_config['BufferingHints'], } - stream = self.kinesis_backend.create_delivery_stream(stream_name, **stream_kwargs) + stream = self.kinesis_backend.create_delivery_stream( + stream_name, **stream_kwargs) return json.dumps({ 'DeliveryStreamARN': stream.arn }) @@ -177,7 +183,8 @@ class KinesisResponse(BaseResponse): stream_name = self.parameters['DeliveryStreamName'] record_data = self.parameters['Record']['Data'] - record = self.kinesis_backend.put_firehose_record(stream_name, record_data) + record = self.kinesis_backend.put_firehose_record( + stream_name, record_data) return json.dumps({ "RecordId": record.record_id, }) @@ -188,7 +195,8 @@ class KinesisResponse(BaseResponse): request_responses = [] for record in records: - record_response = self.kinesis_backend.put_firehose_record(stream_name, record['Data']) + record_response = self.kinesis_backend.put_firehose_record( + stream_name, record['Data']) request_responses.append({ "RecordId": record_response.record_id }) @@ -207,7 +215,8 @@ class KinesisResponse(BaseResponse): stream_name = self.parameters.get('StreamName') exclusive_start_tag_key = self.parameters.get('ExclusiveStartTagKey') limit = self.parameters.get('Limit') - response = self.kinesis_backend.list_tags_for_stream(stream_name, exclusive_start_tag_key, limit) + response = self.kinesis_backend.list_tags_for_stream( + stream_name, exclusive_start_tag_key, limit) return json.dumps(response) def remove_tags_from_stream(self): diff --git a/moto/kinesis/utils.py b/moto/kinesis/utils.py index 0d35b4134..190371b2e 100644 --- a/moto/kinesis/utils.py +++ b/moto/kinesis/utils.py @@ -13,7 +13,8 @@ def compose_new_shard_iterator(stream_name, shard, shard_iterator_type, starting elif shard_iterator_type == "LATEST": last_sequence_id = shard.get_max_sequence_number() else: - raise InvalidArgumentError("Invalid ShardIteratorType: {0}".format(shard_iterator_type)) + raise InvalidArgumentError( + "Invalid ShardIteratorType: {0}".format(shard_iterator_type)) return compose_shard_iterator(stream_name, shard, last_sequence_id) diff --git a/moto/kms/__init__.py b/moto/kms/__init__.py index b6bffa804..b4bb0b639 100644 --- a/moto/kms/__init__.py +++ b/moto/kms/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import kms_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator kms_backend = kms_backends['us-east-1'] mock_kms = base_decorator(kms_backends) diff --git a/moto/kms/models.py b/moto/kms/models.py index 0bfe5791f..37fde9eb8 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -7,6 +7,7 @@ from collections import defaultdict class Key(object): + def __init__(self, policy, key_usage, description, region): self.id = generate_key_id() self.policy = policy @@ -77,7 +78,8 @@ class KmsBackend(BaseBackend): return self.keys.pop(key_id) def describe_key(self, key_id): - # allow the different methods (alias, ARN :key/, keyId, ARN alias) to describe key not just KeyId + # allow the different methods (alias, ARN :key/, keyId, ARN alias) to + # describe key not just KeyId key_id = self.get_key_id(key_id) if r'alias/' in str(key_id).lower(): key_id = self.get_key_id_from_alias(key_id.split('alias/')[1]) @@ -128,6 +130,7 @@ class KmsBackend(BaseBackend): def get_key_policy(self, key_id): return self.keys[self.get_key_id(key_id)].policy + kms_backends = {} for region in boto.kms.regions(): kms_backends[region.name] = KmsBackend() diff --git a/moto/kms/responses.py b/moto/kms/responses.py index 7f0659a64..7ed8927a2 100644 --- a/moto/kms/responses.py +++ b/moto/kms/responses.py @@ -18,6 +18,7 @@ reserved_aliases = [ 'alias/aws/rds', ] + class KmsResponse(BaseResponse): @property @@ -33,13 +34,15 @@ class KmsResponse(BaseResponse): key_usage = self.parameters.get('KeyUsage') description = self.parameters.get('Description') - key = self.kms_backend.create_key(policy, key_usage, description, self.region) + key = self.kms_backend.create_key( + policy, key_usage, description, self.region) return json.dumps(key.to_dict()) def describe_key(self): key_id = self.parameters.get('KeyId') try: - key = self.kms_backend.describe_key(self.kms_backend.get_key_id(key_id)) + key = self.kms_backend.describe_key( + self.kms_backend.get_key_id(key_id)) except KeyError: headers = dict(self.headers) headers['status'] = 404 @@ -70,7 +73,8 @@ class KmsResponse(BaseResponse): body={'message': 'Invalid identifier', '__type': 'ValidationException'}) if alias_name in reserved_aliases: - raise JSONResponseError(400, 'Bad Request', body={'__type': 'NotAuthorizedException'}) + raise JSONResponseError(400, 'Bad Request', body={ + '__type': 'NotAuthorizedException'}) if ':' in alias_name: raise JSONResponseError(400, 'Bad Request', body={ @@ -81,7 +85,7 @@ class KmsResponse(BaseResponse): raise JSONResponseError(400, 'Bad Request', body={ 'message': "1 validation error detected: Value '{alias_name}' at 'aliasName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9:/_-]+$" .format(**locals()), - '__type': 'ValidationException'}) + '__type': 'ValidationException'}) if self.kms_backend.alias_exists(target_key_id): raise JSONResponseError(400, 'Bad Request', body={ @@ -120,7 +124,7 @@ class KmsResponse(BaseResponse): response_aliases = [ { 'AliasArn': u'arn:aws:kms:{region}:012345678912:{reserved_alias}'.format(region=region, - reserved_alias=reserved_alias), + reserved_alias=reserved_alias), 'AliasName': reserved_alias } for reserved_alias in reserved_aliases ] @@ -147,7 +151,7 @@ class KmsResponse(BaseResponse): self.kms_backend.enable_key_rotation(key_id) except KeyError: raise JSONResponseError(404, 'Not Found', body={ - 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region,key_id=key_id), + 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region, key_id=key_id), '__type': 'NotFoundException'}) return json.dumps(None) @@ -159,7 +163,7 @@ class KmsResponse(BaseResponse): self.kms_backend.disable_key_rotation(key_id) except KeyError: raise JSONResponseError(404, 'Not Found', body={ - 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region,key_id=key_id), + 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region, key_id=key_id), '__type': 'NotFoundException'}) return json.dumps(None) @@ -170,7 +174,7 @@ class KmsResponse(BaseResponse): rotation_enabled = self.kms_backend.get_key_rotation_status(key_id) except KeyError: raise JSONResponseError(404, 'Not Found', body={ - 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region,key_id=key_id), + 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region, key_id=key_id), '__type': 'NotFoundException'}) return json.dumps({'KeyRotationEnabled': rotation_enabled}) @@ -185,7 +189,7 @@ class KmsResponse(BaseResponse): self.kms_backend.put_key_policy(key_id, policy) except KeyError: raise JSONResponseError(404, 'Not Found', body={ - 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region,key_id=key_id), + 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region, key_id=key_id), '__type': 'NotFoundException'}) return json.dumps(None) @@ -200,7 +204,7 @@ class KmsResponse(BaseResponse): return json.dumps({'Policy': self.kms_backend.get_key_policy(key_id)}) except KeyError: raise JSONResponseError(404, 'Not Found', body={ - 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region,key_id=key_id), + 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region, key_id=key_id), '__type': 'NotFoundException'}) def list_key_policies(self): @@ -210,7 +214,7 @@ class KmsResponse(BaseResponse): self.kms_backend.describe_key(key_id) except KeyError: raise JSONResponseError(404, 'Not Found', body={ - 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region,key_id=key_id), + 'message': "Key 'arn:aws:kms:{region}:012345678912:key/{key_id}' does not exist".format(region=self.region, key_id=key_id), '__type': 'NotFoundException'}) return json.dumps({'Truncated': False, 'PolicyNames': ['default']}) @@ -233,7 +237,9 @@ class KmsResponse(BaseResponse): def _assert_valid_key_id(key_id): if not re.match(r'^[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}$', key_id, re.IGNORECASE): - raise JSONResponseError(404, 'Not Found', body={'message': ' Invalid keyId', '__type': 'NotFoundException'}) + raise JSONResponseError(404, 'Not Found', body={ + 'message': ' Invalid keyId', '__type': 'NotFoundException'}) + def _assert_default_policy(policy_name): if policy_name != 'default': diff --git a/moto/opsworks/__init__.py b/moto/opsworks/__init__.py index d2da1a6a8..b492b6a53 100644 --- a/moto/opsworks/__init__.py +++ b/moto/opsworks/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import opsworks_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator opsworks_backend = opsworks_backends['us-east-1'] mock_opsworks = base_decorator(opsworks_backends) diff --git a/moto/opsworks/exceptions.py b/moto/opsworks/exceptions.py index b408b82f3..00bdffbc5 100644 --- a/moto/opsworks/exceptions.py +++ b/moto/opsworks/exceptions.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import BadRequest class ResourceNotFoundException(BadRequest): + def __init__(self, message): super(ResourceNotFoundException, self).__init__() self.description = json.dumps({ @@ -14,6 +15,7 @@ class ResourceNotFoundException(BadRequest): class ValidationException(BadRequest): + def __init__(self, message): super(ValidationException, self).__init__() self.description = json.dumps({ diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index 68edade9a..a1b8370dd 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -14,6 +14,7 @@ class OpsworkInstance(object): This metadata exists before any instance reservations are made, and is used to populate a reservation request when "start" is called """ + def __init__(self, stack_id, layer_ids, instance_type, ec2_backend, auto_scale_type=None, hostname=None, @@ -154,8 +155,10 @@ class OpsworkInstance(object): d.update({"ReportedAgentVersion": "2425-20160406102508 (fixed)"}) d.update({"RootDeviceVolumeId": "vol-a20e450a (fixed)"}) if self.ssh_keyname is not None: - d.update({"SshHostDsaKeyFingerprint": "24:36:32:fe:d8:5f:9c:18:b1:ad:37:e9:eb:e8:69:58 (fixed)"}) - d.update({"SshHostRsaKeyFingerprint": "3c:bd:37:52:d7:ca:67:e1:6e:4b:ac:31:86:79:f5:6c (fixed)"}) + d.update( + {"SshHostDsaKeyFingerprint": "24:36:32:fe:d8:5f:9c:18:b1:ad:37:e9:eb:e8:69:58 (fixed)"}) + d.update( + {"SshHostRsaKeyFingerprint": "3c:bd:37:52:d7:ca:67:e1:6e:4b:ac:31:86:79:f5:6c (fixed)"}) d.update({"PrivateDns": self.instance.private_dns}) d.update({"PrivateIp": self.instance.private_ip}) d.update({"PublicDns": getattr(self.instance, 'public_dns', None)}) @@ -164,6 +167,7 @@ class OpsworkInstance(object): class Layer(object): + def __init__(self, stack_id, type, name, shortname, attributes=None, custom_instance_profile_arn=None, @@ -283,11 +287,13 @@ class Layer(object): if self.custom_json is not None: d.update({"CustomJson": self.custom_json}) if self.custom_instance_profile_arn is not None: - d.update({"CustomInstanceProfileArn": self.custom_instance_profile_arn}) + d.update( + {"CustomInstanceProfileArn": self.custom_instance_profile_arn}) return d class Stack(object): + def __init__(self, name, region, service_role_arn, default_instance_profile_arn, vpcid="vpc-1f99bf7a", attributes=None, @@ -393,6 +399,7 @@ class Stack(object): class OpsWorksBackend(BaseBackend): + def __init__(self, ec2_backend): self.stacks = {} self.layers = {} @@ -457,9 +464,12 @@ class OpsWorksBackend(BaseBackend): kwargs.setdefault("subnet_id", stack.default_subnet_id) kwargs.setdefault("root_device_type", stack.default_root_device_type) if layer.custom_instance_profile_arn: - kwargs.setdefault("instance_profile_arn", layer.custom_instance_profile_arn) - kwargs.setdefault("instance_profile_arn", stack.default_instance_profile_arn) - kwargs.setdefault("security_group_ids", layer.custom_security_group_ids) + kwargs.setdefault("instance_profile_arn", + layer.custom_instance_profile_arn) + kwargs.setdefault("instance_profile_arn", + stack.default_instance_profile_arn) + kwargs.setdefault("security_group_ids", + layer.custom_security_group_ids) kwargs.setdefault("associate_public_ip", layer.auto_assign_public_ips) kwargs.setdefault("ebs_optimized", layer.use_ebs_optimized_instances) kwargs.update({"ec2_backend": self.ec2_backend}) @@ -507,14 +517,16 @@ class OpsWorksBackend(BaseBackend): if layer_id not in self.layers: raise ResourceNotFoundException( "Unable to find layer with ID {0}".format(layer_id)) - instances = [i.to_dict() for i in self.instances.values() if layer_id in i.layer_ids] + instances = [i.to_dict() for i in self.instances.values() + if layer_id in i.layer_ids] return instances if stack_id: if stack_id not in self.stacks: raise ResourceNotFoundException( "Unable to find stack with ID {0}".format(stack_id)) - instances = [i.to_dict() for i in self.instances.values() if stack_id==i.stack_id] + instances = [i.to_dict() for i in self.instances.values() + if stack_id == i.stack_id] return instances def start_instance(self, instance_id): diff --git a/moto/opsworks/responses.py b/moto/opsworks/responses.py index 4e0979154..42e0f2c5c 100644 --- a/moto/opsworks/responses.py +++ b/moto/opsworks/responses.py @@ -22,19 +22,24 @@ class OpsWorksResponse(BaseResponse): region=self.parameters.get("Region"), vpcid=self.parameters.get("VpcId"), attributes=self.parameters.get("Attributes"), - default_instance_profile_arn=self.parameters.get("DefaultInstanceProfileArn"), + default_instance_profile_arn=self.parameters.get( + "DefaultInstanceProfileArn"), default_os=self.parameters.get("DefaultOs"), hostname_theme=self.parameters.get("HostnameTheme"), - default_availability_zone=self.parameters.get("DefaultAvailabilityZone"), + default_availability_zone=self.parameters.get( + "DefaultAvailabilityZone"), default_subnet_id=self.parameters.get("DefaultInstanceProfileArn"), custom_json=self.parameters.get("CustomJson"), configuration_manager=self.parameters.get("ConfigurationManager"), chef_configuration=self.parameters.get("ChefConfiguration"), use_custom_cookbooks=self.parameters.get("UseCustomCookbooks"), - use_opsworks_security_groups=self.parameters.get("UseOpsworksSecurityGroups"), - custom_cookbooks_source=self.parameters.get("CustomCookbooksSource"), + use_opsworks_security_groups=self.parameters.get( + "UseOpsworksSecurityGroups"), + custom_cookbooks_source=self.parameters.get( + "CustomCookbooksSource"), default_ssh_keyname=self.parameters.get("DefaultSshKeyName"), - default_root_device_type=self.parameters.get("DefaultRootDeviceType"), + default_root_device_type=self.parameters.get( + "DefaultRootDeviceType"), service_role_arn=self.parameters.get("ServiceRoleArn"), agent_version=self.parameters.get("AgentVersion"), ) @@ -48,18 +53,24 @@ class OpsWorksResponse(BaseResponse): name=self.parameters.get('Name'), shortname=self.parameters.get('Shortname'), attributes=self.parameters.get('Attributes'), - custom_instance_profile_arn=self.parameters.get("CustomInstanceProfileArn"), + custom_instance_profile_arn=self.parameters.get( + "CustomInstanceProfileArn"), custom_json=self.parameters.get("CustomJson"), - custom_security_group_ids=self.parameters.get('CustomSecurityGroupIds'), + custom_security_group_ids=self.parameters.get( + 'CustomSecurityGroupIds'), packages=self.parameters.get('Packages'), volume_configurations=self.parameters.get("VolumeConfigurations"), enable_autohealing=self.parameters.get("EnableAutoHealing"), - auto_assign_elastic_ips=self.parameters.get("AutoAssignElasticIps"), + auto_assign_elastic_ips=self.parameters.get( + "AutoAssignElasticIps"), auto_assign_public_ips=self.parameters.get("AutoAssignPublicIps"), custom_recipes=self.parameters.get("CustomRecipes"), - install_updates_on_boot=self.parameters.get("InstallUpdatesOnBoot"), - use_ebs_optimized_instances=self.parameters.get("UseEbsOptimizedInstances"), - lifecycle_event_configuration=self.parameters.get("LifecycleEventConfiguration") + install_updates_on_boot=self.parameters.get( + "InstallUpdatesOnBoot"), + use_ebs_optimized_instances=self.parameters.get( + "UseEbsOptimizedInstances"), + lifecycle_event_configuration=self.parameters.get( + "LifecycleEventConfiguration") ) layer = self.opsworks_backend.create_layer(**kwargs) return json.dumps({"LayerId": layer.id}, indent=1) @@ -80,7 +91,8 @@ class OpsWorksResponse(BaseResponse): architecture=self.parameters.get("Architecture"), root_device_type=self.parameters.get("RootDeviceType"), block_device_mappings=self.parameters.get("BlockDeviceMappings"), - install_updates_on_boot=self.parameters.get("InstallUpdatesOnBoot"), + install_updates_on_boot=self.parameters.get( + "InstallUpdatesOnBoot"), ebs_optimized=self.parameters.get("EbsOptimized"), agent_version=self.parameters.get("AgentVersion"), ) diff --git a/moto/packages/httpretty/__init__.py b/moto/packages/httpretty/__init__.py index a752b452a..679294a4b 100644 --- a/moto/packages/httpretty/__init__.py +++ b/moto/packages/httpretty/__init__.py @@ -55,6 +55,7 @@ def last_request(): """returns the last request""" return httpretty.last_request + def has_request(): """returns a boolean indicating whether any request has been made""" return not isinstance(httpretty.last_request.headers, EmptyRequestHeaders) diff --git a/moto/packages/httpretty/compat.py b/moto/packages/httpretty/compat.py index 6805cf638..b9e215b13 100644 --- a/moto/packages/httpretty/compat.py +++ b/moto/packages/httpretty/compat.py @@ -38,6 +38,7 @@ if PY3: # pragma: no cover basestring = (str, bytes) class BaseClass(object): + def __repr__(self): return self.__str__() else: # pragma: no cover @@ -49,6 +50,7 @@ else: # pragma: no cover class BaseClass(object): + def __repr__(self): ret = self.__str__() if PY3: # pragma: no cover @@ -63,6 +65,7 @@ try: # pragma: no cover except ImportError: # pragma: no cover from urlparse import urlsplit, urlunsplit, parse_qs, unquote from urllib import quote, quote_plus + def unquote_utf8(qs): if isinstance(qs, text_type): qs = qs.encode('utf-8') diff --git a/moto/packages/httpretty/core.py b/moto/packages/httpretty/core.py index 4764cbba9..b409711cf 100644 --- a/moto/packages/httpretty/core.py +++ b/moto/packages/httpretty/core.py @@ -138,6 +138,7 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): `content-type` headers values: 'application/json' or 'application/x-www-form-urlencoded' """ + def __init__(self, headers, body=''): # first of all, lets make sure that if headers or body are # unicode strings, it must be converted into a utf-8 encoded @@ -149,8 +150,8 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): # `rfile` based on it self.rfile = StringIO(b'\r\n\r\n'.join([self.raw_headers, self.body])) self.wfile = StringIO() # Creating `wfile` as an empty - # StringIO, just to avoid any real - # I/O calls + # StringIO, just to avoid any real + # I/O calls # parsing the request line preemptively self.raw_requestline = self.rfile.readline() @@ -229,12 +230,14 @@ class HTTPrettyRequestEmpty(object): class FakeSockFile(StringIO): + def close(self): self.socket.close() StringIO.close(self) class FakeSSLSocket(object): + def __init__(self, sock, *args, **kw): self._httpretty_sock = sock @@ -243,6 +246,7 @@ class FakeSSLSocket(object): class fakesock(object): + class socket(object): _entry = None debuglevel = 0 @@ -374,13 +378,15 @@ class fakesock(object): self.fd.socket = self try: requestline, _ = data.split(b'\r\n', 1) - method, path, version = parse_requestline(decode_utf8(requestline)) + method, path, version = parse_requestline( + decode_utf8(requestline)) is_parsing_headers = True except ValueError: is_parsing_headers = False if not self._entry: - # If the previous request wasn't mocked, don't mock the subsequent sending of data + # If the previous request wasn't mocked, don't mock the + # subsequent sending of data return self.real_sendall(data, *args, **kw) self.fd.seek(0) @@ -492,6 +498,7 @@ def fake_getaddrinfo( class Entry(BaseClass): + def __init__(self, method, uri, body, adding_headers=None, forcing_headers=None, @@ -543,15 +550,15 @@ class Entry(BaseClass): igot = int(got) except ValueError: warnings.warn( - 'HTTPretty got to register the Content-Length header ' \ + 'HTTPretty got to register the Content-Length header ' 'with "%r" which is not a number' % got, ) if igot > self.body_length: raise HTTPrettyError( - 'HTTPretty got inconsistent parameters. The header ' \ - 'Content-Length you registered expects size "%d" but ' \ - 'the body you registered for that has actually length ' \ + 'HTTPretty got inconsistent parameters. The header ' + 'Content-Length you registered expects size "%d" but ' + 'the body you registered for that has actually length ' '"%d".' % ( igot, self.body_length, ) @@ -588,7 +595,8 @@ class Entry(BaseClass): headers = self.normalize_headers(headers) status = headers.get('status', self.status) if self.body_is_callable: - status, headers, self.body = self.callable_body(self.request, self.info.full_url(), headers) + status, headers, self.body = self.callable_body( + self.request, self.info.full_url(), headers) if self.request.method != "HEAD": headers.update({ 'content-length': len(self.body) @@ -641,6 +649,7 @@ def url_fix(s, charset='utf-8'): class URIInfo(BaseClass): + def __init__(self, username='', password='', @@ -764,7 +773,7 @@ class URIMatcher(object): self.entries = entries - #hash of current_entry pointers, per method. + # hash of current_entry pointers, per method. self.current_entries = {} def matches(self, info): @@ -788,7 +797,7 @@ class URIMatcher(object): if method not in self.current_entries: self.current_entries[method] = 0 - #restrict selection to entries that match the requested method + # restrict selection to entries that match the requested method entries_for_method = [e for e in self.entries if e.method == method] if self.current_entries[method] >= len(entries_for_method): @@ -841,13 +850,14 @@ class httpretty(HttpBaseClass): try: import urllib3 except ImportError: - raise RuntimeError('HTTPretty requires urllib3 installed for recording actual requests.') - + raise RuntimeError( + 'HTTPretty requires urllib3 installed for recording actual requests.') http = urllib3.PoolManager() cls.enable() calls = [] + def record_request(request, uri, headers): cls.disable() @@ -870,7 +880,8 @@ class httpretty(HttpBaseClass): return response.status, response.headers, response.data for method in cls.METHODS: - cls.register_uri(method, re.compile(r'.*', re.M), body=record_request) + cls.register_uri(method, re.compile( + r'.*', re.M), body=record_request) yield cls.disable() @@ -886,7 +897,8 @@ class httpretty(HttpBaseClass): for item in data: uri = item['request']['uri'] method = item['request']['method'] - cls.register_uri(method, uri, body=item['response']['body'], forcing_headers=item['response']['headers']) + cls.register_uri(method, uri, body=item['response'][ + 'body'], forcing_headers=item['response']['headers']) yield cls.disable() diff --git a/moto/packages/httpretty/errors.py b/moto/packages/httpretty/errors.py index cb6479bf5..e2dcad357 100644 --- a/moto/packages/httpretty/errors.py +++ b/moto/packages/httpretty/errors.py @@ -32,6 +32,7 @@ class HTTPrettyError(Exception): class UnmockedError(HTTPrettyError): + def __init__(self): super(UnmockedError, self).__init__( 'No mocking was registered, and real connections are ' diff --git a/moto/packages/responses/responses.py b/moto/packages/responses/responses.py index 735655664..1f5892b25 100644 --- a/moto/packages/responses/responses.py +++ b/moto/packages/responses/responses.py @@ -82,6 +82,7 @@ def get_wrapped(func, wrapper_template, evaldict): class CallList(Sequence, Sized): + def __init__(self): self._calls = [] @@ -298,10 +299,10 @@ class RequestsMock(object): def unbound_on_send(adapter, request, *a, **kwargs): return self._on_request(adapter, request, *a, **kwargs) self._patcher1 = mock.patch('botocore.vendored.requests.adapters.HTTPAdapter.send', - unbound_on_send) + unbound_on_send) self._patcher1.start() self._patcher2 = mock.patch('requests.adapters.HTTPAdapter.send', - unbound_on_send) + unbound_on_send) self._patcher2.start() def stop(self, allow_assert=True): diff --git a/moto/packages/responses/setup.py b/moto/packages/responses/setup.py index bab522865..911c07da4 100644 --- a/moto/packages/responses/setup.py +++ b/moto/packages/responses/setup.py @@ -57,6 +57,7 @@ except Exception: class PyTest(TestCommand): + def finalize_options(self): TestCommand.finalize_options(self) self.test_args = ['test_responses.py'] diff --git a/moto/packages/responses/test_responses.py b/moto/packages/responses/test_responses.py index ba0126ad5..967a535cf 100644 --- a/moto/packages/responses/test_responses.py +++ b/moto/packages/responses/test_responses.py @@ -284,6 +284,7 @@ def test_custom_adapter(): calls = [0] class DummyAdapter(requests.adapters.HTTPAdapter): + def send(self, *a, **k): calls[0] += 1 return super(DummyAdapter, self).send(*a, **k) diff --git a/moto/rds/__init__.py b/moto/rds/__init__.py index 2c8c0ba97..a4086d89c 100644 --- a/moto/rds/__init__.py +++ b/moto/rds/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import rds_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator rds_backend = rds_backends['us-east-1'] mock_rds = base_decorator(rds_backends) diff --git a/moto/rds/exceptions.py b/moto/rds/exceptions.py index 936b979d2..5bcc95560 100644 --- a/moto/rds/exceptions.py +++ b/moto/rds/exceptions.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import BadRequest class RDSClientError(BadRequest): + def __init__(self, code, message): super(RDSClientError, self).__init__() self.description = json.dumps({ @@ -18,6 +19,7 @@ class RDSClientError(BadRequest): class DBInstanceNotFoundError(RDSClientError): + def __init__(self, database_identifier): super(DBInstanceNotFoundError, self).__init__( 'DBInstanceNotFound', @@ -25,6 +27,7 @@ class DBInstanceNotFoundError(RDSClientError): class DBSecurityGroupNotFoundError(RDSClientError): + def __init__(self, security_group_name): super(DBSecurityGroupNotFoundError, self).__init__( 'DBSecurityGroupNotFound', @@ -32,6 +35,7 @@ class DBSecurityGroupNotFoundError(RDSClientError): class DBSubnetGroupNotFoundError(RDSClientError): + def __init__(self, subnet_group_name): super(DBSubnetGroupNotFoundError, self).__init__( 'DBSubnetGroupNotFound', diff --git a/moto/rds/models.py b/moto/rds/models.py index b63a30737..4334a9f72 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import copy import datetime import boto.rds @@ -11,10 +10,10 @@ from moto.core import BaseBackend from moto.core.utils import get_random_hex from moto.ec2.models import ec2_backends from moto.rds2.models import rds2_backends -from .exceptions import DBInstanceNotFoundError, DBSecurityGroupNotFoundError, DBSubnetGroupNotFoundError class Database(object): + def __init__(self, **kwargs): self.status = "available" @@ -35,7 +34,8 @@ class Database(object): self.storage_type = kwargs.get("storage_type") self.master_username = kwargs.get('master_username') self.master_password = kwargs.get('master_password') - self.auto_minor_version_upgrade = kwargs.get('auto_minor_version_upgrade') + self.auto_minor_version_upgrade = kwargs.get( + 'auto_minor_version_upgrade') if self.auto_minor_version_upgrade is None: self.auto_minor_version_upgrade = True self.allocated_storage = kwargs.get('allocated_storage') @@ -57,7 +57,8 @@ class Database(object): self.db_subnet_group_name = kwargs.get("db_subnet_group_name") self.instance_create_time = str(datetime.datetime.utcnow()) if self.db_subnet_group_name: - self.db_subnet_group = rds_backends[self.region].describe_subnet_groups(self.db_subnet_group_name)[0] + self.db_subnet_group = rds_backends[ + self.region].describe_subnet_groups(self.db_subnet_group_name)[0] else: self.db_subnet_group = [] @@ -239,6 +240,7 @@ class Database(object): class SecurityGroup(object): + def __init__(self, group_name, description): self.group_name = group_name self.description = description @@ -284,7 +286,8 @@ class SecurityGroup(object): properties = cloudformation_json['Properties'] group_name = resource_name.lower() + get_random_hex(12) description = properties['GroupDescription'] - security_group_ingress_rules = properties.get('DBSecurityGroupIngress', []) + security_group_ingress_rules = properties.get( + 'DBSecurityGroupIngress', []) tags = properties.get('Tags') ec2_backend = ec2_backends[region_name] @@ -300,10 +303,12 @@ class SecurityGroup(object): if ingress_type == "CIDRIP": security_group.authorize_cidr(ingress_value) elif ingress_type == "EC2SecurityGroupName": - subnet = ec2_backend.get_security_group_from_name(ingress_value) + subnet = ec2_backend.get_security_group_from_name( + ingress_value) security_group.authorize_security_group(subnet) elif ingress_type == "EC2SecurityGroupId": - subnet = ec2_backend.get_security_group_from_id(ingress_value) + subnet = ec2_backend.get_security_group_from_id( + ingress_value) security_group.authorize_security_group(subnet) return security_group @@ -313,6 +318,7 @@ class SecurityGroup(object): class SubnetGroup(object): + def __init__(self, subnet_name, description, subnets): self.subnet_name = subnet_name self.description = description @@ -352,7 +358,8 @@ class SubnetGroup(object): tags = properties.get('Tags') ec2_backend = ec2_backends[region_name] - subnets = [ec2_backend.get_subnet(subnet_id) for subnet_id in subnet_ids] + subnets = [ec2_backend.get_subnet(subnet_id) + for subnet_id in subnet_ids] rds_backend = rds_backends[region_name] subnet_group = rds_backend.create_subnet_group( subnet_name, @@ -385,4 +392,6 @@ class RDSBackend(BaseBackend): def rds2_backend(self): return rds2_backends[self.region] -rds_backends = dict((region.name, RDSBackend(region.name)) for region in boto.rds.regions()) + +rds_backends = dict((region.name, RDSBackend(region.name)) + for region in boto.rds.regions()) diff --git a/moto/rds/responses.py b/moto/rds/responses.py index 5207264f6..6b51c8fe6 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -41,7 +41,8 @@ class RDSResponse(BaseResponse): # VpcSecurityGroupIds.member.N "tags": list(), } - args['tags'] = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) + args['tags'] = self.unpack_complex_list_params( + 'Tags.Tag', ('Key', 'Value')) return args def _get_db_replica_kwargs(self): @@ -65,7 +66,8 @@ class RDSResponse(BaseResponse): while self._get_param('{0}.{1}.{2}'.format(label, count, names[0])): param = dict() for i in range(len(names)): - param[names[i]] = self._get_param('{0}.{1}.{2}'.format(label, count, names[i])) + param[names[i]] = self._get_param( + '{0}.{1}.{2}'.format(label, count, names[i])) unpacked_list.append(param) count += 1 return unpacked_list @@ -93,7 +95,8 @@ class RDSResponse(BaseResponse): def modify_dbinstance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') db_kwargs = self._get_db_kwargs() - database = self.backend.modify_database(db_instance_identifier, db_kwargs) + database = self.backend.modify_database( + db_instance_identifier, db_kwargs) template = self.response_template(MODIFY_DATABASE_TEMPLATE) return template.render(database=database) @@ -107,26 +110,30 @@ class RDSResponse(BaseResponse): group_name = self._get_param('DBSecurityGroupName') description = self._get_param('DBSecurityGroupDescription') tags = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) - security_group = self.backend.create_security_group(group_name, description, tags) + security_group = self.backend.create_security_group( + group_name, description, tags) template = self.response_template(CREATE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) def describe_dbsecurity_groups(self): security_group_name = self._get_param('DBSecurityGroupName') - security_groups = self.backend.describe_security_groups(security_group_name) + security_groups = self.backend.describe_security_groups( + security_group_name) template = self.response_template(DESCRIBE_SECURITY_GROUPS_TEMPLATE) return template.render(security_groups=security_groups) def delete_dbsecurity_group(self): security_group_name = self._get_param('DBSecurityGroupName') - security_group = self.backend.delete_security_group(security_group_name) + security_group = self.backend.delete_security_group( + security_group_name) template = self.response_template(DELETE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) def authorize_dbsecurity_group_ingress(self): security_group_name = self._get_param('DBSecurityGroupName') cidr_ip = self._get_param('CIDRIP') - security_group = self.backend.authorize_security_group(security_group_name, cidr_ip) + security_group = self.backend.authorize_security_group( + security_group_name, cidr_ip) template = self.response_template(AUTHORIZE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) @@ -134,9 +141,11 @@ class RDSResponse(BaseResponse): subnet_name = self._get_param('DBSubnetGroupName') description = self._get_param('DBSubnetGroupDescription') subnet_ids = self._get_multi_param('SubnetIds.member') - subnets = [ec2_backends[self.region].get_subnet(subnet_id) for subnet_id in subnet_ids] + subnets = [ec2_backends[self.region].get_subnet( + subnet_id) for subnet_id in subnet_ids] tags = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) - subnet_group = self.backend.create_subnet_group(subnet_name, description, subnets, tags) + subnet_group = self.backend.create_subnet_group( + subnet_name, description, subnets, tags) template = self.response_template(CREATE_SUBNET_GROUP_TEMPLATE) return template.render(subnet_group=subnet_group) diff --git a/moto/rds2/__init__.py b/moto/rds2/__init__.py index 0feecfac4..723fa0968 100644 --- a/moto/rds2/__init__.py +++ b/moto/rds2/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import rds2_backends -from ..core.models import MockAWS, base_decorator, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator rds2_backend = rds2_backends['us-west-1'] mock_rds2 = base_decorator(rds2_backends) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index 6fcae4b56..29e92941d 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import BadRequest class RDSClientError(BadRequest): + def __init__(self, code, message): super(RDSClientError, self).__init__() template = Template(""" @@ -20,6 +21,7 @@ class RDSClientError(BadRequest): class DBInstanceNotFoundError(RDSClientError): + def __init__(self, database_identifier): super(DBInstanceNotFoundError, self).__init__( 'DBInstanceNotFound', @@ -27,6 +29,7 @@ class DBInstanceNotFoundError(RDSClientError): class DBSecurityGroupNotFoundError(RDSClientError): + def __init__(self, security_group_name): super(DBSecurityGroupNotFoundError, self).__init__( 'DBSecurityGroupNotFound', @@ -34,12 +37,15 @@ class DBSecurityGroupNotFoundError(RDSClientError): class DBSubnetGroupNotFoundError(RDSClientError): + def __init__(self, subnet_group_name): super(DBSubnetGroupNotFoundError, self).__init__( 'DBSubnetGroupNotFound', "Subnet Group {0} not found.".format(subnet_group_name)) + class DBParameterGroupNotFoundError(RDSClientError): + def __init__(self, db_parameter_group_name): super(DBParameterGroupNotFoundError, self).__init__( 'DBParameterGroupNotFound', diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 9bb1f8200..52cb298cd 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -4,7 +4,6 @@ import copy from collections import defaultdict import boto.rds2 -import json from jinja2 import Template from re import compile as re_compile from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -18,8 +17,8 @@ from .exceptions import (RDSClientError, DBParameterGroupNotFoundError) - class Database(object): + def __init__(self, **kwargs): self.status = "available" self.is_replica = False @@ -49,7 +48,8 @@ class Database(object): self.storage_type = kwargs.get("storage_type") self.master_username = kwargs.get('master_username') self.master_user_password = kwargs.get('master_user_password') - self.auto_minor_version_upgrade = kwargs.get('auto_minor_version_upgrade') + self.auto_minor_version_upgrade = kwargs.get( + 'auto_minor_version_upgrade') if self.auto_minor_version_upgrade is None: self.auto_minor_version_upgrade = True self.allocated_storage = kwargs.get('allocated_storage') @@ -69,18 +69,22 @@ class Database(object): self.multi_az = kwargs.get("multi_az") self.db_subnet_group_name = kwargs.get("db_subnet_group_name") if self.db_subnet_group_name: - self.db_subnet_group = rds2_backends[self.region].describe_subnet_groups(self.db_subnet_group_name)[0] + self.db_subnet_group = rds2_backends[ + self.region].describe_subnet_groups(self.db_subnet_group_name)[0] else: self.db_subnet_group = None self.security_groups = kwargs.get('security_groups', []) self.vpc_security_group_ids = kwargs.get('vpc_security_group_ids', []) - self.preferred_maintenance_window = kwargs.get('preferred_maintenance_window', 'wed:06:38-wed:07:08') + self.preferred_maintenance_window = kwargs.get( + 'preferred_maintenance_window', 'wed:06:38-wed:07:08') self.db_parameter_group_name = kwargs.get('db_parameter_group_name') if self.db_parameter_group_name and self.db_parameter_group_name not in rds2_backends[self.region].db_parameter_groups: - raise DBParameterGroupNotFoundError(self.db_parameter_group_name) + raise DBParameterGroupNotFoundError(self.db_parameter_group_name) - self.preferred_backup_window = kwargs.get('preferred_backup_window', '13:14-13:44') - self.license_model = kwargs.get('license_model', 'general-public-license') + self.preferred_backup_window = kwargs.get( + 'preferred_backup_window', '13:14-13:44') + self.license_model = kwargs.get( + 'license_model', 'general-public-license') self.option_group_name = kwargs.get('option_group_name', None) self.default_option_groups = {"MySQL": "default.mysql5.6", "mysql": "default.mysql5.6", @@ -100,9 +104,9 @@ class Database(object): db_family, db_parameter_group_name = self.default_db_parameter_group_details() description = 'Default parameter group for {0}'.format(db_family) return [DBParameterGroup(name=db_parameter_group_name, - family=db_family, - description=description, - tags={})] + family=db_family, + description=description, + tags={})] else: return [rds2_backends[self.region].db_parameter_groups[self.db_parameter_group_name]] @@ -354,12 +358,14 @@ class Database(object): def add_tags(self, tags): new_keys = [tag_set['Key'] for tag_set in tags] - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in new_keys] self.tags.extend(tags) return self.tags def remove_tags(self, tag_keys): - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in tag_keys] def delete(self, region_name): backend = rds2_backends[region_name] @@ -367,6 +373,7 @@ class Database(object): class SecurityGroup(object): + def __init__(self, group_name, description, tags): self.group_name = group_name self.description = description @@ -430,7 +437,8 @@ class SecurityGroup(object): properties = cloudformation_json['Properties'] group_name = resource_name.lower() + get_random_hex(12) description = properties['GroupDescription'] - security_group_ingress_rules = properties.get('DBSecurityGroupIngress', []) + security_group_ingress_rules = properties.get( + 'DBSecurityGroupIngress', []) tags = properties.get('Tags') ec2_backend = ec2_backends[region_name] @@ -445,10 +453,12 @@ class SecurityGroup(object): if ingress_type == "CIDRIP": security_group.authorize_cidr(ingress_value) elif ingress_type == "EC2SecurityGroupName": - subnet = ec2_backend.get_security_group_from_name(ingress_value) + subnet = ec2_backend.get_security_group_from_name( + ingress_value) security_group.authorize_security_group(subnet) elif ingress_type == "EC2SecurityGroupId": - subnet = ec2_backend.get_security_group_from_id(ingress_value) + subnet = ec2_backend.get_security_group_from_id( + ingress_value) security_group.authorize_security_group(subnet) return security_group @@ -457,12 +467,14 @@ class SecurityGroup(object): def add_tags(self, tags): new_keys = [tag_set['Key'] for tag_set in tags] - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in new_keys] self.tags.extend(tags) return self.tags def remove_tags(self, tag_keys): - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in tag_keys] def delete(self, region_name): backend = rds2_backends[region_name] @@ -470,6 +482,7 @@ class SecurityGroup(object): class SubnetGroup(object): + def __init__(self, subnet_name, description, subnets, tags): self.subnet_name = subnet_name self.description = description @@ -530,7 +543,8 @@ class SubnetGroup(object): tags = properties.get('Tags') ec2_backend = ec2_backends[region_name] - subnets = [ec2_backend.get_subnet(subnet_id) for subnet_id in subnet_ids] + subnets = [ec2_backend.get_subnet(subnet_id) + for subnet_id in subnet_ids] rds2_backend = rds2_backends[region_name] subnet_group = rds2_backend.create_subnet_group( subnet_name, @@ -545,12 +559,14 @@ class SubnetGroup(object): def add_tags(self, tags): new_keys = [tag_set['Key'] for tag_set in tags] - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in new_keys] self.tags.extend(tags) return self.tags def remove_tags(self, tag_keys): - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in tag_keys] def delete(self, region_name): backend = rds2_backends[region_name] @@ -561,7 +577,8 @@ class RDS2Backend(BaseBackend): def __init__(self, region): self.region = region - self.arn_regex = re_compile(r'^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$') + self.arn_regex = re_compile( + r'^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$') self.databases = {} self.db_parameter_groups = {} self.option_groups = {} @@ -699,14 +716,16 @@ class RDS2Backend(BaseBackend): raise RDSClientError('InvalidParameterValue', 'The parameter OptionGroupDescription must be provided and must not be blank.') if option_group_kwargs['engine_name'] not in valid_option_group_engines.keys(): - raise RDSClientError('InvalidParameterValue', 'Invalid DB engine: non-existant') + raise RDSClientError('InvalidParameterValue', + 'Invalid DB engine: non-existant') if option_group_kwargs['major_engine_version'] not in\ valid_option_group_engines[option_group_kwargs['engine_name']]: - raise RDSClientError('InvalidParameterCombination', - 'Cannot find major version {0} for {1}'.format( - option_group_kwargs['major_engine_version'], - option_group_kwargs['engine_name'] - )) + raise RDSClientError('InvalidParameterCombination', + 'Cannot find major version {0} for {1}'.format( + option_group_kwargs[ + 'major_engine_version'], + option_group_kwargs['engine_name'] + )) option_group = OptionGroup(**option_group_kwargs) self.option_groups[option_group_id] = option_group return option_group @@ -715,7 +734,8 @@ class RDS2Backend(BaseBackend): if option_group_name in self.option_groups: return self.option_groups.pop(option_group_name) else: - raise RDSClientError('OptionGroupNotFoundFault', 'Specified OptionGroupName: {0} not found.'.format(option_group_name)) + raise RDSClientError( + 'OptionGroupNotFoundFault', 'Specified OptionGroupName: {0} not found.'.format(option_group_name)) def describe_option_groups(self, option_group_kwargs): option_group_list = [] @@ -746,24 +766,25 @@ class RDS2Backend(BaseBackend): if not len(option_group_list): raise RDSClientError('OptionGroupNotFoundFault', 'Specified OptionGroupName: {0} not found.'.format(option_group_kwargs['name'])) - return option_group_list[marker:max_records+marker] + return option_group_list[marker:max_records + marker] @staticmethod def describe_option_group_options(engine_name, major_engine_version=None): default_option_group_options = {'mysql': {'5.6': '\n \n \n \n 5.611211TrueInnodb Memcached for MySQLMEMCACHED1-4294967295STATIC1TrueSpecifies how many memcached read operations (get) to perform before doing a COMMIT to start a new transactionDAEMON_MEMCACHED_R_BATCH_SIZE1-4294967295STATIC1TrueSpecifies how many memcached write operations, such as add, set, or incr, to perform before doing a COMMIT to start a new transactionDAEMON_MEMCACHED_W_BATCH_SIZE1-1073741824DYNAMIC5TrueSpecifies how often to auto-commit idle connections that use the InnoDB memcached interface.INNODB_API_BK_COMMIT_INTERVAL0,1STATIC0TrueDisables the use of row locks when using the InnoDB memcached interface.INNODB_API_DISABLE_ROWLOCK0,1STATIC0TrueLocks the table used by the InnoDB memcached plugin, so that it cannot be dropped or altered by DDL through the SQL interface.INNODB_API_ENABLE_MDL0-3STATIC0TrueLets you control the transaction isolation level on queries processed by the memcached interface.INNODB_API_TRX_LEVELauto,ascii,binarySTATICautoTrueThe binding protocol to use which can be either auto, ascii, or binary. The default is auto which means the server automatically negotiates the protocol with the client.BINDING_PROTOCOL1-2048STATIC1024TrueThe backlog queue configures how many network connections can be waiting to be processed by memcachedBACKLOG_QUEUE_LIMIT0,1STATIC0TrueDisable the use of compare and swap (CAS) which reduces the per-item size by 8 bytes.CAS_DISABLED1-48STATIC48TrueMinimum chunk size in bytes to allocate for the smallest item\'s key, value, and flags. The default is 48 and you can get a significant memory efficiency gain with a lower value.CHUNK_SIZE1-2STATIC1.25TrueChunk size growth factor that controls the size of each successive chunk with each chunk growing times this amount larger than the previous chunk.CHUNK_SIZE_GROWTH_FACTOR0,1STATIC0TrueIf enabled when there is no more memory to store items, memcached will return an error rather than evicting items.ERROR_ON_MEMORY_EXHAUSTED10-1024STATIC1024TrueMaximum number of concurrent connections. Setting this value to anything less than 10 prevents MySQL from starting.MAX_SIMULTANEOUS_CONNECTIONSv,vv,vvvSTATICvTrueVerbose level for memcached.VERBOSITYmysql\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', - 'all': '\n \n \n \n 5.611211TrueInnodb Memcached for MySQLMEMCACHED1-4294967295STATIC1TrueSpecifies how many memcached read operations (get) to perform before doing a COMMIT to start a new transactionDAEMON_MEMCACHED_R_BATCH_SIZE1-4294967295STATIC1TrueSpecifies how many memcached write operations, such as add, set, or incr, to perform before doing a COMMIT to start a new transactionDAEMON_MEMCACHED_W_BATCH_SIZE1-1073741824DYNAMIC5TrueSpecifies how often to auto-commit idle connections that use the InnoDB memcached interface.INNODB_API_BK_COMMIT_INTERVAL0,1STATIC0TrueDisables the use of row locks when using the InnoDB memcached interface.INNODB_API_DISABLE_ROWLOCK0,1STATIC0TrueLocks the table used by the InnoDB memcached plugin, so that it cannot be dropped or altered by DDL through the SQL interface.INNODB_API_ENABLE_MDL0-3STATIC0TrueLets you control the transaction isolation level on queries processed by the memcached interface.INNODB_API_TRX_LEVELauto,ascii,binarySTATICautoTrueThe binding protocol to use which can be either auto, ascii, or binary. The default is auto which means the server automatically negotiates the protocol with the client.BINDING_PROTOCOL1-2048STATIC1024TrueThe backlog queue configures how many network connections can be waiting to be processed by memcachedBACKLOG_QUEUE_LIMIT0,1STATIC0TrueDisable the use of compare and swap (CAS) which reduces the per-item size by 8 bytes.CAS_DISABLED1-48STATIC48TrueMinimum chunk size in bytes to allocate for the smallest item\'s key, value, and flags. The default is 48 and you can get a significant memory efficiency gain with a lower value.CHUNK_SIZE1-2STATIC1.25TrueChunk size growth factor that controls the size of each successive chunk with each chunk growing times this amount larger than the previous chunk.CHUNK_SIZE_GROWTH_FACTOR0,1STATIC0TrueIf enabled when there is no more memory to store items, memcached will return an error rather than evicting items.ERROR_ON_MEMORY_EXHAUSTED10-1024STATIC1024TrueMaximum number of concurrent connections. Setting this value to anything less than 10 prevents MySQL from starting.MAX_SIMULTANEOUS_CONNECTIONSv,vv,vvvSTATICvTrueVerbose level for memcached.VERBOSITYmysql\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, - 'oracle-ee': {'11.2': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', - 'all': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, - 'oracle-sa': {'11.2': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', - 'all': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, - 'oracle-sa1': {'11.2': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', - 'all': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, - 'sqlserver-ee': {'10.50': '\n \n \n \n 10.50SQLServer Database MirroringMirroringsqlserver-ee\n \n 10.50TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', - '11.00': '\n \n \n \n 11.00SQLServer Database MirroringMirroringsqlserver-ee\n \n 11.00TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', - 'all': '\n \n \n \n 10.50SQLServer Database MirroringMirroringsqlserver-ee\n \n 10.50TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n 11.00SQLServer Database MirroringMirroringsqlserver-ee\n \n 11.00TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}} + 'all': '\n \n \n \n 5.611211TrueInnodb Memcached for MySQLMEMCACHED1-4294967295STATIC1TrueSpecifies how many memcached read operations (get) to perform before doing a COMMIT to start a new transactionDAEMON_MEMCACHED_R_BATCH_SIZE1-4294967295STATIC1TrueSpecifies how many memcached write operations, such as add, set, or incr, to perform before doing a COMMIT to start a new transactionDAEMON_MEMCACHED_W_BATCH_SIZE1-1073741824DYNAMIC5TrueSpecifies how often to auto-commit idle connections that use the InnoDB memcached interface.INNODB_API_BK_COMMIT_INTERVAL0,1STATIC0TrueDisables the use of row locks when using the InnoDB memcached interface.INNODB_API_DISABLE_ROWLOCK0,1STATIC0TrueLocks the table used by the InnoDB memcached plugin, so that it cannot be dropped or altered by DDL through the SQL interface.INNODB_API_ENABLE_MDL0-3STATIC0TrueLets you control the transaction isolation level on queries processed by the memcached interface.INNODB_API_TRX_LEVELauto,ascii,binarySTATICautoTrueThe binding protocol to use which can be either auto, ascii, or binary. The default is auto which means the server automatically negotiates the protocol with the client.BINDING_PROTOCOL1-2048STATIC1024TrueThe backlog queue configures how many network connections can be waiting to be processed by memcachedBACKLOG_QUEUE_LIMIT0,1STATIC0TrueDisable the use of compare and swap (CAS) which reduces the per-item size by 8 bytes.CAS_DISABLED1-48STATIC48TrueMinimum chunk size in bytes to allocate for the smallest item\'s key, value, and flags. The default is 48 and you can get a significant memory efficiency gain with a lower value.CHUNK_SIZE1-2STATIC1.25TrueChunk size growth factor that controls the size of each successive chunk with each chunk growing times this amount larger than the previous chunk.CHUNK_SIZE_GROWTH_FACTOR0,1STATIC0TrueIf enabled when there is no more memory to store items, memcached will return an error rather than evicting items.ERROR_ON_MEMORY_EXHAUSTED10-1024STATIC1024TrueMaximum number of concurrent connections. Setting this value to anything less than 10 prevents MySQL from starting.MAX_SIMULTANEOUS_CONNECTIONSv,vv,vvvSTATICvTrueVerbose level for memcached.VERBOSITYmysql\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, + 'oracle-ee': {'11.2': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', + 'all': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, + 'oracle-sa': {'11.2': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', + 'all': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, + 'oracle-sa1': {'11.2': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', + 'all': '\n \n \n \n 11.2XMLDBOracle Application Express Runtime EnvironmentAPEXoracle-ee\n \n 11.2APEXOracle Application Express Development EnvironmentAPEX-DEVoracle-ee\n \n 11.2Oracle Advanced Security - Native Network EncryptionNATIVE_NETWORK_ENCRYPTIONACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired encryption behaviorSQLNET.ENCRYPTION_SERVERACCEPTED,REJECTED,REQUESTED,REQUIREDSTATICREQUESTEDTrueSpecifies the desired data integrity behaviorSQLNET.CRYPTO_CHECKSUM_SERVERRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40STATICRC4_256,AES256,AES192,3DES168,RC4_128,AES128,3DES112,RC4_56,DES,RC4_40,DES40TrueSpecifies list of encryption algorithms in order of intended useSQLNET.ENCRYPTION_TYPES_SERVERSHA1,MD5STATICSHA1,MD5TrueSpecifies list of checksumming algorithms in order of intended useSQLNET.CRYPTO_CHECKSUM_TYPES_SERVERoracle-ee\n \n 11.21158TrueOracle Enterprise Manager (Database Control only)OEMoracle-ee\n \n 11.2Oracle StatspackSTATSPACKoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - Transparent Data EncryptionTDEoracle-ee\n \n 11.2TrueTrueOracle Advanced Security - TDE with HSMTDE_HSMoracle-ee\n \n 11.2TrueTrueChange time zoneTimezoneAfrica/Cairo,Africa/Casablanca,Africa/Harare,Africa/Monrovia,Africa/Nairobi,Africa/Tripoli,Africa/Windhoek,America/Araguaina,America/Asuncion,America/Bogota,America/Caracas,America/Chihuahua,America/Cuiaba,America/Denver,America/Fortaleza,America/Guatemala,America/Halifax,America/Manaus,America/Matamoros,America/Monterrey,America/Montevideo,America/Phoenix,America/Santiago,America/Tijuana,Asia/Amman,Asia/Ashgabat,Asia/Baghdad,Asia/Baku,Asia/Bangkok,Asia/Beirut,Asia/Calcutta,Asia/Damascus,Asia/Dhaka,Asia/Irkutsk,Asia/Jerusalem,Asia/Kabul,Asia/Karachi,Asia/Kathmandu,Asia/Krasnoyarsk,Asia/Magadan,Asia/Muscat,Asia/Novosibirsk,Asia/Riyadh,Asia/Seoul,Asia/Shanghai,Asia/Singapore,Asia/Taipei,Asia/Tehran,Asia/Tokyo,Asia/Ulaanbaatar,Asia/Vladivostok,Asia/Yakutsk,Asia/Yerevan,Atlantic/Azores,Australia/Adelaide,Australia/Brisbane,Australia/Darwin,Australia/Hobart,Australia/Perth,Australia/Sydney,Brazil/East,Canada/Newfoundland,Canada/Saskatchewan,Europe/Amsterdam,Europe/Athens,Europe/Dublin,Europe/Helsinki,Europe/Istanbul,Europe/Kaliningrad,Europe/Moscow,Europe/Paris,Europe/Prague,Europe/Sarajevo,Pacific/Auckland,Pacific/Fiji,Pacific/Guam,Pacific/Honolulu,Pacific/Samoa,US/Alaska,US/Central,US/Eastern,US/East-Indiana,US/Pacific,UTCDYNAMICUTCTrueSpecifies the timezone the user wants to change the system time toTIME_ZONEoracle-ee\n \n 11.2Oracle XMLDB RepositoryXMLDBoracle-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}, + 'sqlserver-ee': {'10.50': '\n \n \n \n 10.50SQLServer Database MirroringMirroringsqlserver-ee\n \n 10.50TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', + '11.00': '\n \n \n \n 11.00SQLServer Database MirroringMirroringsqlserver-ee\n \n 11.00TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n', + 'all': '\n \n \n \n 10.50SQLServer Database MirroringMirroringsqlserver-ee\n \n 10.50TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n 11.00SQLServer Database MirroringMirroringsqlserver-ee\n \n 11.00TrueSQL Server - Transparent Data EncryptionTDEsqlserver-ee\n \n \n \n \n 457f7bb8-9fbf-11e4-9084-5754f80d5144\n \n'}} if engine_name not in default_option_group_options: - raise RDSClientError('InvalidParameterValue', 'Invalid DB engine: {0}'.format(engine_name)) + raise RDSClientError('InvalidParameterValue', + 'Invalid DB engine: {0}'.format(engine_name)) if major_engine_version and major_engine_version not in default_option_group_options[engine_name]: raise RDSClientError('InvalidParameterCombination', 'Cannot find major version {0} for {1}'.format(major_engine_version, engine_name)) @@ -779,9 +800,11 @@ class RDS2Backend(BaseBackend): raise RDSClientError('InvalidParameterValue', 'At least one option must be added, modified, or removed.') if options_to_remove: - self.option_groups[option_group_name].remove_options(options_to_remove) + self.option_groups[option_group_name].remove_options( + options_to_remove) if options_to_include: - self.option_groups[option_group_name].add_options(options_to_include) + self.option_groups[option_group_name].add_options( + options_to_include) return self.option_groups[option_group_name] def create_db_parameter_group(self, db_parameter_group_kwargs): @@ -821,7 +844,7 @@ class RDS2Backend(BaseBackend): else: continue - return db_parameter_group_list[marker:max_records+marker] + return db_parameter_group_list[marker:max_records + marker] def modify_db_parameter_group(self, db_parameter_group_name, db_parameter_group_parameters): if db_parameter_group_name not in self.db_parameter_groups: @@ -832,22 +855,17 @@ class RDS2Backend(BaseBackend): return db_parameter_group - def delete_db_parameter_group(self, db_parameter_group_name): - if db_parameter_group_name in self.db_parameter_groups: - return self.db_parameter_groups.pop(db_parameter_group_name) - else: - raise DBParameterGroupNotFoundError(db_parameter_group_name) - def list_tags_for_resource(self, arn): if self.arn_regex.match(arn): arn_breakdown = arn.split(':') - resource_type = arn_breakdown[len(arn_breakdown)-2] - resource_name = arn_breakdown[len(arn_breakdown)-1] + resource_type = arn_breakdown[len(arn_breakdown) - 2] + resource_name = arn_breakdown[len(arn_breakdown) - 1] if resource_type == 'db': # Database if resource_name in self.databases: return self.databases[resource_name].get_tags() elif resource_type == 'es': # Event Subscription - # TODO: Complete call to tags on resource type Event Subscription + # TODO: Complete call to tags on resource type Event + # Subscription return [] elif resource_type == 'og': # Option Group if resource_name in self.option_groups: @@ -856,7 +874,8 @@ class RDS2Backend(BaseBackend): if resource_name in self.db_parameter_groups: return self.db_parameter_groups[resource_name].get_tags() elif resource_type == 'ri': # Reserved DB instance - # TODO: Complete call to tags on resource type Reserved DB instance + # TODO: Complete call to tags on resource type Reserved DB + # instance return [] elif resource_type == 'secgrp': # DB security group if resource_name in self.security_groups: @@ -875,8 +894,8 @@ class RDS2Backend(BaseBackend): def remove_tags_from_resource(self, arn, tag_keys): if self.arn_regex.match(arn): arn_breakdown = arn.split(':') - resource_type = arn_breakdown[len(arn_breakdown)-2] - resource_name = arn_breakdown[len(arn_breakdown)-1] + resource_type = arn_breakdown[len(arn_breakdown) - 2] + resource_name = arn_breakdown[len(arn_breakdown) - 1] if resource_type == 'db': # Database if resource_name in self.databases: self.databases[resource_name].remove_tags(tag_keys) @@ -904,8 +923,8 @@ class RDS2Backend(BaseBackend): def add_tags_to_resource(self, arn, tags): if self.arn_regex.match(arn): arn_breakdown = arn.split(':') - resource_type = arn_breakdown[len(arn_breakdown)-2] - resource_name = arn_breakdown[len(arn_breakdown)-1] + resource_type = arn_breakdown[len(arn_breakdown) - 2] + resource_name = arn_breakdown[len(arn_breakdown) - 1] if resource_type == 'db': # Database if resource_name in self.databases: return self.databases[resource_name].add_tags(tags) @@ -932,6 +951,7 @@ class RDS2Backend(BaseBackend): class OptionGroup(object): + def __init__(self, name, engine_name, major_engine_version, description=None): self.engine_name = engine_name self.major_engine_version = major_engine_version @@ -966,11 +986,13 @@ class OptionGroup(object): return template.render(option_group=self) def remove_options(self, options_to_remove): - # TODO: Check for option in self.options and remove if exists. Raise error otherwise + # TODO: Check for option in self.options and remove if exists. Raise + # error otherwise return def add_options(self, options_to_add): - # TODO: Validate option and add it to self.options. If invalid raise error + # TODO: Validate option and add it to self.options. If invalid raise + # error return def get_tags(self): @@ -978,22 +1000,26 @@ class OptionGroup(object): def add_tags(self, tags): new_keys = [tag_set['Key'] for tag_set in tags] - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in new_keys] self.tags.extend(tags) return self.tags def remove_tags(self, tag_keys): - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in tag_keys] class OptionGroupOption(object): + def __init__(self, **kwargs): self.default_port = kwargs.get('default_port') self.description = kwargs.get('description') self.engine_name = kwargs.get('engine_name') self.major_engine_version = kwargs.get('major_engine_version') self.name = kwargs.get('name') - self.option_group_option_settings = self._make_option_group_option_settings(kwargs.get('option_group_option_settings', [])) + self.option_group_option_settings = self._make_option_group_option_settings( + kwargs.get('option_group_option_settings', [])) self.options_depended_on = kwargs.get('options_depended_on', []) self.permanent = kwargs.get('permanent') self.persistent = kwargs.get('persistent') @@ -1044,6 +1070,7 @@ class OptionGroupOption(object): class OptionGroupOptionSetting(object): + def __init__(self, *kwargs): self.allowed_values = kwargs.get('allowed_values') self.apply_type = kwargs.get('apply_type') @@ -1063,7 +1090,9 @@ class OptionGroupOptionSetting(object): """) return template.render(option_group_option_setting=self) + class DBParameterGroup(object): + def __init__(self, name, description, family, tags): self.name = name self.description = description @@ -1084,12 +1113,14 @@ class DBParameterGroup(object): def add_tags(self, tags): new_keys = [tag_set['Key'] for tag_set in tags] - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in new_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in new_keys] self.tags.extend(tags) return self.tags def remove_tags(self, tag_keys): - self.tags = [tag_set for tag_set in self.tags if tag_set['Key'] not in tag_keys] + self.tags = [tag_set for tag_set in self.tags if tag_set[ + 'Key'] not in tag_keys] def update_parameters(self, new_parameters): for new_parameter in new_parameters: @@ -1118,9 +1149,11 @@ class DBParameterGroup(object): }) rds2_backend = rds2_backends[region_name] - db_parameter_group = rds2_backend.create_db_parameter_group(db_parameter_group_kwargs) + db_parameter_group = rds2_backend.create_db_parameter_group( + db_parameter_group_kwargs) db_parameter_group.update_parameters(db_parameter_group_parameters) return db_parameter_group -rds2_backends = dict((region.name, RDS2Backend(region.name)) for region in boto.rds2.regions()) +rds2_backends = dict((region.name, RDS2Backend(region.name)) + for region in boto.rds2.regions()) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 879edbdd3..96b98463d 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -5,8 +5,6 @@ from moto.core.responses import BaseResponse from moto.ec2.models import ec2_backends from .models import rds2_backends from .exceptions import DBParameterGroupNotFoundError -import json -import re class RDS2Response(BaseResponse): @@ -45,7 +43,8 @@ class RDS2Response(BaseResponse): # VpcSecurityGroupIds.member.N "tags": list(), } - args['tags'] = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) + args['tags'] = self.unpack_complex_list_params( + 'Tags.Tag', ('Key', 'Value')) return args def _get_db_replica_kwargs(self): @@ -85,7 +84,8 @@ class RDS2Response(BaseResponse): while self._get_param('{0}.{1}.{2}'.format(label, count, names[0])): param = dict() for i in range(len(names)): - param[names[i]] = self._get_param('{0}.{1}.{2}'.format(label, count, names[i])) + param[names[i]] = self._get_param( + '{0}.{1}.{2}'.format(label, count, names[i])) unpacked_list.append(param) count += 1 return unpacked_list @@ -94,7 +94,8 @@ class RDS2Response(BaseResponse): unpacked_list = list() count = 1 while self._get_param('{0}.{1}'.format(label, count)): - unpacked_list.append(self._get_param('{0}.{1}'.format(label, count))) + unpacked_list.append(self._get_param( + '{0}.{1}'.format(label, count))) count += 1 return unpacked_list @@ -132,7 +133,8 @@ class RDS2Response(BaseResponse): def modify_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') db_kwargs = self._get_db_kwargs() - database = self.backend.modify_database(db_instance_identifier, db_kwargs) + database = self.backend.modify_database( + db_instance_identifier, db_kwargs) template = self.response_template(MODIFY_DATABASE_TEMPLATE) return template.render(database=database) @@ -181,7 +183,8 @@ class RDS2Response(BaseResponse): group_name = self._get_param('DBSecurityGroupName') description = self._get_param('DBSecurityGroupDescription') tags = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) - security_group = self.backend.create_security_group(group_name, description, tags) + security_group = self.backend.create_security_group( + group_name, description, tags) template = self.response_template(CREATE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) @@ -190,7 +193,8 @@ class RDS2Response(BaseResponse): def describe_db_security_groups(self): security_group_name = self._get_param('DBSecurityGroupName') - security_groups = self.backend.describe_security_groups(security_group_name) + security_groups = self.backend.describe_security_groups( + security_group_name) template = self.response_template(DESCRIBE_SECURITY_GROUPS_TEMPLATE) return template.render(security_groups=security_groups) @@ -199,7 +203,8 @@ class RDS2Response(BaseResponse): def delete_db_security_group(self): security_group_name = self._get_param('DBSecurityGroupName') - security_group = self.backend.delete_security_group(security_group_name) + security_group = self.backend.delete_security_group( + security_group_name) template = self.response_template(DELETE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) @@ -209,7 +214,8 @@ class RDS2Response(BaseResponse): def authorize_db_security_group_ingress(self): security_group_name = self._get_param('DBSecurityGroupName') cidr_ip = self._get_param('CIDRIP') - security_group = self.backend.authorize_security_group(security_group_name, cidr_ip) + security_group = self.backend.authorize_security_group( + security_group_name, cidr_ip) template = self.response_template(AUTHORIZE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) @@ -221,8 +227,10 @@ class RDS2Response(BaseResponse): description = self._get_param('DBSubnetGroupDescription') subnet_ids = self._get_multi_param('SubnetIds.SubnetIdentifier') tags = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) - subnets = [ec2_backends[self.region].get_subnet(subnet_id) for subnet_id in subnet_ids] - subnet_group = self.backend.create_subnet_group(subnet_name, description, subnets, tags) + subnets = [ec2_backends[self.region].get_subnet( + subnet_id) for subnet_id in subnet_ids] + subnet_group = self.backend.create_subnet_group( + subnet_name, description, subnets, tags) template = self.response_template(CREATE_SUBNET_GROUP_TEMPLATE) return template.render(subnet_group=subnet_group) @@ -267,7 +275,8 @@ class RDS2Response(BaseResponse): def describe_option_group_options(self): engine_name = self._get_param('EngineName') major_engine_version = self._get_param('MajorEngineVersion') - option_group_options = self.backend.describe_option_group_options(engine_name, major_engine_version) + option_group_options = self.backend.describe_option_group_options( + engine_name, major_engine_version) return option_group_options def modify_option_group(self): @@ -287,7 +296,8 @@ class RDS2Response(BaseResponse): count = 1 options_to_remove = [] while self._get_param('OptionsToRemove.member.{0}'.format(count)): - options_to_remove.append(self._get_param('OptionsToRemove.member.{0}'.format(count))) + options_to_remove.append(self._get_param( + 'OptionsToRemove.member.{0}'.format(count))) count += 1 apply_immediately = self._get_param('ApplyImmediately') option_group = self.backend.modify_option_group(option_group_name, @@ -314,7 +324,8 @@ class RDS2Response(BaseResponse): kwargs['max_records'] = self._get_param('MaxRecords') kwargs['marker'] = self._get_param('Marker') db_parameter_groups = self.backend.describe_db_parameter_groups(kwargs) - template = self.response_template(DESCRIBE_DB_PARAMETER_GROUPS_TEMPLATE) + template = self.response_template( + DESCRIBE_DB_PARAMETER_GROUPS_TEMPLATE) return template.render(db_parameter_groups=db_parameter_groups) def modify_dbparameter_group(self): @@ -347,7 +358,8 @@ class RDS2Response(BaseResponse): def describe_db_parameters(self): db_parameter_group_name = self._get_param('DBParameterGroupName') - db_parameter_groups = self.backend.describe_db_parameter_groups({'name': db_parameter_group_name}) + db_parameter_groups = self.backend.describe_db_parameter_groups( + {'name': db_parameter_group_name}) if not db_parameter_groups: raise DBParameterGroupNotFoundError(db_parameter_group_name) @@ -359,7 +371,8 @@ class RDS2Response(BaseResponse): def delete_db_parameter_group(self): kwargs = self._get_db_parameter_group_kwargs() - db_parameter_group = self.backend.delete_db_parameter_group(kwargs['name']) + db_parameter_group = self.backend.delete_db_parameter_group(kwargs[ + 'name']) template = self.response_template(DELETE_DB_PARAMETER_GROUP_TEMPLATE) return template.render(db_parameter_group=db_parameter_group) diff --git a/moto/redshift/__init__.py b/moto/redshift/__init__.py index 58be5fc70..06f778e8d 100644 --- a/moto/redshift/__init__.py +++ b/moto/redshift/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import redshift_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator redshift_backend = redshift_backends['us-east-1'] mock_redshift = base_decorator(redshift_backends) diff --git a/moto/redshift/exceptions.py b/moto/redshift/exceptions.py index 6d1b2c3bb..8bcca807e 100644 --- a/moto/redshift/exceptions.py +++ b/moto/redshift/exceptions.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import BadRequest class RedshiftClientError(BadRequest): + def __init__(self, code, message): super(RedshiftClientError, self).__init__() self.description = json.dumps({ @@ -18,6 +19,7 @@ class RedshiftClientError(BadRequest): class ClusterNotFoundError(RedshiftClientError): + def __init__(self, cluster_identifier): super(ClusterNotFoundError, self).__init__( 'ClusterNotFound', @@ -25,6 +27,7 @@ class ClusterNotFoundError(RedshiftClientError): class ClusterSubnetGroupNotFoundError(RedshiftClientError): + def __init__(self, subnet_identifier): super(ClusterSubnetGroupNotFoundError, self).__init__( 'ClusterSubnetGroupNotFound', @@ -32,6 +35,7 @@ class ClusterSubnetGroupNotFoundError(RedshiftClientError): class ClusterSecurityGroupNotFoundError(RedshiftClientError): + def __init__(self, group_identifier): super(ClusterSecurityGroupNotFoundError, self).__init__( 'ClusterSecurityGroupNotFound', @@ -39,6 +43,7 @@ class ClusterSecurityGroupNotFoundError(RedshiftClientError): class ClusterParameterGroupNotFoundError(RedshiftClientError): + def __init__(self, group_identifier): super(ClusterParameterGroupNotFoundError, self).__init__( 'ClusterParameterGroupNotFound', @@ -46,6 +51,7 @@ class ClusterParameterGroupNotFoundError(RedshiftClientError): class InvalidSubnetError(RedshiftClientError): + def __init__(self, subnet_identifier): super(InvalidSubnetError, self).__init__( 'InvalidSubnet', diff --git a/moto/redshift/models.py b/moto/redshift/models.py index bd81526df..af6c6f643 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -13,13 +13,14 @@ from .exceptions import ( class Cluster(object): + def __init__(self, redshift_backend, cluster_identifier, node_type, master_username, - master_user_password, db_name, cluster_type, cluster_security_groups, - vpc_security_group_ids, cluster_subnet_group_name, availability_zone, - preferred_maintenance_window, cluster_parameter_group_name, - automated_snapshot_retention_period, port, cluster_version, - allow_version_upgrade, number_of_nodes, publicly_accessible, - encrypted, region): + master_user_password, db_name, cluster_type, cluster_security_groups, + vpc_security_group_ids, cluster_subnet_group_name, availability_zone, + preferred_maintenance_window, cluster_parameter_group_name, + automated_snapshot_retention_period, port, cluster_version, + allow_version_upgrade, number_of_nodes, publicly_accessible, + encrypted, region): self.redshift_backend = redshift_backend self.cluster_identifier = cluster_identifier self.node_type = node_type @@ -34,7 +35,8 @@ class Cluster(object): self.allow_version_upgrade = allow_version_upgrade if allow_version_upgrade is not None else True self.cluster_version = cluster_version if cluster_version else "1.0" self.port = int(port) if port else 5439 - self.automated_snapshot_retention_period = int(automated_snapshot_retention_period) if automated_snapshot_retention_period else 1 + self.automated_snapshot_retention_period = int( + automated_snapshot_retention_period) if automated_snapshot_retention_period else 1 self.preferred_maintenance_window = preferred_maintenance_window if preferred_maintenance_window else "Mon:03:00-Mon:03:30" if cluster_parameter_group_name: @@ -68,7 +70,8 @@ class Cluster(object): properties = cloudformation_json['Properties'] if 'ClusterSubnetGroupName' in properties: - subnet_group_name = properties['ClusterSubnetGroupName'].cluster_subnet_group_name + subnet_group_name = properties[ + 'ClusterSubnetGroupName'].cluster_subnet_group_name else: subnet_group_name = None cluster = redshift_backend.create_cluster( @@ -78,13 +81,17 @@ class Cluster(object): master_user_password=properties.get('MasterUserPassword'), db_name=properties.get('DBName'), cluster_type=properties.get('ClusterType'), - cluster_security_groups=properties.get('ClusterSecurityGroups', []), + cluster_security_groups=properties.get( + 'ClusterSecurityGroups', []), vpc_security_group_ids=properties.get('VpcSecurityGroupIds', []), cluster_subnet_group_name=subnet_group_name, availability_zone=properties.get('AvailabilityZone'), - preferred_maintenance_window=properties.get('PreferredMaintenanceWindow'), - cluster_parameter_group_name=properties.get('ClusterParameterGroupName'), - automated_snapshot_retention_period=properties.get('AutomatedSnapshotRetentionPeriod'), + preferred_maintenance_window=properties.get( + 'PreferredMaintenanceWindow'), + cluster_parameter_group_name=properties.get( + 'ClusterParameterGroupName'), + automated_snapshot_retention_period=properties.get( + 'AutomatedSnapshotRetentionPeriod'), port=properties.get('Port'), cluster_version=properties.get('ClusterVersion'), allow_version_upgrade=properties.get('AllowVersionUpgrade'), @@ -214,6 +221,7 @@ class SubnetGroup(object): class SecurityGroup(object): + def __init__(self, cluster_security_group_name, description): self.cluster_security_group_name = cluster_security_group_name self.description = description @@ -293,7 +301,8 @@ class RedshiftBackend(BaseBackend): def modify_cluster(self, **cluster_kwargs): cluster_identifier = cluster_kwargs.pop('cluster_identifier') - new_cluster_identifier = cluster_kwargs.pop('new_cluster_identifier', None) + new_cluster_identifier = cluster_kwargs.pop( + 'new_cluster_identifier', None) cluster = self.describe_clusters(cluster_identifier)[0] @@ -313,7 +322,8 @@ class RedshiftBackend(BaseBackend): raise ClusterNotFoundError(cluster_identifier) def create_cluster_subnet_group(self, cluster_subnet_group_name, description, subnet_ids): - subnet_group = SubnetGroup(self.ec2_backend, cluster_subnet_group_name, description, subnet_ids) + subnet_group = SubnetGroup( + self.ec2_backend, cluster_subnet_group_name, description, subnet_ids) self.subnet_groups[cluster_subnet_group_name] = subnet_group return subnet_group @@ -332,7 +342,8 @@ class RedshiftBackend(BaseBackend): raise ClusterSubnetGroupNotFoundError(subnet_identifier) def create_cluster_security_group(self, cluster_security_group_name, description): - security_group = SecurityGroup(cluster_security_group_name, description) + security_group = SecurityGroup( + cluster_security_group_name, description) self.security_groups[cluster_security_group_name] = security_group return security_group @@ -351,8 +362,9 @@ class RedshiftBackend(BaseBackend): raise ClusterSecurityGroupNotFoundError(security_group_identifier) def create_cluster_parameter_group(self, cluster_parameter_group_name, - group_family, description): - parameter_group = ParameterGroup(cluster_parameter_group_name, group_family, description) + group_family, description): + parameter_group = ParameterGroup( + cluster_parameter_group_name, group_family, description) self.parameter_groups[cluster_parameter_group_name] = parameter_group return parameter_group diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index a9c977b4e..23c653332 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -134,7 +134,8 @@ class RedshiftResponse(BaseResponse): def describe_cluster_subnet_groups(self): subnet_identifier = self._get_param("ClusterSubnetGroupName") - subnet_groups = self.redshift_backend.describe_cluster_subnet_groups(subnet_identifier) + subnet_groups = self.redshift_backend.describe_cluster_subnet_groups( + subnet_identifier) return json.dumps({ "DescribeClusterSubnetGroupsResponse": { @@ -160,7 +161,8 @@ class RedshiftResponse(BaseResponse): }) def create_cluster_security_group(self): - cluster_security_group_name = self._get_param('ClusterSecurityGroupName') + cluster_security_group_name = self._get_param( + 'ClusterSecurityGroupName') description = self._get_param('Description') security_group = self.redshift_backend.create_cluster_security_group( @@ -180,8 +182,10 @@ class RedshiftResponse(BaseResponse): }) def describe_cluster_security_groups(self): - cluster_security_group_name = self._get_param("ClusterSecurityGroupName") - security_groups = self.redshift_backend.describe_cluster_security_groups(cluster_security_group_name) + cluster_security_group_name = self._get_param( + "ClusterSecurityGroupName") + security_groups = self.redshift_backend.describe_cluster_security_groups( + cluster_security_group_name) return json.dumps({ "DescribeClusterSecurityGroupsResponse": { @@ -196,7 +200,8 @@ class RedshiftResponse(BaseResponse): def delete_cluster_security_group(self): security_group_identifier = self._get_param("ClusterSecurityGroupName") - self.redshift_backend.delete_cluster_security_group(security_group_identifier) + self.redshift_backend.delete_cluster_security_group( + security_group_identifier) return json.dumps({ "DeleteClusterSecurityGroupResponse": { @@ -230,7 +235,8 @@ class RedshiftResponse(BaseResponse): def describe_cluster_parameter_groups(self): cluster_parameter_group_name = self._get_param("ParameterGroupName") - parameter_groups = self.redshift_backend.describe_cluster_parameter_groups(cluster_parameter_group_name) + parameter_groups = self.redshift_backend.describe_cluster_parameter_groups( + cluster_parameter_group_name) return json.dumps({ "DescribeClusterParameterGroupsResponse": { @@ -245,7 +251,8 @@ class RedshiftResponse(BaseResponse): def delete_cluster_parameter_group(self): cluster_parameter_group_name = self._get_param("ParameterGroupName") - self.redshift_backend.delete_cluster_parameter_group(cluster_parameter_group_name) + self.redshift_backend.delete_cluster_parameter_group( + cluster_parameter_group_name) return json.dumps({ "DeleteClusterParameterGroupResponse": { diff --git a/moto/route53/models.py b/moto/route53/models.py index 6b293a1ca..338c6d30a 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -10,6 +10,7 @@ from moto.core.utils import get_random_hex class HealthCheck(object): + def __init__(self, health_check_id, health_check_args): self.id = health_check_id self.ip_address = health_check_args.get("ip_address") @@ -63,6 +64,7 @@ class HealthCheck(object): class RecordSet(object): + def __init__(self, kwargs): self.name = kwargs.get('Name') self._type = kwargs.get('Type') @@ -83,25 +85,29 @@ class RecordSet(object): if zone_name: hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) else: - hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"]) + hosted_zone = route53_backend.get_hosted_zone( + properties["HostedZoneId"]) record_set = hosted_zone.add_rrset(properties) return record_set @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): - cls.delete_from_cloudformation_json(original_resource.name, cloudformation_json, region_name) + cls.delete_from_cloudformation_json( + original_resource.name, cloudformation_json, region_name) return cls.create_from_cloudformation_json(new_resource_name, cloudformation_json, region_name) @classmethod def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): - # this will break if you changed the zone the record is in, unfortunately + # this will break if you changed the zone the record is in, + # unfortunately properties = cloudformation_json['Properties'] zone_name = properties.get("HostedZoneName") if zone_name: hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) else: - hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"]) + hosted_zone = route53_backend.get_hosted_zone( + properties["HostedZoneId"]) try: hosted_zone.delete_rrset_by_name(resource_name) @@ -141,7 +147,8 @@ class RecordSet(object): def delete(self, *args, **kwargs): ''' Not exposed as part of the Route 53 API - used for CloudFormation. args are ignored ''' - hosted_zone = route53_backend.get_hosted_zone_by_name(self.hosted_zone_name) + hosted_zone = route53_backend.get_hosted_zone_by_name( + self.hosted_zone_name) if not hosted_zone: hosted_zone = route53_backend.get_hosted_zone(self.hosted_zone_id) hosted_zone.delete_rrset_by_name(self.name) @@ -173,17 +180,21 @@ class FakeZone(object): return new_rrset def delete_rrset_by_name(self, name): - self.rrsets = [record_set for record_set in self.rrsets if record_set.name != name] + self.rrsets = [ + record_set for record_set in self.rrsets if record_set.name != name] def delete_rrset_by_id(self, set_identifier): - self.rrsets = [record_set for record_set in self.rrsets if record_set.set_identifier != set_identifier] + self.rrsets = [ + record_set for record_set in self.rrsets if record_set.set_identifier != set_identifier] def get_record_sets(self, type_filter, name_filter): record_sets = list(self.rrsets) # Copy the list if type_filter: - record_sets = [record_set for record_set in record_sets if record_set._type == type_filter] + record_sets = [ + record_set for record_set in record_sets if record_set._type == type_filter] if name_filter: - record_sets = [record_set for record_set in record_sets if record_set.name == name_filter] + record_sets = [ + record_set for record_set in record_sets if record_set.name == name_filter] return record_sets @@ -196,11 +207,13 @@ class FakeZone(object): properties = cloudformation_json['Properties'] name = properties["Name"] - hosted_zone = route53_backend.create_hosted_zone(name, private_zone=False) + hosted_zone = route53_backend.create_hosted_zone( + name, private_zone=False) return hosted_zone class RecordSetGroup(object): + def __init__(self, hosted_zone_id, record_sets): self.hosted_zone_id = hosted_zone_id self.record_sets = record_sets @@ -232,7 +245,8 @@ class Route53Backend(BaseBackend): def create_hosted_zone(self, name, private_zone, comment=None): new_id = get_random_hex() - new_zone = FakeZone(name, new_id, private_zone=private_zone, comment=comment) + new_zone = FakeZone( + name, new_id, private_zone=private_zone, comment=comment) self.zones[new_id] = new_zone return new_zone @@ -285,4 +299,5 @@ class Route53Backend(BaseBackend): def delete_health_check(self, health_check_id): return self.health_checks.pop(health_check_id, None) + route53_backend = Route53Backend() diff --git a/moto/route53/responses.py b/moto/route53/responses.py index d796660e1..07f6e2303 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -8,43 +8,45 @@ import xmltodict class Route53 (BaseResponse): + def list_or_create_hostzone_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) if request.method == "POST": - elements = xmltodict.parse(self.body) - if "HostedZoneConfig" in elements["CreateHostedZoneRequest"]: - comment = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["Comment"] - try: - # in boto3, this field is set directly in the xml - private_zone = elements["CreateHostedZoneRequest"]["HostedZoneConfig"]["PrivateZone"] - except KeyError: - # if a VPC subsection is only included in xmls params when private_zone=True, - # see boto: boto/route53/connection.py - private_zone = 'VPC' in elements["CreateHostedZoneRequest"] - else: - comment = None - private_zone = False + elements = xmltodict.parse(self.body) + if "HostedZoneConfig" in elements["CreateHostedZoneRequest"]: + comment = elements["CreateHostedZoneRequest"][ + "HostedZoneConfig"]["Comment"] + try: + # in boto3, this field is set directly in the xml + private_zone = elements["CreateHostedZoneRequest"][ + "HostedZoneConfig"]["PrivateZone"] + except KeyError: + # if a VPC subsection is only included in xmls params when private_zone=True, + # see boto: boto/route53/connection.py + private_zone = 'VPC' in elements["CreateHostedZoneRequest"] + else: + comment = None + private_zone = False - name = elements["CreateHostedZoneRequest"]["Name"] + name = elements["CreateHostedZoneRequest"]["Name"] - if name[-1] != ".": - name += "." + if name[-1] != ".": + name += "." - new_zone = route53_backend.create_hosted_zone( - name, - comment=comment, - private_zone=private_zone, - ) - template = Template(CREATE_HOSTED_ZONE_RESPONSE) - return 201, headers, template.render(zone=new_zone) + new_zone = route53_backend.create_hosted_zone( + name, + comment=comment, + private_zone=private_zone, + ) + template = Template(CREATE_HOSTED_ZONE_RESPONSE) + return 201, headers, template.render(zone=new_zone) elif request.method == "GET": all_zones = route53_backend.get_all_hosted_zones() template = Template(LIST_HOSTED_ZONES_RESPONSE) return 200, headers, template.render(zones=all_zones) - def get_or_delete_hostzone_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) parsed_url = urlparse(full_url) @@ -61,7 +63,6 @@ class Route53 (BaseResponse): route53_backend.delete_hosted_zone(zoneid) return 200, headers, DELETE_HOSTED_ZONE_RESPONSE - def rrset_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -76,18 +77,22 @@ class Route53 (BaseResponse): if method == "POST": elements = xmltodict.parse(self.body) - change_list = elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes']['Change'] + change_list = elements['ChangeResourceRecordSetsRequest'][ + 'ChangeBatch']['Changes']['Change'] if not isinstance(change_list, list): - change_list = [elements['ChangeResourceRecordSetsRequest']['ChangeBatch']['Changes']['Change']] + change_list = [elements['ChangeResourceRecordSetsRequest'][ + 'ChangeBatch']['Changes']['Change']] for value in change_list: action = value['Action'] record_set = value['ResourceRecordSet'] if action in ('CREATE', 'UPSERT'): if 'ResourceRecords' in record_set: - resource_records = list(record_set['ResourceRecords'].values())[0] + resource_records = list( + record_set['ResourceRecords'].values())[0] if not isinstance(resource_records, list): - # Depending on how many records there are, this may or may not be a list + # Depending on how many records there are, this may + # or may not be a list resource_records = [resource_records] record_values = [x['Value'] for x in resource_records] elif 'AliasTarget' in record_set: @@ -99,7 +104,8 @@ class Route53 (BaseResponse): the_zone.upsert_rrset(record_set) elif action == "DELETE": if 'SetIdentifier' in record_set: - the_zone.delete_rrset_by_id(record_set["SetIdentifier"]) + the_zone.delete_rrset_by_id( + record_set["SetIdentifier"]) else: the_zone.delete_rrset_by_name(record_set["Name"]) @@ -113,7 +119,6 @@ class Route53 (BaseResponse): record_sets = the_zone.get_record_sets(type_filter, name_filter) return 200, headers, template.render(record_sets=record_sets) - def health_check_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -121,7 +126,8 @@ class Route53 (BaseResponse): method = request.method if method == "POST": - properties = xmltodict.parse(self.body)['CreateHealthCheckRequest']['HealthCheckConfig'] + properties = xmltodict.parse(self.body)['CreateHealthCheckRequest'][ + 'HealthCheckConfig'] health_check_args = { "ip_address": properties.get('IPAddress'), "port": properties.get('Port'), @@ -132,7 +138,8 @@ class Route53 (BaseResponse): "request_interval": properties.get('RequestInterval'), "failure_threshold": properties.get('FailureThreshold'), } - health_check = route53_backend.create_health_check(health_check_args) + health_check = route53_backend.create_health_check( + health_check_args) template = Template(CREATE_HEALTH_CHECK_RESPONSE) return 201, headers, template.render(health_check=health_check) elif method == "DELETE": @@ -152,8 +159,8 @@ class Route53 (BaseResponse): action = 'tags' elif 'trafficpolicyinstances' in full_url: action = 'policies' - raise NotImplementedError("The action for {0} has not been implemented for route 53".format(action)) - + raise NotImplementedError( + "The action for {0} has not been implemented for route 53".format(action)) def list_or_change_tags_for_resource_request(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -182,6 +189,7 @@ class Route53 (BaseResponse): return 200, headers, template.render() + LIST_TAGS_FOR_RESOURCE_RESPONSE = """ diff --git a/moto/s3/__init__.py b/moto/s3/__init__.py index 2c54a8d5a..84c1cbde0 100644 --- a/moto/s3/__init__.py +++ b/moto/s3/__init__.py @@ -3,4 +3,4 @@ from .models import s3_backend s3_backends = {"global": s3_backend} mock_s3 = s3_backend.decorator -mock_s3_deprecated = s3_backend.deprecated_decorator \ No newline at end of file +mock_s3_deprecated = s3_backend.deprecated_decorator diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py index 2f444e2dd..df817ba78 100644 --- a/moto/s3/exceptions.py +++ b/moto/s3/exceptions.py @@ -12,6 +12,7 @@ ERROR_WITH_KEY_NAME = """{% extends 'single_error' %} class S3ClientError(RESTError): + def __init__(self, *args, **kwargs): kwargs.setdefault('template', 'single_error') self.templates['bucket_error'] = ERROR_WITH_BUCKET_NAME @@ -19,6 +20,7 @@ class S3ClientError(RESTError): class BucketError(S3ClientError): + def __init__(self, *args, **kwargs): kwargs.setdefault('template', 'bucket_error') self.templates['bucket_error'] = ERROR_WITH_BUCKET_NAME diff --git a/moto/s3/models.py b/moto/s3/models.py index d5e156498..c7bf557ca 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -120,6 +120,7 @@ class FakeKey(object): class FakeMultipart(object): + def __init__(self, key_name, metadata): self.key_name = key_name self.metadata = metadata @@ -167,6 +168,7 @@ class FakeMultipart(object): class FakeGrantee(object): + def __init__(self, id='', uri='', display_name=''): self.id = id self.uri = uri @@ -177,9 +179,12 @@ class FakeGrantee(object): return 'Group' if self.uri else 'CanonicalUser' -ALL_USERS_GRANTEE = FakeGrantee(uri='http://acs.amazonaws.com/groups/global/AllUsers') -AUTHENTICATED_USERS_GRANTEE = FakeGrantee(uri='http://acs.amazonaws.com/groups/global/AuthenticatedUsers') -LOG_DELIVERY_GRANTEE = FakeGrantee(uri='http://acs.amazonaws.com/groups/s3/LogDelivery') +ALL_USERS_GRANTEE = FakeGrantee( + uri='http://acs.amazonaws.com/groups/global/AllUsers') +AUTHENTICATED_USERS_GRANTEE = FakeGrantee( + uri='http://acs.amazonaws.com/groups/global/AuthenticatedUsers') +LOG_DELIVERY_GRANTEE = FakeGrantee( + uri='http://acs.amazonaws.com/groups/s3/LogDelivery') PERMISSION_FULL_CONTROL = 'FULL_CONTROL' PERMISSION_WRITE = 'WRITE' @@ -189,27 +194,32 @@ PERMISSION_READ_ACP = 'READ_ACP' class FakeGrant(object): + def __init__(self, grantees, permissions): self.grantees = grantees self.permissions = permissions class FakeAcl(object): + def __init__(self, grants=[]): self.grants = grants def get_canned_acl(acl): - owner_grantee = FakeGrantee(id='75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a') + owner_grantee = FakeGrantee( + id='75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a') grants = [FakeGrant([owner_grantee], [PERMISSION_FULL_CONTROL])] if acl == 'private': pass # no other permissions elif acl == 'public-read': grants.append(FakeGrant([ALL_USERS_GRANTEE], [PERMISSION_READ])) elif acl == 'public-read-write': - grants.append(FakeGrant([ALL_USERS_GRANTEE], [PERMISSION_READ, PERMISSION_WRITE])) + grants.append(FakeGrant([ALL_USERS_GRANTEE], [ + PERMISSION_READ, PERMISSION_WRITE])) elif acl == 'authenticated-read': - grants.append(FakeGrant([AUTHENTICATED_USERS_GRANTEE], [PERMISSION_READ])) + grants.append( + FakeGrant([AUTHENTICATED_USERS_GRANTEE], [PERMISSION_READ])) elif acl == 'bucket-owner-read': pass # TODO: bucket owner ACL elif acl == 'bucket-owner-full-control': @@ -217,13 +227,15 @@ def get_canned_acl(acl): elif acl == 'aws-exec-read': pass # TODO: bucket owner, EC2 Read elif acl == 'log-delivery-write': - grants.append(FakeGrant([LOG_DELIVERY_GRANTEE], [PERMISSION_READ_ACP, PERMISSION_WRITE])) + grants.append(FakeGrant([LOG_DELIVERY_GRANTEE], [ + PERMISSION_READ_ACP, PERMISSION_WRITE])) else: assert False, 'Unknown canned acl: %s' % (acl,) return FakeAcl(grants=grants) class LifecycleRule(object): + def __init__(self, id=None, prefix=None, status=None, expiration_days=None, expiration_date=None, transition_days=None, transition_date=None, storage_class=None): @@ -271,7 +283,8 @@ class FakeBucket(object): expiration_date=expiration.get('Date') if expiration else None, transition_days=transition.get('Days') if transition else None, transition_date=transition.get('Date') if transition else None, - storage_class=transition['StorageClass'] if transition else None, + storage_class=transition[ + 'StorageClass'] if transition else None, )) def delete_lifecycle(self): @@ -283,9 +296,11 @@ class FakeBucket(object): def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'DomainName': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "DomainName" ]"') elif attribute_name == 'WebsiteURL': - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"') + raise NotImplementedError( + '"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"') raise UnformattedGetAttTemplateException() def set_acl(self, acl): @@ -470,20 +485,24 @@ class S3Backend(BaseBackend): key_without_prefix = key_name.replace(prefix, "", 1) if delimiter and delimiter in key_without_prefix: # If delimiter, we need to split out folder_results - key_without_delimiter = key_without_prefix.split(delimiter)[0] - folder_results.add("{0}{1}{2}".format(prefix, key_without_delimiter, delimiter)) + key_without_delimiter = key_without_prefix.split(delimiter)[ + 0] + folder_results.add("{0}{1}{2}".format( + prefix, key_without_delimiter, delimiter)) else: key_results.add(key) else: for key_name, key in bucket.keys.items(): if delimiter and delimiter in key_name: # If delimiter, we need to split out folder_results - folder_results.add(key_name.split(delimiter)[0] + delimiter) + folder_results.add(key_name.split( + delimiter)[0] + delimiter) else: key_results.add(key) key_results = sorted(key_results, key=lambda key: key.name) - folder_results = [folder_name for folder_name in sorted(folder_results, key=lambda key: key)] + folder_results = [folder_name for folder_name in sorted( + folder_results, key=lambda key: key)] return key_results, folder_results @@ -502,7 +521,8 @@ class S3Backend(BaseBackend): src_key_name = clean_key_name(src_key_name) dest_key_name = clean_key_name(dest_key_name) dest_bucket = self.get_bucket(dest_bucket_name) - key = self.get_key(src_bucket_name, src_key_name, version_id=src_version_id) + key = self.get_key(src_bucket_name, src_key_name, + version_id=src_version_id) if dest_key_name != src_key_name: key = key.copy(dest_key_name) dest_bucket.keys[dest_key_name] = key diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 07be98e7b..e123d76e1 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -33,6 +33,7 @@ def is_delete_keys(request, path, bucket_name): class ResponseObject(_TemplateEnvironmentMixin): + def __init__(self, backend): super(ResponseObject, self).__init__() self.backend = backend @@ -70,7 +71,8 @@ class ResponseObject(_TemplateEnvironmentMixin): if match: return False - path_based = (host == 's3.amazonaws.com' or re.match(r"s3[\.\-]([^.]*)\.amazonaws\.com", host)) + path_based = (host == 's3.amazonaws.com' or re.match( + r"s3[\.\-]([^.]*)\.amazonaws\.com", host)) return not path_based def is_delete_keys(self, request, path, bucket_name): @@ -148,7 +150,8 @@ class ResponseObject(_TemplateEnvironmentMixin): elif method == 'POST': return self._bucket_response_post(request, body, bucket_name, headers) else: - raise NotImplementedError("Method {0} has not been impelemented in the S3 backend yet".format(method)) + raise NotImplementedError( + "Method {0} has not been impelemented in the S3 backend yet".format(method)) def _bucket_response_head(self, bucket_name, headers): self.backend.get_bucket(bucket_name) @@ -158,11 +161,14 @@ class ResponseObject(_TemplateEnvironmentMixin): if 'uploads' in querystring: for unsup in ('delimiter', 'max-uploads'): if unsup in querystring: - raise NotImplementedError("Listing multipart uploads with {} has not been implemented yet.".format(unsup)) - multiparts = list(self.backend.get_all_multiparts(bucket_name).values()) + raise NotImplementedError( + "Listing multipart uploads with {} has not been implemented yet.".format(unsup)) + multiparts = list( + self.backend.get_all_multiparts(bucket_name).values()) if 'prefix' in querystring: prefix = querystring.get('prefix', [None])[0] - multiparts = [upload for upload in multiparts if upload.key_name.startswith(prefix)] + multiparts = [ + upload for upload in multiparts if upload.key_name.startswith(prefix)] template = self.response_template(S3_ALL_MULTIPARTS) return template.render( bucket_name=bucket_name, @@ -175,7 +181,8 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) if not bucket.rules: return 404, {}, "NoSuchLifecycleConfiguration" - template = self.response_template(S3_BUCKET_LIFECYCLE_CONFIGURATION) + template = self.response_template( + S3_BUCKET_LIFECYCLE_CONFIGURATION) return template.render(rules=bucket.rules) elif 'versioning' in querystring: versioning = self.backend.get_bucket_versioning(bucket_name) @@ -188,7 +195,8 @@ class ResponseObject(_TemplateEnvironmentMixin): return 404, {}, template.render(bucket_name=bucket_name) return 200, {}, policy elif 'website' in querystring: - website_configuration = self.backend.get_bucket_website_configuration(bucket_name) + website_configuration = self.backend.get_bucket_website_configuration( + bucket_name) return website_configuration elif 'acl' in querystring: bucket = self.backend.get_bucket(bucket_name) @@ -226,7 +234,8 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] delimiter = querystring.get('delimiter', [None])[0] - result_keys, result_folders = self.backend.prefix_query(bucket, prefix, delimiter) + result_keys, result_folders = self.backend.prefix_query( + bucket, prefix, delimiter) template = self.response_template(S3_BUCKET_GET_RESPONSE) return 200, {}, template.render( bucket=bucket, @@ -242,7 +251,8 @@ class ResponseObject(_TemplateEnvironmentMixin): prefix = querystring.get('prefix', [None])[0] delimiter = querystring.get('delimiter', [None])[0] - result_keys, result_folders = self.backend.prefix_query(bucket, prefix, delimiter) + result_keys, result_folders = self.backend.prefix_query( + bucket, prefix, delimiter) fetch_owner = querystring.get('fetch-owner', [False])[0] max_keys = int(querystring.get('max-keys', [1000])[0]) @@ -308,7 +318,8 @@ class ResponseObject(_TemplateEnvironmentMixin): return "" else: try: - new_bucket = self.backend.create_bucket(bucket_name, region_name) + new_bucket = self.backend.create_bucket( + bucket_name, region_name) except BucketAlreadyExists: if region_name == DEFAULT_REGION_NAME: # us-east-1 has different behavior @@ -335,7 +346,8 @@ class ResponseObject(_TemplateEnvironmentMixin): return 204, {}, template.render(bucket=removed_bucket) else: # Tried to delete a bucket that still has keys - template = self.response_template(S3_DELETE_BUCKET_WITH_ITEMS_ERROR) + template = self.response_template( + S3_DELETE_BUCKET_WITH_ITEMS_ERROR) return 409, {}, template.render(bucket=removed_bucket) def _bucket_response_post(self, request, body, bucket_name, headers): @@ -393,7 +405,9 @@ class ResponseObject(_TemplateEnvironmentMixin): if ',' in rspec: raise NotImplementedError( "Multiple range specifiers not supported") - toint = lambda i: int(i) if i else None + + def toint(i): + return int(i) if i else None begin, end = map(toint, rspec.split('-')) if begin is not None: # byte range end = last if end is None else min(end, last) @@ -455,7 +469,8 @@ class ResponseObject(_TemplateEnvironmentMixin): elif method == 'POST': return self._key_response_post(request, body, bucket_name, query, key_name, headers) else: - raise NotImplementedError("Method {0} has not been impelemented in the S3 backend yet".format(method)) + raise NotImplementedError( + "Method {0} has not been impelemented in the S3 backend yet".format(method)) def _key_response_get(self, bucket_name, query, key_name, headers): response_headers = {} @@ -489,7 +504,8 @@ class ResponseObject(_TemplateEnvironmentMixin): if 'x-amz-copy-source' in request.headers: src = request.headers.get("x-amz-copy-source") src_bucket, src_key = src.split("/", 1) - src_range = request.headers.get('x-amz-copy-source-range', '').split("bytes=")[-1] + src_range = request.headers.get( + 'x-amz-copy-source-range', '').split("bytes=")[-1] try: start_byte, end_byte = src_range.split("-") @@ -522,7 +538,8 @@ class ResponseObject(_TemplateEnvironmentMixin): # Copy key src_key_parsed = urlparse(request.headers.get("x-amz-copy-source")) src_bucket, src_key = src_key_parsed.path.split("/", 1) - src_version_id = parse_qs(src_key_parsed.query).get('versionId', [None])[0] + src_version_id = parse_qs(src_key_parsed.query).get( + 'versionId', [None])[0] self.backend.copy_key(src_bucket, src_key, bucket_name, key_name, storage=storage_class, acl=acl, src_version_id=src_version_id) new_key = self.backend.get_key(bucket_name, key_name) @@ -557,7 +574,8 @@ class ResponseObject(_TemplateEnvironmentMixin): def _key_response_head(self, bucket_name, query, key_name, headers): response_headers = {} version_id = query.get('versionId', [None])[0] - key = self.backend.get_key(bucket_name, key_name, version_id=version_id) + key = self.backend.get_key( + bucket_name, key_name, version_id=version_id) if key: response_headers.update(key.metadata) response_headers.update(key.response_dict) @@ -585,7 +603,8 @@ class ResponseObject(_TemplateEnvironmentMixin): grantees = [] for key_and_value in value.split(","): - key, value = re.match('([^=]+)="([^"]+)"', key_and_value.strip()).groups() + key, value = re.match( + '([^=]+)="([^"]+)"', key_and_value.strip()).groups() if key.lower() == 'id': grantees.append(FakeGrantee(id=value)) else: @@ -610,7 +629,8 @@ class ResponseObject(_TemplateEnvironmentMixin): ps = minidom.parseString(body).getElementsByTagName('Part') prev = 0 for p in ps: - pn = int(p.getElementsByTagName('PartNumber')[0].firstChild.wholeText) + pn = int(p.getElementsByTagName( + 'PartNumber')[0].firstChild.wholeText) if pn <= prev: raise InvalidPartOrder() yield (pn, p.getElementsByTagName('ETag')[0].firstChild.wholeText) @@ -618,7 +638,8 @@ class ResponseObject(_TemplateEnvironmentMixin): def _key_response_post(self, request, body, bucket_name, query, key_name, headers): if body == b'' and 'uploads' in query: metadata = metadata_from_headers(request.headers) - multipart = self.backend.initiate_multipart(bucket_name, key_name, metadata) + multipart = self.backend.initiate_multipart( + bucket_name, key_name, metadata) template = self.response_template(S3_MULTIPART_INITIATE_RESPONSE) response = template.render( @@ -648,7 +669,9 @@ class ResponseObject(_TemplateEnvironmentMixin): key.restore(int(days)) return r, {}, "" else: - raise NotImplementedError("Method POST had only been implemented for multipart uploads and restore operations, so far") + raise NotImplementedError( + "Method POST had only been implemented for multipart uploads and restore operations, so far") + S3ResponseInstance = ResponseObject(s3_backend) diff --git a/moto/s3/utils.py b/moto/s3/utils.py index 8ea18c207..a121eae3a 100644 --- a/moto/s3/utils.py +++ b/moto/s3/utils.py @@ -29,7 +29,8 @@ def bucket_name_from_url(url): def metadata_from_headers(headers): metadata = {} - meta_regex = re.compile('^x-amz-meta-([a-zA-Z0-9\-_]+)$', flags=re.IGNORECASE) + meta_regex = re.compile( + '^x-amz-meta-([a-zA-Z0-9\-_]+)$', flags=re.IGNORECASE) for header, value in headers.items(): if isinstance(header, six.string_types): result = meta_regex.match(header) diff --git a/moto/server.py b/moto/server.py index 0bb4eb779..c7e7f18fb 100644 --- a/moto/server.py +++ b/moto/server.py @@ -57,11 +57,13 @@ class DomainDispatcherApplication(object): # Fall back to parsing auth header to find service # ['Credential=sdffdsa', '20170220', 'us-east-1', 'sns', 'aws4_request'] try: - _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[1].split("/") + _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[ + 1].split("/") except ValueError: region = 'us-east-1' service = 's3' - host = "{service}.{region}.amazonaws.com".format(service=service, region=region) + host = "{service}.{region}.amazonaws.com".format( + service=service, region=region) with self.lock: backend = self.get_backend_for_host(host) @@ -78,6 +80,7 @@ class DomainDispatcherApplication(object): class RegexConverter(BaseConverter): # http://werkzeug.pocoo.org/docs/routing/#custom-converters + def __init__(self, url_map, *items): super(RegexConverter, self).__init__(url_map) self.regex = items[0] @@ -92,7 +95,7 @@ class AWSTestHelper(FlaskClient): opts = {"Action": action_name} opts.update(kwargs) res = self.get("/?{0}".format(urlencode(opts)), - headers={"Host": "{0}.us-east-1.amazonaws.com".format(self.application.service)}) + headers={"Host": "{0}.us-east-1.amazonaws.com".format(self.application.service)}) return res.data.decode("utf-8") def action_json(self, action_name, **kwargs): @@ -166,10 +169,12 @@ def main(argv=sys.argv[1:]): args = parser.parse_args(argv) # Wrap the main application - main_app = DomainDispatcherApplication(create_backend_app, service=args.service) + main_app = DomainDispatcherApplication( + create_backend_app, service=args.service) main_app.debug = True - run_simple(args.host, args.port, main_app, threaded=True, use_reloader=args.reload) + run_simple(args.host, args.port, main_app, + threaded=True, use_reloader=args.reload) if __name__ == '__main__': diff --git a/moto/ses/__init__.py b/moto/ses/__init__.py index e105b9929..0477d2623 100644 --- a/moto/ses/__init__.py +++ b/moto/ses/__init__.py @@ -3,4 +3,4 @@ from .models import ses_backend ses_backends = {"global": ses_backend} mock_ses = ses_backend.decorator -mock_ses_deprecated = ses_backend.deprecated_decorator \ No newline at end of file +mock_ses_deprecated = ses_backend.deprecated_decorator diff --git a/moto/ses/models.py b/moto/ses/models.py index 6950ead5b..3502d6bc7 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -11,16 +11,19 @@ RECIPIENT_LIMIT = 50 class Message(object): + def __init__(self, message_id): self.id = message_id class RawMessage(object): + def __init__(self, message_id): self.id = message_id class SESQuota(object): + def __init__(self, sent): self.sent = sent @@ -30,6 +33,7 @@ class SESQuota(object): class SESBackend(BaseBackend): + def __init__(self): self.addresses = [] self.domains = [] @@ -97,4 +101,5 @@ class SESBackend(BaseBackend): def get_send_quota(self): return SESQuota(self.sent_message_count) + ses_backend = SESBackend() diff --git a/moto/sns/__init__.py b/moto/sns/__init__.py index a50911e3b..bd36cb23d 100644 --- a/moto/sns/__init__.py +++ b/moto/sns/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import sns_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator sns_backend = sns_backends['us-east-1'] mock_sns = base_decorator(sns_backends) diff --git a/moto/sns/models.py b/moto/sns/models.py index d924b1e5d..0ad00928d 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -20,6 +20,7 @@ DEFAULT_PAGE_SIZE = 100 class Topic(object): + def __init__(self, name, sns_backend): self.name = name self.sns_backend = sns_backend @@ -28,7 +29,8 @@ class Topic(object): self.policy = DEFAULT_TOPIC_POLICY self.delivery_policy = "" self.effective_delivery_policy = DEFAULT_EFFECTIVE_DELIVERY_POLICY - self.arn = make_arn_for_topic(self.account_id, name, sns_backend.region_name) + self.arn = make_arn_for_topic( + self.account_id, name, sns_backend.region_name) self.subscriptions_pending = 0 self.subscriptions_confimed = 0 @@ -60,11 +62,13 @@ class Topic(object): properties.get("TopicName") ) for subscription in properties.get("Subscription", []): - sns_backend.subscribe(topic.arn, subscription['Endpoint'], subscription['Protocol']) + sns_backend.subscribe(topic.arn, subscription[ + 'Endpoint'], subscription['Protocol']) return topic class Subscription(object): + def __init__(self, topic, endpoint, protocol): self.topic = topic self.endpoint = endpoint @@ -96,6 +100,7 @@ class Subscription(object): class PlatformApplication(object): + def __init__(self, region, name, platform, attributes): self.region = region self.name = name @@ -112,6 +117,7 @@ class PlatformApplication(object): class PlatformEndpoint(object): + def __init__(self, region, application, custom_user_data, token, attributes): self.region = region self.application = application @@ -125,9 +131,9 @@ class PlatformEndpoint(object): def __fixup_attributes(self): # When AWS returns the attributes dict, it always contains these two elements, so we need to # automatically ensure they exist as well. - if not 'Token' in self.attributes: + if 'Token' not in self.attributes: self.attributes['Token'] = self.token - if not 'Enabled' in self.attributes: + if 'Enabled' not in self.attributes: self.attributes['Enabled'] = True @property @@ -147,6 +153,7 @@ class PlatformEndpoint(object): class SNSBackend(BaseBackend): + def __init__(self, region_name): super(SNSBackend, self).__init__() self.topics = OrderedDict() @@ -169,7 +176,8 @@ class SNSBackend(BaseBackend): if next_token is None: next_token = 0 next_token = int(next_token) - values = list(values_map.values())[next_token: next_token + DEFAULT_PAGE_SIZE] + values = list(values_map.values())[ + next_token: next_token + DEFAULT_PAGE_SIZE] if len(values) == DEFAULT_PAGE_SIZE: next_token = next_token + DEFAULT_PAGE_SIZE else: @@ -204,7 +212,8 @@ class SNSBackend(BaseBackend): def list_subscriptions(self, topic_arn=None, next_token=None): if topic_arn: topic = self.get_topic(topic_arn) - filtered = OrderedDict([(k, sub) for k, sub in self.subscriptions.items() if sub.topic == topic]) + filtered = OrderedDict( + [(k, sub) for k, sub in self.subscriptions.items() if sub.topic == topic]) return self._get_values_nexttoken(filtered, next_token) else: return self._get_values_nexttoken(self.subscriptions, next_token) @@ -227,7 +236,8 @@ class SNSBackend(BaseBackend): try: return self.applications[arn] except KeyError: - raise SNSNotFoundError("Application with arn {0} not found".format(arn)) + raise SNSNotFoundError( + "Application with arn {0} not found".format(arn)) def set_application_attributes(self, arn, attributes): application = self.get_application(arn) @@ -241,7 +251,8 @@ class SNSBackend(BaseBackend): self.applications.pop(platform_arn) def create_platform_endpoint(self, region, application, custom_user_data, token, attributes): - platform_endpoint = PlatformEndpoint(region, application, custom_user_data, token, attributes) + platform_endpoint = PlatformEndpoint( + region, application, custom_user_data, token, attributes) self.platform_endpoints[platform_endpoint.arn] = platform_endpoint return platform_endpoint @@ -256,7 +267,8 @@ class SNSBackend(BaseBackend): try: return self.platform_endpoints[arn] except KeyError: - raise SNSNotFoundError("Endpoint with arn {0} not found".format(arn)) + raise SNSNotFoundError( + "Endpoint with arn {0} not found".format(arn)) def set_endpoint_attributes(self, arn, attributes): endpoint = self.get_endpoint(arn) @@ -267,7 +279,8 @@ class SNSBackend(BaseBackend): try: del self.platform_endpoints[arn] except KeyError: - raise SNSNotFoundError("Endpoint with arn {0} not found".format(arn)) + raise SNSNotFoundError( + "Endpoint with arn {0} not found".format(arn)) sns_backends = {} diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 9a20dbcb5..edb82e40c 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -109,7 +109,8 @@ class SNSResponse(BaseResponse): attribute_name = self._get_param('AttributeName') attribute_name = camelcase_to_underscores(attribute_name) attribute_value = self._get_param('AttributeValue') - self.backend.set_topic_attribute(topic_arn, attribute_name, attribute_value) + self.backend.set_topic_attribute( + topic_arn, attribute_name, attribute_value) if self.request_json: return json.dumps({ @@ -162,7 +163,8 @@ class SNSResponse(BaseResponse): def list_subscriptions(self): next_token = self._get_param('NextToken') - subscriptions, next_token = self.backend.list_subscriptions(next_token=next_token) + subscriptions, next_token = self.backend.list_subscriptions( + next_token=next_token) if self.request_json: return json.dumps({ @@ -190,7 +192,8 @@ class SNSResponse(BaseResponse): def list_subscriptions_by_topic(self): topic_arn = self._get_param('TopicArn') next_token = self._get_param('NextToken') - subscriptions, next_token = self.backend.list_subscriptions(topic_arn, next_token=next_token) + subscriptions, next_token = self.backend.list_subscriptions( + topic_arn, next_token=next_token) if self.request_json: return json.dumps({ @@ -241,7 +244,8 @@ class SNSResponse(BaseResponse): name = self._get_param('Name') platform = self._get_param('Platform') attributes = self._get_attributes() - platform_application = self.backend.create_platform_application(self.region, name, platform, attributes) + platform_application = self.backend.create_platform_application( + self.region, name, platform, attributes) if self.request_json: return json.dumps({ @@ -274,7 +278,8 @@ class SNSResponse(BaseResponse): } }) - template = self.response_template(GET_PLATFORM_APPLICATION_ATTRIBUTES_TEMPLATE) + template = self.response_template( + GET_PLATFORM_APPLICATION_ATTRIBUTES_TEMPLATE) return template.render(application=application) def set_platform_application_attributes(self): @@ -292,7 +297,8 @@ class SNSResponse(BaseResponse): } }) - template = self.response_template(SET_PLATFORM_APPLICATION_ATTRIBUTES_TEMPLATE) + template = self.response_template( + SET_PLATFORM_APPLICATION_ATTRIBUTES_TEMPLATE) return template.render() def list_platform_applications(self): @@ -361,7 +367,8 @@ class SNSResponse(BaseResponse): def list_endpoints_by_platform_application(self): application_arn = self._get_param('PlatformApplicationArn') - endpoints = self.backend.list_endpoints_by_platform_application(application_arn) + endpoints = self.backend.list_endpoints_by_platform_application( + application_arn) if self.request_json: return json.dumps({ @@ -381,7 +388,8 @@ class SNSResponse(BaseResponse): } }) - template = self.response_template(LIST_ENDPOINTS_BY_PLATFORM_APPLICATION_TEMPLATE) + template = self.response_template( + LIST_ENDPOINTS_BY_PLATFORM_APPLICATION_TEMPLATE) return template.render(endpoints=endpoints) def get_endpoint_attributes(self): @@ -438,7 +446,6 @@ class SNSResponse(BaseResponse): return template.render() - CREATE_TOPIC_TEMPLATE = """ {{ topic.arn }} diff --git a/moto/sqs/__init__.py b/moto/sqs/__init__.py index 946ba8f47..46c83133f 100644 --- a/moto/sqs/__init__.py +++ b/moto/sqs/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import sqs_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator sqs_backend = sqs_backends['us-east-1'] mock_sqs = base_decorator(sqs_backends) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 13b8c34b6..5f4833772 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import hashlib -import time import re from xml.sax.saxutils import escape @@ -18,7 +17,9 @@ from .exceptions import ( DEFAULT_ACCOUNT_ID = 123456789012 DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU" + class Message(object): + def __init__(self, message_id, body): self.id = message_id self._body = body @@ -122,7 +123,8 @@ class Queue(object): self.last_modified_timestamp = now self.maximum_message_size = 64 << 10 self.message_retention_period = 86400 * 4 # four days - self.queue_arn = 'arn:aws:sqs:{0}:123456789012:{1}'.format(self.region, self.name) + self.queue_arn = 'arn:aws:sqs:{0}:123456789012:{1}'.format( + self.region, self.name) self.receive_message_wait_time_seconds = 0 @classmethod @@ -177,7 +179,8 @@ class Queue(object): def attributes(self): result = {} for attribute in self.camelcase_attributes: - result[attribute] = getattr(self, camelcase_to_underscores(attribute)) + result[attribute] = getattr( + self, camelcase_to_underscores(attribute)) return result @property @@ -201,6 +204,7 @@ class Queue(object): class SQSBackend(BaseBackend): + def __init__(self, region_name): self.region_name = region_name self.queues = {} @@ -214,7 +218,8 @@ class SQSBackend(BaseBackend): def create_queue(self, name, visibility_timeout, wait_time_seconds): queue = self.queues.get(name) if queue is None: - queue = Queue(name, visibility_timeout, wait_time_seconds, self.region_name) + queue = Queue(name, visibility_timeout, + wait_time_seconds, self.region_name) self.queues[name] = queue return queue diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index d57ec3430..84886068e 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -27,7 +27,8 @@ class SQSResponse(BaseResponse): @property def attribute(self): if not hasattr(self, '_attribute'): - self._attribute = dict([(a['name'], a['value']) for a in self._get_list_prefix('Attribute')]) + self._attribute = dict([(a['name'], a['value']) + for a in self._get_list_prefix('Attribute')]) return self._attribute def _get_queue_name(self): @@ -59,7 +60,7 @@ class SQSResponse(BaseResponse): def create_queue(self): queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'), - wait_time_seconds=self.attribute.get('WaitTimeSeconds')) + wait_time_seconds=self.attribute.get('WaitTimeSeconds')) template = self.response_template(CREATE_QUEUE_RESPONSE) return template.render(queue=queue) @@ -108,7 +109,8 @@ class SQSResponse(BaseResponse): def set_queue_attributes(self): queue_name = self._get_queue_name() if "Attribute.Name" in self.querystring: - key = camelcase_to_underscores(self.querystring.get("Attribute.Name")[0]) + key = camelcase_to_underscores( + self.querystring.get("Attribute.Name")[0]) value = self.querystring.get("Attribute.Value")[0] self.sqs_backend.set_queue_attribute(queue_name, key, value) for a in self._get_list_prefix("Attribute"): @@ -171,20 +173,25 @@ class SQSResponse(BaseResponse): messages = [] for index in range(1, 11): # Loop through looking for messages - message_key = 'SendMessageBatchRequestEntry.{0}.MessageBody'.format(index) + message_key = 'SendMessageBatchRequestEntry.{0}.MessageBody'.format( + index) message_body = self.querystring.get(message_key) if not message_body: # Found all messages break - message_user_id_key = 'SendMessageBatchRequestEntry.{0}.Id'.format(index) + message_user_id_key = 'SendMessageBatchRequestEntry.{0}.Id'.format( + index) message_user_id = self.querystring.get(message_user_id_key)[0] - delay_key = 'SendMessageBatchRequestEntry.{0}.DelaySeconds'.format(index) + delay_key = 'SendMessageBatchRequestEntry.{0}.DelaySeconds'.format( + index) delay_seconds = self.querystring.get(delay_key, [None])[0] - message = self.sqs_backend.send_message(queue_name, message_body[0], delay_seconds=delay_seconds) + message = self.sqs_backend.send_message( + queue_name, message_body[0], delay_seconds=delay_seconds) message.user_id = message_user_id - message_attributes = parse_message_attributes(self.querystring, base='SendMessageBatchRequestEntry.{0}.'.format(index)) + message_attributes = parse_message_attributes( + self.querystring, base='SendMessageBatchRequestEntry.{0}.'.format(index)) if type(message_attributes) == tuple: return message_attributes[0], message_attributes[1] message.message_attributes = message_attributes @@ -216,7 +223,8 @@ class SQSResponse(BaseResponse): message_ids = [] for index in range(1, 11): # Loop through looking for messages - receipt_key = 'DeleteMessageBatchRequestEntry.{0}.ReceiptHandle'.format(index) + receipt_key = 'DeleteMessageBatchRequestEntry.{0}.ReceiptHandle'.format( + index) receipt_handle = self.querystring.get(receipt_key) if not receipt_handle: # Found all messages @@ -224,7 +232,8 @@ class SQSResponse(BaseResponse): self.sqs_backend.delete_message(queue_name, receipt_handle[0]) - message_user_id_key = 'DeleteMessageBatchRequestEntry.{0}.Id'.format(index) + message_user_id_key = 'DeleteMessageBatchRequestEntry.{0}.Id'.format( + index) message_user_id = self.querystring.get(message_user_id_key)[0] message_ids.append(message_user_id) @@ -258,7 +267,8 @@ class SQSResponse(BaseResponse): except ValueError: return ERROR_MAX_VISIBILITY_TIMEOUT_RESPONSE, dict(status=400) - messages = self.sqs_backend.receive_messages(queue_name, message_count, wait_time, visibility_timeout) + messages = self.sqs_backend.receive_messages( + queue_name, message_count, wait_time, visibility_timeout) template = self.response_template(RECEIVE_MESSAGE_RESPONSE) output = template.render(messages=messages) return output @@ -444,7 +454,8 @@ ERROR_TOO_LONG_RESPONSE = """ diff --git a/moto/sqs/utils.py b/moto/sqs/utils.py index a00ec1c79..78be5f629 100644 --- a/moto/sqs/utils.py +++ b/moto/sqs/utils.py @@ -22,25 +22,32 @@ def parse_message_attributes(querystring, base='', value_namespace='Value.'): # Found all attributes break - data_type_key = base + 'MessageAttribute.{0}.{1}DataType'.format(index, value_namespace) + data_type_key = base + \ + 'MessageAttribute.{0}.{1}DataType'.format(index, value_namespace) data_type = querystring.get(data_type_key) if not data_type: - raise MessageAttributesInvalid("The message attribute '{0}' must contain non-empty message attribute value.".format(name[0])) + raise MessageAttributesInvalid( + "The message attribute '{0}' must contain non-empty message attribute value.".format(name[0])) data_type_parts = data_type[0].split('.') if len(data_type_parts) > 2 or data_type_parts[0] not in ['String', 'Binary', 'Number']: - raise MessageAttributesInvalid("The message attribute '{0}' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String.".format(name[0])) + raise MessageAttributesInvalid( + "The message attribute '{0}' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String.".format(name[0])) type_prefix = 'String' if data_type_parts[0] == 'Binary': type_prefix = 'Binary' - value_key = base + 'MessageAttribute.{0}.{1}{2}Value'.format(index, value_namespace, type_prefix) + value_key = base + \ + 'MessageAttribute.{0}.{1}{2}Value'.format( + index, value_namespace, type_prefix) value = querystring.get(value_key) if not value: - raise MessageAttributesInvalid("The message attribute '{0}' must contain non-empty message attribute value for message attribute type '{1}'.".format(name[0], data_type[0])) + raise MessageAttributesInvalid( + "The message attribute '{0}' must contain non-empty message attribute value for message attribute type '{1}'.".format(name[0], data_type[0])) - message_attributes[name[0]] = {'data_type': data_type[0], type_prefix.lower() + '_value': value[0]} + message_attributes[name[0]] = {'data_type': data_type[ + 0], type_prefix.lower() + '_value': value[0]} index += 1 diff --git a/moto/sts/models.py b/moto/sts/models.py index 9ce629c91..f1c6401d2 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -5,6 +5,7 @@ from moto.core.utils import iso_8601_datetime_with_milliseconds class Token(object): + def __init__(self, duration, name=None, policy=None): now = datetime.datetime.utcnow() self.expiration = now + datetime.timedelta(seconds=duration) @@ -17,6 +18,7 @@ class Token(object): class AssumedRole(object): + def __init__(self, role_session_name, role_arn, policy, duration, external_id): self.session_name = role_session_name self.arn = role_arn @@ -31,6 +33,7 @@ class AssumedRole(object): class STSBackend(BaseBackend): + def get_session_token(self, duration): token = Token(duration=duration) return token @@ -43,4 +46,5 @@ class STSBackend(BaseBackend): role = AssumedRole(**kwargs) return role + sts_backend = STSBackend() diff --git a/moto/sts/responses.py b/moto/sts/responses.py index d721bfaaa..a5abb6b81 100644 --- a/moto/sts/responses.py +++ b/moto/sts/responses.py @@ -43,6 +43,7 @@ class TokenResponse(BaseResponse): template = self.response_template(GET_CALLER_IDENTITY_RESPONSE) return template.render() + GET_SESSION_TOKEN_RESPONSE = """ diff --git a/moto/swf/__init__.py b/moto/swf/__init__.py index 5ac59fbb6..0d626690a 100644 --- a/moto/swf/__init__.py +++ b/moto/swf/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from .models import swf_backends -from ..core.models import MockAWS, base_decorator, HttprettyMockAWS, deprecated_base_decorator +from ..core.models import base_decorator, deprecated_base_decorator swf_backend = swf_backends['us-east-1'] mock_swf = base_decorator(swf_backends) diff --git a/moto/swf/exceptions.py b/moto/swf/exceptions.py index 8bc5c0c9a..232b1f237 100644 --- a/moto/swf/exceptions.py +++ b/moto/swf/exceptions.py @@ -8,6 +8,7 @@ class SWFClientError(JsonRESTError): class SWFUnknownResourceFault(SWFClientError): + def __init__(self, resource_type, resource_name=None): if resource_name: message = "Unknown {0}: {1}".format(resource_type, resource_name) @@ -20,6 +21,7 @@ class SWFUnknownResourceFault(SWFClientError): class SWFDomainAlreadyExistsFault(SWFClientError): + def __init__(self, domain_name): super(SWFDomainAlreadyExistsFault, self).__init__( "com.amazonaws.swf.base.model#DomainAlreadyExistsFault", @@ -28,6 +30,7 @@ class SWFDomainAlreadyExistsFault(SWFClientError): class SWFDomainDeprecatedFault(SWFClientError): + def __init__(self, domain_name): super(SWFDomainDeprecatedFault, self).__init__( "com.amazonaws.swf.base.model#DomainDeprecatedFault", @@ -36,9 +39,11 @@ class SWFDomainDeprecatedFault(SWFClientError): class SWFSerializationException(SWFClientError): + def __init__(self, value): message = "class java.lang.Foo can not be converted to an String " - message += " (not a real SWF exception ; happened on: {0})".format(value) + message += " (not a real SWF exception ; happened on: {0})".format( + value) __type = "com.amazonaws.swf.base.model#SerializationException" super(SWFSerializationException, self).__init__( __type, @@ -47,22 +52,27 @@ class SWFSerializationException(SWFClientError): class SWFTypeAlreadyExistsFault(SWFClientError): + def __init__(self, _type): super(SWFTypeAlreadyExistsFault, self).__init__( "com.amazonaws.swf.base.model#TypeAlreadyExistsFault", - "{0}=[name={1}, version={2}]".format(_type.__class__.__name__, _type.name, _type.version), + "{0}=[name={1}, version={2}]".format( + _type.__class__.__name__, _type.name, _type.version), ) class SWFTypeDeprecatedFault(SWFClientError): + def __init__(self, _type): super(SWFTypeDeprecatedFault, self).__init__( "com.amazonaws.swf.base.model#TypeDeprecatedFault", - "{0}=[name={1}, version={2}]".format(_type.__class__.__name__, _type.name, _type.version), + "{0}=[name={1}, version={2}]".format( + _type.__class__.__name__, _type.name, _type.version), ) class SWFWorkflowExecutionAlreadyStartedFault(SWFClientError): + def __init__(self): super(SWFWorkflowExecutionAlreadyStartedFault, self).__init__( "com.amazonaws.swf.base.model#WorkflowExecutionAlreadyStartedFault", @@ -71,6 +81,7 @@ class SWFWorkflowExecutionAlreadyStartedFault(SWFClientError): class SWFDefaultUndefinedFault(SWFClientError): + def __init__(self, key): # TODO: move that into moto.core.utils maybe? words = key.split("_") @@ -84,6 +95,7 @@ class SWFDefaultUndefinedFault(SWFClientError): class SWFValidationException(SWFClientError): + def __init__(self, message): super(SWFValidationException, self).__init__( "com.amazon.coral.validate#ValidationException", @@ -92,6 +104,7 @@ class SWFValidationException(SWFClientError): class SWFDecisionValidationException(SWFClientError): + def __init__(self, problems): # messages messages = [] @@ -109,7 +122,8 @@ class SWFDecisionValidationException(SWFClientError): ) else: raise ValueError( - "Unhandled decision constraint type: {0}".format(pb["type"]) + "Unhandled decision constraint type: {0}".format(pb[ + "type"]) ) # prefix count = len(problems) @@ -124,5 +138,6 @@ class SWFDecisionValidationException(SWFClientError): class SWFWorkflowExecutionClosedError(Exception): + def __str__(self): return repr("Cannot change this object because the WorkflowExecution is closed") diff --git a/moto/swf/models/__init__.py b/moto/swf/models/__init__.py index 61fe5f52a..833596a23 100644 --- a/moto/swf/models/__init__.py +++ b/moto/swf/models/__init__.py @@ -12,15 +12,15 @@ from ..exceptions import ( SWFTypeDeprecatedFault, SWFValidationException, ) -from .activity_task import ActivityTask -from .activity_type import ActivityType -from .decision_task import DecisionTask -from .domain import Domain -from .generic_type import GenericType -from .history_event import HistoryEvent -from .timeout import Timeout -from .workflow_type import WorkflowType -from .workflow_execution import WorkflowExecution +from .activity_task import ActivityTask # flake8: noqa +from .activity_type import ActivityType # flake8: noqa +from .decision_task import DecisionTask # flake8: noqa +from .domain import Domain # flake8: noqa +from .generic_type import GenericType # flake8: noqa +from .history_event import HistoryEvent # flake8: noqa +from .timeout import Timeout # flake8: noqa +from .workflow_type import WorkflowType # flake8: noqa +from .workflow_execution import WorkflowExecution # flake8: noqa KNOWN_SWF_TYPES = { @@ -30,6 +30,7 @@ KNOWN_SWF_TYPES = { class SWFBackend(BaseBackend): + def __init__(self, region_name): self.region_name = region_name self.domains = [] @@ -246,7 +247,8 @@ class SWFBackend(BaseBackend): if decision_task.state != "STARTED": if decision_task.state == "COMPLETED": raise SWFUnknownResourceFault( - "decision task, scheduledEventId = {0}".format(decision_task.scheduled_event_id) + "decision task, scheduledEventId = {0}".format( + decision_task.scheduled_event_id) ) else: raise ValueError( @@ -300,7 +302,8 @@ class SWFBackend(BaseBackend): count = 0 for _task_list, tasks in domain.activity_task_lists.items(): if _task_list == task_list: - pending = [t for t in tasks if t.state in ["SCHEDULED", "STARTED"]] + pending = [t for t in tasks if t.state in [ + "SCHEDULED", "STARTED"]] count += len(pending) return count @@ -330,7 +333,8 @@ class SWFBackend(BaseBackend): if activity_task.state != "STARTED": if activity_task.state == "COMPLETED": raise SWFUnknownResourceFault( - "activity, scheduledEventId = {0}".format(activity_task.scheduled_event_id) + "activity, scheduledEventId = {0}".format( + activity_task.scheduled_event_id) ) else: raise ValueError( @@ -354,15 +358,18 @@ class SWFBackend(BaseBackend): self._process_timeouts() activity_task = self._find_activity_task_from_token(task_token) wfe = activity_task.workflow_execution - wfe.fail_activity_task(activity_task.task_token, reason=reason, details=details) + wfe.fail_activity_task(activity_task.task_token, + reason=reason, details=details) def terminate_workflow_execution(self, domain_name, workflow_id, child_policy=None, details=None, reason=None, run_id=None): # process timeouts on all objects self._process_timeouts() domain = self._get_domain(domain_name) - wfe = domain.get_workflow_execution(workflow_id, run_id=run_id, raise_if_closed=True) - wfe.terminate(child_policy=child_policy, details=details, reason=reason) + wfe = domain.get_workflow_execution( + workflow_id, run_id=run_id, raise_if_closed=True) + wfe.terminate(child_policy=child_policy, + details=details, reason=reason) def record_activity_task_heartbeat(self, task_token, details=None): # process timeouts on all objects diff --git a/moto/swf/models/activity_task.py b/moto/swf/models/activity_task.py index eb361d258..e205cc07a 100644 --- a/moto/swf/models/activity_task.py +++ b/moto/swf/models/activity_task.py @@ -9,6 +9,7 @@ from .timeout import Timeout class ActivityTask(object): + def __init__(self, activity_id, activity_type, scheduled_event_id, workflow_execution, timeouts, input=None): self.activity_id = activity_id diff --git a/moto/swf/models/activity_type.py b/moto/swf/models/activity_type.py index 95a83ca7a..eb1bbfa68 100644 --- a/moto/swf/models/activity_type.py +++ b/moto/swf/models/activity_type.py @@ -2,6 +2,7 @@ from .generic_type import GenericType class ActivityType(GenericType): + @property def _configuration_keys(self): return [ diff --git a/moto/swf/models/decision_task.py b/moto/swf/models/decision_task.py index bcd28f372..13bddfd7a 100644 --- a/moto/swf/models/decision_task.py +++ b/moto/swf/models/decision_task.py @@ -9,6 +9,7 @@ from .timeout import Timeout class DecisionTask(object): + def __init__(self, workflow_execution, scheduled_event_id): self.workflow_execution = workflow_execution self.workflow_type = workflow_execution.workflow_type @@ -60,7 +61,8 @@ class DecisionTask(object): if not self.started or not self.workflow_execution.open: return None # TODO: handle the "NONE" case - start_to_close_at = self.started_timestamp + int(self.start_to_close_timeout) + start_to_close_at = self.started_timestamp + \ + int(self.start_to_close_timeout) _timeout = Timeout(self, start_to_close_at, "START_TO_CLOSE") if _timeout.reached: return _timeout diff --git a/moto/swf/models/domain.py b/moto/swf/models/domain.py index 4efdc3150..ed7154067 100644 --- a/moto/swf/models/domain.py +++ b/moto/swf/models/domain.py @@ -8,6 +8,7 @@ from ..exceptions import ( class Domain(object): + def __init__(self, name, retention, description=None): self.name = name self.retention = retention diff --git a/moto/swf/models/generic_type.py b/moto/swf/models/generic_type.py index 7c8389fbe..2ae98bb53 100644 --- a/moto/swf/models/generic_type.py +++ b/moto/swf/models/generic_type.py @@ -4,6 +4,7 @@ from moto.core.utils import camelcase_to_underscores class GenericType(object): + def __init__(self, name, version, **kwargs): self.name = name self.version = version diff --git a/moto/swf/models/history_event.py b/moto/swf/models/history_event.py index b181297f7..e841ca38e 100644 --- a/moto/swf/models/history_event.py +++ b/moto/swf/models/history_event.py @@ -28,10 +28,12 @@ SUPPORTED_HISTORY_EVENT_TYPES = ( class HistoryEvent(object): + def __init__(self, event_id, event_type, event_timestamp=None, **kwargs): if event_type not in SUPPORTED_HISTORY_EVENT_TYPES: raise NotImplementedError( - "HistoryEvent does not implement attributes for type '{0}'".format(event_type) + "HistoryEvent does not implement attributes for type '{0}'".format( + event_type) ) self.event_id = event_id self.event_type = event_type diff --git a/moto/swf/models/timeout.py b/moto/swf/models/timeout.py index cf0283760..09e0f6772 100644 --- a/moto/swf/models/timeout.py +++ b/moto/swf/models/timeout.py @@ -2,6 +2,7 @@ from moto.core.utils import unix_time class Timeout(object): + def __init__(self, obj, timestamp, kind): self.obj = obj self.timestamp = timestamp diff --git a/moto/swf/models/workflow_execution.py b/moto/swf/models/workflow_execution.py index a30c2e18d..8b8acda4e 100644 --- a/moto/swf/models/workflow_execution.py +++ b/moto/swf/models/workflow_execution.py @@ -64,9 +64,12 @@ class WorkflowExecution(object): # NB: the order follows boto/SWF order of exceptions appearance (if no # param is set, # SWF will raise DefaultUndefinedFault errors in the # same order as the few lines that follow) - self._set_from_kwargs_or_workflow_type(kwargs, "execution_start_to_close_timeout") - self._set_from_kwargs_or_workflow_type(kwargs, "task_list", "task_list") - self._set_from_kwargs_or_workflow_type(kwargs, "task_start_to_close_timeout") + self._set_from_kwargs_or_workflow_type( + kwargs, "execution_start_to_close_timeout") + self._set_from_kwargs_or_workflow_type( + kwargs, "task_list", "task_list") + self._set_from_kwargs_or_workflow_type( + kwargs, "task_start_to_close_timeout") self._set_from_kwargs_or_workflow_type(kwargs, "child_policy") self.input = kwargs.get("input") # counters @@ -368,13 +371,16 @@ class WorkflowExecution(object): # check decision types mandatory attributes # NB: the real SWF service seems to check attributes even for attributes list # that are not in line with the decisionType, so we do the same - attrs_to_check = [d for d in dcs.keys() if d.endswith("DecisionAttributes")] + attrs_to_check = [ + d for d in dcs.keys() if d.endswith("DecisionAttributes")] if dcs["decisionType"] in self.KNOWN_DECISION_TYPES: decision_type = dcs["decisionType"] - decision_attr = "{0}DecisionAttributes".format(decapitalize(decision_type)) + decision_attr = "{0}DecisionAttributes".format( + decapitalize(decision_type)) attrs_to_check.append(decision_attr) for attr in attrs_to_check: - problems += self._check_decision_attributes(attr, dcs.get(attr, {}), decision_number) + problems += self._check_decision_attributes( + attr, dcs.get(attr, {}), decision_number) # check decision type is correct if dcs["decisionType"] not in self.KNOWN_DECISION_TYPES: problems.append({ @@ -396,12 +402,14 @@ class WorkflowExecution(object): # handle each decision separately, in order for decision in decisions: decision_type = decision["decisionType"] - attributes_key = "{0}DecisionAttributes".format(decapitalize(decision_type)) + attributes_key = "{0}DecisionAttributes".format( + decapitalize(decision_type)) attributes = decision.get(attributes_key, {}) if decision_type == "CompleteWorkflowExecution": self.complete(event_id, attributes.get("result")) elif decision_type == "FailWorkflowExecution": - self.fail(event_id, attributes.get("details"), attributes.get("reason")) + self.fail(event_id, attributes.get( + "details"), attributes.get("reason")) elif decision_type == "ScheduleActivityTask": self.schedule_activity_task(event_id, attributes) else: @@ -415,7 +423,8 @@ class WorkflowExecution(object): # TODO: implement Decision type: SignalExternalWorkflowExecution # TODO: implement Decision type: StartChildWorkflowExecution # TODO: implement Decision type: StartTimer - raise NotImplementedError("Cannot handle decision: {0}".format(decision_type)) + raise NotImplementedError( + "Cannot handle decision: {0}".format(decision_type)) # finally decrement counter if and only if everything went well self.open_counts["openDecisionTasks"] -= 1 @@ -447,7 +456,8 @@ class WorkflowExecution(object): def fail_schedule_activity_task(_type, _cause): # TODO: implement other possible failure mode: OPEN_ACTIVITIES_LIMIT_EXCEEDED # NB: some failure modes are not implemented and probably won't be implemented in - # the future, such as ACTIVITY_CREATION_RATE_EXCEEDED or OPERATION_NOT_PERMITTED + # the future, such as ACTIVITY_CREATION_RATE_EXCEEDED or + # OPERATION_NOT_PERMITTED self._add_event( "ScheduleActivityTaskFailed", activity_id=attributes["activityId"], @@ -591,13 +601,15 @@ class WorkflowExecution(object): def first_timeout(self): if not self.open or not self.start_timestamp: return None - start_to_close_at = self.start_timestamp + int(self.execution_start_to_close_timeout) + start_to_close_at = self.start_timestamp + \ + int(self.execution_start_to_close_timeout) _timeout = Timeout(self, start_to_close_at, "START_TO_CLOSE") if _timeout.reached: return _timeout def timeout(self, timeout): - # TODO: process child policy on child workflows here or in the triggering function + # TODO: process child policy on child workflows here or in the + # triggering function self.execution_status = "CLOSED" self.close_status = "TIMED_OUT" self.timeout_type = timeout.kind diff --git a/moto/swf/models/workflow_type.py b/moto/swf/models/workflow_type.py index ddb2475b2..18d18d415 100644 --- a/moto/swf/models/workflow_type.py +++ b/moto/swf/models/workflow_type.py @@ -2,6 +2,7 @@ from .generic_type import GenericType class WorkflowType(GenericType): + @property def _configuration_keys(self): return [ diff --git a/moto/swf/responses.py b/moto/swf/responses.py index 92d4957fd..1ee89bfc1 100644 --- a/moto/swf/responses.py +++ b/moto/swf/responses.py @@ -64,7 +64,8 @@ class SWFResponse(BaseResponse): reverse_order = self._params.get("reverseOrder", None) self._check_string(domain_name) self._check_string(status) - types = self.swf_backend.list_types(kind, domain_name, status, reverse_order=reverse_order) + types = self.swf_backend.list_types( + kind, domain_name, status, reverse_order=reverse_order) return json.dumps({ "typeInfos": [_type.to_medium_dict() for _type in types] }) @@ -97,7 +98,8 @@ class SWFResponse(BaseResponse): status = self._params["registrationStatus"] self._check_string(status) reverse_order = self._params.get("reverseOrder", None) - domains = self.swf_backend.list_domains(status, reverse_order=reverse_order) + domains = self.swf_backend.list_domains( + status, reverse_order=reverse_order) return json.dumps({ "domainInfos": [domain.to_short_dict() for domain in domains] }) @@ -107,7 +109,8 @@ class SWFResponse(BaseResponse): start_time_filter = self._params.get('startTimeFilter', None) close_time_filter = self._params.get('closeTimeFilter', None) execution_filter = self._params.get('executionFilter', None) - workflow_id = execution_filter['workflowId'] if execution_filter else None + workflow_id = execution_filter[ + 'workflowId'] if execution_filter else None maximum_page_size = self._params.get('maximumPageSize', 1000) reverse_order = self._params.get('reverseOrder', None) tag_filter = self._params.get('tagFilter', None) @@ -162,7 +165,8 @@ class SWFResponse(BaseResponse): domain = self._params['domain'] start_time_filter = self._params['startTimeFilter'] execution_filter = self._params.get('executionFilter', None) - workflow_id = execution_filter['workflowId'] if execution_filter else None + workflow_id = execution_filter[ + 'workflowId'] if execution_filter else None maximum_page_size = self._params.get('maximumPageSize', 1000) reverse_order = self._params.get('reverseOrder', None) tag_filter = self._params.get('tagFilter', None) @@ -234,10 +238,14 @@ class SWFResponse(BaseResponse): task_list = default_task_list.get("name") else: task_list = None - default_task_heartbeat_timeout = self._params.get("defaultTaskHeartbeatTimeout") - default_task_schedule_to_close_timeout = self._params.get("defaultTaskScheduleToCloseTimeout") - default_task_schedule_to_start_timeout = self._params.get("defaultTaskScheduleToStartTimeout") - default_task_start_to_close_timeout = self._params.get("defaultTaskStartToCloseTimeout") + default_task_heartbeat_timeout = self._params.get( + "defaultTaskHeartbeatTimeout") + default_task_schedule_to_close_timeout = self._params.get( + "defaultTaskScheduleToCloseTimeout") + default_task_schedule_to_start_timeout = self._params.get( + "defaultTaskScheduleToStartTimeout") + default_task_start_to_close_timeout = self._params.get( + "defaultTaskStartToCloseTimeout") description = self._params.get("description") self._check_string(domain) @@ -280,8 +288,10 @@ class SWFResponse(BaseResponse): else: task_list = None default_child_policy = self._params.get("defaultChildPolicy") - default_task_start_to_close_timeout = self._params.get("defaultTaskStartToCloseTimeout") - default_execution_start_to_close_timeout = self._params.get("defaultExecutionStartToCloseTimeout") + default_task_start_to_close_timeout = self._params.get( + "defaultTaskStartToCloseTimeout") + default_execution_start_to_close_timeout = self._params.get( + "defaultExecutionStartToCloseTimeout") description = self._params.get("description") self._check_string(domain) @@ -322,10 +332,12 @@ class SWFResponse(BaseResponse): else: task_list = None child_policy = self._params.get("childPolicy") - execution_start_to_close_timeout = self._params.get("executionStartToCloseTimeout") + execution_start_to_close_timeout = self._params.get( + "executionStartToCloseTimeout") input_ = self._params.get("input") tag_list = self._params.get("tagList") - task_start_to_close_timeout = self._params.get("taskStartToCloseTimeout") + task_start_to_close_timeout = self._params.get( + "taskStartToCloseTimeout") self._check_string(domain) self._check_string(workflow_id) @@ -360,7 +372,8 @@ class SWFResponse(BaseResponse): self._check_string(run_id) self._check_string(workflow_id) - wfe = self.swf_backend.describe_workflow_execution(domain_name, run_id, workflow_id) + wfe = self.swf_backend.describe_workflow_execution( + domain_name, run_id, workflow_id) return json.dumps(wfe.to_full_dict()) def get_workflow_execution_history(self): @@ -369,7 +382,8 @@ class SWFResponse(BaseResponse): run_id = _workflow_execution["runId"] workflow_id = _workflow_execution["workflowId"] reverse_order = self._params.get("reverseOrder", None) - wfe = self.swf_backend.describe_workflow_execution(domain_name, run_id, workflow_id) + wfe = self.swf_backend.describe_workflow_execution( + domain_name, run_id, workflow_id) events = wfe.events(reverse_order=reverse_order) return json.dumps({ "events": [evt.to_dict() for evt in events] @@ -399,7 +413,8 @@ class SWFResponse(BaseResponse): task_list = self._params["taskList"]["name"] self._check_string(domain_name) self._check_string(task_list) - count = self.swf_backend.count_pending_decision_tasks(domain_name, task_list) + count = self.swf_backend.count_pending_decision_tasks( + domain_name, task_list) return json.dumps({"count": count, "truncated": False}) def respond_decision_task_completed(self): @@ -435,7 +450,8 @@ class SWFResponse(BaseResponse): task_list = self._params["taskList"]["name"] self._check_string(domain_name) self._check_string(task_list) - count = self.swf_backend.count_pending_activity_tasks(domain_name, task_list) + count = self.swf_backend.count_pending_activity_tasks( + domain_name, task_list) return json.dumps({"count": count, "truncated": False}) def respond_activity_task_completed(self): @@ -453,7 +469,8 @@ class SWFResponse(BaseResponse): reason = self._params.get("reason") details = self._params.get("details") self._check_string(task_token) - # TODO: implement length limits on reason and details (common pb with client libs) + # TODO: implement length limits on reason and details (common pb with + # client libs) self._check_none_or_string(reason) self._check_none_or_string(details) self.swf_backend.respond_activity_task_failed( diff --git a/tests/backport_assert_raises.py b/tests/backport_assert_raises.py index 6ceacaa89..9b20edf9d 100644 --- a/tests/backport_assert_raises.py +++ b/tests/backport_assert_raises.py @@ -19,6 +19,7 @@ try: except TypeError: # this version of assert_raises doesn't support the 1-arg version class AssertRaisesContext(object): + def __init__(self, expected): self.expected = expected diff --git a/tests/helpers.py b/tests/helpers.py index 33509c06e..50615b094 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -8,13 +8,15 @@ def version_tuple(v): return tuple(map(int, (v.split(".")))) -# Note: See https://github.com/spulec/moto/issues/201 for why this is a separate method. +# Note: See https://github.com/spulec/moto/issues/201 for why this is a +# separate method. def skip_test(): raise SkipTest class requires_boto_gte(object): """Decorator for requiring boto version greater than or equal to 'version'""" + def __init__(self, version): self.version = version @@ -27,6 +29,7 @@ class requires_boto_gte(object): class disable_on_py3(object): + def __call__(self, test): if not six.PY3: return test diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index e52bfe0d7..11230658b 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -72,13 +72,15 @@ def test_create_resource(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] root_resource = client.get_resource( restApiId=api_id, resourceId=root_id, ) - root_resource['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + root_resource['ResponseMetadata'].pop('HTTPHeaders', None) root_resource['ResponseMetadata'].pop('RetryAttempts', None) root_resource.should.equal({ 'path': '/', @@ -97,7 +99,8 @@ def test_create_resource(): resources = client.get_resources(restApiId=api_id)['items'] len(resources).should.equal(2) - non_root_resource = [resource for resource in resources if resource['path'] != '/'][0] + non_root_resource = [ + resource for resource in resources if resource['path'] != '/'][0] response = client.delete_resource( restApiId=api_id, @@ -117,7 +120,8 @@ def test_child_resource(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] response = client.create_resource( restApiId=api_id, @@ -137,7 +141,8 @@ def test_child_resource(): restApiId=api_id, resourceId=tags_id, ) - child_resource['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + child_resource['ResponseMetadata'].pop('HTTPHeaders', None) child_resource['ResponseMetadata'].pop('RetryAttempts', None) child_resource.should.equal({ 'path': '/users/tags', @@ -159,7 +164,8 @@ def test_create_method(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] client.put_method( restApiId=api_id, @@ -174,7 +180,8 @@ def test_create_method(): httpMethod='GET' ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'httpMethod': 'GET', @@ -193,7 +200,8 @@ def test_create_method_response(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] client.put_method( restApiId=api_id, @@ -214,7 +222,8 @@ def test_create_method_response(): httpMethod='GET', statusCode='200', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'ResponseMetadata': {'HTTPStatusCode': 200}, @@ -227,7 +236,8 @@ def test_create_method_response(): httpMethod='GET', statusCode='200', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'ResponseMetadata': {'HTTPStatusCode': 200}, @@ -240,7 +250,8 @@ def test_create_method_response(): httpMethod='GET', statusCode='200', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({'ResponseMetadata': {'HTTPStatusCode': 200}}) @@ -255,7 +266,8 @@ def test_integrations(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] client.put_method( restApiId=api_id, @@ -278,7 +290,8 @@ def test_integrations(): type='HTTP', uri='http://httpbin.org/robots.txt', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'ResponseMetadata': {'HTTPStatusCode': 200}, @@ -300,7 +313,8 @@ def test_integrations(): resourceId=root_id, httpMethod='GET' ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'ResponseMetadata': {'HTTPStatusCode': 200}, @@ -321,7 +335,8 @@ def test_integrations(): restApiId=api_id, resourceId=root_id, ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response['resourceMethods']['GET']['methodIntegration'].should.equal({ 'httpMethod': 'GET', @@ -359,7 +374,8 @@ def test_integrations(): ) templates = { - # example based on http://docs.aws.amazon.com/apigateway/latest/developerguide/api-as-kinesis-proxy-export-swagger-with-extensions.html + # example based on + # http://docs.aws.amazon.com/apigateway/latest/developerguide/api-as-kinesis-proxy-export-swagger-with-extensions.html 'application/json': "{\n \"StreamName\": \"$input.params('stream-name')\",\n \"Records\": []\n}" } test_uri = 'http://example.com/foobar.txt' @@ -371,7 +387,8 @@ def test_integrations(): uri=test_uri, requestTemplates=templates ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response['ResponseMetadata'].should.equal({'HTTPStatusCode': 200}) @@ -394,7 +411,8 @@ def test_integration_response(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] client.put_method( restApiId=api_id, @@ -425,7 +443,8 @@ def test_integration_response(): statusCode='200', selectionPattern='foobar', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'statusCode': '200', @@ -442,7 +461,8 @@ def test_integration_response(): httpMethod='GET', statusCode='200', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'statusCode': '200', @@ -458,7 +478,8 @@ def test_integration_response(): resourceId=root_id, httpMethod='GET', ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response['methodIntegration']['integrationResponses'].should.equal({ '200': { @@ -506,23 +527,24 @@ def test_update_stage_configuration(): restApiId=api_id, deploymentId=deployment_id, ) - response.pop('createdDate',None) # createdDate is hard to match against, remove it - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # createdDate is hard to match against, remove it + response.pop('createdDate', None) + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'id': deployment_id, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description' : '1.0.1' + 'description': '1.0.1' }) response = client.create_deployment( - restApiId=api_id, - stageName=stage_name, - description="1.0.2" - ) + restApiId=api_id, + stageName=stage_name, + description="1.0.2" + ) deployment_id2 = response['id'] - stage = client.get_stage( restApiId=api_id, stageName=stage_name @@ -531,11 +553,11 @@ def test_update_stage_configuration(): stage['deploymentId'].should.equal(deployment_id2) stage.shouldnt.have.key('cacheClusterSize') - client.update_stage(restApiId=api_id,stageName=stage_name, + client.update_stage(restApiId=api_id, stageName=stage_name, patchOperations=[ { - "op" : "replace", - "path" : "/cacheClusterEnabled", + "op": "replace", + "path": "/cacheClusterEnabled", "value": "True" } ]) @@ -547,11 +569,11 @@ def test_update_stage_configuration(): stage.should.have.key('cacheClusterSize').which.should.equal("0.5") - client.update_stage(restApiId=api_id,stageName=stage_name, + client.update_stage(restApiId=api_id, stageName=stage_name, patchOperations=[ { - "op" : "replace", - "path" : "/cacheClusterSize", + "op": "replace", + "path": "/cacheClusterSize", "value": "1.6" } ]) @@ -563,56 +585,55 @@ def test_update_stage_configuration(): stage.should.have.key('cacheClusterSize').which.should.equal("1.6") - - client.update_stage(restApiId=api_id,stageName=stage_name, + client.update_stage(restApiId=api_id, stageName=stage_name, patchOperations=[ { - "op" : "replace", - "path" : "/deploymentId", + "op": "replace", + "path": "/deploymentId", "value": deployment_id }, { - "op" : "replace", - "path" : "/variables/environment", - "value" : "dev" + "op": "replace", + "path": "/variables/environment", + "value": "dev" }, { - "op" : "replace", - "path" : "/variables/region", - "value" : "eu-west-1" + "op": "replace", + "path": "/variables/region", + "value": "eu-west-1" }, { - "op" : "replace", - "path" : "/*/*/caching/dataEncrypted", - "value" : "True" + "op": "replace", + "path": "/*/*/caching/dataEncrypted", + "value": "True" }, { - "op" : "replace", - "path" : "/cacheClusterEnabled", - "value" : "True" + "op": "replace", + "path": "/cacheClusterEnabled", + "value": "True" }, { - "op" : "replace", - "path" : "/description", - "value" : "stage description update" + "op": "replace", + "path": "/description", + "value": "stage description update" }, { - "op" : "replace", - "path" : "/cacheClusterSize", - "value" : "1.6" + "op": "replace", + "path": "/cacheClusterSize", + "value": "1.6" } ]) - client.update_stage(restApiId=api_id,stageName=stage_name, + client.update_stage(restApiId=api_id, stageName=stage_name, patchOperations=[ { - "op" : "remove", - "path" : "/variables/region", - "value" : "eu-west-1" + "op": "remove", + "path": "/variables/region", + "value": "eu-west-1" } ]) - stage = client.get_stage(restApiId=api_id,stageName=stage_name) + stage = client.get_stage(restApiId=api_id, stageName=stage_name) stage['description'].should.match('stage description update') stage['cacheClusterSize'].should.equal("1.6") @@ -621,21 +642,23 @@ def test_update_stage_configuration(): stage['cacheClusterEnabled'].should.be.true stage['deploymentId'].should.match(deployment_id) stage['methodSettings'].should.have.key('*/*') - stage['methodSettings']['*/*'].should.have.key('cacheDataEncrypted').which.should.be.true + stage['methodSettings'][ + '*/*'].should.have.key('cacheDataEncrypted').which.should.be.true try: - client.update_stage(restApiId=api_id,stageName=stage_name, - patchOperations=[ - { - "op" : "add", - "path" : "/notasetting", - "value" : "eu-west-1" - } - ]) - assert False.should.be.ok #Fail, should not be here + client.update_stage(restApiId=api_id, stageName=stage_name, + patchOperations=[ + { + "op": "add", + "path": "/notasetting", + "value": "eu-west-1" + } + ]) + assert False.should.be.ok # Fail, should not be here except Exception: assert True.should.be.ok + @mock_apigateway def test_non_existent_stage(): client = boto3.client('apigateway', region_name='us-west-2') @@ -645,9 +668,8 @@ def test_non_existent_stage(): ) api_id = response['id'] - - client.get_stage.when.called_with(restApiId=api_id,stageName='xxx').should.throw(ClientError) - + client.get_stage.when.called_with( + restApiId=api_id, stageName='xxx').should.throw(ClientError) @mock_apigateway @@ -670,13 +692,15 @@ def test_create_stage(): restApiId=api_id, deploymentId=deployment_id, ) - response.pop('createdDate',None) # createdDate is hard to match against, remove it - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # createdDate is hard to match against, remove it + response.pop('createdDate', None) + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'id': deployment_id, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description' : '' + 'description': '' }) response = client.create_deployment( @@ -686,34 +710,37 @@ def test_create_stage(): deployment_id2 = response['id'] - response = client.get_deployments( restApiId=api_id, ) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response['items'][0].pop('createdDate') response['items'][1].pop('createdDate') - response['items'][0]['id'].should.match(r"{0}|{1}".format(deployment_id2,deployment_id)) - response['items'][1]['id'].should.match(r"{0}|{1}".format(deployment_id2,deployment_id)) - + response['items'][0]['id'].should.match( + r"{0}|{1}".format(deployment_id2, deployment_id)) + response['items'][1]['id'].should.match( + r"{0}|{1}".format(deployment_id2, deployment_id)) new_stage_name = 'current' - response = client.create_stage(restApiId=api_id,stageName=new_stage_name,deploymentId=deployment_id2) + response = client.create_stage( + restApiId=api_id, stageName=new_stage_name, deploymentId=deployment_id2) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ - 'stageName':new_stage_name, - 'deploymentId':deployment_id2, - 'methodSettings':{}, - 'variables':{}, + 'stageName': new_stage_name, + 'deploymentId': deployment_id2, + 'methodSettings': {}, + 'variables': {}, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description':'', - 'cacheClusterEnabled':False + 'description': '', + 'cacheClusterEnabled': False }) stage = client.get_stage( @@ -724,20 +751,21 @@ def test_create_stage(): stage['deploymentId'].should.equal(deployment_id2) new_stage_name_with_vars = 'stage_with_vars' - response = client.create_stage(restApiId=api_id,stageName=new_stage_name_with_vars,deploymentId=deployment_id2,variables={ - "env" : "dev" + response = client.create_stage(restApiId=api_id, stageName=new_stage_name_with_vars, deploymentId=deployment_id2, variables={ + "env": "dev" }) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ - 'stageName':new_stage_name_with_vars, - 'deploymentId':deployment_id2, - 'methodSettings':{}, - 'variables':{ "env" : "dev" }, + 'stageName': new_stage_name_with_vars, + 'deploymentId': deployment_id2, + 'methodSettings': {}, + 'variables': {"env": "dev"}, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description':'', + 'description': '', 'cacheClusterEnabled': False }) @@ -750,22 +778,23 @@ def test_create_stage(): stage['variables'].should.have.key('env').which.should.match("dev") new_stage_name = 'stage_with_vars_and_cache_settings' - response = client.create_stage(restApiId=api_id,stageName=new_stage_name,deploymentId=deployment_id2,variables={ - "env" : "dev" - }, cacheClusterEnabled=True,description="hello moto") + response = client.create_stage(restApiId=api_id, stageName=new_stage_name, deploymentId=deployment_id2, variables={ + "env": "dev" + }, cacheClusterEnabled=True, description="hello moto") - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ - 'stageName':new_stage_name, - 'deploymentId':deployment_id2, - 'methodSettings':{}, - 'variables':{ "env" : "dev" }, + 'stageName': new_stage_name, + 'deploymentId': deployment_id2, + 'methodSettings': {}, + 'variables': {"env": "dev"}, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description':'hello moto', + 'description': 'hello moto', 'cacheClusterEnabled': True, - 'cacheClusterSize' : "0.5" + 'cacheClusterSize': "0.5" }) stage = client.get_stage( @@ -776,22 +805,23 @@ def test_create_stage(): stage['cacheClusterSize'].should.equal("0.5") new_stage_name = 'stage_with_vars_and_cache_settings_and_size' - response = client.create_stage(restApiId=api_id,stageName=new_stage_name,deploymentId=deployment_id2,variables={ - "env" : "dev" - }, cacheClusterEnabled=True,cacheClusterSize="1.6",description="hello moto") + response = client.create_stage(restApiId=api_id, stageName=new_stage_name, deploymentId=deployment_id2, variables={ + "env": "dev" + }, cacheClusterEnabled=True, cacheClusterSize="1.6", description="hello moto") - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ - 'stageName':new_stage_name, - 'deploymentId':deployment_id2, - 'methodSettings':{}, - 'variables':{ "env" : "dev" }, + 'stageName': new_stage_name, + 'deploymentId': deployment_id2, + 'methodSettings': {}, + 'variables': {"env": "dev"}, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description':'hello moto', + 'description': 'hello moto', 'cacheClusterEnabled': True, - 'cacheClusterSize' : "1.6" + 'cacheClusterSize': "1.6" }) stage = client.get_stage( @@ -804,7 +834,6 @@ def test_create_stage(): stage['cacheClusterSize'].should.equal("1.6") - @mock_apigateway def test_deployment(): client = boto3.client('apigateway', region_name='us-west-2') @@ -825,13 +854,15 @@ def test_deployment(): restApiId=api_id, deploymentId=deployment_id, ) - response.pop('createdDate',None) # createdDate is hard to match against, remove it - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # createdDate is hard to match against, remove it + response.pop('createdDate', None) + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'id': deployment_id, 'ResponseMetadata': {'HTTPStatusCode': 200}, - 'description' : '' + 'description': '' }) response = client.get_deployments( @@ -898,7 +929,8 @@ def test_http_proxying_integration(): api_id = response['id'] resources = client.get_resources(restApiId=api_id) - root_id = [resource for resource in resources['items'] if resource['path'] == '/'][0]['id'] + root_id = [resource for resource in resources[ + 'items'] if resource['path'] == '/'][0]['id'] client.put_method( restApiId=api_id, @@ -928,7 +960,8 @@ def test_http_proxying_integration(): stageName=stage_name, ) - deploy_url = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}".format(api_id=api_id, region_name=region_name, stage_name=stage_name) + deploy_url = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}".format( + api_id=api_id, region_name=region_name, stage_name=stage_name) if not settings.TEST_SERVER_MODE: requests.get(deploy_url).content.should.equal(b"a fake response") diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 4d0905196..9a6408999 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -16,7 +16,8 @@ from tests.helpers import requires_boto_gte @mock_elb_deprecated def test_create_autoscaling_group(): elb_conn = boto.ec2.elb.connect_to_region('us-east-1') - elb_conn.create_load_balancer('test_lb', zones=[], listeners=[(80, 8080, 'http')]) + elb_conn.create_load_balancer( + 'test_lb', zones=[], listeners=[(80, 8080, 'http')]) conn = boto.ec2.autoscale.connect_to_region('us-east-1') config = LaunchConfiguration( @@ -45,14 +46,15 @@ def test_create_autoscaling_group(): key='test_key', value='test_value', propagate_at_launch=True - ) + ) ], ) conn.create_auto_scaling_group(group) group = conn.get_all_groups()[0] group.name.should.equal('tester_group') - set(group.availability_zones).should.equal(set(['us-east-1c', 'us-east-1b'])) + set(group.availability_zones).should.equal( + set(['us-east-1c', 'us-east-1b'])) group.desired_capacity.should.equal(2) group.max_size.should.equal(2) group.min_size.should.equal(2) @@ -64,7 +66,8 @@ def test_create_autoscaling_group(): group.health_check_type.should.equal("EC2") list(group.load_balancers).should.equal(["test_lb"]) group.placement_group.should.equal("test_placement") - list(group.termination_policies).should.equal(["OldestInstance", "NewestInstance"]) + list(group.termination_policies).should.equal( + ["OldestInstance", "NewestInstance"]) len(list(group.tags)).should.equal(1) tag = list(group.tags)[0] tag.resource_id.should.equal('tester_group') @@ -134,7 +137,8 @@ def test_autoscaling_group_describe_filter(): group.name = 'tester_group3' conn.create_auto_scaling_group(group) - conn.get_all_groups(names=['tester_group', 'tester_group2']).should.have.length_of(2) + conn.get_all_groups( + names=['tester_group', 'tester_group2']).should.have.length_of(2) conn.get_all_groups().should.have.length_of(3) @@ -197,16 +201,16 @@ def test_autoscaling_tags_update(): conn.create_auto_scaling_group(group) conn.create_or_update_tags(tags=[Tag( - resource_id='tester_group', - key='test_key', - value='new_test_value', - propagate_at_launch=True - ), Tag( - resource_id='tester_group', - key='test_key2', - value='test_value2', - propagate_at_launch=True - )]) + resource_id='tester_group', + key='test_key', + value='new_test_value', + propagate_at_launch=True + ), Tag( + resource_id='tester_group', + key='test_key2', + value='test_value2', + propagate_at_launch=True + )]) group = conn.get_all_groups()[0] group.tags.should.have.length_of(2) @@ -372,6 +376,7 @@ def test_set_desired_capacity_the_same(): instances = list(conn.get_all_autoscaling_instances()) instances.should.have.length_of(2) + @mock_autoscaling_deprecated @mock_elb_deprecated def test_autoscaling_group_with_elb(): @@ -402,7 +407,8 @@ def test_autoscaling_group_with_elb(): group.desired_capacity.should.equal(2) elb.instances.should.have.length_of(2) - autoscale_instance_ids = set(instance.instance_id for instance in group.instances) + autoscale_instance_ids = set( + instance.instance_id for instance in group.instances) elb_instace_ids = set(instance.id for instance in elb.instances) autoscale_instance_ids.should.equal(elb_instace_ids) @@ -412,7 +418,8 @@ def test_autoscaling_group_with_elb(): group.desired_capacity.should.equal(3) elb.instances.should.have.length_of(3) - autoscale_instance_ids = set(instance.instance_id for instance in group.instances) + autoscale_instance_ids = set( + instance.instance_id for instance in group.instances) elb_instace_ids = set(instance.id for instance in elb.instances) autoscale_instance_ids.should.equal(elb_instace_ids) @@ -429,38 +436,39 @@ Boto3 @mock_autoscaling def test_create_autoscaling_group_boto3(): - client = boto3.client('autoscaling', region_name='us-east-1') - _ = client.create_launch_configuration( - LaunchConfigurationName='test_launch_configuration' - ) - response = client.create_auto_scaling_group( - AutoScalingGroupName='test_asg', - LaunchConfigurationName='test_launch_configuration', - MinSize=0, - MaxSize=20, - DesiredCapacity=5 - ) - response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + client = boto3.client('autoscaling', region_name='us-east-1') + _ = client.create_launch_configuration( + LaunchConfigurationName='test_launch_configuration' + ) + response = client.create_auto_scaling_group( + AutoScalingGroupName='test_asg', + LaunchConfigurationName='test_launch_configuration', + MinSize=0, + MaxSize=20, + DesiredCapacity=5 + ) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) @mock_autoscaling def test_describe_autoscaling_groups_boto3(): - client = boto3.client('autoscaling', region_name='us-east-1') - _ = client.create_launch_configuration( - LaunchConfigurationName='test_launch_configuration' - ) - _ = client.create_auto_scaling_group( - AutoScalingGroupName='test_asg', - LaunchConfigurationName='test_launch_configuration', - MinSize=0, - MaxSize=20, - DesiredCapacity=5 - ) - response = client.describe_auto_scaling_groups( - AutoScalingGroupNames=["test_asg"] - ) - response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) - response['AutoScalingGroups'][0]['AutoScalingGroupName'].should.equal('test_asg') + client = boto3.client('autoscaling', region_name='us-east-1') + _ = client.create_launch_configuration( + LaunchConfigurationName='test_launch_configuration' + ) + _ = client.create_auto_scaling_group( + AutoScalingGroupName='test_asg', + LaunchConfigurationName='test_launch_configuration', + MinSize=0, + MaxSize=20, + DesiredCapacity=5 + ) + response = client.describe_auto_scaling_groups( + AutoScalingGroupNames=["test_asg"] + ) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['AutoScalingGroups'][0][ + 'AutoScalingGroupName'].should.equal('test_asg') @mock_autoscaling @@ -509,22 +517,23 @@ def test_autoscaling_taqs_update_boto3(): ) client.create_or_update_tags(Tags=[{ - "ResourceId": 'test_asg', - "Key": 'test_key', - "Value": 'updated_test_value', - "PropagateAtLaunch": True - }, { - "ResourceId": 'test_asg', - "Key": 'test_key2', - "Value": 'test_value2', - "PropagateAtLaunch": True - }]) + "ResourceId": 'test_asg', + "Key": 'test_key', + "Value": 'updated_test_value', + "PropagateAtLaunch": True + }, { + "ResourceId": 'test_asg', + "Key": 'test_key2', + "Value": 'test_value2', + "PropagateAtLaunch": True + }]) response = client.describe_auto_scaling_groups( AutoScalingGroupNames=["test_asg"] ) response['AutoScalingGroups'][0]['Tags'].should.have.length_of(2) + @mock_autoscaling def test_autoscaling_describe_policies_boto3(): client = boto3.client('autoscaling', region_name='us-east-1') @@ -577,4 +586,5 @@ def test_autoscaling_describe_policies_boto3(): PolicyTypes=['SimpleScaling'] ) response['ScalingPolicies'].should.have.length_of(1) - response['ScalingPolicies'][0]['PolicyName'].should.equal('test_policy_down') + response['ScalingPolicies'][0][ + 'PolicyName'].should.equal('test_policy_down') diff --git a/tests/test_autoscaling/test_launch_configurations.py b/tests/test_autoscaling/test_launch_configurations.py index b2e21b03e..1c1486421 100644 --- a/tests/test_autoscaling/test_launch_configurations.py +++ b/tests/test_autoscaling/test_launch_configurations.py @@ -30,10 +30,12 @@ def test_create_launch_configuration(): launch_config.image_id.should.equal('ami-abcd1234') launch_config.instance_type.should.equal('t1.micro') launch_config.key_name.should.equal('the_keys') - set(launch_config.security_groups).should.equal(set(['default', 'default2'])) + set(launch_config.security_groups).should.equal( + set(['default', 'default2'])) launch_config.user_data.should.equal(b"This is some user_data") launch_config.instance_monitoring.enabled.should.equal('true') - launch_config.instance_profile_name.should.equal('arn:aws:iam::123456789012:instance-profile/testing') + launch_config.instance_profile_name.should.equal( + 'arn:aws:iam::123456789012:instance-profile/testing') launch_config.spot_price.should.equal(0.1) @@ -78,16 +80,19 @@ def test_create_launch_configuration_with_block_device_mappings(): launch_config.image_id.should.equal('ami-abcd1234') launch_config.instance_type.should.equal('m1.small') launch_config.key_name.should.equal('the_keys') - set(launch_config.security_groups).should.equal(set(['default', 'default2'])) + set(launch_config.security_groups).should.equal( + set(['default', 'default2'])) launch_config.user_data.should.equal(b"This is some user_data") launch_config.instance_monitoring.enabled.should.equal('true') - launch_config.instance_profile_name.should.equal('arn:aws:iam::123456789012:instance-profile/testing') + launch_config.instance_profile_name.should.equal( + 'arn:aws:iam::123456789012:instance-profile/testing') launch_config.spot_price.should.equal(0.1) len(launch_config.block_device_mappings).should.equal(3) returned_mapping = launch_config.block_device_mappings - set(returned_mapping.keys()).should.equal(set(['/dev/xvdb', '/dev/xvdp', '/dev/xvdh'])) + set(returned_mapping.keys()).should.equal( + set(['/dev/xvdb', '/dev/xvdp', '/dev/xvdh'])) returned_mapping['/dev/xvdh'].iops.should.equal(1000) returned_mapping['/dev/xvdh'].size.should.equal(100) @@ -198,7 +203,8 @@ def test_launch_configuration_describe_filter(): config.name = 'tester3' conn.create_launch_configuration(config) - conn.get_all_launch_configurations(names=['tester', 'tester2']).should.have.length_of(2) + conn.get_all_launch_configurations( + names=['tester', 'tester2']).should.have.length_of(2) conn.get_all_launch_configurations().should.have.length_of(3) diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 74e93c373..84e8a8f2b 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -73,8 +73,10 @@ def test_invoke_requestresponse_function(): Payload=json.dumps(in_data)) success_result["StatusCode"].should.equal(202) - base64.b64decode(success_result["LogResult"]).decode('utf-8').should.equal(json.dumps(in_data)) - json.loads(success_result["Payload"].read().decode('utf-8')).should.equal(in_data) + base64.b64decode(success_result["LogResult"]).decode( + 'utf-8').should.equal(json.dumps(in_data)) + json.loads(success_result["Payload"].read().decode( + 'utf-8')).should.equal(in_data) @mock_lambda @@ -101,9 +103,11 @@ def test_invoke_event_function(): ).should.throw(botocore.client.ClientError) in_data = {'msg': 'So long and thanks for all the fish'} - success_result = conn.invoke(FunctionName='testFunction', InvocationType='Event', Payload=json.dumps(in_data)) + success_result = conn.invoke( + FunctionName='testFunction', InvocationType='Event', Payload=json.dumps(in_data)) success_result["StatusCode"].should.equal(202) - json.loads(success_result['Payload'].read().decode('utf-8')).should.equal({}) + json.loads(success_result['Payload'].read().decode( + 'utf-8')).should.equal({}) @mock_ec2 @@ -129,9 +133,11 @@ def test_invoke_function_get_ec2_volume(): ) in_data = {'volume_id': vol.id} - result = conn.invoke(FunctionName='testFunction', InvocationType='RequestResponse', Payload=json.dumps(in_data)) + result = conn.invoke(FunctionName='testFunction', + InvocationType='RequestResponse', Payload=json.dumps(in_data)) result["StatusCode"].should.equal(202) - msg = 'get volume details for %s\nVolume - %s state=%s, size=%s\n%s' % (vol.id, vol.id, vol.state, vol.size, json.dumps(in_data)) + msg = 'get volume details for %s\nVolume - %s state=%s, size=%s\n%s' % ( + vol.id, vol.id, vol.state, vol.size, json.dumps(in_data)) base64.b64decode(result["LogResult"]).decode('utf-8').should.equal(msg) result['Payload'].read().decode('utf-8').should.equal(msg) @@ -189,8 +195,10 @@ def test_create_function_from_aws_bucket(): "SubnetIds": ["subnet-123abc"], }, ) - result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it - result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + # this is hard to match against, so remove it + result['ResponseMetadata'].pop('HTTPHeaders', None) + # Botocore inserts retry attempts not seen in Python27 + result['ResponseMetadata'].pop('RetryAttempts', None) result.pop('LastModified') result.should.equal({ 'FunctionName': 'testFunction', @@ -231,8 +239,10 @@ def test_create_function_from_zipfile(): MemorySize=128, Publish=True, ) - result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it - result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + # this is hard to match against, so remove it + result['ResponseMetadata'].pop('HTTPHeaders', None) + # Botocore inserts retry attempts not seen in Python27 + result['ResponseMetadata'].pop('RetryAttempts', None) result.pop('LastModified') result.should.equal({ @@ -283,8 +293,10 @@ def test_get_function(): ) result = conn.get_function(FunctionName='testFunction') - result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it - result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + # this is hard to match against, so remove it + result['ResponseMetadata'].pop('HTTPHeaders', None) + # Botocore inserts retry attempts not seen in Python27 + result['ResponseMetadata'].pop('RetryAttempts', None) result['Configuration'].pop('LastModified') result.should.equal({ @@ -339,12 +351,15 @@ def test_delete_function(): ) success_result = conn.delete_function(FunctionName='testFunction') - success_result['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it - success_result['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + # this is hard to match against, so remove it + success_result['ResponseMetadata'].pop('HTTPHeaders', None) + # Botocore inserts retry attempts not seen in Python27 + success_result['ResponseMetadata'].pop('RetryAttempts', None) success_result.should.equal({'ResponseMetadata': {'HTTPStatusCode': 204}}) - conn.delete_function.when.called_with(FunctionName='testFunctionThatDoesntExist').should.throw(botocore.client.ClientError) + conn.delete_function.when.called_with( + FunctionName='testFunctionThatDoesntExist').should.throw(botocore.client.ClientError) @mock_lambda @@ -407,8 +422,10 @@ def test_list_create_list_get_delete_list(): func.should.equal(expected_function_result['Configuration']) func = conn.get_function(FunctionName='testFunction') - func['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it - func['ResponseMetadata'].pop('RetryAttempts', None) # Botocore inserts retry attempts not seen in Python27 + # this is hard to match against, so remove it + func['ResponseMetadata'].pop('HTTPHeaders', None) + # Botocore inserts retry attempts not seen in Python27 + func['ResponseMetadata'].pop('RetryAttempts', None) func['Configuration'].pop('LastModified') func.should.equal(expected_function_result) diff --git a/tests/test_cloudformation/fixtures/rds_mysql_with_db_parameter_group.py b/tests/test_cloudformation/fixtures/rds_mysql_with_db_parameter_group.py index 866197125..6f379daa6 100644 --- a/tests/test_cloudformation/fixtures/rds_mysql_with_db_parameter_group.py +++ b/tests/test_cloudformation/fixtures/rds_mysql_with_db_parameter_group.py @@ -1,201 +1,204 @@ from __future__ import unicode_literals template = { - "AWSTemplateFormatVersion" : "2010-09-09", + "AWSTemplateFormatVersion": "2010-09-09", - "Description" : "AWS CloudFormation Sample Template RDS_MySQL_With_Read_Replica: Sample template showing how to create a highly-available, RDS DBInstance with a read replica. **WARNING** This template creates an Amazon Relational Database Service database instance and Amazon CloudWatch alarms. You will be billed for the AWS resources used if you create a stack from this template.", + "Description": "AWS CloudFormation Sample Template RDS_MySQL_With_Read_Replica: Sample template showing how to create a highly-available, RDS DBInstance with a read replica. **WARNING** This template creates an Amazon Relational Database Service database instance and Amazon CloudWatch alarms. You will be billed for the AWS resources used if you create a stack from this template.", - "Parameters": { - "DBName": { - "Default": "MyDatabase", - "Description" : "The database name", - "Type": "String", - "MinLength": "1", - "MaxLength": "64", - "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", - "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters." - }, + "Parameters": { + "DBName": { + "Default": "MyDatabase", + "Description": "The database name", + "Type": "String", + "MinLength": "1", + "MaxLength": "64", + "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*", + "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters." + }, - "DBInstanceIdentifier": { - "Type": "String" - }, + "DBInstanceIdentifier": { + "Type": "String" + }, - "DBUser": { - "NoEcho": "true", - "Description" : "The database admin account username", - "Type": "String", - "MinLength": "1", - "MaxLength": "16", - "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", - "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters." - }, + "DBUser": { + "NoEcho": "true", + "Description": "The database admin account username", + "Type": "String", + "MinLength": "1", + "MaxLength": "16", + "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*", + "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters." + }, - "DBPassword": { - "NoEcho": "true", - "Description" : "The database admin account password", - "Type": "String", - "MinLength": "1", - "MaxLength": "41", - "AllowedPattern" : "[a-zA-Z0-9]+", - "ConstraintDescription" : "must contain only alphanumeric characters." - }, + "DBPassword": { + "NoEcho": "true", + "Description": "The database admin account password", + "Type": "String", + "MinLength": "1", + "MaxLength": "41", + "AllowedPattern": "[a-zA-Z0-9]+", + "ConstraintDescription": "must contain only alphanumeric characters." + }, - "DBAllocatedStorage": { - "Default": "5", - "Description" : "The size of the database (Gb)", - "Type": "Number", - "MinValue": "5", - "MaxValue": "1024", - "ConstraintDescription" : "must be between 5 and 1024Gb." - }, + "DBAllocatedStorage": { + "Default": "5", + "Description": "The size of the database (Gb)", + "Type": "Number", + "MinValue": "5", + "MaxValue": "1024", + "ConstraintDescription": "must be between 5 and 1024Gb." + }, - "DBInstanceClass": { - "Description" : "The database instance type", - "Type": "String", - "Default": "db.m1.small", - "AllowedValues" : [ "db.t1.micro", "db.m1.small", "db.m1.medium", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.m3.medium", "db.m3.large", "db.m3.xlarge", "db.m3.2xlarge", "db.r3.large", "db.r3.xlarge", "db.r3.2xlarge", "db.r3.4xlarge", "db.r3.8xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.cr1.8xlarge"] -, - "ConstraintDescription" : "must select a valid database instance type." - }, + "DBInstanceClass": { + "Description": "The database instance type", + "Type": "String", + "Default": "db.m1.small", + "AllowedValues": ["db.t1.micro", "db.m1.small", "db.m1.medium", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.m3.medium", "db.m3.large", "db.m3.xlarge", "db.m3.2xlarge", "db.r3.large", "db.r3.xlarge", "db.r3.2xlarge", "db.r3.4xlarge", "db.r3.8xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.cr1.8xlarge"], + "ConstraintDescription": "must select a valid database instance type." + }, - "EC2SecurityGroup": { - "Description" : "The EC2 security group that contains instances that need access to the database", - "Default": "default", - "Type": "String", - "AllowedPattern" : "[a-zA-Z0-9\\-]+", - "ConstraintDescription" : "must be a valid security group name." - }, + "EC2SecurityGroup": { + "Description": "The EC2 security group that contains instances that need access to the database", + "Default": "default", + "Type": "String", + "AllowedPattern": "[a-zA-Z0-9\\-]+", + "ConstraintDescription": "must be a valid security group name." + }, - "MultiAZ" : { - "Description" : "Multi-AZ master database", - "Type" : "String", - "Default" : "false", - "AllowedValues" : [ "true", "false" ], - "ConstraintDescription" : "must be true or false." - } - }, - - "Conditions" : { - "Is-EC2-VPC" : { "Fn::Or" : [ {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "eu-central-1" ]}, - {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "cn-north-1" ]}]}, - "Is-EC2-Classic" : { "Fn::Not" : [{ "Condition" : "Is-EC2-VPC"}]} - }, - - "Resources" : { - "DBParameterGroup": { - "Type": "AWS::RDS::DBParameterGroup", - "Properties" : { - "Description": "DB Parameter Goup", - "Family" : "MySQL5.1", - "Parameters": { - "BACKLOG_QUEUE_LIMIT": "2048" + "MultiAZ": { + "Description": "Multi-AZ master database", + "Type": "String", + "Default": "false", + "AllowedValues": ["true", "false"], + "ConstraintDescription": "must be true or false." } - } }, - "DBEC2SecurityGroup": { - "Type": "AWS::EC2::SecurityGroup", - "Condition" : "Is-EC2-VPC", - "Properties" : { - "GroupDescription": "Open database for access", - "SecurityGroupIngress" : [{ - "IpProtocol" : "tcp", - "FromPort" : "3306", - "ToPort" : "3306", - "SourceSecurityGroupName" : { "Ref" : "EC2SecurityGroup" } - }] - } + "Conditions": { + "Is-EC2-VPC": {"Fn::Or": [{"Fn::Equals": [{"Ref": "AWS::Region"}, "eu-central-1"]}, + {"Fn::Equals": [{"Ref": "AWS::Region"}, "cn-north-1"]}]}, + "Is-EC2-Classic": {"Fn::Not": [{"Condition": "Is-EC2-VPC"}]} }, - "DBSecurityGroup": { - "Type": "AWS::RDS::DBSecurityGroup", - "Condition" : "Is-EC2-Classic", - "Properties": { - "DBSecurityGroupIngress": [{ - "EC2SecurityGroupName": { "Ref": "EC2SecurityGroup" } - }], - "GroupDescription": "database access" - } + "Resources": { + "DBParameterGroup": { + "Type": "AWS::RDS::DBParameterGroup", + "Properties": { + "Description": "DB Parameter Goup", + "Family": "MySQL5.1", + "Parameters": { + "BACKLOG_QUEUE_LIMIT": "2048" + } + } + }, + + "DBEC2SecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Condition": "Is-EC2-VPC", + "Properties": { + "GroupDescription": "Open database for access", + "SecurityGroupIngress": [{ + "IpProtocol": "tcp", + "FromPort": "3306", + "ToPort": "3306", + "SourceSecurityGroupName": {"Ref": "EC2SecurityGroup"} + }] + } + }, + + "DBSecurityGroup": { + "Type": "AWS::RDS::DBSecurityGroup", + "Condition": "Is-EC2-Classic", + "Properties": { + "DBSecurityGroupIngress": [{ + "EC2SecurityGroupName": {"Ref": "EC2SecurityGroup"} + }], + "GroupDescription": "database access" + } + }, + + "my_vpc": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + } + }, + + "EC2Subnet": { + "Type": "AWS::EC2::Subnet", + "Condition": "Is-EC2-VPC", + "Properties": { + "AvailabilityZone": "eu-central-1a", + "CidrBlock": "10.0.1.0/24", + "VpcId": {"Ref": "my_vpc"} + } + }, + + "DBSubnet": { + "Type": "AWS::RDS::DBSubnetGroup", + "Condition": "Is-EC2-VPC", + "Properties": { + "DBSubnetGroupDescription": "my db subnet group", + "SubnetIds": [{"Ref": "EC2Subnet"}], + } + }, + + "MasterDB": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceIdentifier": {"Ref": "DBInstanceIdentifier"}, + "DBName": {"Ref": "DBName"}, + "AllocatedStorage": {"Ref": "DBAllocatedStorage"}, + "DBInstanceClass": {"Ref": "DBInstanceClass"}, + "Engine": "MySQL", + "DBSubnetGroupName": {"Fn::If": ["Is-EC2-VPC", {"Ref": "DBSubnet"}, {"Ref": "AWS::NoValue"}]}, + "MasterUsername": {"Ref": "DBUser"}, + "MasterUserPassword": {"Ref": "DBPassword"}, + "MultiAZ": {"Ref": "MultiAZ"}, + "Tags": [{"Key": "Name", "Value": "Master Database"}], + "VPCSecurityGroups": {"Fn::If": ["Is-EC2-VPC", [{"Fn::GetAtt": ["DBEC2SecurityGroup", "GroupId"]}], {"Ref": "AWS::NoValue"}]}, + "DBSecurityGroups": {"Fn::If": ["Is-EC2-Classic", [{"Ref": "DBSecurityGroup"}], {"Ref": "AWS::NoValue"}]} + }, + "DeletionPolicy": "Snapshot" + }, + + "ReplicaDB": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "SourceDBInstanceIdentifier": {"Ref": "MasterDB"}, + "DBInstanceClass": {"Ref": "DBInstanceClass"}, + "Tags": [{"Key": "Name", "Value": "Read Replica Database"}] + } + } }, - "my_vpc": { - "Type" : "AWS::EC2::VPC", - "Properties" : { - "CidrBlock" : "10.0.0.0/16", - } - }, + "Outputs": { + "EC2Platform": { + "Description": "Platform in which this stack is deployed", + "Value": {"Fn::If": ["Is-EC2-VPC", "EC2-VPC", "EC2-Classic"]} + }, - "EC2Subnet": { - "Type" : "AWS::EC2::Subnet", - "Condition" : "Is-EC2-VPC", - "Properties" : { - "AvailabilityZone" : "eu-central-1a", - "CidrBlock" : "10.0.1.0/24", - "VpcId" : { "Ref" : "my_vpc" } - } - }, - - "DBSubnet": { - "Type": "AWS::RDS::DBSubnetGroup", - "Condition" : "Is-EC2-VPC", - "Properties": { - "DBSubnetGroupDescription": "my db subnet group", - "SubnetIds" : [ { "Ref": "EC2Subnet" } ], - } - }, - - "MasterDB" : { - "Type" : "AWS::RDS::DBInstance", - "Properties" : { - "DBInstanceIdentifier": { "Ref": "DBInstanceIdentifier" }, - "DBName" : { "Ref" : "DBName" }, - "AllocatedStorage" : { "Ref" : "DBAllocatedStorage" }, - "DBInstanceClass" : { "Ref" : "DBInstanceClass" }, - "Engine" : "MySQL", - "DBSubnetGroupName": {"Fn::If": ["Is-EC2-VPC", { "Ref": "DBSubnet" }, { "Ref": "AWS::NoValue" }]}, - "MasterUsername" : { "Ref" : "DBUser" }, - "MasterUserPassword" : { "Ref" : "DBPassword" }, - "MultiAZ" : { "Ref" : "MultiAZ" }, - "Tags" : [{ "Key" : "Name", "Value" : "Master Database" }], - "VPCSecurityGroups": { "Fn::If" : [ "Is-EC2-VPC", [ { "Fn::GetAtt": [ "DBEC2SecurityGroup", "GroupId" ] } ], { "Ref" : "AWS::NoValue"}]}, - "DBSecurityGroups": { "Fn::If" : [ "Is-EC2-Classic", [ { "Ref": "DBSecurityGroup" } ], { "Ref" : "AWS::NoValue"}]} - }, - "DeletionPolicy" : "Snapshot" - }, - - "ReplicaDB" : { - "Type" : "AWS::RDS::DBInstance", - "Properties" : { - "SourceDBInstanceIdentifier" : { "Ref" : "MasterDB" }, - "DBInstanceClass" : { "Ref" : "DBInstanceClass" }, - "Tags" : [{ "Key" : "Name", "Value" : "Read Replica Database" }] - } + "MasterJDBCConnectionString": { + "Description": "JDBC connection string for the master database", + "Value": {"Fn::Join": ["", ["jdbc:mysql://", + {"Fn::GetAtt": [ + "MasterDB", "Endpoint.Address"]}, + ":", + {"Fn::GetAtt": [ + "MasterDB", "Endpoint.Port"]}, + "/", + {"Ref": "DBName"}]]} + }, + "ReplicaJDBCConnectionString": { + "Description": "JDBC connection string for the replica database", + "Value": {"Fn::Join": ["", ["jdbc:mysql://", + {"Fn::GetAtt": [ + "ReplicaDB", "Endpoint.Address"]}, + ":", + {"Fn::GetAtt": [ + "ReplicaDB", "Endpoint.Port"]}, + "/", + {"Ref": "DBName"}]]} + } } - }, - - "Outputs" : { - "EC2Platform" : { - "Description" : "Platform in which this stack is deployed", - "Value" : { "Fn::If" : [ "Is-EC2-VPC", "EC2-VPC", "EC2-Classic" ]} - }, - - "MasterJDBCConnectionString": { - "Description" : "JDBC connection string for the master database", - "Value" : { "Fn::Join": [ "", [ "jdbc:mysql://", - { "Fn::GetAtt": [ "MasterDB", "Endpoint.Address" ] }, - ":", - { "Fn::GetAtt": [ "MasterDB", "Endpoint.Port" ] }, - "/", - { "Ref": "DBName" }]]} - }, - "ReplicaJDBCConnectionString": { - "Description" : "JDBC connection string for the replica database", - "Value" : { "Fn::Join": [ "", [ "jdbc:mysql://", - { "Fn::GetAtt": [ "ReplicaDB", "Endpoint.Address" ] }, - ":", - { "Fn::GetAtt": [ "ReplicaDB", "Endpoint.Port" ] }, - "/", - { "Ref": "DBName" }]]} - } - } } diff --git a/tests/test_cloudformation/fixtures/rds_mysql_with_read_replica.py b/tests/test_cloudformation/fixtures/rds_mysql_with_read_replica.py index 3e5efa04a..2fbfb4cad 100644 --- a/tests/test_cloudformation/fixtures/rds_mysql_with_read_replica.py +++ b/tests/test_cloudformation/fixtures/rds_mysql_with_read_replica.py @@ -1,190 +1,193 @@ from __future__ import unicode_literals template = { - "AWSTemplateFormatVersion" : "2010-09-09", + "AWSTemplateFormatVersion": "2010-09-09", - "Description" : "AWS CloudFormation Sample Template RDS_MySQL_With_Read_Replica: Sample template showing how to create a highly-available, RDS DBInstance with a read replica. **WARNING** This template creates an Amazon Relational Database Service database instance and Amazon CloudWatch alarms. You will be billed for the AWS resources used if you create a stack from this template.", + "Description": "AWS CloudFormation Sample Template RDS_MySQL_With_Read_Replica: Sample template showing how to create a highly-available, RDS DBInstance with a read replica. **WARNING** This template creates an Amazon Relational Database Service database instance and Amazon CloudWatch alarms. You will be billed for the AWS resources used if you create a stack from this template.", - "Parameters": { - "DBName": { - "Default": "MyDatabase", - "Description" : "The database name", - "Type": "String", - "MinLength": "1", - "MaxLength": "64", - "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", - "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters." + "Parameters": { + "DBName": { + "Default": "MyDatabase", + "Description": "The database name", + "Type": "String", + "MinLength": "1", + "MaxLength": "64", + "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*", + "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters." + }, + + "DBInstanceIdentifier": { + "Type": "String" + }, + + "DBUser": { + "NoEcho": "true", + "Description": "The database admin account username", + "Type": "String", + "MinLength": "1", + "MaxLength": "16", + "AllowedPattern": "[a-zA-Z][a-zA-Z0-9]*", + "ConstraintDescription": "must begin with a letter and contain only alphanumeric characters." + }, + + "DBPassword": { + "NoEcho": "true", + "Description": "The database admin account password", + "Type": "String", + "MinLength": "1", + "MaxLength": "41", + "AllowedPattern": "[a-zA-Z0-9]+", + "ConstraintDescription": "must contain only alphanumeric characters." + }, + + "DBAllocatedStorage": { + "Default": "5", + "Description": "The size of the database (Gb)", + "Type": "Number", + "MinValue": "5", + "MaxValue": "1024", + "ConstraintDescription": "must be between 5 and 1024Gb." + }, + + "DBInstanceClass": { + "Description": "The database instance type", + "Type": "String", + "Default": "db.m1.small", + "AllowedValues": ["db.t1.micro", "db.m1.small", "db.m1.medium", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.m3.medium", "db.m3.large", "db.m3.xlarge", "db.m3.2xlarge", "db.r3.large", "db.r3.xlarge", "db.r3.2xlarge", "db.r3.4xlarge", "db.r3.8xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.cr1.8xlarge"], + "ConstraintDescription": "must select a valid database instance type." + }, + + "EC2SecurityGroup": { + "Description": "The EC2 security group that contains instances that need access to the database", + "Default": "default", + "Type": "String", + "AllowedPattern": "[a-zA-Z0-9\\-]+", + "ConstraintDescription": "must be a valid security group name." + }, + + "MultiAZ": { + "Description": "Multi-AZ master database", + "Type": "String", + "Default": "false", + "AllowedValues": ["true", "false"], + "ConstraintDescription": "must be true or false." + } }, - "DBInstanceIdentifier": { - "Type": "String" + "Conditions": { + "Is-EC2-VPC": {"Fn::Or": [{"Fn::Equals": [{"Ref": "AWS::Region"}, "eu-central-1"]}, + {"Fn::Equals": [{"Ref": "AWS::Region"}, "cn-north-1"]}]}, + "Is-EC2-Classic": {"Fn::Not": [{"Condition": "Is-EC2-VPC"}]} }, - "DBUser": { - "NoEcho": "true", - "Description" : "The database admin account username", - "Type": "String", - "MinLength": "1", - "MaxLength": "16", - "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*", - "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters." + "Resources": { + "DBEC2SecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Condition": "Is-EC2-VPC", + "Properties": { + "GroupDescription": "Open database for access", + "SecurityGroupIngress": [{ + "IpProtocol": "tcp", + "FromPort": "3306", + "ToPort": "3306", + "SourceSecurityGroupName": {"Ref": "EC2SecurityGroup"} + }] + } + }, + + "DBSecurityGroup": { + "Type": "AWS::RDS::DBSecurityGroup", + "Condition": "Is-EC2-Classic", + "Properties": { + "DBSecurityGroupIngress": [{ + "EC2SecurityGroupName": {"Ref": "EC2SecurityGroup"} + }], + "GroupDescription": "database access" + } + }, + + "my_vpc": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + } + }, + + "EC2Subnet": { + "Type": "AWS::EC2::Subnet", + "Condition": "Is-EC2-VPC", + "Properties": { + "AvailabilityZone": "eu-central-1a", + "CidrBlock": "10.0.1.0/24", + "VpcId": {"Ref": "my_vpc"} + } + }, + + "DBSubnet": { + "Type": "AWS::RDS::DBSubnetGroup", + "Condition": "Is-EC2-VPC", + "Properties": { + "DBSubnetGroupDescription": "my db subnet group", + "SubnetIds": [{"Ref": "EC2Subnet"}], + } + }, + + "MasterDB": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceIdentifier": {"Ref": "DBInstanceIdentifier"}, + "DBName": {"Ref": "DBName"}, + "AllocatedStorage": {"Ref": "DBAllocatedStorage"}, + "DBInstanceClass": {"Ref": "DBInstanceClass"}, + "Engine": "MySQL", + "DBSubnetGroupName": {"Fn::If": ["Is-EC2-VPC", {"Ref": "DBSubnet"}, {"Ref": "AWS::NoValue"}]}, + "MasterUsername": {"Ref": "DBUser"}, + "MasterUserPassword": {"Ref": "DBPassword"}, + "MultiAZ": {"Ref": "MultiAZ"}, + "Tags": [{"Key": "Name", "Value": "Master Database"}], + "VPCSecurityGroups": {"Fn::If": ["Is-EC2-VPC", [{"Fn::GetAtt": ["DBEC2SecurityGroup", "GroupId"]}], {"Ref": "AWS::NoValue"}]}, + "DBSecurityGroups": {"Fn::If": ["Is-EC2-Classic", [{"Ref": "DBSecurityGroup"}], {"Ref": "AWS::NoValue"}]} + }, + "DeletionPolicy": "Snapshot" + }, + + "ReplicaDB": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "SourceDBInstanceIdentifier": {"Ref": "MasterDB"}, + "DBInstanceClass": {"Ref": "DBInstanceClass"}, + "Tags": [{"Key": "Name", "Value": "Read Replica Database"}] + } + } }, - "DBPassword": { - "NoEcho": "true", - "Description" : "The database admin account password", - "Type": "String", - "MinLength": "1", - "MaxLength": "41", - "AllowedPattern" : "[a-zA-Z0-9]+", - "ConstraintDescription" : "must contain only alphanumeric characters." - }, + "Outputs": { + "EC2Platform": { + "Description": "Platform in which this stack is deployed", + "Value": {"Fn::If": ["Is-EC2-VPC", "EC2-VPC", "EC2-Classic"]} + }, - "DBAllocatedStorage": { - "Default": "5", - "Description" : "The size of the database (Gb)", - "Type": "Number", - "MinValue": "5", - "MaxValue": "1024", - "ConstraintDescription" : "must be between 5 and 1024Gb." - }, - - "DBInstanceClass": { - "Description" : "The database instance type", - "Type": "String", - "Default": "db.m1.small", - "AllowedValues" : [ "db.t1.micro", "db.m1.small", "db.m1.medium", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.m3.medium", "db.m3.large", "db.m3.xlarge", "db.m3.2xlarge", "db.r3.large", "db.r3.xlarge", "db.r3.2xlarge", "db.r3.4xlarge", "db.r3.8xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.cr1.8xlarge"] -, - "ConstraintDescription" : "must select a valid database instance type." - }, - - "EC2SecurityGroup": { - "Description" : "The EC2 security group that contains instances that need access to the database", - "Default": "default", - "Type": "String", - "AllowedPattern" : "[a-zA-Z0-9\\-]+", - "ConstraintDescription" : "must be a valid security group name." - }, - - "MultiAZ" : { - "Description" : "Multi-AZ master database", - "Type" : "String", - "Default" : "false", - "AllowedValues" : [ "true", "false" ], - "ConstraintDescription" : "must be true or false." + "MasterJDBCConnectionString": { + "Description": "JDBC connection string for the master database", + "Value": {"Fn::Join": ["", ["jdbc:mysql://", + {"Fn::GetAtt": [ + "MasterDB", "Endpoint.Address"]}, + ":", + {"Fn::GetAtt": [ + "MasterDB", "Endpoint.Port"]}, + "/", + {"Ref": "DBName"}]]} + }, + "ReplicaJDBCConnectionString": { + "Description": "JDBC connection string for the replica database", + "Value": {"Fn::Join": ["", ["jdbc:mysql://", + {"Fn::GetAtt": [ + "ReplicaDB", "Endpoint.Address"]}, + ":", + {"Fn::GetAtt": [ + "ReplicaDB", "Endpoint.Port"]}, + "/", + {"Ref": "DBName"}]]} + } } - }, - - "Conditions" : { - "Is-EC2-VPC" : { "Fn::Or" : [ {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "eu-central-1" ]}, - {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "cn-north-1" ]}]}, - "Is-EC2-Classic" : { "Fn::Not" : [{ "Condition" : "Is-EC2-VPC"}]} - }, - - "Resources" : { - "DBEC2SecurityGroup": { - "Type": "AWS::EC2::SecurityGroup", - "Condition" : "Is-EC2-VPC", - "Properties" : { - "GroupDescription": "Open database for access", - "SecurityGroupIngress" : [{ - "IpProtocol" : "tcp", - "FromPort" : "3306", - "ToPort" : "3306", - "SourceSecurityGroupName" : { "Ref" : "EC2SecurityGroup" } - }] - } - }, - - "DBSecurityGroup": { - "Type": "AWS::RDS::DBSecurityGroup", - "Condition" : "Is-EC2-Classic", - "Properties": { - "DBSecurityGroupIngress": [{ - "EC2SecurityGroupName": { "Ref": "EC2SecurityGroup" } - }], - "GroupDescription": "database access" - } - }, - - "my_vpc": { - "Type" : "AWS::EC2::VPC", - "Properties" : { - "CidrBlock" : "10.0.0.0/16", - } - }, - - "EC2Subnet": { - "Type" : "AWS::EC2::Subnet", - "Condition" : "Is-EC2-VPC", - "Properties" : { - "AvailabilityZone" : "eu-central-1a", - "CidrBlock" : "10.0.1.0/24", - "VpcId" : { "Ref" : "my_vpc" } - } - }, - - "DBSubnet": { - "Type": "AWS::RDS::DBSubnetGroup", - "Condition" : "Is-EC2-VPC", - "Properties": { - "DBSubnetGroupDescription": "my db subnet group", - "SubnetIds" : [ { "Ref": "EC2Subnet" } ], - } - }, - - "MasterDB" : { - "Type" : "AWS::RDS::DBInstance", - "Properties" : { - "DBInstanceIdentifier": { "Ref": "DBInstanceIdentifier" }, - "DBName" : { "Ref" : "DBName" }, - "AllocatedStorage" : { "Ref" : "DBAllocatedStorage" }, - "DBInstanceClass" : { "Ref" : "DBInstanceClass" }, - "Engine" : "MySQL", - "DBSubnetGroupName": {"Fn::If": ["Is-EC2-VPC", { "Ref": "DBSubnet" }, { "Ref": "AWS::NoValue" }]}, - "MasterUsername" : { "Ref" : "DBUser" }, - "MasterUserPassword" : { "Ref" : "DBPassword" }, - "MultiAZ" : { "Ref" : "MultiAZ" }, - "Tags" : [{ "Key" : "Name", "Value" : "Master Database" }], - "VPCSecurityGroups": { "Fn::If" : [ "Is-EC2-VPC", [ { "Fn::GetAtt": [ "DBEC2SecurityGroup", "GroupId" ] } ], { "Ref" : "AWS::NoValue"}]}, - "DBSecurityGroups": { "Fn::If" : [ "Is-EC2-Classic", [ { "Ref": "DBSecurityGroup" } ], { "Ref" : "AWS::NoValue"}]} - }, - "DeletionPolicy" : "Snapshot" - }, - - "ReplicaDB" : { - "Type" : "AWS::RDS::DBInstance", - "Properties" : { - "SourceDBInstanceIdentifier" : { "Ref" : "MasterDB" }, - "DBInstanceClass" : { "Ref" : "DBInstanceClass" }, - "Tags" : [{ "Key" : "Name", "Value" : "Read Replica Database" }] - } - } - }, - - "Outputs" : { - "EC2Platform" : { - "Description" : "Platform in which this stack is deployed", - "Value" : { "Fn::If" : [ "Is-EC2-VPC", "EC2-VPC", "EC2-Classic" ]} - }, - - "MasterJDBCConnectionString": { - "Description" : "JDBC connection string for the master database", - "Value" : { "Fn::Join": [ "", [ "jdbc:mysql://", - { "Fn::GetAtt": [ "MasterDB", "Endpoint.Address" ] }, - ":", - { "Fn::GetAtt": [ "MasterDB", "Endpoint.Port" ] }, - "/", - { "Ref": "DBName" }]]} - }, - "ReplicaJDBCConnectionString": { - "Description" : "JDBC connection string for the replica database", - "Value" : { "Fn::Join": [ "", [ "jdbc:mysql://", - { "Fn::GetAtt": [ "ReplicaDB", "Endpoint.Address" ] }, - ":", - { "Fn::GetAtt": [ "ReplicaDB", "Endpoint.Port" ] }, - "/", - { "Ref": "DBName" }]]} - } - } } diff --git a/tests/test_cloudformation/fixtures/redshift.py b/tests/test_cloudformation/fixtures/redshift.py index 90e171659..317e213bc 100644 --- a/tests/test_cloudformation/fixtures/redshift.py +++ b/tests/test_cloudformation/fixtures/redshift.py @@ -1,187 +1,187 @@ from __future__ import unicode_literals template = { - "AWSTemplateFormatVersion": "2010-09-09", - "Parameters" : { - "DatabaseName" : { - "Description" : "The name of the first database to be created when the cluster is created", - "Type" : "String", - "Default" : "dev", - "AllowedPattern" : "([a-z]|[0-9])+" - }, - "ClusterType" : { - "Description" : "The type of cluster", - "Type" : "String", - "Default" : "single-node", - "AllowedValues" : [ "single-node", "multi-node" ] - }, - "NumberOfNodes" : { - "Description" : "The number of compute nodes in the cluster. For multi-node clusters, the NumberOfNodes parameter must be greater than 1", - "Type" : "Number", - "Default" : "1" - }, - "NodeType" : { - "Description" : "The type of node to be provisioned", - "Type" : "String", - "Default" : "dw1.xlarge", - "AllowedValues" : [ "dw1.xlarge", "dw1.8xlarge", "dw2.large", "dw2.8xlarge" ] - }, - "MasterUsername" : { - "Description" : "The user name that is associated with the master user account for the cluster that is being created", - "Type" : "String", - "Default" : "defaultuser", - "AllowedPattern" : "([a-z])([a-z]|[0-9])*" - }, - "MasterUserPassword" : { - "Description" : "The password that is associated with the master user account for the cluster that is being created.", - "Type" : "String", - "NoEcho" : "true" - }, - "InboundTraffic" : { - "Description" : "Allow inbound traffic to the cluster from this CIDR range.", - "Type" : "String", - "MinLength": "9", - "MaxLength": "18", - "Default" : "0.0.0.0/0", - "AllowedPattern" : "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", - "ConstraintDescription" : "must be a valid CIDR range of the form x.x.x.x/x." - }, - "PortNumber" : { - "Description" : "The port number on which the cluster accepts incoming connections.", - "Type" : "Number", - "Default" : "5439" - } - }, - "Conditions" : { - "IsMultiNodeCluster" : { - "Fn::Equals" : [{ "Ref" : "ClusterType" }, "multi-node" ] - } - }, - "Resources" : { - "RedshiftCluster" : { - "Type" : "AWS::Redshift::Cluster", - "DependsOn" : "AttachGateway", - "Properties" : { - "ClusterType" : { "Ref" : "ClusterType" }, - "NumberOfNodes" : { "Fn::If" : [ "IsMultiNodeCluster", { "Ref" : "NumberOfNodes" }, { "Ref" : "AWS::NoValue" }]}, - "NodeType" : { "Ref" : "NodeType" }, - "DBName" : { "Ref" : "DatabaseName" }, - "MasterUsername" : { "Ref" : "MasterUsername" }, - "MasterUserPassword" : { "Ref" : "MasterUserPassword" }, - "ClusterParameterGroupName" : { "Ref" : "RedshiftClusterParameterGroup" }, - "VpcSecurityGroupIds" : [ { "Ref" : "SecurityGroup" } ], - "ClusterSubnetGroupName" : { "Ref" : "RedshiftClusterSubnetGroup" }, - "PubliclyAccessible" : "true", - "Port" : { "Ref" : "PortNumber" } - } - }, - "RedshiftClusterParameterGroup" : { - "Type" : "AWS::Redshift::ClusterParameterGroup", - "Properties" : { - "Description" : "Cluster parameter group", - "ParameterGroupFamily" : "redshift-1.0", - "Parameters" : [{ - "ParameterName" : "enable_user_activity_logging", - "ParameterValue" : "true" - }] - } - }, - "RedshiftClusterSubnetGroup" : { - "Type" : "AWS::Redshift::ClusterSubnetGroup", - "Properties" : { - "Description" : "Cluster subnet group", - "SubnetIds" : [ { "Ref" : "PublicSubnet" } ] - } - }, - "VPC" : { - "Type" : "AWS::EC2::VPC", - "Properties" : { - "CidrBlock" : "10.0.0.0/16" - } - }, - "PublicSubnet" : { - "Type" : "AWS::EC2::Subnet", - "Properties" : { - "CidrBlock" : "10.0.0.0/24", - "VpcId" : { "Ref" : "VPC" } - } - }, - "SecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "Security group", - "SecurityGroupIngress" : [ { - "CidrIp" : { "Ref": "InboundTraffic" }, - "FromPort" : { "Ref" : "PortNumber" }, - "ToPort" : { "Ref" : "PortNumber" }, - "IpProtocol" : "tcp" - } ], - "VpcId" : { "Ref" : "VPC" } - } - }, - "myInternetGateway" : { - "Type" : "AWS::EC2::InternetGateway" - }, - "AttachGateway" : { - "Type" : "AWS::EC2::VPCGatewayAttachment", - "Properties" : { - "VpcId" : { "Ref" : "VPC" }, - "InternetGatewayId" : { "Ref" : "myInternetGateway" } - } - }, - "PublicRouteTable" : { - "Type" : "AWS::EC2::RouteTable", - "Properties" : { - "VpcId" : { - "Ref" : "VPC" - } - } - }, - "PublicRoute" : { - "Type" : "AWS::EC2::Route", - "DependsOn" : "AttachGateway", - "Properties" : { - "RouteTableId" : { - "Ref" : "PublicRouteTable" + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "DatabaseName": { + "Description": "The name of the first database to be created when the cluster is created", + "Type": "String", + "Default": "dev", + "AllowedPattern": "([a-z]|[0-9])+" }, - "DestinationCidrBlock" : "0.0.0.0/0", - "GatewayId" : { - "Ref" : "myInternetGateway" - } - } - }, - "PublicSubnetRouteTableAssociation" : { - "Type" : "AWS::EC2::SubnetRouteTableAssociation", - "Properties" : { - "SubnetId" : { - "Ref" : "PublicSubnet" + "ClusterType": { + "Description": "The type of cluster", + "Type": "String", + "Default": "single-node", + "AllowedValues": ["single-node", "multi-node"] }, - "RouteTableId" : { - "Ref" : "PublicRouteTable" + "NumberOfNodes": { + "Description": "The number of compute nodes in the cluster. For multi-node clusters, the NumberOfNodes parameter must be greater than 1", + "Type": "Number", + "Default": "1" + }, + "NodeType": { + "Description": "The type of node to be provisioned", + "Type": "String", + "Default": "dw1.xlarge", + "AllowedValues": ["dw1.xlarge", "dw1.8xlarge", "dw2.large", "dw2.8xlarge"] + }, + "MasterUsername": { + "Description": "The user name that is associated with the master user account for the cluster that is being created", + "Type": "String", + "Default": "defaultuser", + "AllowedPattern": "([a-z])([a-z]|[0-9])*" + }, + "MasterUserPassword": { + "Description": "The password that is associated with the master user account for the cluster that is being created.", + "Type": "String", + "NoEcho": "true" + }, + "InboundTraffic": { + "Description": "Allow inbound traffic to the cluster from this CIDR range.", + "Type": "String", + "MinLength": "9", + "MaxLength": "18", + "Default": "0.0.0.0/0", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "ConstraintDescription": "must be a valid CIDR range of the form x.x.x.x/x." + }, + "PortNumber": { + "Description": "The port number on which the cluster accepts incoming connections.", + "Type": "Number", + "Default": "5439" + } + }, + "Conditions": { + "IsMultiNodeCluster": { + "Fn::Equals": [{"Ref": "ClusterType"}, "multi-node"] + } + }, + "Resources": { + "RedshiftCluster": { + "Type": "AWS::Redshift::Cluster", + "DependsOn": "AttachGateway", + "Properties": { + "ClusterType": {"Ref": "ClusterType"}, + "NumberOfNodes": {"Fn::If": ["IsMultiNodeCluster", {"Ref": "NumberOfNodes"}, {"Ref": "AWS::NoValue"}]}, + "NodeType": {"Ref": "NodeType"}, + "DBName": {"Ref": "DatabaseName"}, + "MasterUsername": {"Ref": "MasterUsername"}, + "MasterUserPassword": {"Ref": "MasterUserPassword"}, + "ClusterParameterGroupName": {"Ref": "RedshiftClusterParameterGroup"}, + "VpcSecurityGroupIds": [{"Ref": "SecurityGroup"}], + "ClusterSubnetGroupName": {"Ref": "RedshiftClusterSubnetGroup"}, + "PubliclyAccessible": "true", + "Port": {"Ref": "PortNumber"} + } + }, + "RedshiftClusterParameterGroup": { + "Type": "AWS::Redshift::ClusterParameterGroup", + "Properties": { + "Description": "Cluster parameter group", + "ParameterGroupFamily": "redshift-1.0", + "Parameters": [{ + "ParameterName": "enable_user_activity_logging", + "ParameterValue": "true" + }] + } + }, + "RedshiftClusterSubnetGroup": { + "Type": "AWS::Redshift::ClusterSubnetGroup", + "Properties": { + "Description": "Cluster subnet group", + "SubnetIds": [{"Ref": "PublicSubnet"}] + } + }, + "VPC": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16" + } + }, + "PublicSubnet": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/24", + "VpcId": {"Ref": "VPC"} + } + }, + "SecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group", + "SecurityGroupIngress": [{ + "CidrIp": {"Ref": "InboundTraffic"}, + "FromPort": {"Ref": "PortNumber"}, + "ToPort": {"Ref": "PortNumber"}, + "IpProtocol": "tcp" + }], + "VpcId": {"Ref": "VPC"} + } + }, + "myInternetGateway": { + "Type": "AWS::EC2::InternetGateway" + }, + "AttachGateway": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": {"Ref": "VPC"}, + "InternetGatewayId": {"Ref": "myInternetGateway"} + } + }, + "PublicRouteTable": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPC" + } + } + }, + "PublicRoute": { + "Type": "AWS::EC2::Route", + "DependsOn": "AttachGateway", + "Properties": { + "RouteTableId": { + "Ref": "PublicRouteTable" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "myInternetGateway" + } + } + }, + "PublicSubnetRouteTableAssociation": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "SubnetId": { + "Ref": "PublicSubnet" + }, + "RouteTableId": { + "Ref": "PublicRouteTable" + } + } + } + }, + "Outputs": { + "ClusterEndpoint": { + "Description": "Cluster endpoint", + "Value": {"Fn::Join": [":", [{"Fn::GetAtt": ["RedshiftCluster", "Endpoint.Address"]}, {"Fn::GetAtt": ["RedshiftCluster", "Endpoint.Port"]}]]} + }, + "ClusterName": { + "Description": "Name of cluster", + "Value": {"Ref": "RedshiftCluster"} + }, + "ParameterGroupName": { + "Description": "Name of parameter group", + "Value": {"Ref": "RedshiftClusterParameterGroup"} + }, + "RedshiftClusterSubnetGroupName": { + "Description": "Name of cluster subnet group", + "Value": {"Ref": "RedshiftClusterSubnetGroup"} + }, + "RedshiftClusterSecurityGroupName": { + "Description": "Name of cluster security group", + "Value": {"Ref": "SecurityGroup"} } - } } - }, - "Outputs" : { - "ClusterEndpoint" : { - "Description" : "Cluster endpoint", - "Value" : { "Fn::Join" : [ ":", [ { "Fn::GetAtt" : [ "RedshiftCluster", "Endpoint.Address" ] }, { "Fn::GetAtt" : [ "RedshiftCluster", "Endpoint.Port" ] } ] ] } - }, - "ClusterName" : { - "Description" : "Name of cluster", - "Value" : { "Ref" : "RedshiftCluster" } - }, - "ParameterGroupName" : { - "Description" : "Name of parameter group", - "Value" : { "Ref" : "RedshiftClusterParameterGroup" } - }, - "RedshiftClusterSubnetGroupName" : { - "Description" : "Name of cluster subnet group", - "Value" : { "Ref" : "RedshiftClusterSubnetGroup" } - }, - "RedshiftClusterSecurityGroupName" : { - "Description" : "Name of cluster security group", - "Value" : { "Ref" : "SecurityGroup" } - } - } -} \ No newline at end of file +} diff --git a/tests/test_cloudformation/fixtures/route53_ec2_instance_with_public_ip.py b/tests/test_cloudformation/fixtures/route53_ec2_instance_with_public_ip.py index 02fa57b8f..5e66bbd86 100644 --- a/tests/test_cloudformation/fixtures/route53_ec2_instance_with_public_ip.py +++ b/tests/test_cloudformation/fixtures/route53_ec2_instance_with_public_ip.py @@ -1,40 +1,40 @@ from __future__ import unicode_literals template = { - "Resources" : { - "Ec2Instance" : { - "Type" : "AWS::EC2::Instance", - "Properties" : { - "ImageId" : "ami-1234abcd", + "Resources": { + "Ec2Instance": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": "ami-1234abcd", "PrivateIpAddress": "10.0.0.25", } }, "HostedZone": { - "Type" : "AWS::Route53::HostedZone", - "Properties" : { - "Name" : "my_zone" + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "my_zone" } }, - "myDNSRecord" : { - "Type" : "AWS::Route53::RecordSet", - "Properties" : { - "HostedZoneName" : { "Ref" : "HostedZone" }, - "Comment" : "DNS name for my instance.", - "Name" : { - "Fn::Join" : [ "", [ - {"Ref" : "Ec2Instance"}, ".", - {"Ref" : "AWS::Region"}, ".", - {"Ref" : "HostedZone"} ,"." - ] ] - }, - "Type" : "A", - "TTL" : "900", - "ResourceRecords" : [ - { "Fn::GetAtt" : [ "Ec2Instance", "PrivateIp" ] } - ] - } + "myDNSRecord": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "HostedZoneName": {"Ref": "HostedZone"}, + "Comment": "DNS name for my instance.", + "Name": { + "Fn::Join": ["", [ + {"Ref": "Ec2Instance"}, ".", + {"Ref": "AWS::Region"}, ".", + {"Ref": "HostedZone"}, "." + ]] + }, + "Type": "A", + "TTL": "900", + "ResourceRecords": [ + {"Fn::GetAtt": ["Ec2Instance", "PrivateIp"]} + ] + } } }, -} \ No newline at end of file +} diff --git a/tests/test_cloudformation/fixtures/route53_health_check.py b/tests/test_cloudformation/fixtures/route53_health_check.py index 6c6159fde..f6a2c9b8e 100644 --- a/tests/test_cloudformation/fixtures/route53_health_check.py +++ b/tests/test_cloudformation/fixtures/route53_health_check.py @@ -1,39 +1,39 @@ from __future__ import unicode_literals template = { - "Resources" : { + "Resources": { "HostedZone": { - "Type" : "AWS::Route53::HostedZone", - "Properties" : { - "Name" : "my_zone" + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "my_zone" } }, "my_health_check": { "Type": "AWS::Route53::HealthCheck", - "Properties" : { - "HealthCheckConfig" : { - "FailureThreshold" : 3, - "IPAddress" : "10.0.0.4", - "Port" : 80, - "RequestInterval" : 10, - "ResourcePath" : "/", - "Type" : "HTTP", + "Properties": { + "HealthCheckConfig": { + "FailureThreshold": 3, + "IPAddress": "10.0.0.4", + "Port": 80, + "RequestInterval": 10, + "ResourcePath": "/", + "Type": "HTTP", } } }, - "myDNSRecord" : { - "Type" : "AWS::Route53::RecordSet", - "Properties" : { - "HostedZoneName" : { "Ref" : "HostedZone" }, - "Comment" : "DNS name for my instance.", - "Name" : "my_record_set", - "Type" : "A", - "TTL" : "900", - "ResourceRecords" : ["my.example.com"], - "HealthCheckId": {"Ref": "my_health_check"}, - } + "myDNSRecord": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "HostedZoneName": {"Ref": "HostedZone"}, + "Comment": "DNS name for my instance.", + "Name": "my_record_set", + "Type": "A", + "TTL": "900", + "ResourceRecords": ["my.example.com"], + "HealthCheckId": {"Ref": "my_health_check"}, + } } }, -} \ No newline at end of file +} diff --git a/tests/test_cloudformation/fixtures/route53_roundrobin.py b/tests/test_cloudformation/fixtures/route53_roundrobin.py index d985623bb..da4fecd4d 100644 --- a/tests/test_cloudformation/fixtures/route53_roundrobin.py +++ b/tests/test_cloudformation/fixtures/route53_roundrobin.py @@ -1,47 +1,47 @@ from __future__ import unicode_literals template = { - "AWSTemplateFormatVersion" : "2010-09-09", + "AWSTemplateFormatVersion": "2010-09-09", - "Description" : "AWS CloudFormation Sample Template Route53_RoundRobin: Sample template showing how to use weighted round robin (WRR) DNS entried via Amazon Route 53. This contrived sample uses weighted CNAME records to illustrate that the weighting influences the return records. It assumes that you already have a Hosted Zone registered with Amazon Route 53. **WARNING** This template creates one or more AWS resources. You will be billed for the AWS resources used if you create a stack from this template.", + "Description": "AWS CloudFormation Sample Template Route53_RoundRobin: Sample template showing how to use weighted round robin (WRR) DNS entried via Amazon Route 53. This contrived sample uses weighted CNAME records to illustrate that the weighting influences the return records. It assumes that you already have a Hosted Zone registered with Amazon Route 53. **WARNING** This template creates one or more AWS resources. You will be billed for the AWS resources used if you create a stack from this template.", - "Resources" : { + "Resources": { - "MyZone": { - "Type" : "AWS::Route53::HostedZone", - "Properties" : { - "Name" : "my_zone" - } + "MyZone": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "my_zone" + } + }, + + "MyDNSRecord": { + "Type": "AWS::Route53::RecordSetGroup", + "Properties": { + "HostedZoneName": {"Ref": "MyZone"}, + "Comment": "Contrived example to redirect to aws.amazon.com 75% of the time and www.amazon.com 25% of the time.", + "RecordSets": [{ + "SetIdentifier": {"Fn::Join": [" ", [{"Ref": "AWS::StackName"}, "AWS"]]}, + "Name": {"Fn::Join": ["", [{"Ref": "AWS::StackName"}, ".", {"Ref": "AWS::Region"}, ".", {"Ref": "MyZone"}, "."]]}, + "Type": "CNAME", + "TTL": "900", + "ResourceRecords": ["aws.amazon.com"], + "Weight": "3" + }, { + "SetIdentifier": {"Fn::Join": [" ", [{"Ref": "AWS::StackName"}, "Amazon"]]}, + "Name": {"Fn::Join": ["", [{"Ref": "AWS::StackName"}, ".", {"Ref": "AWS::Region"}, ".", {"Ref": "MyZone"}, "."]]}, + "Type": "CNAME", + "TTL": "900", + "ResourceRecords": ["www.amazon.com"], + "Weight": "1" + }] + } + } }, - "MyDNSRecord" : { - "Type" : "AWS::Route53::RecordSetGroup", - "Properties" : { - "HostedZoneName" : {"Ref": "MyZone"}, - "Comment" : "Contrived example to redirect to aws.amazon.com 75% of the time and www.amazon.com 25% of the time.", - "RecordSets" : [{ - "SetIdentifier" : { "Fn::Join" : [ " ", [{"Ref" : "AWS::StackName"}, "AWS" ]]}, - "Name" : { "Fn::Join" : [ "", [{"Ref" : "AWS::StackName"}, ".", {"Ref" : "AWS::Region"}, ".", {"Ref" : "MyZone"}, "."]]}, - "Type" : "CNAME", - "TTL" : "900", - "ResourceRecords" : ["aws.amazon.com"], - "Weight" : "3" - },{ - "SetIdentifier" : { "Fn::Join" : [ " ", [{"Ref" : "AWS::StackName"}, "Amazon" ]]}, - "Name" : { "Fn::Join" : [ "", [{"Ref" : "AWS::StackName"}, ".", {"Ref" : "AWS::Region"}, ".", {"Ref" : "MyZone"}, "."]]}, - "Type" : "CNAME", - "TTL" : "900", - "ResourceRecords" : ["www.amazon.com"], - "Weight" : "1" - }] - } + "Outputs": { + "DomainName": { + "Description": "Fully qualified domain name", + "Value": {"Ref": "MyDNSRecord"} + } } - }, - - "Outputs" : { - "DomainName" : { - "Description" : "Fully qualified domain name", - "Value" : { "Ref" : "MyDNSRecord" } - } - } -} \ No newline at end of file +} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 3d41c9d91..619d8c3da 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -35,8 +35,8 @@ dummy_template3 = { "VPC": { "Properties": { "CidrBlock": "192.168.0.0/16", - }, - "Type": "AWS::EC2::VPC" + }, + "Type": "AWS::EC2::VPC" } }, } @@ -91,7 +91,8 @@ def test_create_stack_with_notification_arn(): ) stack = conn.describe_stacks()[0] - [n.value for n in stack.notification_arns].should.contain('arn:aws:sns:us-east-1:123456789012:fake-queue') + [n.value for n in stack.notification_arns].should.contain( + 'arn:aws:sns:us-east-1:123456789012:fake-queue') @mock_cloudformation_deprecated @@ -111,16 +112,16 @@ def test_create_stack_from_s3_url(): stack.stack_name.should.equal('new-stack') stack.get_template().should.equal( { - 'GetTemplateResponse': { - 'GetTemplateResult': { - 'TemplateBody': dummy_template_json, - 'ResponseMetadata': { - 'RequestId': '2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE' + 'GetTemplateResponse': { + 'GetTemplateResult': { + 'TemplateBody': dummy_template_json, + 'ResponseMetadata': { + 'RequestId': '2d06e36c-ac1d-11e0-a958-f9382b6eb86bEXAMPLE' + } } } - } - }) + }) @mock_cloudformation_deprecated @@ -271,7 +272,8 @@ def test_cloudformation_params(): } dummy_template_json = json.dumps(dummy_template) cfn = boto.connect_cloudformation() - cfn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[('APPNAME', 'testing123')]) + cfn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[ + ('APPNAME', 'testing123')]) stack = cfn.describe_stacks('test_stack1')[0] stack.parameters.should.have.length_of(1) param = stack.parameters[0] @@ -342,23 +344,28 @@ def test_update_stack(): @mock_cloudformation_deprecated def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() - stack_id = conn.create_stack("test_stack", template_body=dummy_template_json) + stack_id = conn.create_stack( + "test_stack", template_body=dummy_template_json) - cloudformation_backends[conn.region.name].stacks[stack_id].status = 'ROLLBACK_COMPLETE' + cloudformation_backends[conn.region.name].stacks[ + stack_id].status = 'ROLLBACK_COMPLETE' with assert_raises(BotoServerError) as err: conn.update_stack("test_stack", dummy_template_json) ex = err.exception - ex.body.should.match(r'is in ROLLBACK_COMPLETE state and can not be updated') + ex.body.should.match( + r'is in ROLLBACK_COMPLETE state and can not be updated') ex.error_code.should.equal('ValidationError') ex.reason.should.equal('Bad Request') ex.status.should.equal(400) + @mock_cloudformation_deprecated def test_describe_stack_events_shows_create_update_and_delete(): conn = boto.connect_cloudformation() - stack_id = conn.create_stack("test_stack", template_body=dummy_template_json) + stack_id = conn.create_stack( + "test_stack", template_body=dummy_template_json) conn.update_stack(stack_id, template_body=dummy_template_json2) conn.delete_stack(stack_id) @@ -367,7 +374,8 @@ def test_describe_stack_events_shows_create_update_and_delete(): events[0].resource_type.should.equal("AWS::CloudFormation::Stack") events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") - # testing ordering of stack events without assuming resource events will not exist + # testing ordering of stack events without assuming resource events will + # not exist stack_events_to_look_for = iter([ ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), @@ -381,12 +389,13 @@ def test_describe_stack_events_shows_create_update_and_delete(): event.logical_resource_id.should.equal("test_stack") event.physical_resource_id.should.equal(stack_id) - status_to_look_for, reason_to_look_for = next(stack_events_to_look_for) + status_to_look_for, reason_to_look_for = next( + stack_events_to_look_for) event.resource_status.should.equal(status_to_look_for) if reason_to_look_for is not None: - event.resource_status_reason.should.equal(reason_to_look_for) + event.resource_status_reason.should.equal( + reason_to_look_for) except StopIteration: assert False, "Too many stack events" list(stack_events_to_look_for).should.be.empty - diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 2ee74f886..29e2dfa10 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -124,7 +124,8 @@ def test_create_stack_from_s3_url(): s3_conn = boto3.resource('s3') bucket = s3_conn.create_bucket(Bucket="foobar") - key = s3_conn.Object('foobar', 'template-key').put(Body=dummy_template_json) + key = s3_conn.Object( + 'foobar', 'template-key').put(Body=dummy_template_json) key_url = s3.generate_presigned_url( ClientMethod='get_object', Params={ @@ -160,6 +161,7 @@ def test_describe_stack_resources(): resource['ResourceType'].should.equal('AWS::EC2::Instance') resource['StackId'].should.equal(stack['StackId']) + @mock_cloudformation def test_describe_stack_by_name(): cf_conn = boto3.client('cloudformation', region_name='us-east-1') @@ -249,6 +251,7 @@ def test_describe_deleted_stack(): stack_by_id['StackName'].should.equal("test_stack") stack_by_id['StackStatus'].should.equal("DELETE_COMPLETE") + @mock_cloudformation def test_describe_updated_stack(): cf_conn = boto3.client('cloudformation', region_name='us-east-1') @@ -299,9 +302,9 @@ def test_cloudformation_params(): StackName='test_stack', TemplateBody=dummy_template_with_params_json, Parameters=[{ - "ParameterKey": "APPNAME", - "ParameterValue": "testing123", - }], + "ParameterKey": "APPNAME", + "ParameterValue": "testing123", + }], ) stack.parameters.should.have.length_of(1) @@ -334,6 +337,7 @@ def test_stack_tags(): item for items in [tag.items() for tag in tags] for item in items) observed_tag_items.should.equal(expected_tag_items) + @mock_cloudformation def test_stack_events(): cf = boto3.resource('cloudformation', region_name='us-east-1') @@ -350,7 +354,8 @@ def test_stack_events(): events[0].resource_type.should.equal("AWS::CloudFormation::Stack") events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") - # testing ordering of stack events without assuming resource events will not exist + # testing ordering of stack events without assuming resource events will + # not exist stack_events_to_look_for = iter([ ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), @@ -364,10 +369,12 @@ def test_stack_events(): event.logical_resource_id.should.equal("test_stack") event.physical_resource_id.should.equal(stack.stack_id) - status_to_look_for, reason_to_look_for = next(stack_events_to_look_for) + status_to_look_for, reason_to_look_for = next( + stack_events_to_look_for) event.resource_status.should.equal(status_to_look_for) if reason_to_look_for is not None: - event.resource_status_reason.should.equal(reason_to_look_for) + event.resource_status_reason.should.equal( + reason_to_look_for) except StopIteration: assert False, "Too many stack events" diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 609a0b46d..e2304f840 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -143,15 +143,18 @@ def test_update_stack(): sqs_conn = boto.sqs.connect_to_region("us-west-1") queues = sqs_conn.get_all_queues() queues.should.have.length_of(1) - queues[0].get_attributes('VisibilityTimeout')['VisibilityTimeout'].should.equal('60') + queues[0].get_attributes('VisibilityTimeout')[ + 'VisibilityTimeout'].should.equal('60') - sqs_template['Resources']['QueueGroup']['Properties']['VisibilityTimeout'] = 100 + sqs_template['Resources']['QueueGroup'][ + 'Properties']['VisibilityTimeout'] = 100 sqs_template_json = json.dumps(sqs_template) conn.update_stack("test_stack", sqs_template_json) queues = sqs_conn.get_all_queues() queues.should.have.length_of(1) - queues[0].get_attributes('VisibilityTimeout')['VisibilityTimeout'].should.equal('100') + queues[0].get_attributes('VisibilityTimeout')[ + 'VisibilityTimeout'].should.equal('100') @mock_cloudformation_deprecated() @@ -395,7 +398,8 @@ def test_stack_elb_integration_with_update(): load_balancer = elb_conn.get_all_load_balancers()[0] load_balancer.availability_zones[0].should.equal('us-west-1a') - elb_template['Resources']['MyELB']['Properties']['AvailabilityZones'] = ['us-west-1b'] + elb_template['Resources']['MyELB']['Properties'][ + 'AvailabilityZones'] = ['us-west-1b'] elb_template_json = json.dumps(elb_template) conn.update_stack( "elb_stack", @@ -431,7 +435,8 @@ def test_redshift_stack(): redshift_conn = boto.redshift.connect_to_region("us-west-2") cluster_res = redshift_conn.describe_clusters() - clusters = cluster_res['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + clusters = cluster_res['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'] clusters.should.have.length_of(1) cluster = clusters[0] cluster['DBName'].should.equal("mydb") @@ -499,12 +504,14 @@ def test_stack_security_groups(): conn.create_stack( "security_group_stack", template_body=security_group_template_json, - tags={"foo":"bar"} + tags={"foo": "bar"} ) ec2_conn = boto.ec2.connect_to_region("us-west-1") - instance_group = ec2_conn.get_all_security_groups(filters={'description': ['My security group']})[0] - other_group = ec2_conn.get_all_security_groups(filters={'description': ['My other group']})[0] + instance_group = ec2_conn.get_all_security_groups( + filters={'description': ['My security group']})[0] + other_group = ec2_conn.get_all_security_groups( + filters={'description': ['My other group']})[0] reservation = ec2_conn.get_all_instances()[0] ec2_instance = reservation.instances[0] @@ -597,13 +604,17 @@ def test_autoscaling_group_with_elb(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() - as_group_resource = [resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::AutoScalingGroup'][0] + as_group_resource = [resource for resource in resources if resource.resource_type == + 'AWS::AutoScaling::AutoScalingGroup'][0] as_group_resource.physical_resource_id.should.contain("my-as-group") - launch_config_resource = [resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::LaunchConfiguration'][0] - launch_config_resource.physical_resource_id.should.contain("my-launch-config") + launch_config_resource = [ + resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::LaunchConfiguration'][0] + launch_config_resource.physical_resource_id.should.contain( + "my-launch-config") - elb_resource = [resource for resource in resources if resource.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer'][0] + elb_resource = [resource for resource in resources if resource.resource_type == + 'AWS::ElasticLoadBalancing::LoadBalancer'][0] elb_resource.physical_resource_id.should.contain("my-elb") @@ -687,26 +698,32 @@ def test_vpc_single_instance_in_subnet(): eip.domain.should.equal('vpc') eip.instance_id.should.equal(instance.id) - security_group = ec2_conn.get_all_security_groups(filters={'vpc_id': [vpc.id]})[0] + security_group = ec2_conn.get_all_security_groups( + filters={'vpc_id': [vpc.id]})[0] security_group.vpc_id.should.equal(vpc.id) stack = conn.describe_stacks()[0] resources = stack.describe_resources() - vpc_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::VPC'][0] + vpc_resource = [ + resource for resource in resources if resource.resource_type == 'AWS::EC2::VPC'][0] vpc_resource.physical_resource_id.should.equal(vpc.id) - subnet_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::Subnet'][0] + subnet_resource = [ + resource for resource in resources if resource.resource_type == 'AWS::EC2::Subnet'][0] subnet_resource.physical_resource_id.should.equal(subnet.id) - eip_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] + eip_resource = [ + resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] eip_resource.physical_resource_id.should.equal(eip.allocation_id) + @mock_cloudformation() @mock_ec2() @mock_rds2() def test_rds_db_parameter_groups(): ec2_conn = boto3.client("ec2", region_name="us-west-1") - ec2_conn.create_security_group(GroupName='application', Description='Our Application Group') + ec2_conn.create_security_group( + GroupName='application', Description='Our Application Group') template_json = json.dumps(rds_mysql_with_db_parameter_group.template) cf_conn = boto3.client('cloudformation', 'us-west-1') @@ -714,16 +731,16 @@ def test_rds_db_parameter_groups(): StackName="test_stack", TemplateBody=template_json, Parameters=[{'ParameterKey': key, 'ParameterValue': value} for - key, value in [ - ("DBInstanceIdentifier", "master_db"), - ("DBName", "my_db"), - ("DBUser", "my_user"), - ("DBPassword", "my_password"), - ("DBAllocatedStorage", "20"), - ("DBInstanceClass", "db.m1.medium"), - ("EC2SecurityGroup", "application"), - ("MultiAZ", "true"), - ] + key, value in [ + ("DBInstanceIdentifier", "master_db"), + ("DBName", "my_db"), + ("DBUser", "my_user"), + ("DBPassword", "my_password"), + ("DBAllocatedStorage", "20"), + ("DBInstanceClass", "db.m1.medium"), + ("EC2SecurityGroup", "application"), + ("MultiAZ", "true"), + ] ], ) @@ -731,7 +748,8 @@ def test_rds_db_parameter_groups(): db_parameter_groups = rds_conn.describe_db_parameter_groups() len(db_parameter_groups['DBParameterGroups']).should.equal(1) - db_parameter_group_name = db_parameter_groups['DBParameterGroups'][0]['DBParameterGroupName'] + db_parameter_group_name = db_parameter_groups[ + 'DBParameterGroups'][0]['DBParameterGroupName'] found_cloudformation_set_parameter = False for db_parameter in rds_conn.describe_db_parameters(DBParameterGroupName=db_parameter_group_name)['Parameters']: @@ -741,7 +759,6 @@ def test_rds_db_parameter_groups(): found_cloudformation_set_parameter.should.equal(True) - @mock_cloudformation_deprecated() @mock_ec2_deprecated() @mock_rds_deprecated() @@ -906,15 +923,20 @@ def test_iam_roles(): iam_conn = boto.iam.connect_to_region("us-west-1") - role_result = iam_conn.list_roles()['list_roles_response']['list_roles_result']['roles'][0] + role_result = iam_conn.list_roles()['list_roles_response'][ + 'list_roles_result']['roles'][0] role = iam_conn.get_role(role_result.role_name) role.role_name.should.contain("my-role") role.path.should.equal("my-path") - instance_profile_response = iam_conn.list_instance_profiles()['list_instance_profiles_response'] - cfn_instance_profile = instance_profile_response['list_instance_profiles_result']['instance_profiles'][0] - instance_profile = iam_conn.get_instance_profile(cfn_instance_profile.instance_profile_name) - instance_profile.instance_profile_name.should.contain("my-instance-profile") + instance_profile_response = iam_conn.list_instance_profiles()[ + 'list_instance_profiles_response'] + cfn_instance_profile = instance_profile_response[ + 'list_instance_profiles_result']['instance_profiles'][0] + instance_profile = iam_conn.get_instance_profile( + cfn_instance_profile.instance_profile_name) + instance_profile.instance_profile_name.should.contain( + "my-instance-profile") instance_profile.path.should.equal("my-path") instance_profile.role_id.should.equal(role.role_id) @@ -924,10 +946,13 @@ def test_iam_roles(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() - instance_profile_resource = [resource for resource in resources if resource.resource_type == 'AWS::IAM::InstanceProfile'][0] - instance_profile_resource.physical_resource_id.should.equal(instance_profile.instance_profile_name) + instance_profile_resource = [ + resource for resource in resources if resource.resource_type == 'AWS::IAM::InstanceProfile'][0] + instance_profile_resource.physical_resource_id.should.equal( + instance_profile.instance_profile_name) - role_resource = [resource for resource in resources if resource.resource_type == 'AWS::IAM::Role'][0] + role_resource = [ + resource for resource in resources if resource.resource_type == 'AWS::IAM::Role'][0] role_resource.physical_resource_id.should.equal(role.role_id) @@ -949,13 +974,15 @@ def test_single_instance_with_ebs_volume(): volumes = ec2_conn.get_all_volumes() # Grab the mounted drive - volume = [volume for volume in volumes if volume.attach_data.device == '/dev/sdh'][0] + volume = [ + volume for volume in volumes if volume.attach_data.device == '/dev/sdh'][0] volume.volume_state().should.equal('in-use') volume.attach_data.instance_id.should.equal(ec2_instance.id) stack = conn.describe_stacks()[0] resources = stack.describe_resources() - ebs_volumes = [resource for resource in resources if resource.resource_type == 'AWS::EC2::Volume'] + ebs_volumes = [ + resource for resource in resources if resource.resource_type == 'AWS::EC2::Volume'] ebs_volumes[0].physical_resource_id.should.equal(volume.id) @@ -981,7 +1008,8 @@ def test_classic_eip(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() - cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] + cfn_eip = [ + resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] cfn_eip.physical_resource_id.should.equal(eip.public_ip) @@ -997,7 +1025,8 @@ def test_vpc_eip(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() - cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] + cfn_eip = [ + resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] cfn_eip.physical_resource_id.should.equal(eip.allocation_id) @@ -1111,7 +1140,8 @@ def test_conditional_if_handling(): ec2_instance.terminate() conn = boto.cloudformation.connect_to_region("us-west-2") - conn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[("ENV", "prd")]) + conn.create_stack( + 'test_stack1', template_body=dummy_template_json, parameters=[("ENV", "prd")]) ec2_conn = boto.ec2.connect_to_region("us-west-2") reservation = ec2_conn.get_all_instances()[0] ec2_instance = reservation.instances[0] @@ -1175,7 +1205,8 @@ def test_route53_roundrobin(): template_body=template_json, ) - zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse'][ + 'HostedZones'] list(zones).should.have.length_of(1) zone_id = zones[0]['Id'] zone_id = zone_id.split('/') @@ -1203,7 +1234,8 @@ def test_route53_roundrobin(): stack = conn.describe_stacks()[0] output = stack.outputs[0] output.key.should.equal('DomainName') - output.value.should.equal('arn:aws:route53:::hostedzone/{0}'.format(zone_id)) + output.value.should.equal( + 'arn:aws:route53:::hostedzone/{0}'.format(zone_id)) @mock_cloudformation_deprecated() @@ -1222,13 +1254,13 @@ def test_route53_ec2_instance_with_public_ip(): instance_id = ec2_conn.get_all_reservations()[0].instances[0].id - zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse'][ + 'HostedZones'] list(zones).should.have.length_of(1) zone_id = zones[0]['Id'] zone_id = zone_id.split('/') zone_id = zone_id[2] - rrsets = route53_conn.get_all_rrsets(zone_id) rrsets.should.have.length_of(1) @@ -1253,7 +1285,8 @@ def test_route53_associate_health_check(): template_body=template_json, ) - checks = route53_conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + checks = route53_conn.get_list_health_checks()['ListHealthChecksResponse'][ + 'HealthChecks'] list(checks).should.have.length_of(1) check = checks[0] health_check_id = check['Id'] @@ -1265,7 +1298,8 @@ def test_route53_associate_health_check(): config["ResourcePath"].should.equal("/") config["Type"].should.equal("HTTP") - zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse'][ + 'HostedZones'] list(zones).should.have.length_of(1) zone_id = zones[0]['Id'] zone_id = zone_id.split('/') @@ -1290,7 +1324,8 @@ def test_route53_with_update(): template_body=template_json, ) - zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse'][ + 'HostedZones'] list(zones).should.have.length_of(1) zone_id = zones[0]['Id'] zone_id = zone_id.split('/') @@ -1302,14 +1337,16 @@ def test_route53_with_update(): record_set = rrsets[0] record_set.resource_records.should.equal(["my.example.com"]) - route53_health_check.template['Resources']['myDNSRecord']['Properties']['ResourceRecords'] = ["my_other.example.com"] + route53_health_check.template['Resources']['myDNSRecord'][ + 'Properties']['ResourceRecords'] = ["my_other.example.com"] template_json = json.dumps(route53_health_check.template) cf_conn.update_stack( "test_stack", template_body=template_json, ) - zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse']['HostedZones'] + zones = route53_conn.get_all_hosted_zones()['ListHostedZonesResponse'][ + 'HostedZones'] list(zones).should.have.length_of(1) zone_id = zones[0]['Id'] zone_id = zone_id.split('/') @@ -1355,12 +1392,14 @@ def test_sns_topic(): ) sns_conn = boto.sns.connect_to_region("us-west-1") - topics = sns_conn.get_all_topics()["ListTopicsResponse"]["ListTopicsResult"]["Topics"] + topics = sns_conn.get_all_topics()["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"] topics.should.have.length_of(1) topic_arn = topics[0]['TopicArn'] topic_arn.should.contain("my_topics") - subscriptions = sns_conn.get_all_subscriptions()["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["Subscriptions"] + subscriptions = sns_conn.get_all_subscriptions()["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["Subscriptions"] subscriptions.should.have.length_of(1) subscription = subscriptions[0] subscription["TopicArn"].should.equal(topic_arn) @@ -1504,12 +1543,15 @@ def test_multiple_security_group_ingress_separate_from_security_group_by_id(): ) ec2_conn = boto.ec2.connect_to_region("us-west-1") - security_group1 = ec2_conn.get_all_security_groups(filters={"tag:sg-name": "sg1"})[0] - security_group2 = ec2_conn.get_all_security_groups(filters={"tag:sg-name": "sg2"})[0] + security_group1 = ec2_conn.get_all_security_groups( + filters={"tag:sg-name": "sg1"})[0] + security_group2 = ec2_conn.get_all_security_groups( + filters={"tag:sg-name": "sg2"})[0] security_group1.rules.should.have.length_of(1) security_group1.rules[0].grants.should.have.length_of(1) - security_group1.rules[0].grants[0].group_id.should.equal(security_group2.id) + security_group1.rules[0].grants[ + 0].group_id.should.equal(security_group2.id) security_group1.rules[0].ip_protocol.should.equal('tcp') security_group1.rules[0].from_port.should.equal('80') security_group1.rules[0].to_port.should.equal('8080') @@ -1519,7 +1561,8 @@ def test_multiple_security_group_ingress_separate_from_security_group_by_id(): @mock_ec2_deprecated def test_security_group_ingress_separate_from_security_group_by_id(): ec2_conn = boto.ec2.connect_to_region("us-west-1") - ec2_conn.create_security_group("test-security-group1", "test security group") + ec2_conn.create_security_group( + "test-security-group1", "test security group") template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -1555,12 +1598,15 @@ def test_security_group_ingress_separate_from_security_group_by_id(): "test_stack", template_body=template_json, ) - security_group1 = ec2_conn.get_all_security_groups(groupnames=["test-security-group1"])[0] - security_group2 = ec2_conn.get_all_security_groups(filters={"tag:sg-name": "sg2"})[0] + security_group1 = ec2_conn.get_all_security_groups( + groupnames=["test-security-group1"])[0] + security_group2 = ec2_conn.get_all_security_groups( + filters={"tag:sg-name": "sg2"})[0] security_group1.rules.should.have.length_of(1) security_group1.rules[0].grants.should.have.length_of(1) - security_group1.rules[0].grants[0].group_id.should.equal(security_group2.id) + security_group1.rules[0].grants[ + 0].group_id.should.equal(security_group2.id) security_group1.rules[0].ip_protocol.should.equal('tcp') security_group1.rules[0].from_port.should.equal('80') security_group1.rules[0].to_port.should.equal('8080') @@ -1621,12 +1667,15 @@ def test_security_group_ingress_separate_from_security_group_by_id_using_vpc(): "test_stack", template_body=template_json, ) - security_group1 = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg1"})[0] - security_group2 = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg2"})[0] + security_group1 = vpc_conn.get_all_security_groups( + filters={"tag:sg-name": "sg1"})[0] + security_group2 = vpc_conn.get_all_security_groups( + filters={"tag:sg-name": "sg2"})[0] security_group1.rules.should.have.length_of(1) security_group1.rules[0].grants.should.have.length_of(1) - security_group1.rules[0].grants[0].group_id.should.equal(security_group2.id) + security_group1.rules[0].grants[ + 0].group_id.should.equal(security_group2.id) security_group1.rules[0].ip_protocol.should.equal('tcp') security_group1.rules[0].from_port.should.equal('80') security_group1.rules[0].to_port.should.equal('8080') @@ -1663,17 +1712,20 @@ def test_security_group_with_update(): "test_stack", template_body=template_json, ) - security_group = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg"})[0] + security_group = vpc_conn.get_all_security_groups( + filters={"tag:sg-name": "sg"})[0] security_group.vpc_id.should.equal(vpc1.id) vpc2 = vpc_conn.create_vpc("10.1.0.0/16") - template['Resources']['test-security-group']['Properties']['VpcId'] = vpc2.id + template['Resources'][ + 'test-security-group']['Properties']['VpcId'] = vpc2.id template_json = json.dumps(template) cf_conn.update_stack( "test_stack", template_body=template_json, ) - security_group = vpc_conn.get_all_security_groups(filters={"tag:sg-name": "sg"})[0] + security_group = vpc_conn.get_all_security_groups( + filters={"tag:sg-name": "sg"})[0] security_group.vpc_id.should.equal(vpc2.id) @@ -1779,11 +1831,14 @@ def test_datapipeline(): data_pipelines = dp_conn.list_pipelines() data_pipelines['pipelineIdList'].should.have.length_of(1) - data_pipelines['pipelineIdList'][0]['name'].should.equal('testDataPipeline') + data_pipelines['pipelineIdList'][0][ + 'name'].should.equal('testDataPipeline') stack_resources = cf_conn.list_stack_resources(stack_id) stack_resources.should.have.length_of(1) - stack_resources[0].physical_resource_id.should.equal(data_pipelines['pipelineIdList'][0]['id']) + stack_resources[0].physical_resource_id.should.equal( + data_pipelines['pipelineIdList'][0]['id']) + def _process_lamda(pfunc): import io @@ -1849,33 +1904,35 @@ def test_lambda_function(): def test_nat_gateway(): ec2_conn = boto3.client('ec2', 'us-east-1') vpc_id = ec2_conn.create_vpc(CidrBlock="10.0.0.0/16")['Vpc']['VpcId'] - subnet_id = ec2_conn.create_subnet(CidrBlock='10.0.1.0/24', VpcId=vpc_id)['Subnet']['SubnetId'] - route_table_id = ec2_conn.create_route_table(VpcId=vpc_id)['RouteTable']['RouteTableId'] + subnet_id = ec2_conn.create_subnet( + CidrBlock='10.0.1.0/24', VpcId=vpc_id)['Subnet']['SubnetId'] + route_table_id = ec2_conn.create_route_table( + VpcId=vpc_id)['RouteTable']['RouteTableId'] template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { - "NAT" : { - "DependsOn" : "vpcgatewayattachment", - "Type" : "AWS::EC2::NatGateway", - "Properties" : { - "AllocationId" : { "Fn::GetAtt" : ["EIP", "AllocationId"]}, - "SubnetId" : subnet_id - } - }, - "EIP" : { - "Type" : "AWS::EC2::EIP", - "Properties" : { - "Domain" : "vpc" + "NAT": { + "DependsOn": "vpcgatewayattachment", + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": {"Fn::GetAtt": ["EIP", "AllocationId"]}, + "SubnetId": subnet_id } }, - "Route" : { - "Type" : "AWS::EC2::Route", - "Properties" : { - "RouteTableId" : route_table_id, - "DestinationCidrBlock" : "0.0.0.0/0", - "NatGatewayId" : { "Ref" : "NAT" } - } + "EIP": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "Route": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": route_table_id, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": {"Ref": "NAT"} + } }, "internetgateway": { "Type": "AWS::EC2::InternetGateway" @@ -1905,6 +1962,7 @@ def test_nat_gateway(): result['NatGateways'][0]['SubnetId'].should.equal(subnet_id) result['NatGateways'][0]['State'].should.equal('available') + @mock_cloudformation() @mock_kms() def test_stack_kms(): @@ -1944,42 +2002,43 @@ def test_stack_spot_fleet(): conn = boto3.client('ec2', 'us-east-1') vpc = conn.create_vpc(CidrBlock="10.0.0.0/8")['Vpc'] - subnet = conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] + subnet = conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] subnet_id = subnet['SubnetId'] spot_fleet_template = { 'Resources': { "SpotFleet": { - "Type": "AWS::EC2::SpotFleet", - "Properties": { - "SpotFleetRequestConfigData": { - "IamFleetRole": "arn:aws:iam::123456789012:role/fleet", - "SpotPrice": "0.12", - "TargetCapacity": 6, - "AllocationStrategy": "diversified", - "LaunchSpecifications": [ - { - "EbsOptimized": "false", - "InstanceType": 't2.small', - "ImageId": "ami-1234", - "SubnetId": subnet_id, - "WeightedCapacity": "2", - "SpotPrice": "0.13", - }, - { - "EbsOptimized": "true", - "InstanceType": 't2.large', - "ImageId": "ami-1234", - "Monitoring": { "Enabled": "true" }, - "SecurityGroups": [{"GroupId": "sg-123"}], - "SubnetId": subnet_id, - "IamInstanceProfile": {"Arn": "arn:aws:iam::123456789012:role/fleet"}, - "WeightedCapacity": "4", - "SpotPrice": "10.00", - } - ] + "Type": "AWS::EC2::SpotFleet", + "Properties": { + "SpotFleetRequestConfigData": { + "IamFleetRole": "arn:aws:iam::123456789012:role/fleet", + "SpotPrice": "0.12", + "TargetCapacity": 6, + "AllocationStrategy": "diversified", + "LaunchSpecifications": [ + { + "EbsOptimized": "false", + "InstanceType": 't2.small', + "ImageId": "ami-1234", + "SubnetId": subnet_id, + "WeightedCapacity": "2", + "SpotPrice": "0.13", + }, + { + "EbsOptimized": "true", + "InstanceType": 't2.large', + "ImageId": "ami-1234", + "Monitoring": {"Enabled": "true"}, + "SecurityGroups": [{"GroupId": "sg-123"}], + "SubnetId": subnet_id, + "IamInstanceProfile": {"Arn": "arn:aws:iam::123456789012:role/fleet"}, + "WeightedCapacity": "4", + "SpotPrice": "10.00", + } + ] + } } - } } } } @@ -1993,9 +2052,11 @@ def test_stack_spot_fleet(): stack_resources = cf_conn.list_stack_resources(StackName=stack_id) stack_resources['StackResourceSummaries'].should.have.length_of(1) - spot_fleet_id = stack_resources['StackResourceSummaries'][0]['PhysicalResourceId'] + spot_fleet_id = stack_resources[ + 'StackResourceSummaries'][0]['PhysicalResourceId'] - spot_fleet_requests = conn.describe_spot_fleet_requests(SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs'] + spot_fleet_requests = conn.describe_spot_fleet_requests( + SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs'] len(spot_fleet_requests).should.equal(1) spot_fleet_request = spot_fleet_requests[0] spot_fleet_request['SpotFleetRequestState'].should.equal("active") @@ -2003,7 +2064,8 @@ def test_stack_spot_fleet(): spot_fleet_config['SpotPrice'].should.equal('0.12') spot_fleet_config['TargetCapacity'].should.equal(6) - spot_fleet_config['IamFleetRole'].should.equal('arn:aws:iam::123456789012:role/fleet') + spot_fleet_config['IamFleetRole'].should.equal( + 'arn:aws:iam::123456789012:role/fleet') spot_fleet_config['AllocationStrategy'].should.equal('diversified') spot_fleet_config['FulfilledCapacity'].should.equal(6.0) diff --git a/tests/test_cloudformation/test_server.py b/tests/test_cloudformation/test_server.py index b4f50024b..de3ab77b5 100644 --- a/tests/test_cloudformation/test_server.py +++ b/tests/test_cloudformation/test_server.py @@ -20,11 +20,14 @@ def test_cloudformation_server_get(): "Resources": {}, } create_stack_resp = test_client.action_data("CreateStack", StackName=stack_name, - TemplateBody=json.dumps(template_body)) - create_stack_resp.should.match(r".*.*.*.*.*", re.DOTALL) - stack_id_from_create_response = re.search("(.*)", create_stack_resp).groups()[0] + TemplateBody=json.dumps(template_body)) + create_stack_resp.should.match( + r".*.*.*.*.*", re.DOTALL) + stack_id_from_create_response = re.search( + "(.*)", create_stack_resp).groups()[0] list_stacks_resp = test_client.action_data("ListStacks") - stack_id_from_list_response = re.search("(.*)", list_stacks_resp).groups()[0] + stack_id_from_list_response = re.search( + "(.*)", list_stacks_resp).groups()[0] stack_id_from_create_response.should.equal(stack_id_from_list_response) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 42208810f..be459eff1 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -25,8 +25,8 @@ dummy_template = { } }, "S3Bucket": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain" + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" }, }, } @@ -71,15 +71,19 @@ get_attribute_output = { } } -outputs_template = dict(list(dummy_template.items()) + list(output_dict.items())) -bad_outputs_template = dict(list(dummy_template.items()) + list(bad_output.items())) -get_attribute_outputs_template = dict(list(dummy_template.items()) + list(get_attribute_output.items())) +outputs_template = dict(list(dummy_template.items()) + + list(output_dict.items())) +bad_outputs_template = dict( + list(dummy_template.items()) + list(bad_output.items())) +get_attribute_outputs_template = dict( + list(dummy_template.items()) + list(get_attribute_output.items())) dummy_template_json = json.dumps(dummy_template) name_type_template_json = json.dumps(name_type_template) output_type_template_json = json.dumps(outputs_template) bad_output_template_json = json.dumps(bad_outputs_template) -get_attribute_outputs_template_json = json.dumps(get_attribute_outputs_template) +get_attribute_outputs_template_json = json.dumps( + get_attribute_outputs_template) def test_parse_stack_resources(): @@ -104,7 +108,8 @@ def test_parse_stack_resources(): @patch("moto.cloudformation.parsing.logger") def test_missing_resource_logs(logger): resource_class_from_type("foobar") - logger.warning.assert_called_with('No Moto CloudFormation support for %s', 'foobar') + logger.warning.assert_called_with( + 'No Moto CloudFormation support for %s', 'foobar') def test_parse_stack_with_name_type_resource(): diff --git a/tests/test_cloudwatch/test_cloudwatch.py b/tests/test_cloudwatch/test_cloudwatch.py index 88a3190c6..9b3f76c36 100644 --- a/tests/test_cloudwatch/test_cloudwatch.py +++ b/tests/test_cloudwatch/test_cloudwatch.py @@ -4,6 +4,7 @@ import sure # noqa from moto import mock_cloudwatch_deprecated + def alarm_fixture(name="tester", action=None): action = action or ['arn:alarm'] return MetricAlarm( @@ -23,6 +24,7 @@ def alarm_fixture(name="tester", action=None): unit='Seconds', ) + @mock_cloudwatch_deprecated def test_create_alarm(): conn = boto.connect_cloudwatch() @@ -42,7 +44,8 @@ def test_create_alarm(): alarm.evaluation_periods.should.equal(5) alarm.statistic.should.equal('Average') alarm.description.should.equal('A test') - dict(alarm.dimensions).should.equal({'InstanceId': ['i-0123456,i-0123457']}) + dict(alarm.dimensions).should.equal( + {'InstanceId': ['i-0123456,i-0123457']}) list(alarm.alarm_actions).should.equal(['arn:alarm']) list(alarm.ok_actions).should.equal(['arn:ok']) list(alarm.insufficient_data_actions).should.equal(['arn:insufficient']) @@ -84,7 +87,8 @@ def test_put_metric_data(): metric = metrics[0] metric.namespace.should.equal('tester') metric.name.should.equal('metric') - dict(metric.dimensions).should.equal({'InstanceId': ['i-0123456,i-0123457']}) + dict(metric.dimensions).should.equal( + {'InstanceId': ['i-0123456,i-0123457']}) @mock_cloudwatch_deprecated @@ -103,7 +107,8 @@ def test_describe_alarms(): alarms.should.have.length_of(4) alarms = conn.describe_alarms(alarm_name_prefix="nfoo") alarms.should.have.length_of(2) - alarms = conn.describe_alarms(alarm_names=["nfoobar", "nbarfoo", "nbazfoo"]) + alarms = conn.describe_alarms( + alarm_names=["nfoobar", "nbarfoo", "nbazfoo"]) alarms.should.have.length_of(3) alarms = conn.describe_alarms(action_prefix="afoo") alarms.should.have.length_of(2) @@ -114,10 +119,11 @@ def test_describe_alarms(): alarms = conn.describe_alarms() alarms.should.have.length_of(0) + @mock_cloudwatch_deprecated def test_describe_state_value_unimplemented(): conn = boto.connect_cloudwatch() conn.describe_alarms() - conn.describe_alarms.when.called_with(state_value="foo").should.throw(NotImplementedError) - + conn.describe_alarms.when.called_with( + state_value="foo").should.throw(NotImplementedError) diff --git a/tests/test_core/test_decorator_calls.py b/tests/test_core/test_decorator_calls.py index 81dc0639a..9e3638cc2 100644 --- a/tests/test_core/test_decorator_calls.py +++ b/tests/test_core/test_decorator_calls.py @@ -59,11 +59,13 @@ def test_decorater_wrapped_gets_set(): """ Moto decorator's __wrapped__ should get set to the tests function """ - test_decorater_wrapped_gets_set.__wrapped__.__name__.should.equal('test_decorater_wrapped_gets_set') + test_decorater_wrapped_gets_set.__wrapped__.__name__.should.equal( + 'test_decorater_wrapped_gets_set') @mock_ec2_deprecated class Tester(object): + def test_the_class(self): conn = boto.connect_ec2() list(conn.get_all_instances()).should.have.length_of(0) @@ -75,6 +77,7 @@ class Tester(object): @mock_s3_deprecated class TesterWithSetup(unittest.TestCase): + def setUp(self): self.conn = boto.connect_s3() self.conn.create_bucket('mybucket') diff --git a/tests/test_core/test_instance_metadata.py b/tests/test_core/test_instance_metadata.py index 80dd501e7..69b9052e9 100644 --- a/tests/test_core/test_instance_metadata.py +++ b/tests/test_core/test_instance_metadata.py @@ -30,13 +30,15 @@ def test_meta_data_iam(): @mock_ec2 def test_meta_data_security_credentials(): - res = requests.get("{0}/latest/meta-data/iam/security-credentials/".format(BASE_URL)) + res = requests.get( + "{0}/latest/meta-data/iam/security-credentials/".format(BASE_URL)) res.content.should.equal(b"default-role") @mock_ec2 def test_meta_data_default_role(): - res = requests.get("{0}/latest/meta-data/iam/security-credentials/default-role".format(BASE_URL)) + res = requests.get( + "{0}/latest/meta-data/iam/security-credentials/default-role".format(BASE_URL)) json_response = res.json() json_response.should.contain('AccessKeyId') json_response.should.contain('SecretAccessKey') diff --git a/tests/test_core/test_responses.py b/tests/test_core/test_responses.py index aa89ac840..c3cc27aef 100644 --- a/tests/test_core/test_responses.py +++ b/tests/test_core/test_responses.py @@ -7,7 +7,8 @@ from moto.core.responses import flatten_json_request_body def test_flatten_json_request_body(): - spec = AWSServiceSpec('data/emr/2009-03-31/service-2.json').input_spec('RunJobFlow') + spec = AWSServiceSpec( + 'data/emr/2009-03-31/service-2.json').input_spec('RunJobFlow') body = { 'Name': 'cluster', @@ -42,25 +43,32 @@ def test_flatten_json_request_body(): flat['Name'].should.equal(body['Name']) flat['Instances.Ec2KeyName'].should.equal(body['Instances']['Ec2KeyName']) for idx in range(2): - flat['Instances.InstanceGroups.member.' + str(idx + 1) + '.InstanceRole'].should.equal(body['Instances']['InstanceGroups'][idx]['InstanceRole']) - flat['Instances.InstanceGroups.member.' + str(idx + 1) + '.InstanceType'].should.equal(body['Instances']['InstanceGroups'][idx]['InstanceType']) - flat['Instances.Placement.AvailabilityZone'].should.equal(body['Instances']['Placement']['AvailabilityZone']) + flat['Instances.InstanceGroups.member.' + str(idx + 1) + '.InstanceRole'].should.equal( + body['Instances']['InstanceGroups'][idx]['InstanceRole']) + flat['Instances.InstanceGroups.member.' + str(idx + 1) + '.InstanceType'].should.equal( + body['Instances']['InstanceGroups'][idx]['InstanceType']) + flat['Instances.Placement.AvailabilityZone'].should.equal( + body['Instances']['Placement']['AvailabilityZone']) for idx in range(1): prefix = 'Steps.member.' + str(idx + 1) + '.HadoopJarStep' step = body['Steps'][idx]['HadoopJarStep'] i = 0 while prefix + '.Properties.member.' + str(i + 1) + '.Key' in flat: - flat[prefix + '.Properties.member.' + str(i + 1) + '.Key'].should.equal(step['Properties'][i]['Key']) - flat[prefix + '.Properties.member.' + str(i + 1) + '.Value'].should.equal(step['Properties'][i]['Value']) + flat[prefix + '.Properties.member.' + + str(i + 1) + '.Key'].should.equal(step['Properties'][i]['Key']) + flat[prefix + '.Properties.member.' + + str(i + 1) + '.Value'].should.equal(step['Properties'][i]['Value']) i += 1 i = 0 while prefix + '.Args.member.' + str(i + 1) in flat: - flat[prefix + '.Args.member.' + str(i + 1)].should.equal(step['Args'][i]) + flat[prefix + '.Args.member.' + + str(i + 1)].should.equal(step['Args'][i]) i += 1 for idx in range(2): - flat['Configurations.member.' + str(idx + 1) + '.Classification'].should.equal(body['Configurations'][idx]['Classification']) + flat['Configurations.member.' + str(idx + 1) + '.Classification'].should.equal( + body['Configurations'][idx]['Classification']) props = {} i = 1 diff --git a/tests/test_core/test_server.py b/tests/test_core/test_server.py index a0fb328cf..b7290e351 100644 --- a/tests/test_core/test_server.py +++ b/tests/test_core/test_server.py @@ -32,19 +32,22 @@ def test_port_argument(run_simple): def test_domain_dispatched(): dispatcher = DomainDispatcherApplication(create_backend_app) - backend_app = dispatcher.get_application({"HTTP_HOST": "email.us-east1.amazonaws.com"}) + backend_app = dispatcher.get_application( + {"HTTP_HOST": "email.us-east1.amazonaws.com"}) keys = list(backend_app.view_functions.keys()) keys[0].should.equal('EmailResponse.dispatch') def test_domain_without_matches(): dispatcher = DomainDispatcherApplication(create_backend_app) - dispatcher.get_application.when.called_with({"HTTP_HOST": "not-matching-anything.com"}).should.throw(RuntimeError) + dispatcher.get_application.when.called_with( + {"HTTP_HOST": "not-matching-anything.com"}).should.throw(RuntimeError) def test_domain_dispatched_with_service(): # If we pass a particular service, always return that. dispatcher = DomainDispatcherApplication(create_backend_app, service="s3") - backend_app = dispatcher.get_application({"HTTP_HOST": "s3.us-east1.amazonaws.com"}) + backend_app = dispatcher.get_application( + {"HTTP_HOST": "s3.us-east1.amazonaws.com"}) keys = set(backend_app.view_functions.keys()) keys.should.contain('ResponseObject.key_response') diff --git a/tests/test_core/test_url_mapping.py b/tests/test_core/test_url_mapping.py index 4e4e19a3a..8f7921a5a 100644 --- a/tests/test_core/test_url_mapping.py +++ b/tests/test_core/test_url_mapping.py @@ -14,7 +14,8 @@ def test_flask_path_converting_simple(): def test_flask_path_converting_regex(): - convert_regex_to_flask_path("/(?P[a-zA-Z0-9\-_]+)").should.equal('/') + convert_regex_to_flask_path( + "/(?P[a-zA-Z0-9\-_]+)").should.equal('/') convert_regex_to_flask_path("(?P\d+)/(?P.*)$").should.equal( '/' diff --git a/tests/test_datapipeline/test_datapipeline.py b/tests/test_datapipeline/test_datapipeline.py index aaa9f7f77..520142c2e 100644 --- a/tests/test_datapipeline/test_datapipeline.py +++ b/tests/test_datapipeline/test_datapipeline.py @@ -20,7 +20,8 @@ def test_create_pipeline(): res = conn.create_pipeline("mypipeline", "some-unique-id") pipeline_id = res["pipelineId"] - pipeline_descriptions = conn.describe_pipelines([pipeline_id])["pipelineDescriptionList"] + pipeline_descriptions = conn.describe_pipelines( + [pipeline_id])["pipelineDescriptionList"] pipeline_descriptions.should.have.length_of(1) pipeline_description = pipeline_descriptions[0] @@ -105,7 +106,8 @@ def test_describing_pipeline_objects(): conn.put_pipeline_definition(PIPELINE_OBJECTS, pipeline_id) - objects = conn.describe_objects(["Schedule", "Default"], pipeline_id)['pipelineObjects'] + objects = conn.describe_objects(["Schedule", "Default"], pipeline_id)[ + 'pipelineObjects'] objects.should.have.length_of(2) default_object = [x for x in objects if x['id'] == 'Default'][0] @@ -125,7 +127,8 @@ def test_activate_pipeline(): pipeline_id = res["pipelineId"] conn.activate_pipeline(pipeline_id) - pipeline_descriptions = conn.describe_pipelines([pipeline_id])["pipelineDescriptionList"] + pipeline_descriptions = conn.describe_pipelines( + [pipeline_id])["pipelineDescriptionList"] pipeline_descriptions.should.have.length_of(1) pipeline_description = pipeline_descriptions[0] fields = pipeline_description['fields'] diff --git a/tests/test_datapipeline/test_server.py b/tests/test_datapipeline/test_server.py index 012c5ad55..03c77b034 100644 --- a/tests/test_datapipeline/test_server.py +++ b/tests/test_datapipeline/test_server.py @@ -17,9 +17,10 @@ def test_list_streams(): test_client = backend.test_client() res = test_client.post('/', - data={"pipelineIds": ["ASdf"]}, - headers={"X-Amz-Target": "DataPipeline.DescribePipelines"}, - ) + data={"pipelineIds": ["ASdf"]}, + headers={ + "X-Amz-Target": "DataPipeline.DescribePipelines"}, + ) json_data = json.loads(res.data.decode("utf-8")) json_data.should.equal({ diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index f2df39a22..d48519755 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -16,15 +16,18 @@ from boto.exception import DynamoDBResponseError @mock_dynamodb_deprecated def test_list_tables(): name = 'TestTable' - dynamodb_backend.create_table(name, hash_key_attr="name", hash_key_type="S") + dynamodb_backend.create_table( + name, hash_key_attr="name", hash_key_type="S") conn = boto.connect_dynamodb('the_key', 'the_secret') assert conn.list_tables() == ['TestTable'] @mock_dynamodb_deprecated def test_list_tables_layer_1(): - dynamodb_backend.create_table("test_1", hash_key_attr="name", hash_key_type="S") - dynamodb_backend.create_table("test_2", hash_key_attr="name", hash_key_type="S") + dynamodb_backend.create_table( + "test_1", hash_key_attr="name", hash_key_type="S") + dynamodb_backend.create_table( + "test_2", hash_key_attr="name", hash_key_type="S") conn = boto.connect_dynamodb('the_key', 'the_secret') res = conn.layer1.list_tables(limit=1) expected = {"TableNames": ["test_1"], "LastEvaluatedTableName": "test_1"} diff --git a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py index c7832b08f..2a482b31e 100644 --- a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py @@ -69,7 +69,8 @@ def test_delete_table(): conn.layer1.delete_table('messages') conn.list_tables().should.have.length_of(0) - conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) + conn.layer1.delete_table.when.called_with( + 'messages').should.throw(DynamoDBResponseError) @mock_dynamodb_deprecated @@ -192,7 +193,8 @@ def test_get_item_without_range_key(): new_item = table.new_item(hash_key=hash_key, range_key=range_key) new_item.put() - table.get_item.when.called_with(hash_key=hash_key).should.throw(DynamoDBValidationError) + table.get_item.when.called_with( + hash_key=hash_key).should.throw(DynamoDBValidationError) @mock_dynamodb_deprecated @@ -304,22 +306,28 @@ def test_query(): ) item.put() - results = table.query(hash_key='the-key', range_key_condition=condition.GT('1')) + results = table.query(hash_key='the-key', + range_key_condition=condition.GT('1')) results.response['Items'].should.have.length_of(3) - results = table.query(hash_key='the-key', range_key_condition=condition.GT('234')) + results = table.query(hash_key='the-key', + range_key_condition=condition.GT('234')) results.response['Items'].should.have.length_of(2) - results = table.query(hash_key='the-key', range_key_condition=condition.GT('9999')) + results = table.query(hash_key='the-key', + range_key_condition=condition.GT('9999')) results.response['Items'].should.have.length_of(0) - results = table.query(hash_key='the-key', range_key_condition=condition.CONTAINS('12')) + results = table.query(hash_key='the-key', + range_key_condition=condition.CONTAINS('12')) results.response['Items'].should.have.length_of(1) - results = table.query(hash_key='the-key', range_key_condition=condition.BEGINS_WITH('7')) + results = table.query(hash_key='the-key', + range_key_condition=condition.BEGINS_WITH('7')) results.response['Items'].should.have.length_of(1) - results = table.query(hash_key='the-key', range_key_condition=condition.BETWEEN('567', '890')) + results = table.query(hash_key='the-key', + range_key_condition=condition.BETWEEN('567', '890')) results.response['Items'].should.have.length_of(1) diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py index 18d353928..ebd0c2051 100644 --- a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py @@ -63,7 +63,8 @@ def test_delete_table(): conn.layer1.delete_table('messages') conn.list_tables().should.have.length_of(0) - conn.layer1.delete_table.when.called_with('messages').should.throw(DynamoDBResponseError) + conn.layer1.delete_table.when.called_with( + 'messages').should.throw(DynamoDBResponseError) @mock_dynamodb_deprecated diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 9e92e7985..860333e50 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -15,17 +15,18 @@ try: except ImportError: print("This boto version is not supported") + @requires_boto_gte("2.9") @mock_dynamodb2_deprecated def test_list_tables(): name = 'TestTable' #{'schema': } - dynamodb_backend2.create_table(name,schema=[ + dynamodb_backend2.create_table(name, schema=[ {u'KeyType': u'HASH', u'AttributeName': u'forum_name'}, {u'KeyType': u'RANGE', u'AttributeName': u'subject'} ]) - conn = boto.dynamodb2.connect_to_region( - 'us-west-2', + conn = boto.dynamodb2.connect_to_region( + 'us-west-2', aws_access_key_id="ak", aws_secret_access_key="sk") assert conn.list_tables()["TableNames"] == [name] @@ -34,13 +35,13 @@ def test_list_tables(): @requires_boto_gte("2.9") @mock_dynamodb2_deprecated def test_list_tables_layer_1(): - dynamodb_backend2.create_table("test_1",schema=[ + dynamodb_backend2.create_table("test_1", schema=[ {u'KeyType': u'HASH', u'AttributeName': u'name'} ]) - dynamodb_backend2.create_table("test_2",schema=[ + dynamodb_backend2.create_table("test_2", schema=[ {u'KeyType': u'HASH', u'AttributeName': u'name'} ]) - conn = boto.dynamodb2.connect_to_region( + conn = boto.dynamodb2.connect_to_region( 'us-west-2', aws_access_key_id="ak", aws_secret_access_key="sk") @@ -57,7 +58,7 @@ def test_list_tables_layer_1(): @requires_boto_gte("2.9") @mock_dynamodb2_deprecated def test_describe_missing_table(): - conn = boto.dynamodb2.connect_to_region( + conn = boto.dynamodb2.connect_to_region( 'us-west-2', aws_access_key_id="ak", aws_secret_access_key="sk") diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index 029506378..58e0d66d1 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -140,7 +140,8 @@ def test_delete_table(): table.delete() conn.list_tables()["TableNames"].should.have.length_of(0) - conn.delete_table.when.called_with('messages').should.throw(JSONResponseError) + conn.delete_table.when.called_with( + 'messages').should.throw(JSONResponseError) @requires_boto_gte("2.9") @@ -181,7 +182,8 @@ def test_item_add_and_describe_and_update(): }) ok.should.equal(True) - table.get_item(forum_name="LOLCat Forum", subject='Check this out!').should_not.be.none + table.get_item(forum_name="LOLCat Forum", + subject='Check this out!').should_not.be.none returned_item = table.get_item( forum_name='LOLCat Forum', @@ -224,7 +226,8 @@ def test_item_partial_save(): } table.put_item(data=data) - returned_item = table.get_item(forum_name="LOLCat Forum", subject='The LOLz') + returned_item = table.get_item( + forum_name="LOLCat Forum", subject='The LOLz') returned_item['SentBy'] = 'User B' returned_item.partial_save() @@ -270,7 +273,8 @@ def test_get_missing_item(): @mock_dynamodb2_deprecated def test_get_item_with_undeclared_table(): table = Table('undeclared-table') - table.get_item.when.called_with(test_hash=3241526475).should.throw(JSONResponseError) + table.get_item.when.called_with( + test_hash=3241526475).should.throw(JSONResponseError) @requires_boto_gte("2.9") @@ -287,7 +291,8 @@ def test_get_item_without_range_key(): hash_key = 3241526475 range_key = 1234567890987 table.put_item(data={'test_hash': hash_key, 'test_range': range_key}) - table.get_item.when.called_with(test_hash=hash_key).should.throw(ValidationException) + table.get_item.when.called_with( + test_hash=hash_key).should.throw(ValidationException) @requires_boto_gte("2.30.0") @@ -355,19 +360,23 @@ def test_query(): table.count().should.equal(4) - results = table.query_2(forum_name__eq='the-key', subject__gt='1', consistent=True) + results = table.query_2(forum_name__eq='the-key', + subject__gt='1', consistent=True) expected = ["123", "456", "789"] for index, item in enumerate(results): item["subject"].should.equal(expected[index]) - results = table.query_2(forum_name__eq="the-key", subject__gt='1', reverse=True) + results = table.query_2(forum_name__eq="the-key", + subject__gt='1', reverse=True) for index, item in enumerate(results): item["subject"].should.equal(expected[len(expected) - 1 - index]) - results = table.query_2(forum_name__eq='the-key', subject__gt='1', consistent=True) + results = table.query_2(forum_name__eq='the-key', + subject__gt='1', consistent=True) sum(1 for _ in results).should.equal(3) - results = table.query_2(forum_name__eq='the-key', subject__gt='234', consistent=True) + results = table.query_2(forum_name__eq='the-key', + subject__gt='234', consistent=True) sum(1 for _ in results).should.equal(2) results = table.query_2(forum_name__eq='the-key', subject__gt='9999') @@ -379,7 +388,8 @@ def test_query(): results = table.query_2(forum_name__eq='the-key', subject__beginswith='7') sum(1 for _ in results).should.equal(1) - results = table.query_2(forum_name__eq='the-key', subject__between=['567', '890']) + results = table.query_2(forum_name__eq='the-key', + subject__between=['567', '890']) sum(1 for _ in results).should.equal(1) @@ -558,15 +568,15 @@ def test_create_with_global_indexes(): RangeKey('version'), ], global_indexes=[ GlobalAllIndex('topic-created_at-index', - parts=[ - HashKey('topic'), - RangeKey('created_at', data_type='N') - ], - throughput={ - 'read': 6, - 'write': 1 - } - ), + parts=[ + HashKey('topic'), + RangeKey('created_at', data_type='N') + ], + throughput={ + 'read': 6, + 'write': 1 + } + ), ]) table_description = conn.describe_table("messages") @@ -601,25 +611,25 @@ def test_query_with_global_indexes(): RangeKey('version'), ], global_indexes=[ GlobalAllIndex('topic-created_at-index', - parts=[ - HashKey('topic'), - RangeKey('created_at', data_type='N') - ], - throughput={ - 'read': 6, - 'write': 1 - } - ), + parts=[ + HashKey('topic'), + RangeKey('created_at', data_type='N') + ], + throughput={ + 'read': 6, + 'write': 1 + } + ), GlobalAllIndex('status-created_at-index', - parts=[ - HashKey('status'), - RangeKey('created_at', data_type='N') - ], - throughput={ - 'read': 2, - 'write': 1 - } - ) + parts=[ + HashKey('status'), + RangeKey('created_at', data_type='N') + ], + throughput={ + 'read': 2, + 'write': 1 + } + ) ]) item_data = { @@ -653,7 +663,8 @@ def test_query_with_local_indexes(): item['version'] = '2' item.save(overwrite=True) - results = table.query(forum_name__eq='Cool Forum', index='threads_index', threads__eq=1) + results = table.query(forum_name__eq='Cool Forum', + index='threads_index', threads__eq=1) list(results).should.have.length_of(1) @@ -888,7 +899,8 @@ def test_failed_overwrite(): table.put_item(data=data2, overwrite=True) data3 = {'id': '123', 'range': 'abc', 'data': '812'} - table.put_item.when.called_with(data=data3).should.throw(ConditionalCheckFailedException) + table.put_item.when.called_with(data=data3).should.throw( + ConditionalCheckFailedException) returned_item = table.lookup('123', 'abc') dict(returned_item).should.equal(data2) @@ -972,7 +984,8 @@ def test_boto3_conditions(): # Test a query returning all items results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('1'), + KeyConditionExpression=Key('forum_name').eq( + 'the-key') & Key("subject").gt('1'), ScanIndexForward=True, ) expected = ["123", "456", "789"] @@ -981,7 +994,8 @@ def test_boto3_conditions(): # Return all items again, but in reverse results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('1'), + KeyConditionExpression=Key('forum_name').eq( + 'the-key') & Key("subject").gt('1'), ScanIndexForward=False, ) for index, item in enumerate(reversed(results['Items'])): @@ -989,29 +1003,34 @@ def test_boto3_conditions(): # Filter the subjects to only return some of the results results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('234'), + KeyConditionExpression=Key('forum_name').eq( + 'the-key') & Key("subject").gt('234'), ConsistentRead=True, ) results['Count'].should.equal(2) # Filter to return no results results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('9999') + KeyConditionExpression=Key('forum_name').eq( + 'the-key') & Key("subject").gt('9999') ) results['Count'].should.equal(0) results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").begins_with('12') + KeyConditionExpression=Key('forum_name').eq( + 'the-key') & Key("subject").begins_with('12') ) results['Count'].should.equal(1) results = table.query( - KeyConditionExpression=Key("subject").begins_with('7') & Key('forum_name').eq('the-key') + KeyConditionExpression=Key("subject").begins_with( + '7') & Key('forum_name').eq('the-key') ) results['Count'].should.equal(1) results = table.query( - KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").between('567', '890') + KeyConditionExpression=Key('forum_name').eq( + 'the-key') & Key("subject").between('567', '890') ) results['Count'].should.equal(1) @@ -1337,7 +1356,8 @@ def test_boto3_query_gsi_range_comparison(): # Test a query returning all johndoe items results = table.query( - KeyConditionExpression=Key('username').eq('johndoe') & Key("created").gt(0), + KeyConditionExpression=Key('username').eq( + 'johndoe') & Key("created").gt(0), ScanIndexForward=True, IndexName='TestGSI', ) @@ -1347,7 +1367,8 @@ def test_boto3_query_gsi_range_comparison(): # Return all johndoe items again, but in reverse results = table.query( - KeyConditionExpression=Key('username').eq('johndoe') & Key("created").gt(0), + KeyConditionExpression=Key('username').eq( + 'johndoe') & Key("created").gt(0), ScanIndexForward=False, IndexName='TestGSI', ) @@ -1357,7 +1378,8 @@ def test_boto3_query_gsi_range_comparison(): # Filter the creation to only return some of the results # And reverse order of hash + range key results = table.query( - KeyConditionExpression=Key("created").gt(1) & Key('username').eq('johndoe'), + KeyConditionExpression=Key("created").gt( + 1) & Key('username').eq('johndoe'), ConsistentRead=True, IndexName='TestGSI', ) @@ -1365,20 +1387,23 @@ def test_boto3_query_gsi_range_comparison(): # Filter to return no results results = table.query( - KeyConditionExpression=Key('username').eq('janedoe') & Key("created").gt(9), + KeyConditionExpression=Key('username').eq( + 'janedoe') & Key("created").gt(9), IndexName='TestGSI', ) results['Count'].should.equal(0) results = table.query( - KeyConditionExpression=Key('username').eq('janedoe') & Key("created").eq(5), + KeyConditionExpression=Key('username').eq( + 'janedoe') & Key("created").eq(5), IndexName='TestGSI', ) results['Count'].should.equal(1) # Test range key sorting results = table.query( - KeyConditionExpression=Key('username').eq('johndoe') & Key("created").gt(0), + KeyConditionExpression=Key('username').eq( + 'johndoe') & Key("created").gt(0), IndexName='TestGSI', ) expected = [Decimal('1'), Decimal('2'), Decimal('3')] @@ -1516,7 +1541,6 @@ def test_boto3_update_table_gsi_throughput(): gsi_throughput['WriteCapacityUnits'].should.equal(11) - @mock_dynamodb2 def test_update_table_gsi_create(): dynamodb = boto3.resource('dynamodb', region_name='us-east-1') diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 83eff6519..36e1b6c61 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -71,7 +71,8 @@ def test_delete_table(): conn.delete_table('messages') conn.list_tables()["TableNames"].should.have.length_of(0) - conn.delete_table.when.called_with('messages').should.throw(JSONResponseError) + conn.delete_table.when.called_with( + 'messages').should.throw(JSONResponseError) @requires_boto_gte("2.9") @@ -239,7 +240,8 @@ def test_query_with_undeclared_table(): conn.query.when.called_with( table_name='undeclared-table', - key_conditions={"forum_name": {"ComparisonOperator": "EQ", "AttributeValueList": [{"S": "the-key"}]}} + key_conditions={"forum_name": { + "ComparisonOperator": "EQ", "AttributeValueList": [{"S": "the-key"}]}} ).should.throw(JSONResponseError) @@ -396,7 +398,8 @@ def test_get_key_fields(): @mock_dynamodb2_deprecated def test_get_missing_item(): table = create_table() - table.get_item.when.called_with(forum_name='missing').should.throw(ItemNotFound) + table.get_item.when.called_with( + forum_name='missing').should.throw(ItemNotFound) @requires_boto_gte("2.9") @@ -436,7 +439,8 @@ def test_update_item_remove(): } # Then remove the SentBy field - conn.update_item("messages", key_map, update_expression="REMOVE SentBy, SentTo") + conn.update_item("messages", key_map, + update_expression="REMOVE SentBy, SentTo") returned_item = table.get_item(username="steve") dict(returned_item).should.equal({ @@ -460,7 +464,8 @@ def test_update_item_set(): 'username': {"S": "steve"} } - conn.update_item("messages", key_map, update_expression="SET foo=bar, blah=baz REMOVE SentBy") + conn.update_item("messages", key_map, + update_expression="SET foo=bar, blah=baz REMOVE SentBy") returned_item = table.get_item(username="steve") dict(returned_item).should.equal({ @@ -470,7 +475,6 @@ def test_update_item_set(): }) - @mock_dynamodb2_deprecated def test_failed_overwrite(): table = Table.create('messages', schema=[ @@ -487,7 +491,8 @@ def test_failed_overwrite(): table.put_item(data=data2, overwrite=True) data3 = {'id': '123', 'data': '812'} - table.put_item.when.called_with(data=data3).should.throw(ConditionalCheckFailedException) + table.put_item.when.called_with(data=data3).should.throw( + ConditionalCheckFailedException) returned_item = table.lookup('123') dict(returned_item).should.equal(data2) @@ -521,6 +526,7 @@ def test_conflicting_writes(): boto3 """ + @mock_dynamodb2 def test_boto3_create_table(): dynamodb = boto3.resource('dynamodb', region_name='us-east-1') @@ -617,7 +623,6 @@ def test_boto3_put_item_conditions_pass(): assert dict(returned_item)['Item']['foo'].should.equal("baz") - @mock_dynamodb2 def test_scan_pagination(): table = _create_user_table() diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 4c154ae84..40cc5fe24 100755 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -20,10 +20,12 @@ def test_ami_create_and_delete(): instance = reservation.instances[0] with assert_raises(EC2ResponseError) as ex: - image_id = conn.create_image(instance.id, "test-ami", "this is a test ami", dry_run=True) + image_id = conn.create_image( + instance.id, "test-ami", "this is a test ami", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateImage operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateImage operation: Request would have succeeded, but DryRun flag is set') image_id = conn.create_image(instance.id, "test-ami", "this is a test ami") @@ -47,8 +49,10 @@ def test_ami_create_and_delete(): snapshots.should.have.length_of(1) snapshot = snapshots[0] - image.block_device_mapping.current_value.snapshot_id.should.equal(snapshot.id) - snapshot.description.should.equal("Auto-created snapshot for AMI {0}".format(image.id)) + image.block_device_mapping.current_value.snapshot_id.should.equal( + snapshot.id) + snapshot.description.should.equal( + "Auto-created snapshot for AMI {0}".format(image.id)) snapshot.volume_id.should.equal(volume.id) # Deregister @@ -56,7 +60,8 @@ def test_ami_create_and_delete(): success = conn.deregister_image(image_id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeregisterImage operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeregisterImage operation: Request would have succeeded, but DryRun flag is set') success = conn.deregister_image(image_id) success.should.be.true @@ -75,23 +80,29 @@ def test_ami_copy(): reservation = conn.run_instances('ami-1234abcd') instance = reservation.instances[0] - source_image_id = conn.create_image(instance.id, "test-ami", "this is a test ami") + source_image_id = conn.create_image( + instance.id, "test-ami", "this is a test ami") instance.terminate() source_image = conn.get_all_images(image_ids=[source_image_id])[0] - # Boto returns a 'CopyImage' object with an image_id attribute here. Use the image_id to fetch the full info. + # Boto returns a 'CopyImage' object with an image_id attribute here. Use + # the image_id to fetch the full info. with assert_raises(EC2ResponseError) as ex: - copy_image_ref = conn.copy_image(source_image.region.name, source_image.id, "test-copy-ami", "this is a test copy ami", dry_run=True) + copy_image_ref = conn.copy_image( + source_image.region.name, source_image.id, "test-copy-ami", "this is a test copy ami", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CopyImage operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CopyImage operation: Request would have succeeded, but DryRun flag is set') - copy_image_ref = conn.copy_image(source_image.region.name, source_image.id, "test-copy-ami", "this is a test copy ami") + copy_image_ref = conn.copy_image( + source_image.region.name, source_image.id, "test-copy-ami", "this is a test copy ami") copy_image_id = copy_image_ref.image_id copy_image = conn.get_all_images(image_ids=[copy_image_id])[0] copy_image.id.should.equal(copy_image_id) - copy_image.virtualization_type.should.equal(source_image.virtualization_type) + copy_image.virtualization_type.should.equal( + source_image.virtualization_type) copy_image.architecture.should.equal(source_image.architecture) copy_image.kernel_id.should.equal(source_image.kernel_id) copy_image.platform.should.equal(source_image.platform) @@ -105,15 +116,18 @@ def test_ami_copy(): # Copy from non-existent source ID. with assert_raises(EC2ResponseError) as cm: - conn.copy_image(source_image.region.name, 'ami-abcd1234', "test-copy-ami", "this is a test copy ami") + conn.copy_image(source_image.region.name, 'ami-abcd1234', + "test-copy-ami", "this is a test copy ami") cm.exception.code.should.equal('InvalidAMIID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none # Copy from non-existent source region. with assert_raises(EC2ResponseError) as cm: - invalid_region = 'us-east-1' if (source_image.region.name != 'us-east-1') else 'us-west-1' - conn.copy_image(invalid_region, source_image.id, "test-copy-ami", "this is a test copy ami") + invalid_region = 'us-east-1' if (source_image.region.name != + 'us-east-1') else 'us-west-1' + conn.copy_image(invalid_region, source_image.id, + "test-copy-ami", "this is a test copy ami") cm.exception.code.should.equal('InvalidAMIID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none @@ -131,7 +145,8 @@ def test_ami_tagging(): image.add_tag("a key", "some value", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') image.add_tag("a key", "some value") @@ -179,7 +194,8 @@ def test_ami_filters(): instanceA.modify_attribute("kernel", "k-1234abcd") instanceA.modify_attribute("platform", "windows") instanceA.modify_attribute("virtualization_type", "hvm") - imageA_id = conn.create_image(instanceA.id, "test-ami-A", "this is a test ami") + imageA_id = conn.create_image( + instanceA.id, "test-ami-A", "this is a test ami") imageA = conn.get_image(imageA_id) reservationB = conn.run_instances('ami-abcd1234') @@ -188,18 +204,22 @@ def test_ami_filters(): instanceB.modify_attribute("kernel", "k-abcd1234") instanceB.modify_attribute("platform", "linux") instanceB.modify_attribute("virtualization_type", "paravirtual") - imageB_id = conn.create_image(instanceB.id, "test-ami-B", "this is a test ami") + imageB_id = conn.create_image( + instanceB.id, "test-ami-B", "this is a test ami") imageB = conn.get_image(imageB_id) imageB.set_launch_permissions(group_names=("all")) - amis_by_architecture = conn.get_all_images(filters={'architecture': 'x86_64'}) + amis_by_architecture = conn.get_all_images( + filters={'architecture': 'x86_64'}) set([ami.id for ami in amis_by_architecture]).should.equal(set([imageB.id])) amis_by_kernel = conn.get_all_images(filters={'kernel-id': 'k-abcd1234'}) set([ami.id for ami in amis_by_kernel]).should.equal(set([imageB.id])) - amis_by_virtualization = conn.get_all_images(filters={'virtualization-type': 'paravirtual'}) - set([ami.id for ami in amis_by_virtualization]).should.equal(set([imageB.id])) + amis_by_virtualization = conn.get_all_images( + filters={'virtualization-type': 'paravirtual'}) + set([ami.id for ami in amis_by_virtualization] + ).should.equal(set([imageB.id])) amis_by_platform = conn.get_all_images(filters={'platform': 'windows'}) set([ami.id for ami in amis_by_platform]).should.equal(set([imageA.id])) @@ -208,7 +228,8 @@ def test_ami_filters(): set([ami.id for ami in amis_by_id]).should.equal(set([imageA.id])) amis_by_state = conn.get_all_images(filters={'state': 'available'}) - set([ami.id for ami in amis_by_state]).should.equal(set([imageA.id, imageB.id])) + set([ami.id for ami in amis_by_state]).should.equal( + set([imageA.id, imageB.id])) amis_by_name = conn.get_all_images(filters={'name': imageA.name}) set([ami.id for ami in amis_by_name]).should.equal(set([imageA.id])) @@ -226,20 +247,23 @@ def test_ami_filtering_via_tag(): reservationA = conn.run_instances('ami-1234abcd') instanceA = reservationA.instances[0] - imageA_id = conn.create_image(instanceA.id, "test-ami-A", "this is a test ami") + imageA_id = conn.create_image( + instanceA.id, "test-ami-A", "this is a test ami") imageA = conn.get_image(imageA_id) imageA.add_tag("a key", "some value") reservationB = conn.run_instances('ami-abcd1234') instanceB = reservationB.instances[0] - imageB_id = conn.create_image(instanceB.id, "test-ami-B", "this is a test ami") + imageB_id = conn.create_image( + instanceB.id, "test-ami-B", "this is a test ami") imageB = conn.get_image(imageB_id) imageB.add_tag("another key", "some other value") amis_by_tagA = conn.get_all_images(filters={'tag:a key': 'some value'}) set([ami.id for ami in amis_by_tagA]).should.equal(set([imageA.id])) - amis_by_tagB = conn.get_all_images(filters={'tag:another key': 'some other value'}) + amis_by_tagB = conn.get_all_images( + filters={'tag:another key': 'some other value'}) set([ami.id for ami in amis_by_tagB]).should.equal(set([imageB.id])) @@ -274,7 +298,8 @@ def test_ami_attribute_group_permissions(): image = conn.get_image(image_id) # Baseline - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.name.should.equal('launch_permission') attributes.attrs.should.have.length_of(0) @@ -290,32 +315,38 @@ def test_ami_attribute_group_permissions(): # Add 'all' group and confirm with assert_raises(EC2ResponseError) as ex: - conn.modify_image_attribute(**dict(ADD_GROUP_ARGS, **{'dry_run': True})) + conn.modify_image_attribute( + **dict(ADD_GROUP_ARGS, **{'dry_run': True})) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyImageAttribute operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyImageAttribute operation: Request would have succeeded, but DryRun flag is set') conn.modify_image_attribute(**ADD_GROUP_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs['groups'].should.have.length_of(1) attributes.attrs['groups'].should.equal(['all']) image = conn.get_image(image_id) image.is_public.should.equal(True) # Add is idempotent - conn.modify_image_attribute.when.called_with(**ADD_GROUP_ARGS).should_not.throw(EC2ResponseError) + conn.modify_image_attribute.when.called_with( + **ADD_GROUP_ARGS).should_not.throw(EC2ResponseError) # Remove 'all' group and confirm conn.modify_image_attribute(**REMOVE_GROUP_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs.should.have.length_of(0) image = conn.get_image(image_id) image.is_public.should.equal(False) # Remove is idempotent - conn.modify_image_attribute.when.called_with(**REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError) + conn.modify_image_attribute.when.called_with( + **REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError) @mock_emr_deprecated @@ -327,7 +358,8 @@ def test_ami_attribute_user_permissions(): image = conn.get_image(image_id) # Baseline - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.name.should.equal('launch_permission') attributes.attrs.should.have.length_of(0) @@ -353,19 +385,23 @@ def test_ami_attribute_user_permissions(): # Add multiple users and confirm conn.modify_image_attribute(**ADD_USERS_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs['user_ids'].should.have.length_of(2) - set(attributes.attrs['user_ids']).should.equal(set([str(USER1), str(USER2)])) + set(attributes.attrs['user_ids']).should.equal( + set([str(USER1), str(USER2)])) image = conn.get_image(image_id) image.is_public.should.equal(False) # Add is idempotent - conn.modify_image_attribute.when.called_with(**ADD_USERS_ARGS).should_not.throw(EC2ResponseError) + conn.modify_image_attribute.when.called_with( + **ADD_USERS_ARGS).should_not.throw(EC2ResponseError) # Remove single user and confirm conn.modify_image_attribute(**REMOVE_SINGLE_USER_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs['user_ids'].should.have.length_of(1) set(attributes.attrs['user_ids']).should.equal(set([str(USER2)])) image = conn.get_image(image_id) @@ -374,13 +410,15 @@ def test_ami_attribute_user_permissions(): # Remove multiple users and confirm conn.modify_image_attribute(**REMOVE_USERS_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs.should.have.length_of(0) image = conn.get_image(image_id) image.is_public.should.equal(False) # Remove is idempotent - conn.modify_image_attribute.when.called_with(**REMOVE_USERS_ARGS).should_not.throw(EC2ResponseError) + conn.modify_image_attribute.when.called_with( + **REMOVE_USERS_ARGS).should_not.throw(EC2ResponseError) @mock_emr_deprecated @@ -397,7 +435,8 @@ def test_ami_attribute_user_and_group_permissions(): image = conn.get_image(image_id) # Baseline - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.name.should.equal('launch_permission') attributes.attrs.should.have.length_of(0) @@ -419,7 +458,8 @@ def test_ami_attribute_user_and_group_permissions(): # Add and confirm conn.modify_image_attribute(**ADD_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs['user_ids'].should.have.length_of(2) set(attributes.attrs['user_ids']).should.equal(set([USER1, USER2])) set(attributes.attrs['groups']).should.equal(set(['all'])) @@ -429,7 +469,8 @@ def test_ami_attribute_user_and_group_permissions(): # Remove and confirm conn.modify_image_attribute(**REMOVE_ARGS) - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs.should.have.length_of(0) image = conn.get_image(image_id) image.is_public.should.equal(False) @@ -483,7 +524,8 @@ def test_ami_attribute_error_cases(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - # Error: Add with one invalid user ID among other valid IDs, ensure no partial changes. + # Error: Add with one invalid user ID among other valid IDs, ensure no + # partial changes. with assert_raises(EC2ResponseError) as cm: conn.modify_image_attribute(image.id, attribute='launchPermission', @@ -493,7 +535,8 @@ def test_ami_attribute_error_cases(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - attributes = conn.get_image_attribute(image.id, attribute='launchPermission') + attributes = conn.get_image_attribute( + image.id, attribute='launchPermission') attributes.attrs.should.have.length_of(0) # Error: Add with invalid image ID diff --git a/tests/test_ec2/test_customer_gateways.py b/tests/test_ec2/test_customer_gateways.py index 93e35dc6a..589f887f6 100644 --- a/tests/test_ec2/test_customer_gateways.py +++ b/tests/test_ec2/test_customer_gateways.py @@ -12,26 +12,31 @@ from moto import mock_ec2_deprecated def test_create_customer_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') - customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) + customer_gateway = conn.create_customer_gateway( + 'ipsec.1', '205.251.242.54', 65534) customer_gateway.should_not.be.none customer_gateway.id.should.match(r'cgw-\w+') customer_gateway.type.should.equal('ipsec.1') customer_gateway.bgp_asn.should.equal(65534) customer_gateway.ip_address.should.equal('205.251.242.54') + @mock_ec2_deprecated def test_describe_customer_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') - customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) + customer_gateway = conn.create_customer_gateway( + 'ipsec.1', '205.251.242.54', 65534) cgws = conn.get_all_customer_gateways() cgws.should.have.length_of(1) cgws[0].id.should.match(customer_gateway.id) + @mock_ec2_deprecated def test_delete_customer_gateways(): conn = boto.connect_vpc('the_key', 'the_secret') - customer_gateway = conn.create_customer_gateway('ipsec.1', '205.251.242.54', 65534) + customer_gateway = conn.create_customer_gateway( + 'ipsec.1', '205.251.242.54', 65534) customer_gateway.should_not.be.none cgws = conn.get_all_customer_gateways() cgws[0].id.should.match(customer_gateway.id) @@ -39,6 +44,7 @@ def test_delete_customer_gateways(): cgws = conn.get_all_customer_gateways() cgws.should.have.length_of(0) + @mock_ec2_deprecated def test_delete_customer_gateways_bad_id(): conn = boto.connect_vpc('the_key', 'the_secret') diff --git a/tests/test_ec2/test_dhcp_options.py b/tests/test_ec2/test_dhcp_options.py index 0279a3d54..4e2520241 100644 --- a/tests/test_ec2/test_dhcp_options.py +++ b/tests/test_ec2/test_dhcp_options.py @@ -19,7 +19,8 @@ SAMPLE_NAME_SERVERS = [u'10.0.0.6', u'10.0.0.7'] def test_dhcp_options_associate(): """ associate dhcp option """ conn = boto.connect_vpc('the_key', 'the_secret') - dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) + dhcp_options = conn.create_dhcp_options( + SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) vpc = conn.create_vpc("10.0.0.0/16") rval = conn.associate_dhcp_options(dhcp_options.id, vpc.id) @@ -43,7 +44,8 @@ def test_dhcp_options_associate_invalid_dhcp_id(): def test_dhcp_options_associate_invalid_vpc_id(): """ associate dhcp option invalid vpc id """ conn = boto.connect_vpc('the_key', 'the_secret') - dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) + dhcp_options = conn.create_dhcp_options( + SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) with assert_raises(EC2ResponseError) as cm: conn.associate_dhcp_options(dhcp_options.id, "foo") @@ -56,7 +58,8 @@ def test_dhcp_options_associate_invalid_vpc_id(): def test_dhcp_options_delete_with_vpc(): """Test deletion of dhcp options with vpc""" conn = boto.connect_vpc('the_key', 'the_secret') - dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) + dhcp_options = conn.create_dhcp_options( + SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) dhcp_options_id = dhcp_options.id vpc = conn.create_vpc("10.0.0.0/16") @@ -83,10 +86,13 @@ def test_create_dhcp_options(): """Create most basic dhcp option""" conn = boto.connect_vpc('the_key', 'the_secret') - dhcp_option = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) + dhcp_option = conn.create_dhcp_options( + SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) dhcp_option.options[u'domain-name'][0].should.be.equal(SAMPLE_DOMAIN_NAME) - dhcp_option.options[u'domain-name-servers'][0].should.be.equal(SAMPLE_NAME_SERVERS[0]) - dhcp_option.options[u'domain-name-servers'][1].should.be.equal(SAMPLE_NAME_SERVERS[1]) + dhcp_option.options[ + u'domain-name-servers'][0].should.be.equal(SAMPLE_NAME_SERVERS[0]) + dhcp_option.options[ + u'domain-name-servers'][1].should.be.equal(SAMPLE_NAME_SERVERS[1]) @mock_ec2_deprecated @@ -210,8 +216,10 @@ def test_dhcp_options_get_by_tag(): dhcp_options_sets = conn.get_all_dhcp_options(filters=filters) dhcp_options_sets.should.have.length_of(1) - dhcp_options_sets[0].options['domain-name'][0].should.be.equal('example.com') - dhcp_options_sets[0].options['domain-name-servers'][0].should.be.equal('10.0.10.2') + dhcp_options_sets[0].options[ + 'domain-name'][0].should.be.equal('example.com') + dhcp_options_sets[0].options[ + 'domain-name-servers'][0].should.be.equal('10.0.10.2') dhcp_options_sets[0].tags['Name'].should.equal('TestDhcpOptions1') dhcp_options_sets[0].tags['test-tag'].should.equal('test-value') @@ -219,8 +227,10 @@ def test_dhcp_options_get_by_tag(): dhcp_options_sets = conn.get_all_dhcp_options(filters=filters) dhcp_options_sets.should.have.length_of(1) - dhcp_options_sets[0].options['domain-name'][0].should.be.equal('example.com') - dhcp_options_sets[0].options['domain-name-servers'][0].should.be.equal('10.0.20.2') + dhcp_options_sets[0].options[ + 'domain-name'][0].should.be.equal('example.com') + dhcp_options_sets[0].options[ + 'domain-name-servers'][0].should.be.equal('10.0.20.2') dhcp_options_sets[0].tags['Name'].should.equal('TestDhcpOptions2') dhcp_options_sets[0].tags['test-tag'].should.equal('test-value') @@ -247,17 +257,21 @@ def test_dhcp_options_get_by_id(): dhcp_options_sets = conn.get_all_dhcp_options() dhcp_options_sets.should.have.length_of(2) - dhcp_options_sets = conn.get_all_dhcp_options(filters={'dhcp-options-id': dhcp1_id}) + dhcp_options_sets = conn.get_all_dhcp_options( + filters={'dhcp-options-id': dhcp1_id}) dhcp_options_sets.should.have.length_of(1) dhcp_options_sets[0].options['domain-name'][0].should.be.equal('test1.com') - dhcp_options_sets[0].options['domain-name-servers'][0].should.be.equal('10.0.10.2') + dhcp_options_sets[0].options[ + 'domain-name-servers'][0].should.be.equal('10.0.10.2') - dhcp_options_sets = conn.get_all_dhcp_options(filters={'dhcp-options-id': dhcp2_id}) + dhcp_options_sets = conn.get_all_dhcp_options( + filters={'dhcp-options-id': dhcp2_id}) dhcp_options_sets.should.have.length_of(1) dhcp_options_sets[0].options['domain-name'][0].should.be.equal('test2.com') - dhcp_options_sets[0].options['domain-name-servers'][0].should.be.equal('10.0.20.2') + dhcp_options_sets[0].options[ + 'domain-name-servers'][0].should.be.equal('10.0.20.2') @mock_ec2 @@ -315,4 +329,5 @@ def test_dhcp_options_get_by_invalid_filter(): conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) filters = {'invalid-filter': 'invalid-value'} - conn.get_all_dhcp_options.when.called_with(filters=filters).should.throw(NotImplementedError) + conn.get_all_dhcp_options.when.called_with( + filters=filters).should.throw(NotImplementedError) diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index 6491412e3..83c89d129 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -28,7 +28,8 @@ def test_create_and_delete_volume(): volume.delete(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteVolume operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeleteVolume operation: Request would have succeeded, but DryRun flag is set') volume.delete() @@ -42,7 +43,6 @@ def test_create_and_delete_volume(): cm.exception.request_id.should_not.be.none - @mock_ec2_deprecated def test_create_encrypted_volume_dryrun(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -50,7 +50,8 @@ def test_create_encrypted_volume_dryrun(): conn.create_volume(80, "us-east-1a", encrypted=True, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') @mock_ec2_deprecated @@ -62,7 +63,8 @@ def test_create_encrypted_volume(): conn.create_volume(80, "us-east-1a", encrypted=True, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateVolume operation: Request would have succeeded, but DryRun flag is set') all_volumes = conn.get_all_volumes() all_volumes[0].encrypted.should.be(True) @@ -108,29 +110,42 @@ def test_volume_filters(): block_mapping = instance.block_device_mapping['/dev/sda1'] - volumes_by_attach_time = conn.get_all_volumes(filters={'attachment.attach-time': block_mapping.attach_time}) - set([vol.id for vol in volumes_by_attach_time]).should.equal(set([block_mapping.volume_id])) + volumes_by_attach_time = conn.get_all_volumes( + filters={'attachment.attach-time': block_mapping.attach_time}) + set([vol.id for vol in volumes_by_attach_time] + ).should.equal(set([block_mapping.volume_id])) - volumes_by_attach_device = conn.get_all_volumes(filters={'attachment.device': '/dev/sda1'}) - set([vol.id for vol in volumes_by_attach_device]).should.equal(set([block_mapping.volume_id])) + volumes_by_attach_device = conn.get_all_volumes( + filters={'attachment.device': '/dev/sda1'}) + set([vol.id for vol in volumes_by_attach_device] + ).should.equal(set([block_mapping.volume_id])) - volumes_by_attach_instance_id = conn.get_all_volumes(filters={'attachment.instance-id': instance.id}) - set([vol.id for vol in volumes_by_attach_instance_id]).should.equal(set([block_mapping.volume_id])) + volumes_by_attach_instance_id = conn.get_all_volumes( + filters={'attachment.instance-id': instance.id}) + set([vol.id for vol in volumes_by_attach_instance_id] + ).should.equal(set([block_mapping.volume_id])) - volumes_by_attach_status = conn.get_all_volumes(filters={'attachment.status': 'attached'}) - set([vol.id for vol in volumes_by_attach_status]).should.equal(set([block_mapping.volume_id])) + volumes_by_attach_status = conn.get_all_volumes( + filters={'attachment.status': 'attached'}) + set([vol.id for vol in volumes_by_attach_status] + ).should.equal(set([block_mapping.volume_id])) - volumes_by_create_time = conn.get_all_volumes(filters={'create-time': volume4.create_time}) - set([vol.create_time for vol in volumes_by_create_time]).should.equal(set([volume4.create_time])) + volumes_by_create_time = conn.get_all_volumes( + filters={'create-time': volume4.create_time}) + set([vol.create_time for vol in volumes_by_create_time] + ).should.equal(set([volume4.create_time])) volumes_by_size = conn.get_all_volumes(filters={'size': volume2.size}) set([vol.id for vol in volumes_by_size]).should.equal(set([volume2.id])) - volumes_by_snapshot_id = conn.get_all_volumes(filters={'snapshot-id': snapshot.id}) - set([vol.id for vol in volumes_by_snapshot_id]).should.equal(set([volume4.id])) + volumes_by_snapshot_id = conn.get_all_volumes( + filters={'snapshot-id': snapshot.id}) + set([vol.id for vol in volumes_by_snapshot_id] + ).should.equal(set([volume4.id])) volumes_by_status = conn.get_all_volumes(filters={'status': 'in-use'}) - set([vol.id for vol in volumes_by_status]).should.equal(set([block_mapping.volume_id])) + set([vol.id for vol in volumes_by_status]).should.equal( + set([block_mapping.volume_id])) volumes_by_id = conn.get_all_volumes(filters={'volume-id': volume1.id}) set([vol.id for vol in volumes_by_id]).should.equal(set([volume1.id])) @@ -138,13 +153,17 @@ def test_volume_filters(): volumes_by_tag_key = conn.get_all_volumes(filters={'tag-key': 'testkey1'}) set([vol.id for vol in volumes_by_tag_key]).should.equal(set([volume1.id])) - volumes_by_tag_value = conn.get_all_volumes(filters={'tag-value': 'testvalue1'}) - set([vol.id for vol in volumes_by_tag_value]).should.equal(set([volume1.id])) + volumes_by_tag_value = conn.get_all_volumes( + filters={'tag-value': 'testvalue1'}) + set([vol.id for vol in volumes_by_tag_value] + ).should.equal(set([volume1.id])) - volumes_by_tag = conn.get_all_volumes(filters={'tag:testkey1': 'testvalue1'}) + volumes_by_tag = conn.get_all_volumes( + filters={'tag:testkey1': 'testvalue1'}) set([vol.id for vol in volumes_by_tag]).should.equal(set([volume1.id])) - volumes_by_unencrypted = conn.get_all_volumes(filters={'encrypted': 'false'}) + volumes_by_unencrypted = conn.get_all_volumes( + filters={'encrypted': 'false'}) set([vol.id for vol in volumes_by_unencrypted]).should.equal( set([block_mapping.volume_id, volume2.id]) ) @@ -169,7 +188,8 @@ def test_volume_attach_and_detach(): volume.attach(instance.id, "/dev/sdh", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AttachVolume operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the AttachVolume operation: Request would have succeeded, but DryRun flag is set') volume.attach(instance.id, "/dev/sdh") @@ -183,7 +203,8 @@ def test_volume_attach_and_detach(): volume.detach(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DetachVolume operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DetachVolume operation: Request would have succeeded, but DryRun flag is set') volume.detach() @@ -218,7 +239,8 @@ def test_create_snapshot(): snapshot = volume.create_snapshot('a dryrun snapshot', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSnapshot operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateSnapshot operation: Request would have succeeded, but DryRun flag is set') snapshot = volume.create_snapshot('a test snapshot') snapshot.update() @@ -294,32 +316,50 @@ def test_snapshot_filters(): conn.create_tags([snapshot1.id], {'testkey1': 'testvalue1'}) conn.create_tags([snapshot2.id], {'testkey2': 'testvalue2'}) - snapshots_by_description = conn.get_all_snapshots(filters={'description': 'testsnapshot1'}) - set([snap.id for snap in snapshots_by_description]).should.equal(set([snapshot1.id])) + snapshots_by_description = conn.get_all_snapshots( + filters={'description': 'testsnapshot1'}) + set([snap.id for snap in snapshots_by_description] + ).should.equal(set([snapshot1.id])) - snapshots_by_id = conn.get_all_snapshots(filters={'snapshot-id': snapshot1.id}) - set([snap.id for snap in snapshots_by_id]).should.equal(set([snapshot1.id])) + snapshots_by_id = conn.get_all_snapshots( + filters={'snapshot-id': snapshot1.id}) + set([snap.id for snap in snapshots_by_id] + ).should.equal(set([snapshot1.id])) - snapshots_by_start_time = conn.get_all_snapshots(filters={'start-time': snapshot1.start_time}) - set([snap.start_time for snap in snapshots_by_start_time]).should.equal(set([snapshot1.start_time])) + snapshots_by_start_time = conn.get_all_snapshots( + filters={'start-time': snapshot1.start_time}) + set([snap.start_time for snap in snapshots_by_start_time] + ).should.equal(set([snapshot1.start_time])) - snapshots_by_volume_id = conn.get_all_snapshots(filters={'volume-id': volume1.id}) - set([snap.id for snap in snapshots_by_volume_id]).should.equal(set([snapshot1.id, snapshot2.id])) + snapshots_by_volume_id = conn.get_all_snapshots( + filters={'volume-id': volume1.id}) + set([snap.id for snap in snapshots_by_volume_id] + ).should.equal(set([snapshot1.id, snapshot2.id])) - snapshots_by_volume_size = conn.get_all_snapshots(filters={'volume-size': volume1.size}) - set([snap.id for snap in snapshots_by_volume_size]).should.equal(set([snapshot1.id, snapshot2.id])) + snapshots_by_volume_size = conn.get_all_snapshots( + filters={'volume-size': volume1.size}) + set([snap.id for snap in snapshots_by_volume_size] + ).should.equal(set([snapshot1.id, snapshot2.id])) - snapshots_by_tag_key = conn.get_all_snapshots(filters={'tag-key': 'testkey1'}) - set([snap.id for snap in snapshots_by_tag_key]).should.equal(set([snapshot1.id])) + snapshots_by_tag_key = conn.get_all_snapshots( + filters={'tag-key': 'testkey1'}) + set([snap.id for snap in snapshots_by_tag_key] + ).should.equal(set([snapshot1.id])) - snapshots_by_tag_value = conn.get_all_snapshots(filters={'tag-value': 'testvalue1'}) - set([snap.id for snap in snapshots_by_tag_value]).should.equal(set([snapshot1.id])) + snapshots_by_tag_value = conn.get_all_snapshots( + filters={'tag-value': 'testvalue1'}) + set([snap.id for snap in snapshots_by_tag_value] + ).should.equal(set([snapshot1.id])) - snapshots_by_tag = conn.get_all_snapshots(filters={'tag:testkey1': 'testvalue1'}) - set([snap.id for snap in snapshots_by_tag]).should.equal(set([snapshot1.id])) + snapshots_by_tag = conn.get_all_snapshots( + filters={'tag:testkey1': 'testvalue1'}) + set([snap.id for snap in snapshots_by_tag] + ).should.equal(set([snapshot1.id])) - snapshots_by_encrypted = conn.get_all_snapshots(filters={'encrypted': 'true'}) - set([snap.id for snap in snapshots_by_encrypted]).should.equal(set([snapshot3.id])) + snapshots_by_encrypted = conn.get_all_snapshots( + filters={'encrypted': 'true'}) + set([snap.id for snap in snapshots_by_encrypted] + ).should.equal(set([snapshot3.id])) @mock_ec2_deprecated @@ -331,7 +371,8 @@ def test_snapshot_attribute(): snapshot = volume.create_snapshot() # Baseline - attributes = conn.get_snapshot_attribute(snapshot.id, attribute='createVolumePermission') + attributes = conn.get_snapshot_attribute( + snapshot.id, attribute='createVolumePermission') attributes.name.should.equal('create_volume_permission') attributes.attrs.should.have.length_of(0) @@ -348,34 +389,42 @@ def test_snapshot_attribute(): # Add 'all' group and confirm with assert_raises(EC2ResponseError) as ex: - conn.modify_snapshot_attribute(**dict(ADD_GROUP_ARGS, **{'dry_run': True})) + conn.modify_snapshot_attribute( + **dict(ADD_GROUP_ARGS, **{'dry_run': True})) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifySnapshotAttribute operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifySnapshotAttribute operation: Request would have succeeded, but DryRun flag is set') conn.modify_snapshot_attribute(**ADD_GROUP_ARGS) - attributes = conn.get_snapshot_attribute(snapshot.id, attribute='createVolumePermission') + attributes = conn.get_snapshot_attribute( + snapshot.id, attribute='createVolumePermission') attributes.attrs['groups'].should.have.length_of(1) attributes.attrs['groups'].should.equal(['all']) # Add is idempotent - conn.modify_snapshot_attribute.when.called_with(**ADD_GROUP_ARGS).should_not.throw(EC2ResponseError) + conn.modify_snapshot_attribute.when.called_with( + **ADD_GROUP_ARGS).should_not.throw(EC2ResponseError) # Remove 'all' group and confirm with assert_raises(EC2ResponseError) as ex: - conn.modify_snapshot_attribute(**dict(REMOVE_GROUP_ARGS, **{'dry_run': True})) + conn.modify_snapshot_attribute( + **dict(REMOVE_GROUP_ARGS, **{'dry_run': True})) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifySnapshotAttribute operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifySnapshotAttribute operation: Request would have succeeded, but DryRun flag is set') conn.modify_snapshot_attribute(**REMOVE_GROUP_ARGS) - attributes = conn.get_snapshot_attribute(snapshot.id, attribute='createVolumePermission') + attributes = conn.get_snapshot_attribute( + snapshot.id, attribute='createVolumePermission') attributes.attrs.should.have.length_of(0) # Remove is idempotent - conn.modify_snapshot_attribute.when.called_with(**REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError) + conn.modify_snapshot_attribute.when.called_with( + **REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError) # Error: Add with group != 'all' with assert_raises(EC2ResponseError) as cm: @@ -428,7 +477,8 @@ def test_create_volume_from_snapshot(): snapshot = volume.create_snapshot('a test snapshot', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSnapshot operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateSnapshot operation: Request would have succeeded, but DryRun flag is set') snapshot = volume.create_snapshot('a test snapshot') snapshot.update() @@ -469,16 +519,19 @@ def test_modify_attribute_blockDeviceMapping(): instance = reservation.instances[0] with assert_raises(EC2ResponseError) as ex: - instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True}, dry_run=True) + instance.modify_attribute('blockDeviceMapping', { + '/dev/sda1': True}, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceAttribute operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyInstanceAttribute operation: Request would have succeeded, but DryRun flag is set') instance.modify_attribute('blockDeviceMapping', {'/dev/sda1': True}) instance = ec2_backends[conn.region.name].get_instance(instance.id) instance.block_device_mapping.should.have.key('/dev/sda1') - instance.block_device_mapping['/dev/sda1'].delete_on_termination.should.be(True) + instance.block_device_mapping[ + '/dev/sda1'].delete_on_termination.should.be(True) @mock_ec2_deprecated @@ -491,8 +544,10 @@ def test_volume_tag_escaping(): snapshot.add_tags({'key': ''}, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') - dict(conn.get_all_snapshots()[0].tags).should_not.be.equal({'key': ''}) + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + dict(conn.get_all_snapshots()[0].tags).should_not.be.equal( + {'key': ''}) snapshot.add_tags({'key': ''}) diff --git a/tests/test_ec2/test_elastic_ip_addresses.py b/tests/test_ec2/test_elastic_ip_addresses.py index f92c4df8b..2e1ae189a 100644 --- a/tests/test_ec2/test_elastic_ip_addresses.py +++ b/tests/test_ec2/test_elastic_ip_addresses.py @@ -24,7 +24,8 @@ def test_eip_allocate_classic(): standard = conn.allocate_address(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AllocateAddress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the AllocateAddress operation: Request would have succeeded, but DryRun flag is set') standard = conn.allocate_address() standard.should.be.a(boto.ec2.address.Address) @@ -36,7 +37,8 @@ def test_eip_allocate_classic(): standard.release(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ReleaseAddress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ReleaseAddress operation: Request would have succeeded, but DryRun flag is set') standard.release() standard.should_not.be.within(conn.get_all_addresses()) @@ -51,7 +53,8 @@ def test_eip_allocate_vpc(): vpc = conn.allocate_address(domain="vpc", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AllocateAddress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the AllocateAddress operation: Request would have succeeded, but DryRun flag is set') vpc = conn.allocate_address(domain="vpc") vpc.should.be.a(boto.ec2.address.Address) @@ -90,23 +93,28 @@ def test_eip_associate_classic(): cm.exception.request_id.should_not.be.none with assert_raises(EC2ResponseError) as ex: - conn.associate_address(instance_id=instance.id, public_ip=eip.public_ip, dry_run=True) + conn.associate_address(instance_id=instance.id, + public_ip=eip.public_ip, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AssociateAddress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the AssociateAddress operation: Request would have succeeded, but DryRun flag is set') conn.associate_address(instance_id=instance.id, public_ip=eip.public_ip) - eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + # no .update() on address ): + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] eip.instance_id.should.be.equal(instance.id) with assert_raises(EC2ResponseError) as ex: conn.disassociate_address(public_ip=eip.public_ip, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DisAssociateAddress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DisAssociateAddress operation: Request would have succeeded, but DryRun flag is set') conn.disassociate_address(public_ip=eip.public_ip) - eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + # no .update() on address ): + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] eip.instance_id.should.be.equal(u'') eip.release() eip.should_not.be.within(conn.get_all_addresses()) @@ -114,6 +122,7 @@ def test_eip_associate_classic(): instance.terminate() + @mock_ec2_deprecated def test_eip_associate_vpc(): """Associate/Disassociate EIP to VPC instance""" @@ -131,11 +140,14 @@ def test_eip_associate_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - conn.associate_address(instance_id=instance.id, allocation_id=eip.allocation_id) - eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + conn.associate_address(instance_id=instance.id, + allocation_id=eip.allocation_id) + # no .update() on address ): + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] eip.instance_id.should.be.equal(instance.id) conn.disassociate_address(association_id=eip.association_id) - eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + # no .update() on address ): + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] eip.instance_id.should.be.equal(u'') eip.association_id.should.be.none @@ -143,13 +155,15 @@ def test_eip_associate_vpc(): eip.release(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ReleaseAddress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ReleaseAddress operation: Request would have succeeded, but DryRun flag is set') eip.release() eip = None instance.terminate() + @mock_ec2 def test_eip_boto3_vpc_association(): """Associate EIP to VPC instance in a new subnet with boto3""" @@ -157,7 +171,7 @@ def test_eip_boto3_vpc_association(): client = boto3.client('ec2', region_name='us-west-1') vpc_res = client.create_vpc(CidrBlock='10.0.0.0/24') subnet_res = client.create_subnet( - VpcId=vpc_res['Vpc']['VpcId'], CidrBlock='10.0.0.0/24') + VpcId=vpc_res['Vpc']['VpcId'], CidrBlock='10.0.0.0/24') instance = service.create_instances(**{ 'InstanceType': 't2.micro', 'ImageId': 'ami-test', @@ -192,17 +206,21 @@ def test_eip_associate_network_interface(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - conn.associate_address(network_interface_id=eni.id, allocation_id=eip.allocation_id) - eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + conn.associate_address(network_interface_id=eni.id, + allocation_id=eip.allocation_id) + # no .update() on address ): + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] eip.network_interface_id.should.be.equal(eni.id) conn.disassociate_address(association_id=eip.association_id) - eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + # no .update() on address ): + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] eip.network_interface_id.should.be.equal(u'') eip.association_id.should.be.none eip.release() eip = None + @mock_ec2_deprecated def test_eip_reassociate(): """reassociate EIP""" @@ -219,12 +237,14 @@ def test_eip_reassociate(): # Different ID detects resource association with assert_raises(EC2ResponseError) as cm: - conn.associate_address(instance_id=instance2.id, public_ip=eip.public_ip, allow_reassociation=False) + conn.associate_address( + instance_id=instance2.id, public_ip=eip.public_ip, allow_reassociation=False) cm.exception.code.should.equal('Resource.AlreadyAssociated') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - conn.associate_address.when.called_with(instance_id=instance2.id, public_ip=eip.public_ip, allow_reassociation=True).should_not.throw(EC2ResponseError) + conn.associate_address.when.called_with( + instance_id=instance2.id, public_ip=eip.public_ip, allow_reassociation=True).should_not.throw(EC2ResponseError) eip.release() eip = None @@ -232,6 +252,7 @@ def test_eip_reassociate(): instance1.terminate() instance2.terminate() + @mock_ec2_deprecated def test_eip_reassociate_nic(): """reassociate EIP""" @@ -243,23 +264,28 @@ def test_eip_reassociate_nic(): eni2 = conn.create_network_interface(subnet.id) eip = conn.allocate_address() - conn.associate_address(network_interface_id=eni1.id, public_ip=eip.public_ip) + conn.associate_address(network_interface_id=eni1.id, + public_ip=eip.public_ip) # Same ID is idempotent - conn.associate_address(network_interface_id=eni1.id, public_ip=eip.public_ip) + conn.associate_address(network_interface_id=eni1.id, + public_ip=eip.public_ip) # Different ID detects resource association with assert_raises(EC2ResponseError) as cm: - conn.associate_address(network_interface_id=eni2.id, public_ip=eip.public_ip) + conn.associate_address( + network_interface_id=eni2.id, public_ip=eip.public_ip) cm.exception.code.should.equal('Resource.AlreadyAssociated') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - conn.associate_address.when.called_with(network_interface_id=eni2.id, public_ip=eip.public_ip, allow_reassociation=True).should_not.throw(EC2ResponseError) + conn.associate_address.when.called_with( + network_interface_id=eni2.id, public_ip=eip.public_ip, allow_reassociation=True).should_not.throw(EC2ResponseError) eip.release() eip = None + @mock_ec2_deprecated def test_eip_associate_invalid_args(): """Associate EIP, invalid args """ @@ -290,6 +316,7 @@ def test_eip_disassociate_bogus_association(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_eip_release_bogus_eip(): """Release bogus EIP""" @@ -334,7 +361,7 @@ def test_eip_describe(): number_of_classic_ips = 2 number_of_vpc_ips = 2 - #allocate some IPs + # allocate some IPs for _ in range(number_of_classic_ips): eips.append(conn.allocate_address()) for _ in range(number_of_vpc_ips): @@ -344,19 +371,22 @@ def test_eip_describe(): # Can we find each one individually? for eip in eips: if eip.allocation_id: - lookup_addresses = conn.get_all_addresses(allocation_ids=[eip.allocation_id]) + lookup_addresses = conn.get_all_addresses( + allocation_ids=[eip.allocation_id]) else: - lookup_addresses = conn.get_all_addresses(addresses=[eip.public_ip]) + lookup_addresses = conn.get_all_addresses( + addresses=[eip.public_ip]) len(lookup_addresses).should.be.equal(1) lookup_addresses[0].public_ip.should.be.equal(eip.public_ip) # Can we find first two when we search for them? - lookup_addresses = conn.get_all_addresses(addresses=[eips[0].public_ip, eips[1].public_ip]) + lookup_addresses = conn.get_all_addresses( + addresses=[eips[0].public_ip, eips[1].public_ip]) len(lookup_addresses).should.be.equal(2) lookup_addresses[0].public_ip.should.be.equal(eips[0].public_ip) lookup_addresses[1].public_ip.should.be.equal(eips[1].public_ip) - #Release all IPs + # Release all IPs for eip in eips: eip.release() len(conn.get_all_addresses()).should.be.equal(0) @@ -372,4 +402,3 @@ def test_eip_describe_none(): cm.exception.code.should.equal('InvalidAddress.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py index 9027e0448..4ec23b919 100644 --- a/tests/test_ec2/test_elastic_network_interfaces.py +++ b/tests/test_ec2/test_elastic_network_interfaces.py @@ -27,7 +27,8 @@ def test_elastic_network_interfaces(): eni = conn.create_network_interface(subnet.id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateNetworkInterface operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateNetworkInterface operation: Request would have succeeded, but DryRun flag is set') eni = conn.create_network_interface(subnet.id) @@ -41,7 +42,8 @@ def test_elastic_network_interfaces(): conn.delete_network_interface(eni.id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteNetworkInterface operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeleteNetworkInterface operation: Request would have succeeded, but DryRun flag is set') conn.delete_network_interface(eni.id) @@ -89,16 +91,20 @@ def test_elastic_network_interfaces_with_groups(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - security_group1 = conn.create_security_group('test security group #1', 'this is a test security group') - security_group2 = conn.create_security_group('test security group #2', 'this is a test security group') - conn.create_network_interface(subnet.id, groups=[security_group1.id, security_group2.id]) + security_group1 = conn.create_security_group( + 'test security group #1', 'this is a test security group') + security_group2 = conn.create_security_group( + 'test security group #2', 'this is a test security group') + conn.create_network_interface( + subnet.id, groups=[security_group1.id, security_group2.id]) all_enis = conn.get_all_network_interfaces() all_enis.should.have.length_of(1) eni = all_enis[0] eni.groups.should.have.length_of(2) - set([group.id for group in eni.groups]).should.equal(set([security_group1.id, security_group2.id])) + set([group.id for group in eni.groups]).should.equal( + set([security_group1.id, security_group2.id])) @requires_boto_gte("2.12.0") @@ -107,8 +113,10 @@ def test_elastic_network_interfaces_modify_attribute(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - security_group1 = conn.create_security_group('test security group #1', 'this is a test security group') - security_group2 = conn.create_security_group('test security group #2', 'this is a test security group') + security_group1 = conn.create_security_group( + 'test security group #1', 'this is a test security group') + security_group2 = conn.create_security_group( + 'test security group #2', 'this is a test security group') conn.create_network_interface(subnet.id, groups=[security_group1.id]) all_enis = conn.get_all_network_interfaces() @@ -119,12 +127,15 @@ def test_elastic_network_interfaces_modify_attribute(): eni.groups[0].id.should.equal(security_group1.id) with assert_raises(EC2ResponseError) as ex: - conn.modify_network_interface_attribute(eni.id, 'groupset', [security_group2.id], dry_run=True) + conn.modify_network_interface_attribute( + eni.id, 'groupset', [security_group2.id], dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyNetworkInterface operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyNetworkInterface operation: Request would have succeeded, but DryRun flag is set') - conn.modify_network_interface_attribute(eni.id, 'groupset', [security_group2.id]) + conn.modify_network_interface_attribute( + eni.id, 'groupset', [security_group2.id]) all_enis = conn.get_all_network_interfaces() all_enis.should.have.length_of(1) @@ -140,11 +151,15 @@ def test_elastic_network_interfaces_filtering(): vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - security_group1 = conn.create_security_group('test security group #1', 'this is a test security group') - security_group2 = conn.create_security_group('test security group #2', 'this is a test security group') + security_group1 = conn.create_security_group( + 'test security group #1', 'this is a test security group') + security_group2 = conn.create_security_group( + 'test security group #2', 'this is a test security group') - eni1 = conn.create_network_interface(subnet.id, groups=[security_group1.id, security_group2.id]) - eni2 = conn.create_network_interface(subnet.id, groups=[security_group1.id]) + eni1 = conn.create_network_interface( + subnet.id, groups=[security_group1.id, security_group2.id]) + eni2 = conn.create_network_interface( + subnet.id, groups=[security_group1.id]) eni3 = conn.create_network_interface(subnet.id) all_enis = conn.get_all_network_interfaces() @@ -156,22 +171,26 @@ def test_elastic_network_interfaces_filtering(): set([eni.id for eni in enis_by_id]).should.equal(set([eni1.id])) # Filter by ENI ID - enis_by_id = conn.get_all_network_interfaces(filters={'network-interface-id': eni1.id}) + enis_by_id = conn.get_all_network_interfaces( + filters={'network-interface-id': eni1.id}) enis_by_id.should.have.length_of(1) set([eni.id for eni in enis_by_id]).should.equal(set([eni1.id])) # Filter by Security Group - enis_by_group = conn.get_all_network_interfaces(filters={'group-id': security_group1.id}) + enis_by_group = conn.get_all_network_interfaces( + filters={'group-id': security_group1.id}) enis_by_group.should.have.length_of(2) set([eni.id for eni in enis_by_group]).should.equal(set([eni1.id, eni2.id])) # Filter by ENI ID and Security Group - enis_by_group = conn.get_all_network_interfaces(filters={'network-interface-id': eni1.id, 'group-id': security_group1.id}) + enis_by_group = conn.get_all_network_interfaces( + filters={'network-interface-id': eni1.id, 'group-id': security_group1.id}) enis_by_group.should.have.length_of(1) set([eni.id for eni in enis_by_group]).should.equal(set([eni1.id])) # Unsupported filter - conn.get_all_network_interfaces.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) + conn.get_all_network_interfaces.when.called_with( + filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) @mock_ec2 @@ -180,15 +199,19 @@ def test_elastic_network_interfaces_get_by_tag_name(): ec2_client = boto3.client('ec2', region_name='us-west-2') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') - eni1 = ec2.create_network_interface(SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') + eni1 = ec2.create_network_interface( + SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') with assert_raises(ClientError) as ex: eni1.create_tags(Tags=[{'Key': 'Name', 'Value': 'eni1'}], DryRun=True) ex.exception.response['Error']['Code'].should.equal('DryRunOperation') - ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) - ex.exception.response['Error']['Message'].should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.response['ResponseMetadata'][ + 'HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') eni1.create_tags(Tags=[{'Key': 'Name', 'Value': 'eni1'}]) @@ -211,9 +234,11 @@ def test_elastic_network_interfaces_get_by_private_ip(): ec2_client = boto3.client('ec2', region_name='us-west-2') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') - eni1 = ec2.create_network_interface(SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') + eni1 = ec2.create_network_interface( + SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') # The status of the new interface should be 'available' waiter = ec2_client.get_waiter('network_interface_available') @@ -242,9 +267,11 @@ def test_elastic_network_interfaces_get_by_vpc_id(): ec2_client = boto3.client('ec2', region_name='us-west-2') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') - eni1 = ec2.create_network_interface(SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') + eni1 = ec2.create_network_interface( + SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') # The status of the new interface should be 'available' waiter = ec2_client.get_waiter('network_interface_available') @@ -265,9 +292,11 @@ def test_elastic_network_interfaces_get_by_subnet_id(): ec2_client = boto3.client('ec2', region_name='us-west-2') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') - eni1 = ec2.create_network_interface(SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') + eni1 = ec2.create_network_interface( + SubnetId=subnet.id, PrivateIpAddress='10.0.10.5') # The status of the new interface should be 'available' waiter = ec2_client.get_waiter('network_interface_available') @@ -297,5 +326,6 @@ def test_elastic_network_interfaces_cloudformation(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() - cfn_eni = [resource for resource in resources if resource.resource_type == 'AWS::EC2::NetworkInterface'][0] + cfn_eni = [resource for resource in resources if resource.resource_type == + 'AWS::EC2::NetworkInterface'][0] cfn_eni.physical_resource_id.should.equal(eni.id) diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index b6601e87f..49020555b 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -45,7 +45,8 @@ def test_instance_launch_and_terminate(): reservation = conn.run_instances('ami-1234abcd', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RunInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the RunInstance operation: Request would have succeeded, but DryRun flag is set') reservation = conn.run_instances('ami-1234abcd') reservation.should.be.a(Reservation) @@ -66,7 +67,8 @@ def test_instance_launch_and_terminate(): instance.placement.should.equal('us-east-1a') root_device_name = instance.root_device_name - instance.block_device_mapping[root_device_name].status.should.equal('in-use') + instance.block_device_mapping[ + root_device_name].status.should.equal('in-use') volume_id = instance.block_device_mapping[root_device_name].volume_id volume_id.should.match(r'vol-\w+') @@ -78,7 +80,8 @@ def test_instance_launch_and_terminate(): conn.terminate_instances([instance.id], dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the TerminateInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the TerminateInstance operation: Request would have succeeded, but DryRun flag is set') conn.terminate_instances([instance.id]) @@ -90,7 +93,8 @@ def test_instance_launch_and_terminate(): @mock_ec2_deprecated def test_terminate_empty_instances(): conn = boto.connect_ec2('the_key', 'the_secret') - conn.terminate_instances.when.called_with([]).should.throw(EC2ResponseError) + conn.terminate_instances.when.called_with( + []).should.throw(EC2ResponseError) @freeze_time("2014-01-01 05:00:00") @@ -117,8 +121,10 @@ def test_instance_attach_volume(): for v in conn.get_all_volumes(volume_ids=[instance.block_device_mapping['/dev/sdc1'].volume_id]): v.attach_data.instance_id.should.equal(instance.id) - v.attach_data.attach_time.should.equal(instance.launch_time) # can do due to freeze_time decorator. - v.create_time.should.equal(instance.launch_time) # can do due to freeze_time decorator. + # can do due to freeze_time decorator. + v.attach_data.attach_time.should.equal(instance.launch_time) + # can do due to freeze_time decorator. + v.create_time.should.equal(instance.launch_time) v.region.name.should.equal(instance.region.name) v.status.should.equal('in-use') @@ -135,7 +141,8 @@ def test_get_instances_by_id(): reservation.instances.should.have.length_of(1) reservation.instances[0].id.should.equal(instance1.id) - reservations = conn.get_all_instances(instance_ids=[instance1.id, instance2.id]) + reservations = conn.get_all_instances( + instance_ids=[instance1.id, instance2.id]) reservations.should.have.length_of(1) reservation = reservations[0] reservation.instances.should.have.length_of(2) @@ -158,25 +165,31 @@ def test_get_instances_filtering_by_state(): conn.terminate_instances([instance1.id]) - reservations = conn.get_all_instances(filters={'instance-state-name': 'running'}) + reservations = conn.get_all_instances( + filters={'instance-state-name': 'running'}) reservations.should.have.length_of(1) - # Since we terminated instance1, only instance2 and instance3 should be returned + # Since we terminated instance1, only instance2 and instance3 should be + # returned instance_ids = [instance.id for instance in reservations[0].instances] set(instance_ids).should.equal(set([instance2.id, instance3.id])) - reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'running'}) + reservations = conn.get_all_instances( + [instance2.id], filters={'instance-state-name': 'running'}) reservations.should.have.length_of(1) instance_ids = [instance.id for instance in reservations[0].instances] instance_ids.should.equal([instance2.id]) - reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'terminated'}) + reservations = conn.get_all_instances( + [instance2.id], filters={'instance-state-name': 'terminated'}) list(reservations).should.equal([]) # get_all_instances should still return all 3 reservations = conn.get_all_instances() reservations[0].instances.should.have.length_of(3) - conn.get_all_instances.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) + conn.get_all_instances.when.called_with( + filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) + @mock_ec2_deprecated def test_get_instances_filtering_by_instance_id(): @@ -184,16 +197,19 @@ def test_get_instances_filtering_by_instance_id(): reservation = conn.run_instances('ami-1234abcd', min_count=3) instance1, instance2, instance3 = reservation.instances - reservations = conn.get_all_instances(filters={'instance-id': instance1.id}) + reservations = conn.get_all_instances( + filters={'instance-id': instance1.id}) # get_all_instances should return just instance1 reservations[0].instances.should.have.length_of(1) reservations[0].instances[0].id.should.equal(instance1.id) - reservations = conn.get_all_instances(filters={'instance-id': [instance1.id, instance2.id]}) + reservations = conn.get_all_instances( + filters={'instance-id': [instance1.id, instance2.id]}) # get_all_instances should return two reservations[0].instances.should.have.length_of(2) - reservations = conn.get_all_instances(filters={'instance-id': 'non-existing-id'}) + reservations = conn.get_all_instances( + filters={'instance-id': 'non-existing-id'}) reservations.should.have.length_of(0) @@ -207,22 +223,25 @@ def test_get_instances_filtering_by_instance_type(): reservation3 = conn.run_instances('ami-1234abcd', instance_type='t1.micro') instance3 = reservation3.instances[0] - reservations = conn.get_all_instances(filters={'instance-type': 'm1.small'}) + reservations = conn.get_all_instances( + filters={'instance-type': 'm1.small'}) # get_all_instances should return instance1,2 reservations.should.have.length_of(2) reservations[0].instances.should.have.length_of(1) reservations[1].instances.should.have.length_of(1) - instance_ids = [ reservations[0].instances[0].id, - reservations[1].instances[0].id ] + instance_ids = [reservations[0].instances[0].id, + reservations[1].instances[0].id] set(instance_ids).should.equal(set([instance1.id, instance2.id])) - reservations = conn.get_all_instances(filters={'instance-type': 't1.micro'}) + reservations = conn.get_all_instances( + filters={'instance-type': 't1.micro'}) # get_all_instances should return one reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(1) reservations[0].instances[0].id.should.equal(instance3.id) - reservations = conn.get_all_instances(filters={'instance-type': ['t1.micro', 'm1.small']}) + reservations = conn.get_all_instances( + filters={'instance-type': ['t1.micro', 'm1.small']}) reservations.should.have.length_of(3) reservations[0].instances.should.have.length_of(1) reservations[1].instances.should.have.length_of(1) @@ -231,13 +250,15 @@ def test_get_instances_filtering_by_instance_type(): reservations[0].instances[0].id, reservations[1].instances[0].id, reservations[2].instances[0].id, - ] - set(instance_ids).should.equal(set([instance1.id, instance2.id, instance3.id])) + ] + set(instance_ids).should.equal( + set([instance1.id, instance2.id, instance3.id])) reservations = conn.get_all_instances(filters={'instance-type': 'bogus'}) - #bogus instance-type should return none + # bogus instance-type should return none reservations.should.have.length_of(0) + @mock_ec2_deprecated def test_get_instances_filtering_by_reason_code(): conn = boto.connect_ec2() @@ -246,10 +267,12 @@ def test_get_instances_filtering_by_reason_code(): instance1.stop() instance2.terminate() - reservations = conn.get_all_instances(filters={'state-reason-code': 'Client.UserInitiatedShutdown'}) + reservations = conn.get_all_instances( + filters={'state-reason-code': 'Client.UserInitiatedShutdown'}) # get_all_instances should return instance1 and instance2 reservations[0].instances.should.have.length_of(2) - set([instance1.id, instance2.id]).should.equal(set([i.id for i in reservations[0].instances])) + set([instance1.id, instance2.id]).should.equal( + set([i.id for i in reservations[0].instances])) reservations = conn.get_all_instances(filters={'state-reason-code': ''}) # get_all_instances should return instance 3 @@ -262,10 +285,13 @@ def test_get_instances_filtering_by_source_dest_check(): conn = boto.connect_ec2() reservation = conn.run_instances('ami-1234abcd', min_count=2) instance1, instance2 = reservation.instances - conn.modify_instance_attribute(instance1.id, attribute='sourceDestCheck', value=False) + conn.modify_instance_attribute( + instance1.id, attribute='sourceDestCheck', value=False) - source_dest_check_false = conn.get_all_instances(filters={'source-dest-check': 'false'}) - source_dest_check_true = conn.get_all_instances(filters={'source-dest-check': 'true'}) + source_dest_check_false = conn.get_all_instances( + filters={'source-dest-check': 'false'}) + source_dest_check_true = conn.get_all_instances( + filters={'source-dest-check': 'true'}) source_dest_check_false[0].instances.should.have.length_of(1) source_dest_check_false[0].instances[0].id.should.equal(instance1.id) @@ -279,12 +305,14 @@ def test_get_instances_filtering_by_vpc_id(): conn = boto.connect_vpc('the_key', 'the_secret') vpc1 = conn.create_vpc("10.0.0.0/16") subnet1 = conn.create_subnet(vpc1.id, "10.0.0.0/27") - reservation1 = conn.run_instances('ami-1234abcd', min_count=1, subnet_id=subnet1.id) + reservation1 = conn.run_instances( + 'ami-1234abcd', min_count=1, subnet_id=subnet1.id) instance1 = reservation1.instances[0] vpc2 = conn.create_vpc("10.1.0.0/16") subnet2 = conn.create_subnet(vpc2.id, "10.1.0.0/27") - reservation2 = conn.run_instances('ami-1234abcd', min_count=1, subnet_id=subnet2.id) + reservation2 = conn.run_instances( + 'ami-1234abcd', min_count=1, subnet_id=subnet2.id) instance2 = reservation2.instances[0] reservations1 = conn.get_all_instances(filters={'vpc-id': vpc1.id}) @@ -320,31 +348,35 @@ def test_get_instances_filtering_by_tag(): instance2.add_tag('tag2', 'wrong value') instance3.add_tag('tag2', 'value2') - reservations = conn.get_all_instances(filters={'tag:tag0' : 'value0'}) + reservations = conn.get_all_instances(filters={'tag:tag0': 'value0'}) # get_all_instances should return no instances reservations.should.have.length_of(0) - reservations = conn.get_all_instances(filters={'tag:tag1' : 'value1'}) + reservations = conn.get_all_instances(filters={'tag:tag1': 'value1'}) # get_all_instances should return both instances with this tag value reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(2) reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance2.id) - reservations = conn.get_all_instances(filters={'tag:tag1' : 'value1', 'tag:tag2' : 'value2'}) + reservations = conn.get_all_instances( + filters={'tag:tag1': 'value1', 'tag:tag2': 'value2'}) # get_all_instances should return the instance with both tag values reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(1) reservations[0].instances[0].id.should.equal(instance1.id) - reservations = conn.get_all_instances(filters={'tag:tag1' : 'value1', 'tag:tag2' : 'value2'}) + reservations = conn.get_all_instances( + filters={'tag:tag1': 'value1', 'tag:tag2': 'value2'}) # get_all_instances should return the instance with both tag values reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(1) reservations[0].instances[0].id.should.equal(instance1.id) - reservations = conn.get_all_instances(filters={'tag:tag2' : ['value2', 'bogus']}) - # get_all_instances should return both instances with one of the acceptable tag values + reservations = conn.get_all_instances( + filters={'tag:tag2': ['value2', 'bogus']}) + # get_all_instances should return both instances with one of the + # acceptable tag values reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(2) reservations[0].instances[0].id.should.equal(instance1.id) @@ -362,32 +394,37 @@ def test_get_instances_filtering_by_tag_value(): instance2.add_tag('tag2', 'wrong value') instance3.add_tag('tag2', 'value2') - reservations = conn.get_all_instances(filters={'tag-value' : 'value0'}) + reservations = conn.get_all_instances(filters={'tag-value': 'value0'}) # get_all_instances should return no instances reservations.should.have.length_of(0) - reservations = conn.get_all_instances(filters={'tag-value' : 'value1'}) + reservations = conn.get_all_instances(filters={'tag-value': 'value1'}) # get_all_instances should return both instances with this tag value reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(2) reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance2.id) - reservations = conn.get_all_instances(filters={'tag-value' : ['value2', 'value1']}) - # get_all_instances should return both instances with one of the acceptable tag values + reservations = conn.get_all_instances( + filters={'tag-value': ['value2', 'value1']}) + # get_all_instances should return both instances with one of the + # acceptable tag values reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(3) reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance2.id) reservations[0].instances[2].id.should.equal(instance3.id) - reservations = conn.get_all_instances(filters={'tag-value' : ['value2', 'bogus']}) - # get_all_instances should return both instances with one of the acceptable tag values + reservations = conn.get_all_instances( + filters={'tag-value': ['value2', 'bogus']}) + # get_all_instances should return both instances with one of the + # acceptable tag values reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(2) reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance3.id) + @mock_ec2_deprecated def test_get_instances_filtering_by_tag_name(): conn = boto.connect_ec2() @@ -399,25 +436,28 @@ def test_get_instances_filtering_by_tag_name(): instance2.add_tag('tag2X') instance3.add_tag('tag3') - reservations = conn.get_all_instances(filters={'tag-key' : 'tagX'}) + reservations = conn.get_all_instances(filters={'tag-key': 'tagX'}) # get_all_instances should return no instances reservations.should.have.length_of(0) - reservations = conn.get_all_instances(filters={'tag-key' : 'tag1'}) + reservations = conn.get_all_instances(filters={'tag-key': 'tag1'}) # get_all_instances should return both instances with this tag value reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(2) reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance2.id) - reservations = conn.get_all_instances(filters={'tag-key' : ['tag1', 'tag3']}) - # get_all_instances should return both instances with one of the acceptable tag values + reservations = conn.get_all_instances( + filters={'tag-key': ['tag1', 'tag3']}) + # get_all_instances should return both instances with one of the + # acceptable tag values reservations.should.have.length_of(1) reservations[0].instances.should.have.length_of(3) reservations[0].instances[0].id.should.equal(instance1.id) reservations[0].instances[1].id.should.equal(instance2.id) reservations[0].instances[2].id.should.equal(instance3.id) + @mock_ec2_deprecated def test_instance_start_and_stop(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -431,7 +471,8 @@ def test_instance_start_and_stop(): stopped_instances = conn.stop_instances(instance_ids, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the StopInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the StopInstance operation: Request would have succeeded, but DryRun flag is set') stopped_instances = conn.stop_instances(instance_ids) @@ -439,10 +480,12 @@ def test_instance_start_and_stop(): instance.state.should.equal('stopping') with assert_raises(EC2ResponseError) as ex: - started_instances = conn.start_instances([instances[0].id], dry_run=True) + started_instances = conn.start_instances( + [instances[0].id], dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the StartInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the StartInstance operation: Request would have succeeded, but DryRun flag is set') started_instances = conn.start_instances([instances[0].id]) started_instances[0].state.should.equal('pending') @@ -458,7 +501,8 @@ def test_instance_reboot(): instance.reboot(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RebootInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the RebootInstance operation: Request would have succeeded, but DryRun flag is set') instance.reboot() instance.state.should.equal('pending') @@ -474,7 +518,8 @@ def test_instance_attribute_instance_type(): instance.modify_attribute("instanceType", "m1.small", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceType operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyInstanceType operation: Request would have succeeded, but DryRun flag is set') instance.modify_attribute("instanceType", "m1.small") @@ -482,6 +527,7 @@ def test_instance_attribute_instance_type(): instance_attribute.should.be.a(InstanceAttribute) instance_attribute.get('instanceType').should.equal("m1.small") + @mock_ec2_deprecated def test_modify_instance_attribute_security_groups(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -495,7 +541,8 @@ def test_modify_instance_attribute_security_groups(): instance.modify_attribute("groupSet", [sg_id, sg_id2], dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceSecurityGroups operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyInstanceSecurityGroups operation: Request would have succeeded, but DryRun flag is set') instance.modify_attribute("groupSet", [sg_id, sg_id2]) @@ -513,10 +560,12 @@ def test_instance_attribute_user_data(): instance = reservation.instances[0] with assert_raises(EC2ResponseError) as ex: - instance.modify_attribute("userData", "this is my user data", dry_run=True) + instance.modify_attribute( + "userData", "this is my user data", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyUserData operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyUserData operation: Request would have succeeded, but DryRun flag is set') instance.modify_attribute("userData", "this is my user data") @@ -544,7 +593,8 @@ def test_instance_attribute_source_dest_check(): instance.modify_attribute("sourceDestCheck", False, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifySourceDestCheck operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifySourceDestCheck operation: Request would have succeeded, but DryRun flag is set') instance.modify_attribute("sourceDestCheck", False) @@ -585,10 +635,12 @@ def test_run_instance_with_security_group_name(): conn = boto.connect_ec2('the_key', 'the_secret') with assert_raises(EC2ResponseError) as ex: - group = conn.create_security_group('group1', "some description", dry_run=True) + group = conn.create_security_group( + 'group1', "some description", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSecurityGroup operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateSecurityGroup operation: Request would have succeeded, but DryRun flag is set') group = conn.create_security_group('group1', "some description") @@ -658,14 +710,16 @@ def test_run_instance_with_nic_autocreated(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - security_group1 = conn.create_security_group('test security group #1', 'this is a test security group') - security_group2 = conn.create_security_group('test security group #2', 'this is a test security group') + security_group1 = conn.create_security_group( + 'test security group #1', 'this is a test security group') + security_group2 = conn.create_security_group( + 'test security group #2', 'this is a test security group') private_ip = "54.0.0.1" reservation = conn.run_instances('ami-1234abcd', subnet_id=subnet.id, - security_groups=[security_group1.name], - security_group_ids=[security_group2.id], - private_ip_address=private_ip) + security_groups=[security_group1.name], + security_group_ids=[security_group2.id], + private_ip_address=private_ip) instance = reservation.instances[0] all_enis = conn.get_all_network_interfaces() @@ -677,11 +731,13 @@ def test_run_instance_with_nic_autocreated(): instance.subnet_id.should.equal(subnet.id) instance.groups.should.have.length_of(2) - set([group.id for group in instance.groups]).should.equal(set([security_group1.id,security_group2.id])) + set([group.id for group in instance.groups]).should.equal( + set([security_group1.id, security_group2.id])) eni.subnet_id.should.equal(subnet.id) eni.groups.should.have.length_of(2) - set([group.id for group in eni.groups]).should.equal(set([security_group1.id,security_group2.id])) + set([group.id for group in eni.groups]).should.equal( + set([security_group1.id, security_group2.id])) eni.private_ip_addresses.should.have.length_of(1) eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip) @@ -691,20 +747,24 @@ def test_run_instance_with_nic_preexisting(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - security_group1 = conn.create_security_group('test security group #1', 'this is a test security group') - security_group2 = conn.create_security_group('test security group #2', 'this is a test security group') + security_group1 = conn.create_security_group( + 'test security group #1', 'this is a test security group') + security_group2 = conn.create_security_group( + 'test security group #2', 'this is a test security group') private_ip = "54.0.0.1" - eni = conn.create_network_interface(subnet.id, private_ip, groups=[security_group1.id]) + eni = conn.create_network_interface( + subnet.id, private_ip, groups=[security_group1.id]) # Boto requires NetworkInterfaceCollection of NetworkInterfaceSpecifications... # annoying, but generates the desired querystring. from boto.ec2.networkinterface import NetworkInterfaceSpecification, NetworkInterfaceCollection - interface = NetworkInterfaceSpecification(network_interface_id=eni.id, device_index=0) + interface = NetworkInterfaceSpecification( + network_interface_id=eni.id, device_index=0) interfaces = NetworkInterfaceCollection(interface) # end Boto objects reservation = conn.run_instances('ami-1234abcd', network_interfaces=interfaces, - security_group_ids=[security_group2.id]) + security_group_ids=[security_group2.id]) instance = reservation.instances[0] instance.subnet_id.should.equal(subnet.id) @@ -718,9 +778,11 @@ def test_run_instance_with_nic_preexisting(): instance_eni.subnet_id.should.equal(subnet.id) instance_eni.groups.should.have.length_of(2) - set([group.id for group in instance_eni.groups]).should.equal(set([security_group1.id,security_group2.id])) + set([group.id for group in instance_eni.groups]).should.equal( + set([security_group1.id, security_group2.id])) instance_eni.private_ip_addresses.should.have.length_of(1) - instance_eni.private_ip_addresses[0].private_ip_address.should.equal(private_ip) + instance_eni.private_ip_addresses[ + 0].private_ip_address.should.equal(private_ip) @requires_boto_gte("2.32.0") @@ -730,10 +792,13 @@ def test_instance_with_nic_attach_detach(): vpc = conn.create_vpc("10.0.0.0/16") subnet = conn.create_subnet(vpc.id, "10.0.0.0/18") - security_group1 = conn.create_security_group('test security group #1', 'this is a test security group') - security_group2 = conn.create_security_group('test security group #2', 'this is a test security group') + security_group1 = conn.create_security_group( + 'test security group #1', 'this is a test security group') + security_group2 = conn.create_security_group( + 'test security group #2', 'this is a test security group') - reservation = conn.run_instances('ami-1234abcd', security_group_ids=[security_group1.id]) + reservation = conn.run_instances( + 'ami-1234abcd', security_group_ids=[security_group1.id]) instance = reservation.instances[0] eni = conn.create_network_interface(subnet.id, groups=[security_group2.id]) @@ -742,14 +807,17 @@ def test_instance_with_nic_attach_detach(): instance.interfaces.should.have.length_of(1) eni.groups.should.have.length_of(1) - set([group.id for group in eni.groups]).should.equal(set([security_group2.id])) + set([group.id for group in eni.groups]).should.equal( + set([security_group2.id])) # Attach with assert_raises(EC2ResponseError) as ex: - conn.attach_network_interface(eni.id, instance.id, device_index=1, dry_run=True) + conn.attach_network_interface( + eni.id, instance.id, device_index=1, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AttachNetworkInterface operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the AttachNetworkInterface operation: Request would have succeeded, but DryRun flag is set') conn.attach_network_interface(eni.id, instance.id, device_index=1) @@ -759,18 +827,22 @@ def test_instance_with_nic_attach_detach(): instance_eni = instance.interfaces[1] instance_eni.id.should.equal(eni.id) instance_eni.groups.should.have.length_of(2) - set([group.id for group in instance_eni.groups]).should.equal(set([security_group1.id,security_group2.id])) + set([group.id for group in instance_eni.groups]).should.equal( + set([security_group1.id, security_group2.id])) - eni = conn.get_all_network_interfaces(filters={'network-interface-id': eni.id})[0] + eni = conn.get_all_network_interfaces( + filters={'network-interface-id': eni.id})[0] eni.groups.should.have.length_of(2) - set([group.id for group in eni.groups]).should.equal(set([security_group1.id,security_group2.id])) + set([group.id for group in eni.groups]).should.equal( + set([security_group1.id, security_group2.id])) # Detach with assert_raises(EC2ResponseError) as ex: conn.detach_network_interface(instance_eni.attachment.id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DetachNetworkInterface operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DetachNetworkInterface operation: Request would have succeeded, but DryRun flag is set') conn.detach_network_interface(instance_eni.attachment.id) @@ -778,9 +850,11 @@ def test_instance_with_nic_attach_detach(): instance.update() instance.interfaces.should.have.length_of(1) - eni = conn.get_all_network_interfaces(filters={'network-interface-id': eni.id})[0] + eni = conn.get_all_network_interfaces( + filters={'network-interface-id': eni.id})[0] eni.groups.should.have.length_of(1) - set([group.id for group in eni.groups]).should.equal(set([security_group2.id])) + set([group.id for group in eni.groups]).should.equal( + set([security_group2.id])) # Detach with invalid attachment ID with assert_raises(EC2ResponseError) as cm: @@ -851,6 +925,7 @@ def test_describe_instance_status_with_instance_filter(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @requires_boto_gte("2.32.0") @mock_ec2_deprecated def test_describe_instance_status_with_non_running_instances(): @@ -877,6 +952,7 @@ def test_describe_instance_status_with_non_running_instances(): status3 = next((s for s in all_status if s.id == instance3.id), None) status3.state_name.should.equal('running') + @mock_ec2_deprecated def test_get_instance_by_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -887,12 +963,15 @@ def test_get_instance_by_security_group(): security_group = conn.create_security_group('test', 'test') with assert_raises(EC2ResponseError) as ex: - conn.modify_instance_attribute(instance.id, "groupSet", [security_group.id], dry_run=True) + conn.modify_instance_attribute(instance.id, "groupSet", [ + security_group.id], dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ModifyInstanceSecurityGroups operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ModifyInstanceSecurityGroups operation: Request would have succeeded, but DryRun flag is set') - conn.modify_instance_attribute(instance.id, "groupSet", [security_group.id]) + conn.modify_instance_attribute( + instance.id, "groupSet", [security_group.id]) security_group_instances = security_group.instances() diff --git a/tests/test_ec2/test_internet_gateways.py b/tests/test_ec2/test_internet_gateways.py index fe5e4945d..5842621cd 100644 --- a/tests/test_ec2/test_internet_gateways.py +++ b/tests/test_ec2/test_internet_gateways.py @@ -13,9 +13,10 @@ import sure # noqa from moto import mock_ec2_deprecated -VPC_CIDR="10.0.0.0/16" -BAD_VPC="vpc-deadbeef" -BAD_IGW="igw-deadbeef" +VPC_CIDR = "10.0.0.0/16" +BAD_VPC = "vpc-deadbeef" +BAD_IGW = "igw-deadbeef" + @mock_ec2_deprecated def test_igw_create(): @@ -28,7 +29,8 @@ def test_igw_create(): igw = conn.create_internet_gateway(dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateInternetGateway operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateInternetGateway operation: Request would have succeeded, but DryRun flag is set') igw = conn.create_internet_gateway() conn.get_all_internet_gateways().should.have.length_of(1) @@ -37,6 +39,7 @@ def test_igw_create(): igw = conn.get_all_internet_gateways()[0] igw.attachments.should.have.length_of(0) + @mock_ec2_deprecated def test_igw_attach(): """ internet gateway attach """ @@ -48,13 +51,15 @@ def test_igw_attach(): conn.attach_internet_gateway(igw.id, vpc.id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the AttachInternetGateway operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the AttachInternetGateway operation: Request would have succeeded, but DryRun flag is set') conn.attach_internet_gateway(igw.id, vpc.id) igw = conn.get_all_internet_gateways()[0] igw.attachments[0].vpc_id.should.be.equal(vpc.id) + @mock_ec2_deprecated def test_igw_attach_bad_vpc(): """ internet gateway fail to attach w/ bad vpc """ @@ -67,6 +72,7 @@ def test_igw_attach_bad_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_igw_attach_twice(): """ internet gateway fail to attach twice """ @@ -82,6 +88,7 @@ def test_igw_attach_twice(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_igw_detach(): """ internet gateway detach""" @@ -94,12 +101,14 @@ def test_igw_detach(): conn.detach_internet_gateway(igw.id, vpc.id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DetachInternetGateway operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DetachInternetGateway operation: Request would have succeeded, but DryRun flag is set') conn.detach_internet_gateway(igw.id, vpc.id) igw = conn.get_all_internet_gateways()[0] igw.attachments.should.have.length_of(0) + @mock_ec2_deprecated def test_igw_detach_wrong_vpc(): """ internet gateway fail to detach w/ wrong vpc """ @@ -115,6 +124,7 @@ def test_igw_detach_wrong_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_igw_detach_invalid_vpc(): """ internet gateway fail to detach w/ invalid vpc """ @@ -129,6 +139,7 @@ def test_igw_detach_invalid_vpc(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_igw_detach_unattached(): """ internet gateway fail to detach unattached """ @@ -142,6 +153,7 @@ def test_igw_detach_unattached(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_igw_delete(): """ internet gateway delete""" @@ -155,11 +167,13 @@ def test_igw_delete(): conn.delete_internet_gateway(igw.id, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteInternetGateway operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeleteInternetGateway operation: Request would have succeeded, but DryRun flag is set') conn.delete_internet_gateway(igw.id) conn.get_all_internet_gateways().should.have.length_of(0) + @mock_ec2_deprecated def test_igw_delete_attached(): """ internet gateway fail to delete attached """ @@ -174,6 +188,7 @@ def test_igw_delete_attached(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + @mock_ec2_deprecated def test_igw_desribe(): """ internet gateway fetch by id """ @@ -182,6 +197,7 @@ def test_igw_desribe(): igw_by_search = conn.get_all_internet_gateways([igw.id])[0] igw.id.should.equal(igw_by_search.id) + @mock_ec2_deprecated def test_igw_desribe_bad_id(): """ internet gateway fail to fetch by bad id """ @@ -203,7 +219,8 @@ def test_igw_filter_by_vpc_id(): vpc = conn.create_vpc(VPC_CIDR) conn.attach_internet_gateway(igw1.id, vpc.id) - result = conn.get_all_internet_gateways(filters={"attachment.vpc-id": vpc.id}) + result = conn.get_all_internet_gateways( + filters={"attachment.vpc-id": vpc.id}) result.should.have.length_of(1) result[0].id.should.equal(igw1.id) @@ -230,7 +247,8 @@ def test_igw_filter_by_internet_gateway_id(): igw1 = conn.create_internet_gateway() igw2 = conn.create_internet_gateway() - result = conn.get_all_internet_gateways(filters={"internet-gateway-id": igw1.id}) + result = conn.get_all_internet_gateways( + filters={"internet-gateway-id": igw1.id}) result.should.have.length_of(1) result[0].id.should.equal(igw1.id) @@ -245,6 +263,7 @@ def test_igw_filter_by_attachment_state(): vpc = conn.create_vpc(VPC_CIDR) conn.attach_internet_gateway(igw1.id, vpc.id) - result = conn.get_all_internet_gateways(filters={"attachment.state": "available"}) + result = conn.get_all_internet_gateways( + filters={"attachment.state": "available"}) result.should.have.length_of(1) result[0].id.should.equal(igw1.id) diff --git a/tests/test_ec2/test_key_pairs.py b/tests/test_ec2/test_key_pairs.py index 6c4773200..ec979a871 100644 --- a/tests/test_ec2/test_key_pairs.py +++ b/tests/test_ec2/test_key_pairs.py @@ -36,7 +36,8 @@ def test_key_pairs_create(): kp = conn.create_key_pair('foo', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateKeyPair operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateKeyPair operation: Request would have succeeded, but DryRun flag is set') kp = conn.create_key_pair('foo') assert kp.material.startswith('---- BEGIN RSA PRIVATE KEY ----') @@ -91,7 +92,8 @@ def test_key_pairs_delete_exist(): r = conn.delete_key_pair('foo', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteKeyPair operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeleteKeyPair operation: Request would have succeeded, but DryRun flag is set') r = conn.delete_key_pair('foo') r.should.be.ok @@ -106,7 +108,8 @@ def test_key_pairs_import(): kp = conn.import_key_pair('foo', b'content', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the ImportKeyPair operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the ImportKeyPair operation: Request would have succeeded, but DryRun flag is set') kp = conn.import_key_pair('foo', b'content') assert kp.name == 'foo' diff --git a/tests/test_ec2/test_nat_gateway.py b/tests/test_ec2/test_nat_gateway.py index b9c95f7c3..27e8753be 100644 --- a/tests/test_ec2/test_nat_gateway.py +++ b/tests/test_ec2/test_nat_gateway.py @@ -56,7 +56,8 @@ def test_delete_nat_gateway(): nat_gateway_id = nat_gateway['NatGateway']['NatGatewayId'] response = conn.delete_nat_gateway(NatGatewayId=nat_gateway_id) - response['ResponseMetadata'].pop('HTTPHeaders', None) # this is hard to match against, so remove it + # this is hard to match against, so remove it + response['ResponseMetadata'].pop('HTTPHeaders', None) response['ResponseMetadata'].pop('RetryAttempts', None) response.should.equal({ 'NatGatewayId': nat_gateway_id, @@ -89,14 +90,20 @@ def test_create_and_describe_nat_gateway(): enis = conn.describe_network_interfaces()['NetworkInterfaces'] eni_id = enis[0]['NetworkInterfaceId'] - public_ip = conn.describe_addresses(AllocationIds=[allocation_id])['Addresses'][0]['PublicIp'] + public_ip = conn.describe_addresses(AllocationIds=[allocation_id])[ + 'Addresses'][0]['PublicIp'] describe_response['NatGateways'].should.have.length_of(1) - describe_response['NatGateways'][0]['NatGatewayId'].should.equal(nat_gateway_id) + describe_response['NatGateways'][0][ + 'NatGatewayId'].should.equal(nat_gateway_id) describe_response['NatGateways'][0]['State'].should.equal('available') describe_response['NatGateways'][0]['SubnetId'].should.equal(subnet_id) describe_response['NatGateways'][0]['VpcId'].should.equal(vpc_id) - describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['AllocationId'].should.equal(allocation_id) - describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['NetworkInterfaceId'].should.equal(eni_id) - assert describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['PrivateIp'].startswith('10.') - describe_response['NatGateways'][0]['NatGatewayAddresses'][0]['PublicIp'].should.equal(public_ip) + describe_response['NatGateways'][0]['NatGatewayAddresses'][ + 0]['AllocationId'].should.equal(allocation_id) + describe_response['NatGateways'][0]['NatGatewayAddresses'][ + 0]['NetworkInterfaceId'].should.equal(eni_id) + assert describe_response['NatGateways'][0][ + 'NatGatewayAddresses'][0]['PrivateIp'].startswith('10.') + describe_response['NatGateways'][0]['NatGatewayAddresses'][ + 0]['PublicIp'].should.equal(public_ip) diff --git a/tests/test_ec2/test_regions.py b/tests/test_ec2/test_regions.py index 07e02c526..4beca7c67 100644 --- a/tests/test_ec2/test_regions.py +++ b/tests/test_ec2/test_regions.py @@ -50,9 +50,11 @@ def test_add_servers_to_multiple_regions(): @mock_elb_deprecated def test_create_autoscaling_group(): elb_conn = boto.ec2.elb.connect_to_region('us-east-1') - elb_conn.create_load_balancer('us_test_lb', zones=[], listeners=[(80, 8080, 'http')]) + elb_conn.create_load_balancer( + 'us_test_lb', zones=[], listeners=[(80, 8080, 'http')]) elb_conn = boto.ec2.elb.connect_to_region('ap-northeast-1') - elb_conn.create_load_balancer('ap_test_lb', zones=[], listeners=[(80, 8080, 'http')]) + elb_conn.create_load_balancer( + 'ap_test_lb', zones=[], listeners=[(80, 8080, 'http')]) us_conn = boto.ec2.autoscale.connect_to_region('us-east-1') config = boto.ec2.autoscale.LaunchConfiguration( @@ -79,7 +81,6 @@ def test_create_autoscaling_group(): ) us_conn.create_auto_scaling_group(group) - ap_conn = boto.ec2.autoscale.connect_to_region('ap-northeast-1') config = boto.ec2.autoscale.LaunchConfiguration( name='ap_tester', @@ -105,7 +106,6 @@ def test_create_autoscaling_group(): ) ap_conn.create_auto_scaling_group(group) - len(us_conn.get_all_groups()).should.equal(1) len(ap_conn.get_all_groups()).should.equal(1) @@ -122,7 +122,8 @@ def test_create_autoscaling_group(): us_group.health_check_type.should.equal("EC2") list(us_group.load_balancers).should.equal(["us_test_lb"]) us_group.placement_group.should.equal("us_test_placement") - list(us_group.termination_policies).should.equal(["OldestInstance", "NewestInstance"]) + list(us_group.termination_policies).should.equal( + ["OldestInstance", "NewestInstance"]) ap_group = ap_conn.get_all_groups()[0] ap_group.name.should.equal('ap_tester_group') @@ -137,4 +138,5 @@ def test_create_autoscaling_group(): ap_group.health_check_type.should.equal("EC2") list(ap_group.load_balancers).should.equal(["ap_test_lb"]) ap_group.placement_group.should.equal("ap_test_placement") - list(ap_group.termination_policies).should.equal(["OldestInstance", "NewestInstance"]) + list(ap_group.termination_policies).should.equal( + ["OldestInstance", "NewestInstance"]) diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 3aa4b460a..6e6c62741 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -91,28 +91,34 @@ def test_route_tables_filters_standard(): all_route_tables.should.have.length_of(5) # Filter by main route table - main_route_tables = conn.get_all_route_tables(filters={'association.main':'true'}) + main_route_tables = conn.get_all_route_tables( + filters={'association.main': 'true'}) main_route_tables.should.have.length_of(3) - main_route_table_ids = [route_table.id for route_table in main_route_tables] + main_route_table_ids = [ + route_table.id for route_table in main_route_tables] main_route_table_ids.should_not.contain(route_table1.id) main_route_table_ids.should_not.contain(route_table2.id) # Filter by VPC - vpc1_route_tables = conn.get_all_route_tables(filters={'vpc-id':vpc1.id}) + vpc1_route_tables = conn.get_all_route_tables(filters={'vpc-id': vpc1.id}) vpc1_route_tables.should.have.length_of(2) - vpc1_route_table_ids = [route_table.id for route_table in vpc1_route_tables] + vpc1_route_table_ids = [ + route_table.id for route_table in vpc1_route_tables] vpc1_route_table_ids.should.contain(route_table1.id) vpc1_route_table_ids.should_not.contain(route_table2.id) # Filter by VPC and main route table - vpc2_main_route_tables = conn.get_all_route_tables(filters={'association.main':'true', 'vpc-id':vpc2.id}) + vpc2_main_route_tables = conn.get_all_route_tables( + filters={'association.main': 'true', 'vpc-id': vpc2.id}) vpc2_main_route_tables.should.have.length_of(1) - vpc2_main_route_table_ids = [route_table.id for route_table in vpc2_main_route_tables] + vpc2_main_route_table_ids = [ + route_table.id for route_table in vpc2_main_route_tables] vpc2_main_route_table_ids.should_not.contain(route_table1.id) vpc2_main_route_table_ids.should_not.contain(route_table2.id) # Unsupported filter - conn.get_all_route_tables.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) + conn.get_all_route_tables.when.called_with( + filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) @mock_ec2_deprecated @@ -134,19 +140,22 @@ def test_route_tables_filters_associations(): all_route_tables.should.have.length_of(4) # Filter by association ID - association1_route_tables = conn.get_all_route_tables(filters={'association.route-table-association-id':association_id1}) + association1_route_tables = conn.get_all_route_tables( + filters={'association.route-table-association-id': association_id1}) association1_route_tables.should.have.length_of(1) association1_route_tables[0].id.should.equal(route_table1.id) association1_route_tables[0].associations.should.have.length_of(2) # Filter by route table ID - route_table2_route_tables = conn.get_all_route_tables(filters={'association.route-table-id':route_table2.id}) + route_table2_route_tables = conn.get_all_route_tables( + filters={'association.route-table-id': route_table2.id}) route_table2_route_tables.should.have.length_of(1) route_table2_route_tables[0].id.should.equal(route_table2.id) route_table2_route_tables[0].associations.should.have.length_of(1) # Filter by subnet ID - subnet_route_tables = conn.get_all_route_tables(filters={'association.subnet-id':subnet1.id}) + subnet_route_tables = conn.get_all_route_tables( + filters={'association.subnet-id': subnet1.id}) subnet_route_tables.should.have.length_of(1) subnet_route_tables[0].id.should.equal(route_table1.id) association1_route_tables[0].associations.should.have.length_of(2) @@ -179,7 +188,8 @@ def test_route_table_associations(): route_table.associations[0].subnet_id.should.equal(subnet.id) # Associate is idempotent - association_id_idempotent = conn.associate_route_table(route_table.id, subnet.id) + association_id_idempotent = conn.associate_route_table( + route_table.id, subnet.id) association_id_idempotent.should.equal(association_id) # Error: Attempt delete associated route table. @@ -255,7 +265,8 @@ def test_route_table_replace_route_table_association(): route_table1.associations[0].subnet_id.should.equal(subnet.id) # Replace Association - association_id2 = conn.replace_route_table_association_with_assoc(association_id1, route_table2.id) + association_id2 = conn.replace_route_table_association_with_assoc( + association_id1, route_table2.id) # Refresh route_table1 = conn.get_all_route_tables(route_table1.id)[0] @@ -271,19 +282,22 @@ def test_route_table_replace_route_table_association(): route_table2.associations[0].subnet_id.should.equal(subnet.id) # Replace Association is idempotent - association_id_idempotent = conn.replace_route_table_association_with_assoc(association_id2, route_table2.id) + association_id_idempotent = conn.replace_route_table_association_with_assoc( + association_id2, route_table2.id) association_id_idempotent.should.equal(association_id2) # Error: Replace association with invalid association ID with assert_raises(EC2ResponseError) as cm: - conn.replace_route_table_association_with_assoc("rtbassoc-1234abcd", route_table1.id) + conn.replace_route_table_association_with_assoc( + "rtbassoc-1234abcd", route_table1.id) cm.exception.code.should.equal('InvalidAssociationID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none # Error: Replace association with invalid route table ID with assert_raises(EC2ResponseError) as cm: - conn.replace_route_table_association_with_assoc(association_id2, "rtb-1234abcd") + conn.replace_route_table_association_with_assoc( + association_id2, "rtb-1234abcd") cm.exception.code.should.equal('InvalidRouteTableID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none @@ -298,7 +312,8 @@ def test_route_table_get_by_tag(): route_table = conn.create_route_table(vpc.id) route_table.add_tag('Name', 'TestRouteTable') - route_tables = conn.get_all_route_tables(filters={'tag:Name': 'TestRouteTable'}) + route_tables = conn.get_all_route_tables( + filters={'tag:Name': 'TestRouteTable'}) route_tables.should.have.length_of(1) route_tables[0].vpc_id.should.equal(vpc.id) @@ -323,7 +338,8 @@ def test_route_table_get_by_tag_boto3(): route_tables[0].vpc_id.should.equal(vpc.id) route_tables[0].id.should.equal(route_table.id) route_tables[0].tags.should.have.length_of(1) - route_tables[0].tags[0].should.equal({'Key': 'Name', 'Value': 'TestRouteTable'}) + route_tables[0].tags[0].should.equal( + {'Key': 'Name', 'Value': 'TestRouteTable'}) @mock_ec2_deprecated @@ -337,10 +353,12 @@ def test_routes_additional(): conn.create_route(main_route_table.id, ROUTE_CIDR, gateway_id=igw.id) - main_route_table = conn.get_all_route_tables(filters={'vpc-id': vpc.id})[0] # Refresh route table + main_route_table = conn.get_all_route_tables( + filters={'vpc-id': vpc.id})[0] # Refresh route table main_route_table.routes.should.have.length_of(2) - new_routes = [route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] + new_routes = [ + route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] new_routes.should.have.length_of(1) new_route = new_routes[0] @@ -351,10 +369,12 @@ def test_routes_additional(): conn.delete_route(main_route_table.id, ROUTE_CIDR) - main_route_table = conn.get_all_route_tables(filters={'vpc-id': vpc.id})[0] # Refresh route table + main_route_table = conn.get_all_route_tables( + filters={'vpc-id': vpc.id})[0] # Refresh route table main_route_table.routes.should.have.length_of(1) - new_routes = [route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] + new_routes = [ + route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] new_routes.should.have.length_of(0) with assert_raises(EC2ResponseError) as cm: @@ -368,7 +388,8 @@ def test_routes_additional(): def test_routes_replace(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") - main_route_table = conn.get_all_route_tables(filters={'association.main':'true','vpc-id':vpc.id})[0] + main_route_table = conn.get_all_route_tables( + filters={'association.main': 'true', 'vpc-id': vpc.id})[0] local_route = main_route_table.routes[0] ROUTE_CIDR = "10.0.0.4/24" @@ -384,11 +405,13 @@ def test_routes_replace(): # Replace... def get_target_route(): route_table = conn.get_all_route_tables(main_route_table.id)[0] - routes = [route for route in route_table.routes if route.destination_cidr_block != vpc.cidr_block] + routes = [ + route for route in route_table.routes if route.destination_cidr_block != vpc.cidr_block] routes.should.have.length_of(1) return routes[0] - conn.replace_route(main_route_table.id, ROUTE_CIDR, instance_id=instance.id) + conn.replace_route(main_route_table.id, ROUTE_CIDR, + instance_id=instance.id) target_route = get_target_route() target_route.gateway_id.should.be.none @@ -422,12 +445,14 @@ def test_routes_not_supported(): ROUTE_CIDR = "10.0.0.4/24" # Create - conn.create_route.when.called_with(main_route_table.id, ROUTE_CIDR, interface_id='eni-1234abcd').should.throw(NotImplementedError) + conn.create_route.when.called_with( + main_route_table.id, ROUTE_CIDR, interface_id='eni-1234abcd').should.throw(NotImplementedError) # Replace igw = conn.create_internet_gateway() conn.create_route(main_route_table.id, ROUTE_CIDR, gateway_id=igw.id) - conn.replace_route.when.called_with(main_route_table.id, ROUTE_CIDR, interface_id='eni-1234abcd').should.throw(NotImplementedError) + conn.replace_route.when.called_with( + main_route_table.id, ROUTE_CIDR, interface_id='eni-1234abcd').should.throw(NotImplementedError) @requires_boto_gte("2.34.0") @@ -435,18 +460,21 @@ def test_routes_not_supported(): def test_routes_vpc_peering_connection(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") - main_route_table = conn.get_all_route_tables(filters={'association.main':'true','vpc-id':vpc.id})[0] + main_route_table = conn.get_all_route_tables( + filters={'association.main': 'true', 'vpc-id': vpc.id})[0] local_route = main_route_table.routes[0] ROUTE_CIDR = "10.0.0.4/24" peer_vpc = conn.create_vpc("11.0.0.0/16") vpc_pcx = conn.create_vpc_peering_connection(vpc.id, peer_vpc.id) - conn.create_route(main_route_table.id, ROUTE_CIDR, vpc_peering_connection_id=vpc_pcx.id) + conn.create_route(main_route_table.id, ROUTE_CIDR, + vpc_peering_connection_id=vpc_pcx.id) # Refresh route table main_route_table = conn.get_all_route_tables(main_route_table.id)[0] - new_routes = [route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] + new_routes = [ + route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] new_routes.should.have.length_of(1) new_route = new_routes[0] @@ -463,7 +491,8 @@ def test_routes_vpn_gateway(): conn = boto.connect_vpc('the_key', 'the_secret') vpc = conn.create_vpc("10.0.0.0/16") - main_route_table = conn.get_all_route_tables(filters={'association.main':'true','vpc-id':vpc.id})[0] + main_route_table = conn.get_all_route_tables( + filters={'association.main': 'true', 'vpc-id': vpc.id})[0] ROUTE_CIDR = "10.0.0.4/24" vpn_gw = conn.create_vpn_gateway(type="ipsec.1") @@ -471,7 +500,8 @@ def test_routes_vpn_gateway(): conn.create_route(main_route_table.id, ROUTE_CIDR, gateway_id=vpn_gw.id) main_route_table = conn.get_all_route_tables(main_route_table.id)[0] - new_routes = [route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] + new_routes = [ + route for route in main_route_table.routes if route.destination_cidr_block != vpc.cidr_block] new_routes.should.have.length_of(1) new_route = new_routes[0] diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py index 3056331be..21ecad11e 100644 --- a/tests/test_ec2/test_security_groups.py +++ b/tests/test_ec2/test_security_groups.py @@ -20,25 +20,30 @@ def test_create_and_describe_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') with assert_raises(EC2ResponseError) as ex: - security_group = conn.create_security_group('test security group', 'this is a test security group', dry_run=True) + security_group = conn.create_security_group( + 'test security group', 'this is a test security group', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateSecurityGroup operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateSecurityGroup operation: Request would have succeeded, but DryRun flag is set') - security_group = conn.create_security_group('test security group', 'this is a test security group') + security_group = conn.create_security_group( + 'test security group', 'this is a test security group') security_group.name.should.equal('test security group') security_group.description.should.equal('this is a test security group') # Trying to create another group with the same name should throw an error with assert_raises(EC2ResponseError) as cm: - conn.create_security_group('test security group', 'this is a test security group') + conn.create_security_group( + 'test security group', 'this is a test security group') cm.exception.code.should.equal('InvalidGroup.Duplicate') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none all_groups = conn.get_all_security_groups() - all_groups.should.have.length_of(3) # The default group gets created automatically + # The default group gets created automatically + all_groups.should.have.length_of(3) group_names = [group.name for group in all_groups] set(group_names).should.equal(set(["default", "test security group"])) @@ -66,16 +71,19 @@ def test_default_security_group(): def test_create_and_describe_vpc_security_group(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = 'vpc-5300000c' - security_group = conn.create_security_group('test security group', 'this is a test security group', vpc_id=vpc_id) + security_group = conn.create_security_group( + 'test security group', 'this is a test security group', vpc_id=vpc_id) security_group.vpc_id.should.equal(vpc_id) security_group.name.should.equal('test security group') security_group.description.should.equal('this is a test security group') - # Trying to create another group with the same name in the same VPC should throw an error + # Trying to create another group with the same name in the same VPC should + # throw an error with assert_raises(EC2ResponseError) as cm: - conn.create_security_group('test security group', 'this is a test security group', vpc_id) + conn.create_security_group( + 'test security group', 'this is a test security group', vpc_id) cm.exception.code.should.equal('InvalidGroup.Duplicate') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none @@ -94,8 +102,10 @@ def test_create_two_security_groups_with_same_name_in_different_vpc(): vpc_id = 'vpc-5300000c' vpc_id2 = 'vpc-5300000d' - conn.create_security_group('test security group', 'this is a test security group', vpc_id) - conn.create_security_group('test security group', 'this is a test security group', vpc_id2) + conn.create_security_group( + 'test security group', 'this is a test security group', vpc_id) + conn.create_security_group( + 'test security group', 'this is a test security group', vpc_id2) all_groups = conn.get_all_security_groups() @@ -125,7 +135,8 @@ def test_deleting_security_groups(): conn.delete_security_group('test2', dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteSecurityGroup operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeleteSecurityGroup operation: Request would have succeeded, but DryRun flag is set') conn.delete_security_group('test2') conn.get_all_security_groups().should.have.length_of(3) @@ -151,65 +162,83 @@ def test_authorize_ip_range_and_revoke(): security_group = conn.create_security_group('test', 'test') with assert_raises(EC2ResponseError) as ex: - success = security_group.authorize(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) + success = security_group.authorize( + ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the GrantSecurityGroupIngress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the GrantSecurityGroupIngress operation: Request would have succeeded, but DryRun flag is set') - success = security_group.authorize(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") + success = security_group.authorize( + ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") assert success.should.be.true security_group = conn.get_all_security_groups(groupnames=['test'])[0] int(security_group.rules[0].to_port).should.equal(2222) - security_group.rules[0].grants[0].cidr_ip.should.equal("123.123.123.123/32") + security_group.rules[0].grants[ + 0].cidr_ip.should.equal("123.123.123.123/32") # Wrong Cidr should throw error with assert_raises(EC2ResponseError) as cm: - security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.122/32") + security_group.revoke(ip_protocol="tcp", from_port="22", + to_port="2222", cidr_ip="123.123.123.122/32") cm.exception.code.should.equal('InvalidPermission.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none # Actually revoke with assert_raises(EC2ResponseError) as ex: - security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) + security_group.revoke(ip_protocol="tcp", from_port="22", + to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RevokeSecurityGroupIngress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the RevokeSecurityGroupIngress operation: Request would have succeeded, but DryRun flag is set') - security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") + security_group.revoke(ip_protocol="tcp", from_port="22", + to_port="2222", cidr_ip="123.123.123.123/32") security_group = conn.get_all_security_groups()[0] security_group.rules.should.have.length_of(0) # Test for egress as well - egress_security_group = conn.create_security_group('testegress', 'testegress', vpc_id='vpc-3432589') + egress_security_group = conn.create_security_group( + 'testegress', 'testegress', vpc_id='vpc-3432589') with assert_raises(EC2ResponseError) as ex: - success = conn.authorize_security_group_egress(egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) + success = conn.authorize_security_group_egress( + egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the GrantSecurityGroupEgress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the GrantSecurityGroupEgress operation: Request would have succeeded, but DryRun flag is set') - success = conn.authorize_security_group_egress(egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") + success = conn.authorize_security_group_egress( + egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") assert success.should.be.true - egress_security_group = conn.get_all_security_groups(groupnames='testegress')[0] + egress_security_group = conn.get_all_security_groups( + groupnames='testegress')[0] # There are two egress rules associated with the security group: # the default outbound rule and the new one int(egress_security_group.rules_egress[1].to_port).should.equal(2222) - egress_security_group.rules_egress[1].grants[0].cidr_ip.should.equal("123.123.123.123/32") + egress_security_group.rules_egress[1].grants[ + 0].cidr_ip.should.equal("123.123.123.123/32") # Wrong Cidr should throw error - egress_security_group.revoke.when.called_with(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.122/32").should.throw(EC2ResponseError) + egress_security_group.revoke.when.called_with( + ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.122/32").should.throw(EC2ResponseError) # Actually revoke with assert_raises(EC2ResponseError) as ex: - conn.revoke_security_group_egress(egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) + conn.revoke_security_group_egress( + egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the RevokeSecurityGroupEgress operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the RevokeSecurityGroupEgress operation: Request would have succeeded, but DryRun flag is set') - conn.revoke_security_group_egress(egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") + conn.revoke_security_group_egress( + egress_security_group.id, "tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123/32") egress_security_group = conn.get_all_security_groups()[0] # There is still the default outbound rule @@ -223,24 +252,30 @@ def test_authorize_other_group_and_revoke(): other_security_group = conn.create_security_group('other', 'other') wrong_group = conn.create_security_group('wrong', 'wrong') - success = security_group.authorize(ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) + success = security_group.authorize( + ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) assert success.should.be.true - security_group = [group for group in conn.get_all_security_groups() if group.name == 'test'][0] + security_group = [ + group for group in conn.get_all_security_groups() if group.name == 'test'][0] int(security_group.rules[0].to_port).should.equal(2222) - security_group.rules[0].grants[0].group_id.should.equal(other_security_group.id) + security_group.rules[0].grants[ + 0].group_id.should.equal(other_security_group.id) # Wrong source group should throw error with assert_raises(EC2ResponseError) as cm: - security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", src_group=wrong_group) + security_group.revoke(ip_protocol="tcp", from_port="22", + to_port="2222", src_group=wrong_group) cm.exception.code.should.equal('InvalidPermission.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none # Actually revoke - security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) + security_group.revoke(ip_protocol="tcp", from_port="22", + to_port="2222", src_group=other_security_group) - security_group = [group for group in conn.get_all_security_groups() if group.name == 'test'][0] + security_group = [ + group for group in conn.get_all_security_groups() if group.name == 'test'][0] security_group.rules.should.have.length_of(0) @@ -250,8 +285,10 @@ def test_authorize_other_group_egress_and_revoke(): vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - sg01 = ec2.create_security_group(GroupName='sg01', Description='Test security group sg01', VpcId=vpc.id) - sg02 = ec2.create_security_group(GroupName='sg02', Description='Test security group sg02', VpcId=vpc.id) + sg01 = ec2.create_security_group( + GroupName='sg01', Description='Test security group sg01', VpcId=vpc.id) + sg02 = ec2.create_security_group( + GroupName='sg02', Description='Test security group sg02', VpcId=vpc.id) ip_permission = { 'IpProtocol': 'tcp', @@ -278,27 +315,33 @@ def test_authorize_group_in_vpc(): security_group = conn.create_security_group('test1', 'test1', vpc_id) other_security_group = conn.create_security_group('test2', 'test2', vpc_id) - success = security_group.authorize(ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) + success = security_group.authorize( + ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) success.should.be.true # Check that the rule is accurate - security_group = [group for group in conn.get_all_security_groups() if group.name == 'test1'][0] + security_group = [ + group for group in conn.get_all_security_groups() if group.name == 'test1'][0] int(security_group.rules[0].to_port).should.equal(2222) - security_group.rules[0].grants[0].group_id.should.equal(other_security_group.id) + security_group.rules[0].grants[ + 0].group_id.should.equal(other_security_group.id) # Now remove the rule - success = security_group.revoke(ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) + success = security_group.revoke( + ip_protocol="tcp", from_port="22", to_port="2222", src_group=other_security_group) success.should.be.true # And check that it gets revoked - security_group = [group for group in conn.get_all_security_groups() if group.name == 'test1'][0] + security_group = [ + group for group in conn.get_all_security_groups() if group.name == 'test1'][0] security_group.rules.should.have.length_of(0) @mock_ec2_deprecated def test_get_all_security_groups(): conn = boto.connect_ec2() - sg1 = conn.create_security_group(name='test1', description='test1', vpc_id='vpc-mjm05d27') + sg1 = conn.create_security_group( + name='test1', description='test1', vpc_id='vpc-mjm05d27') conn.create_security_group(name='test2', description='test2') resp = conn.get_all_security_groups(groupnames=['test1']) @@ -326,7 +369,8 @@ def test_authorize_bad_cidr_throws_invalid_parameter_value(): conn = boto.connect_ec2('the_key', 'the_secret') security_group = conn.create_security_group('test', 'test') with assert_raises(EC2ResponseError) as cm: - security_group.authorize(ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123") + security_group.authorize( + ip_protocol="tcp", from_port="22", to_port="2222", cidr_ip="123.123.123.123") cm.exception.code.should.equal('InvalidParameterValue') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none @@ -343,7 +387,8 @@ def test_security_group_tagging(): sg.add_tag("Test", "Tag", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') sg.add_tag("Test", "Tag") @@ -362,7 +407,8 @@ def test_security_group_tag_filtering(): sg = conn.create_security_group("test-sg", "Test SG") sg.add_tag("test-tag", "test-value") - groups = conn.get_all_security_groups(filters={"tag:test-tag": "test-value"}) + groups = conn.get_all_security_groups( + filters={"tag:test-tag": "test-value"}) groups.should.have.length_of(1) @@ -507,18 +553,18 @@ def test_sec_group_rule_limit_vpc(): cm.exception.error_code.should.equal('RulesPerSecurityGroupLimitExceeded') - - ''' Boto3 ''' + @mock_ec2 def test_add_same_rule_twice_throws_error(): ec2 = boto3.resource('ec2', region_name='us-west-1') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - sg = ec2.create_security_group(GroupName='sg1', Description='Test security group sg1', VpcId=vpc.id) + sg = ec2.create_security_group( + GroupName='sg1', Description='Test security group sg1', VpcId=vpc.id) ip_permissions = [ { @@ -541,13 +587,18 @@ def test_security_group_tagging_boto3(): sg = conn.create_security_group(GroupName="test-sg", Description="Test SG") with assert_raises(ClientError) as ex: - conn.create_tags(Resources=[sg['GroupId']], Tags=[{'Key': 'Test', 'Value': 'Tag'}], DryRun=True) + conn.create_tags(Resources=[sg['GroupId']], Tags=[ + {'Key': 'Test', 'Value': 'Tag'}], DryRun=True) ex.exception.response['Error']['Code'].should.equal('DryRunOperation') - ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) - ex.exception.response['Error']['Message'].should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.response['ResponseMetadata'][ + 'HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') - conn.create_tags(Resources=[sg['GroupId']], Tags=[{'Key': 'Test', 'Value': 'Tag'}]) - describe = conn.describe_security_groups(Filters=[{'Name': 'tag-value', 'Values': ['Tag']}]) + conn.create_tags(Resources=[sg['GroupId']], Tags=[ + {'Key': 'Test', 'Value': 'Tag'}]) + describe = conn.describe_security_groups( + Filters=[{'Name': 'tag-value', 'Values': ['Tag']}]) tag = describe["SecurityGroups"][0]['Tags'][0] tag['Value'].should.equal("Tag") tag['Key'].should.equal("Test") @@ -559,9 +610,12 @@ def test_authorize_and_revoke_in_bulk(): vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - sg01 = ec2.create_security_group(GroupName='sg01', Description='Test security group sg01', VpcId=vpc.id) - sg02 = ec2.create_security_group(GroupName='sg02', Description='Test security group sg02', VpcId=vpc.id) - sg03 = ec2.create_security_group(GroupName='sg03', Description='Test security group sg03') + sg01 = ec2.create_security_group( + GroupName='sg01', Description='Test security group sg01', VpcId=vpc.id) + sg02 = ec2.create_security_group( + GroupName='sg02', Description='Test security group sg02', VpcId=vpc.id) + sg03 = ec2.create_security_group( + GroupName='sg03', Description='Test security group sg03') ip_permissions = [ { @@ -611,15 +665,19 @@ def test_authorize_and_revoke_in_bulk(): for ip_permission in expected_ip_permissions: sg01.ip_permissions_egress.shouldnt.contain(ip_permission) + @mock_ec2_deprecated def test_get_all_security_groups_filter_with_same_vpc_id(): conn = boto.connect_ec2('the_key', 'the_secret') vpc_id = 'vpc-5300000c' - security_group = conn.create_security_group('test1', 'test1', vpc_id=vpc_id) - security_group2 = conn.create_security_group('test2', 'test2', vpc_id=vpc_id) + security_group = conn.create_security_group( + 'test1', 'test1', vpc_id=vpc_id) + security_group2 = conn.create_security_group( + 'test2', 'test2', vpc_id=vpc_id) security_group.vpc_id.should.equal(vpc_id) security_group2.vpc_id.should.equal(vpc_id) - security_groups = conn.get_all_security_groups(group_ids=[security_group.id], filters={'vpc-id': [vpc_id]}) + security_groups = conn.get_all_security_groups( + group_ids=[security_group.id], filters={'vpc-id': [vpc_id]}) security_groups.should.have.length_of(1) diff --git a/tests/test_ec2/test_server.py b/tests/test_ec2/test_server.py index e6e9998ba..00be62593 100644 --- a/tests/test_ec2/test_server.py +++ b/tests/test_ec2/test_server.py @@ -18,7 +18,8 @@ def test_ec2_server_get(): headers={"Host": "ec2.us-east-1.amazonaws.com"} ) - groups = re.search("(.*)", res.data.decode('utf-8')) + groups = re.search("(.*)", + res.data.decode('utf-8')) instance_id = groups.groups()[0] res = test_client.get('/?Action=DescribeInstances') diff --git a/tests/test_ec2/test_spot_fleet.py b/tests/test_ec2/test_spot_fleet.py index 5b51ae68a..8ac91c57b 100644 --- a/tests/test_ec2/test_spot_fleet.py +++ b/tests/test_ec2/test_spot_fleet.py @@ -5,9 +5,11 @@ import sure # noqa from moto import mock_ec2 + def get_subnet_id(conn): vpc = conn.create_vpc(CidrBlock="10.0.0.0/8")['Vpc'] - subnet = conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] + subnet = conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] subnet_id = subnet['SubnetId'] return subnet_id @@ -19,60 +21,60 @@ def spot_config(subnet_id, allocation_strategy="lowestPrice"): 'TargetCapacity': 6, 'IamFleetRole': 'arn:aws:iam::123456789012:role/fleet', 'LaunchSpecifications': [{ - 'ImageId': 'ami-123', - 'KeyName': 'my-key', - 'SecurityGroups': [ - { + 'ImageId': 'ami-123', + 'KeyName': 'my-key', + 'SecurityGroups': [ + { 'GroupId': 'sg-123' - }, - ], - 'UserData': 'some user data', - 'InstanceType': 't2.small', - 'BlockDeviceMappings': [ - { + }, + ], + 'UserData': 'some user data', + 'InstanceType': 't2.small', + 'BlockDeviceMappings': [ + { 'VirtualName': 'string', 'DeviceName': 'string', 'Ebs': { 'SnapshotId': 'string', 'VolumeSize': 123, - 'DeleteOnTermination': True|False, + 'DeleteOnTermination': True | False, 'VolumeType': 'standard', 'Iops': 123, - 'Encrypted': True|False + 'Encrypted': True | False }, - 'NoDevice': 'string' - }, - ], - 'Monitoring': { - 'Enabled': True + 'NoDevice': 'string' }, - 'SubnetId': subnet_id, - 'IamInstanceProfile': { - 'Arn': 'arn:aws:iam::123456789012:role/fleet' - }, - 'EbsOptimized': False, - 'WeightedCapacity': 2.0, - 'SpotPrice': '0.13' + ], + 'Monitoring': { + 'Enabled': True + }, + 'SubnetId': subnet_id, + 'IamInstanceProfile': { + 'Arn': 'arn:aws:iam::123456789012:role/fleet' + }, + 'EbsOptimized': False, + 'WeightedCapacity': 2.0, + 'SpotPrice': '0.13' }, { - 'ImageId': 'ami-123', - 'KeyName': 'my-key', - 'SecurityGroups': [ - { - 'GroupId': 'sg-123' - }, - ], - 'UserData': 'some user data', - 'InstanceType': 't2.large', - 'Monitoring': { - 'Enabled': True + 'ImageId': 'ami-123', + 'KeyName': 'my-key', + 'SecurityGroups': [ + { + 'GroupId': 'sg-123' }, - 'SubnetId': subnet_id, - 'IamInstanceProfile': { - 'Arn': 'arn:aws:iam::123456789012:role/fleet' - }, - 'EbsOptimized': False, - 'WeightedCapacity': 4.0, - 'SpotPrice': '10.00', + ], + 'UserData': 'some user data', + 'InstanceType': 't2.large', + 'Monitoring': { + 'Enabled': True + }, + 'SubnetId': subnet_id, + 'IamInstanceProfile': { + 'Arn': 'arn:aws:iam::123456789012:role/fleet' + }, + 'EbsOptimized': False, + 'WeightedCapacity': 4.0, + 'SpotPrice': '10.00', }], 'AllocationStrategy': allocation_strategy, 'FulfilledCapacity': 6, @@ -89,7 +91,8 @@ def test_create_spot_fleet_with_lowest_price(): ) spot_fleet_id = spot_fleet_res['SpotFleetRequestId'] - spot_fleet_requests = conn.describe_spot_fleet_requests(SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs'] + spot_fleet_requests = conn.describe_spot_fleet_requests( + SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs'] len(spot_fleet_requests).should.equal(1) spot_fleet_request = spot_fleet_requests[0] spot_fleet_request['SpotFleetRequestState'].should.equal("active") @@ -97,7 +100,8 @@ def test_create_spot_fleet_with_lowest_price(): spot_fleet_config['SpotPrice'].should.equal('0.12') spot_fleet_config['TargetCapacity'].should.equal(6) - spot_fleet_config['IamFleetRole'].should.equal('arn:aws:iam::123456789012:role/fleet') + spot_fleet_config['IamFleetRole'].should.equal( + 'arn:aws:iam::123456789012:role/fleet') spot_fleet_config['AllocationStrategy'].should.equal('lowestPrice') spot_fleet_config['FulfilledCapacity'].should.equal(6.0) @@ -106,7 +110,8 @@ def test_create_spot_fleet_with_lowest_price(): launch_spec['EbsOptimized'].should.equal(False) launch_spec['SecurityGroups'].should.equal([{"GroupId": "sg-123"}]) - launch_spec['IamInstanceProfile'].should.equal({"Arn": "arn:aws:iam::123456789012:role/fleet"}) + launch_spec['IamInstanceProfile'].should.equal( + {"Arn": "arn:aws:iam::123456789012:role/fleet"}) launch_spec['ImageId'].should.equal("ami-123") launch_spec['InstanceType'].should.equal("t2.small") launch_spec['KeyName'].should.equal("my-key") @@ -116,7 +121,8 @@ def test_create_spot_fleet_with_lowest_price(): launch_spec['UserData'].should.equal("some user data") launch_spec['WeightedCapacity'].should.equal(2.0) - instance_res = conn.describe_spot_fleet_instances(SpotFleetRequestId=spot_fleet_id) + instance_res = conn.describe_spot_fleet_instances( + SpotFleetRequestId=spot_fleet_id) instances = instance_res['ActiveInstances'] len(instances).should.equal(3) @@ -125,14 +131,16 @@ def test_create_spot_fleet_with_lowest_price(): def test_create_diversified_spot_fleet(): conn = boto3.client("ec2", region_name='us-west-2') subnet_id = get_subnet_id(conn) - diversified_config = spot_config(subnet_id, allocation_strategy='diversified') + diversified_config = spot_config( + subnet_id, allocation_strategy='diversified') spot_fleet_res = conn.request_spot_fleet( SpotFleetRequestConfig=diversified_config ) spot_fleet_id = spot_fleet_res['SpotFleetRequestId'] - instance_res = conn.describe_spot_fleet_instances(SpotFleetRequestId=spot_fleet_id) + instance_res = conn.describe_spot_fleet_instances( + SpotFleetRequestId=spot_fleet_id) instances = instance_res['ActiveInstances'] len(instances).should.equal(2) instance_types = set([instance['InstanceType'] for instance in instances]) @@ -150,7 +158,9 @@ def test_cancel_spot_fleet_request(): ) spot_fleet_id = spot_fleet_res['SpotFleetRequestId'] - conn.cancel_spot_fleet_requests(SpotFleetRequestIds=[spot_fleet_id], TerminateInstances=True) + conn.cancel_spot_fleet_requests( + SpotFleetRequestIds=[spot_fleet_id], TerminateInstances=True) - spot_fleet_requests = conn.describe_spot_fleet_requests(SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs'] + spot_fleet_requests = conn.describe_spot_fleet_requests( + SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs'] len(spot_fleet_requests).should.equal(0) diff --git a/tests/test_ec2/test_spot_instances.py b/tests/test_ec2/test_spot_instances.py index 2d3cb3036..5c3bdff12 100644 --- a/tests/test_ec2/test_spot_instances.py +++ b/tests/test_ec2/test_spot_instances.py @@ -18,7 +18,8 @@ from moto.core.utils import iso_8601_datetime_with_milliseconds def test_request_spot_instances(): conn = boto3.client('ec2', 'us-east-1') vpc = conn.create_vpc(CidrBlock="10.0.0.0/8")['Vpc'] - subnet = conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] + subnet = conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.0.0.0/16', AvailabilityZone='us-east-1a')['Subnet'] subnet_id = subnet['SubnetId'] conn.create_security_group(GroupName='group1', Description='description') @@ -53,29 +54,31 @@ def test_request_spot_instances(): DryRun=True, ) ex.exception.response['Error']['Code'].should.equal('DryRunOperation') - ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) - ex.exception.response['Error']['Message'].should.equal('An error occurred (DryRunOperation) when calling the RequestSpotInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.response['ResponseMetadata'][ + 'HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal( + 'An error occurred (DryRunOperation) when calling the RequestSpotInstance operation: Request would have succeeded, but DryRun flag is set') request = conn.request_spot_instances( - SpotPrice="0.5", InstanceCount=1, Type='one-time', - ValidFrom=start, ValidUntil=end, LaunchGroup="the-group", - AvailabilityZoneGroup='my-group', - LaunchSpecification={ - "ImageId": 'ami-abcd1234', - "KeyName": "test", - "SecurityGroups": ['group1', 'group2'], - "UserData": b"some test data", - "InstanceType": 'm1.small', - "Placement": { - "AvailabilityZone": 'us-east-1c', - }, - "KernelId": "test-kernel", - "RamdiskId": "test-ramdisk", - "Monitoring": { - "Enabled": True, - }, - "SubnetId": subnet_id, + SpotPrice="0.5", InstanceCount=1, Type='one-time', + ValidFrom=start, ValidUntil=end, LaunchGroup="the-group", + AvailabilityZoneGroup='my-group', + LaunchSpecification={ + "ImageId": 'ami-abcd1234', + "KeyName": "test", + "SecurityGroups": ['group1', 'group2'], + "UserData": b"some test data", + "InstanceType": 'm1.small', + "Placement": { + "AvailabilityZone": 'us-east-1c', }, + "KernelId": "test-kernel", + "RamdiskId": "test-ramdisk", + "Monitoring": { + "Enabled": True, + }, + "SubnetId": subnet_id, + }, ) requests = conn.describe_spot_instance_requests()['SpotInstanceRequests'] @@ -91,7 +94,8 @@ def test_request_spot_instances(): request['AvailabilityZoneGroup'].should.equal('my-group') launch_spec = request['LaunchSpecification'] - security_group_names = [group['GroupName'] for group in launch_spec['SecurityGroups']] + security_group_names = [group['GroupName'] + for group in launch_spec['SecurityGroups']] set(security_group_names).should.equal(set(['group1', 'group2'])) launch_spec['ImageId'].should.equal('ami-abcd1234') @@ -112,7 +116,7 @@ def test_request_spot_instances_default_arguments(): request = conn.request_spot_instances( SpotPrice="0.5", LaunchSpecification={ - "ImageId": 'ami-abcd1234', + "ImageId": 'ami-abcd1234', } ) @@ -130,7 +134,8 @@ def test_request_spot_instances_default_arguments(): launch_spec = request['LaunchSpecification'] - security_group_names = [group['GroupName'] for group in launch_spec['SecurityGroups']] + security_group_names = [group['GroupName'] + for group in launch_spec['SecurityGroups']] security_group_names.should.equal(["default"]) launch_spec['ImageId'].should.equal('ami-abcd1234') @@ -152,12 +157,12 @@ def test_cancel_spot_instance_request(): requests = conn.get_all_spot_instance_requests() requests.should.have.length_of(1) - with assert_raises(EC2ResponseError) as ex: conn.cancel_spot_instance_requests([requests[0].id], dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CancelSpotInstance operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CancelSpotInstance operation: Request would have succeeded, but DryRun flag is set') conn.cancel_spot_instance_requests([requests[0].id]) @@ -239,10 +244,12 @@ def test_get_all_spot_instance_requests_filtering(): requests = conn.get_all_spot_instance_requests(filters={'state': 'open'}) requests.should.have.length_of(3) - requests = conn.get_all_spot_instance_requests(filters={'tag:tag1': 'value1'}) + requests = conn.get_all_spot_instance_requests( + filters={'tag:tag1': 'value1'}) requests.should.have.length_of(2) - requests = conn.get_all_spot_instance_requests(filters={'tag:tag1': 'value1', 'tag:tag2': 'value2'}) + requests = conn.get_all_spot_instance_requests( + filters={'tag:tag1': 'value1', 'tag:tag2': 'value2'}) requests.should.have.length_of(1) @@ -259,4 +266,3 @@ def test_request_spot_instances_setting_instance_id(): request = conn.get_all_spot_instance_requests()[0] assert request.state == 'active' assert request.instance_id == 'i-12345678' - diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py index 0a9b41b8e..38565a28f 100644 --- a/tests/test_ec2/test_subnets.py +++ b/tests/test_ec2/test_subnets.py @@ -69,7 +69,8 @@ def test_subnet_tagging(): def test_subnet_should_have_proper_availability_zone_set(): conn = boto.vpc.connect_to_region('us-west-1') vpcA = conn.create_vpc("10.0.0.0/16") - subnetA = conn.create_subnet(vpcA.id, "10.0.0.0/24", availability_zone='us-west-1b') + subnetA = conn.create_subnet( + vpcA.id, "10.0.0.0/24", availability_zone='us-west-1b') subnetA.availability_zone.should.equal('us-west-1b') @@ -82,7 +83,8 @@ def test_default_subnet(): default_vpc.reload() default_vpc.is_default.should.be.ok - subnet = ec2.create_subnet(VpcId=default_vpc.id, CidrBlock='172.31.0.0/20', AvailabilityZone='us-west-1a') + subnet = ec2.create_subnet( + VpcId=default_vpc.id, CidrBlock='172.31.0.0/20', AvailabilityZone='us-west-1a') subnet.reload() subnet.map_public_ip_on_launch.shouldnt.be.ok @@ -109,7 +111,8 @@ def test_boto3_non_default_subnet(): vpc.reload() vpc.is_default.shouldnt.be.ok - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a') subnet.reload() subnet.map_public_ip_on_launch.shouldnt.be.ok @@ -122,7 +125,8 @@ def test_modify_subnet_attribute(): # Get the default VPC vpc = list(ec2.vpcs.all())[0] - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a') # 'map_public_ip_on_launch' is set when calling 'DescribeSubnets' action subnet.reload() @@ -130,11 +134,13 @@ def test_modify_subnet_attribute(): # For non default subnet, attribute value should be 'False' subnet.map_public_ip_on_launch.shouldnt.be.ok - client.modify_subnet_attribute(SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': False}) + client.modify_subnet_attribute( + SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': False}) subnet.reload() subnet.map_public_ip_on_launch.shouldnt.be.ok - client.modify_subnet_attribute(SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': True}) + client.modify_subnet_attribute( + SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': True}) subnet.reload() subnet.map_public_ip_on_launch.should.be.ok @@ -144,10 +150,12 @@ def test_modify_subnet_attribute_validation(): ec2 = boto3.resource('ec2', region_name='us-west-1') client = boto3.client('ec2', region_name='us-west-1') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') - subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a') + subnet = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-1a') with assert_raises(ParamValidationError): - client.modify_subnet_attribute(SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': 'invalid'}) + client.modify_subnet_attribute( + SubnetId=subnet.id, MapPublicIpOnLaunch={'Value': 'invalid'}) @mock_ec2_deprecated @@ -155,10 +163,13 @@ def test_get_subnets_filtering(): ec2 = boto.ec2.connect_to_region('us-west-1') conn = boto.vpc.connect_to_region('us-west-1') vpcA = conn.create_vpc("10.0.0.0/16") - subnetA = conn.create_subnet(vpcA.id, "10.0.0.0/24", availability_zone='us-west-1a') + subnetA = conn.create_subnet( + vpcA.id, "10.0.0.0/24", availability_zone='us-west-1a') vpcB = conn.create_vpc("10.0.0.0/16") - subnetB1 = conn.create_subnet(vpcB.id, "10.0.0.0/24", availability_zone='us-west-1a') - subnetB2 = conn.create_subnet(vpcB.id, "10.0.1.0/24", availability_zone='us-west-1b') + subnetB1 = conn.create_subnet( + vpcB.id, "10.0.0.0/24", availability_zone='us-west-1a') + subnetB2 = conn.create_subnet( + vpcB.id, "10.0.1.0/24", availability_zone='us-west-1b') all_subnets = conn.get_all_subnets() all_subnets.should.have.length_of(3 + len(ec2.get_all_zones())) @@ -166,25 +177,33 @@ def test_get_subnets_filtering(): # Filter by VPC ID subnets_by_vpc = conn.get_all_subnets(filters={'vpc-id': vpcB.id}) subnets_by_vpc.should.have.length_of(2) - set([subnet.id for subnet in subnets_by_vpc]).should.equal(set([subnetB1.id, subnetB2.id])) + set([subnet.id for subnet in subnets_by_vpc]).should.equal( + set([subnetB1.id, subnetB2.id])) # Filter by CIDR variations subnets_by_cidr1 = conn.get_all_subnets(filters={'cidr': "10.0.0.0/24"}) subnets_by_cidr1.should.have.length_of(2) - set([subnet.id for subnet in subnets_by_cidr1]).should.equal(set([subnetA.id, subnetB1.id])) + set([subnet.id for subnet in subnets_by_cidr1] + ).should.equal(set([subnetA.id, subnetB1.id])) - subnets_by_cidr2 = conn.get_all_subnets(filters={'cidr-block': "10.0.0.0/24"}) + subnets_by_cidr2 = conn.get_all_subnets( + filters={'cidr-block': "10.0.0.0/24"}) subnets_by_cidr2.should.have.length_of(2) - set([subnet.id for subnet in subnets_by_cidr2]).should.equal(set([subnetA.id, subnetB1.id])) + set([subnet.id for subnet in subnets_by_cidr2] + ).should.equal(set([subnetA.id, subnetB1.id])) - subnets_by_cidr3 = conn.get_all_subnets(filters={'cidrBlock': "10.0.0.0/24"}) + subnets_by_cidr3 = conn.get_all_subnets( + filters={'cidrBlock': "10.0.0.0/24"}) subnets_by_cidr3.should.have.length_of(2) - set([subnet.id for subnet in subnets_by_cidr3]).should.equal(set([subnetA.id, subnetB1.id])) + set([subnet.id for subnet in subnets_by_cidr3] + ).should.equal(set([subnetA.id, subnetB1.id])) # Filter by VPC ID and CIDR - subnets_by_vpc_and_cidr = conn.get_all_subnets(filters={'vpc-id': vpcB.id, 'cidr': "10.0.0.0/24"}) + subnets_by_vpc_and_cidr = conn.get_all_subnets( + filters={'vpc-id': vpcB.id, 'cidr': "10.0.0.0/24"}) subnets_by_vpc_and_cidr.should.have.length_of(1) - set([subnet.id for subnet in subnets_by_vpc_and_cidr]).should.equal(set([subnetB1.id])) + set([subnet.id for subnet in subnets_by_vpc_and_cidr] + ).should.equal(set([subnetB1.id])) # Filter by subnet ID subnets_by_id = conn.get_all_subnets(filters={'subnet-id': subnetA.id}) @@ -192,9 +211,11 @@ def test_get_subnets_filtering(): set([subnet.id for subnet in subnets_by_id]).should.equal(set([subnetA.id])) # Filter by availabilityZone - subnets_by_az = conn.get_all_subnets(filters={'availabilityZone': 'us-west-1a', 'vpc-id': vpcB.id}) + subnets_by_az = conn.get_all_subnets( + filters={'availabilityZone': 'us-west-1a', 'vpc-id': vpcB.id}) subnets_by_az.should.have.length_of(1) - set([subnet.id for subnet in subnets_by_az]).should.equal(set([subnetB1.id])) + set([subnet.id for subnet in subnets_by_az] + ).should.equal(set([subnetB1.id])) # Filter by defaultForAz @@ -202,7 +223,8 @@ def test_get_subnets_filtering(): subnets_by_az.should.have.length_of(len(conn.get_all_zones())) # Unsupported filter - conn.get_all_subnets.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) + conn.get_all_subnets.when.called_with( + filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) @mock_ec2_deprecated diff --git a/tests/test_ec2/test_tags.py b/tests/test_ec2/test_tags.py index 23b7d0bd4..bb3a8d36b 100644 --- a/tests/test_ec2/test_tags.py +++ b/tests/test_ec2/test_tags.py @@ -22,11 +22,13 @@ def test_add_tag(): instance.add_tag("a key", "some value", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') instance.add_tag("a key", "some value") chain = itertools.chain.from_iterable - existing_instances = list(chain([res.instances for res in conn.get_all_instances()])) + existing_instances = list( + chain([res.instances for res in conn.get_all_instances()])) existing_instances.should.have.length_of(1) existing_instance = existing_instances[0] existing_instance.tags["a key"].should.equal("some value") @@ -49,7 +51,8 @@ def test_remove_tag(): instance.remove_tag("a key", dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the DeleteTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the DeleteTags operation: Request would have succeeded, but DryRun flag is set') instance.remove_tag("a key") conn.get_all_tags().should.have.length_of(0) @@ -100,12 +103,15 @@ def test_create_tags(): conn.create_tags(instance.id, tag_dict, dry_run=True) ex.exception.error_code.should.equal('DryRunOperation') ex.exception.status.should.equal(400) - ex.exception.message.should.equal('An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') + ex.exception.message.should.equal( + 'An error occurred (DryRunOperation) when calling the CreateTags operation: Request would have succeeded, but DryRun flag is set') conn.create_tags(instance.id, tag_dict) tags = conn.get_all_tags() - set([key for key in tag_dict]).should.equal(set([tag.name for tag in tags])) - set([tag_dict[key] for key in tag_dict]).should.equal(set([tag.value for tag in tags])) + set([key for key in tag_dict]).should.equal( + set([tag.name for tag in tags])) + set([tag_dict[key] for key in tag_dict]).should.equal( + set([tag.value for tag in tags])) @mock_ec2_deprecated @@ -115,7 +121,7 @@ def test_tag_limit_exceeded(): instance = reservation.instances[0] tag_dict = {} for i in range(51): - tag_dict['{0:02d}'.format(i+1)] = '' + tag_dict['{0:02d}'.format(i + 1)] = '' with assert_raises(EC2ResponseError) as cm: conn.create_tags(instance.id, tag_dict) @@ -342,7 +348,8 @@ def test_retrieved_snapshots_must_contain_their_tags(): tag_key = 'Tag name' tag_value = 'Tag value' tags_to_be_set = {tag_key: tag_value} - conn = boto.connect_ec2(aws_access_key_id='the_key', aws_secret_access_key='the_secret') + conn = boto.connect_ec2(aws_access_key_id='the_key', + aws_secret_access_key='the_secret') volume = conn.create_volume(80, "eu-west-1a") snapshot = conn.create_snapshot(volume.id) conn.create_tags([snapshot.id], tags_to_be_set) @@ -361,7 +368,8 @@ def test_retrieved_snapshots_must_contain_their_tags(): @mock_ec2_deprecated def test_filter_instances_by_wildcard_tags(): - conn = boto.connect_ec2(aws_access_key_id='the_key', aws_secret_access_key='the_secret') + conn = boto.connect_ec2(aws_access_key_id='the_key', + aws_secret_access_key='the_secret') reservation = conn.run_instances('ami-1234abcd') instance_a = reservation.instances[0] instance_a.add_tag("Key1", "Value1") diff --git a/tests/test_ec2/test_virtual_private_gateways.py b/tests/test_ec2/test_virtual_private_gateways.py index 0a7e34ea5..d90e97b45 100644 --- a/tests/test_ec2/test_virtual_private_gateways.py +++ b/tests/test_ec2/test_virtual_private_gateways.py @@ -16,6 +16,7 @@ def test_virtual_private_gateways(): vpn_gateway.state.should.equal('available') vpn_gateway.availability_zone.should.equal('us-east-1a') + @mock_ec2_deprecated def test_describe_vpn_gateway(): conn = boto.connect_vpc('the_key', 'the_secret') diff --git a/tests/test_ec2/test_vpc_peering.py b/tests/test_ec2/test_vpc_peering.py index c6a2feffb..6722eed60 100644 --- a/tests/test_ec2/test_vpc_peering.py +++ b/tests/test_ec2/test_vpc_peering.py @@ -93,4 +93,3 @@ def test_vpc_peering_connections_delete(): cm.exception.code.should.equal('InvalidVpcPeeringConnectionId.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none - diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py index c4dbf788e..904603f6d 100644 --- a/tests/test_ec2/test_vpcs.py +++ b/tests/test_ec2/test_vpcs.py @@ -42,13 +42,16 @@ def test_vpc_defaults(): conn.get_all_vpcs().should.have.length_of(2) conn.get_all_route_tables().should.have.length_of(2) - conn.get_all_security_groups(filters={'vpc-id': [vpc.id]}).should.have.length_of(1) + conn.get_all_security_groups( + filters={'vpc-id': [vpc.id]}).should.have.length_of(1) vpc.delete() conn.get_all_vpcs().should.have.length_of(1) conn.get_all_route_tables().should.have.length_of(1) - conn.get_all_security_groups(filters={'vpc-id': [vpc.id]}).should.have.length_of(0) + conn.get_all_security_groups( + filters={'vpc-id': [vpc.id]}).should.have.length_of(0) + @mock_ec2_deprecated def test_vpc_isdefault_filter(): @@ -80,6 +83,7 @@ def test_vpc_state_available_filter(): vpc.delete() conn.get_all_vpcs(filters={'state': 'available'}).should.have.length_of(2) + @mock_ec2_deprecated def test_vpc_tagging(): conn = boto.connect_vpc() @@ -127,7 +131,8 @@ def test_vpc_get_by_cidr_block(): @mock_ec2_deprecated def test_vpc_get_by_dhcp_options_id(): conn = boto.connect_vpc() - dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) + dhcp_options = conn.create_dhcp_options( + SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) vpc1 = conn.create_vpc("10.0.0.0/16") vpc2 = conn.create_vpc("10.0.0.0/16") conn.create_vpc("10.0.0.0/24") @@ -284,6 +289,7 @@ def test_non_default_vpc(): attr = response.get('EnableDnsHostnames') attr.get('Value').shouldnt.be.ok + @mock_ec2 def test_vpc_dedicated_tenancy(): ec2 = boto3.resource('ec2', region_name='us-west-1') @@ -298,6 +304,7 @@ def test_vpc_dedicated_tenancy(): vpc.instance_tenancy.should.equal('dedicated') + @mock_ec2 def test_vpc_modify_enable_dns_support(): ec2 = boto3.resource('ec2', region_name='us-west-1') @@ -339,10 +346,12 @@ def test_vpc_modify_enable_dns_hostnames(): attr = response.get('EnableDnsHostnames') attr.get('Value').should.be.ok + @mock_ec2_deprecated def test_vpc_associate_dhcp_options(): conn = boto.connect_vpc() - dhcp_options = conn.create_dhcp_options(SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) + dhcp_options = conn.create_dhcp_options( + SAMPLE_DOMAIN_NAME, SAMPLE_NAME_SERVERS) vpc = conn.create_vpc("10.0.0.0/16") conn.associate_dhcp_options(dhcp_options.id, vpc.id) diff --git a/tests/test_ec2/test_vpn_connections.py b/tests/test_ec2/test_vpn_connections.py index 864c1c3ee..e95aa76ee 100644 --- a/tests/test_ec2/test_vpn_connections.py +++ b/tests/test_ec2/test_vpn_connections.py @@ -10,27 +10,32 @@ from moto import mock_ec2_deprecated @mock_ec2_deprecated def test_create_vpn_connections(): conn = boto.connect_vpc('the_key', 'the_secret') - vpn_connection = conn.create_vpn_connection('ipsec.1', 'vgw-0123abcd', 'cgw-0123abcd') + vpn_connection = conn.create_vpn_connection( + 'ipsec.1', 'vgw-0123abcd', 'cgw-0123abcd') vpn_connection.should_not.be.none vpn_connection.id.should.match(r'vpn-\w+') vpn_connection.type.should.equal('ipsec.1') + @mock_ec2_deprecated def test_delete_vpn_connections(): conn = boto.connect_vpc('the_key', 'the_secret') - vpn_connection = conn.create_vpn_connection('ipsec.1', 'vgw-0123abcd', 'cgw-0123abcd') + vpn_connection = conn.create_vpn_connection( + 'ipsec.1', 'vgw-0123abcd', 'cgw-0123abcd') list_of_vpn_connections = conn.get_all_vpn_connections() list_of_vpn_connections.should.have.length_of(1) conn.delete_vpn_connection(vpn_connection.id) list_of_vpn_connections = conn.get_all_vpn_connections() list_of_vpn_connections.should.have.length_of(0) + @mock_ec2_deprecated def test_delete_vpn_connections_bad_id(): conn = boto.connect_vpc('the_key', 'the_secret') with assert_raises(EC2ResponseError): conn.delete_vpn_connection('vpn-0123abcd') + @mock_ec2_deprecated def test_describe_vpn_connections(): conn = boto.connect_vpc('the_key', 'the_secret') diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index f073628a9..044d827c9 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -20,7 +20,8 @@ def test_create_cluster(): clusterName='test_ecs_cluster' ) response['cluster']['clusterName'].should.equal('test_ecs_cluster') - response['cluster']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['cluster']['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') response['cluster']['status'].should.equal('ACTIVE') response['cluster']['registeredContainerInstancesCount'].should.equal(0) response['cluster']['runningTasksCount'].should.equal(0) @@ -38,8 +39,10 @@ def test_list_clusters(): clusterName='test_cluster1' ) response = client.list_clusters() - response['clusterArns'].should.contain('arn:aws:ecs:us-east-1:012345678910:cluster/test_cluster0') - response['clusterArns'].should.contain('arn:aws:ecs:us-east-1:012345678910:cluster/test_cluster1') + response['clusterArns'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_cluster0') + response['clusterArns'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_cluster1') @mock_ecs @@ -50,7 +53,8 @@ def test_delete_cluster(): ) response = client.delete_cluster(cluster='test_ecs_cluster') response['cluster']['clusterName'].should.equal('test_ecs_cluster') - response['cluster']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['cluster']['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') response['cluster']['status'].should.equal('ACTIVE') response['cluster']['registeredContainerInstancesCount'].should.equal(0) response['cluster']['runningTasksCount'].should.equal(0) @@ -82,15 +86,24 @@ def test_register_task_definition(): ] ) type(response['taskDefinition']).should.be(dict) - response['taskDefinition']['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') - response['taskDefinition']['containerDefinitions'][0]['name'].should.equal('hello_world') - response['taskDefinition']['containerDefinitions'][0]['image'].should.equal('docker/hello-world:latest') - response['taskDefinition']['containerDefinitions'][0]['cpu'].should.equal(1024) - response['taskDefinition']['containerDefinitions'][0]['memory'].should.equal(400) - response['taskDefinition']['containerDefinitions'][0]['essential'].should.equal(True) - response['taskDefinition']['containerDefinitions'][0]['environment'][0]['name'].should.equal('AWS_ACCESS_KEY_ID') - response['taskDefinition']['containerDefinitions'][0]['environment'][0]['value'].should.equal('SOME_ACCESS_KEY') - response['taskDefinition']['containerDefinitions'][0]['logConfiguration']['logDriver'].should.equal('json-file') + response['taskDefinition']['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['taskDefinition']['containerDefinitions'][ + 0]['name'].should.equal('hello_world') + response['taskDefinition']['containerDefinitions'][0][ + 'image'].should.equal('docker/hello-world:latest') + response['taskDefinition']['containerDefinitions'][ + 0]['cpu'].should.equal(1024) + response['taskDefinition']['containerDefinitions'][ + 0]['memory'].should.equal(400) + response['taskDefinition']['containerDefinitions'][ + 0]['essential'].should.equal(True) + response['taskDefinition']['containerDefinitions'][0][ + 'environment'][0]['name'].should.equal('AWS_ACCESS_KEY_ID') + response['taskDefinition']['containerDefinitions'][0][ + 'environment'][0]['value'].should.equal('SOME_ACCESS_KEY') + response['taskDefinition']['containerDefinitions'][0][ + 'logConfiguration']['logDriver'].should.equal('json-file') @mock_ecs @@ -132,8 +145,10 @@ def test_list_task_definitions(): ) response = client.list_task_definitions() len(response['taskDefinitionArns']).should.equal(2) - response['taskDefinitionArns'][0].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') - response['taskDefinitionArns'][1].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:2') + response['taskDefinitionArns'][0].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['taskDefinitionArns'][1].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:2') @mock_ecs @@ -191,10 +206,13 @@ def test_describe_task_definition(): ] ) response = client.describe_task_definition(taskDefinition='test_ecs_task') - response['taskDefinition']['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:3') + response['taskDefinition']['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:3') - response = client.describe_task_definition(taskDefinition='test_ecs_task:2') - response['taskDefinition']['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:2') + response = client.describe_task_definition( + taskDefinition='test_ecs_task:2') + response['taskDefinition']['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:2') @mock_ecs @@ -221,15 +239,24 @@ def test_deregister_task_definition(): taskDefinition='test_ecs_task:1' ) type(response['taskDefinition']).should.be(dict) - response['taskDefinition']['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') - response['taskDefinition']['containerDefinitions'][0]['name'].should.equal('hello_world') - response['taskDefinition']['containerDefinitions'][0]['image'].should.equal('docker/hello-world:latest') - response['taskDefinition']['containerDefinitions'][0]['cpu'].should.equal(1024) - response['taskDefinition']['containerDefinitions'][0]['memory'].should.equal(400) - response['taskDefinition']['containerDefinitions'][0]['essential'].should.equal(True) - response['taskDefinition']['containerDefinitions'][0]['environment'][0]['name'].should.equal('AWS_ACCESS_KEY_ID') - response['taskDefinition']['containerDefinitions'][0]['environment'][0]['value'].should.equal('SOME_ACCESS_KEY') - response['taskDefinition']['containerDefinitions'][0]['logConfiguration']['logDriver'].should.equal('json-file') + response['taskDefinition']['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['taskDefinition']['containerDefinitions'][ + 0]['name'].should.equal('hello_world') + response['taskDefinition']['containerDefinitions'][0][ + 'image'].should.equal('docker/hello-world:latest') + response['taskDefinition']['containerDefinitions'][ + 0]['cpu'].should.equal(1024) + response['taskDefinition']['containerDefinitions'][ + 0]['memory'].should.equal(400) + response['taskDefinition']['containerDefinitions'][ + 0]['essential'].should.equal(True) + response['taskDefinition']['containerDefinitions'][0][ + 'environment'][0]['name'].should.equal('AWS_ACCESS_KEY_ID') + response['taskDefinition']['containerDefinitions'][0][ + 'environment'][0]['value'].should.equal('SOME_ACCESS_KEY') + response['taskDefinition']['containerDefinitions'][0][ + 'logConfiguration']['logDriver'].should.equal('json-file') @mock_ecs @@ -261,16 +288,19 @@ def test_create_service(): taskDefinition='test_ecs_task', desiredCount=2 ) - response['service']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['service']['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') response['service']['desiredCount'].should.equal(2) len(response['service']['events']).should.equal(0) len(response['service']['loadBalancers']).should.equal(0) response['service']['pendingCount'].should.equal(0) response['service']['runningCount'].should.equal(0) - response['service']['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') + response['service']['serviceArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') response['service']['serviceName'].should.equal('test_ecs_service') response['service']['status'].should.equal('ACTIVE') - response['service']['taskDefinition'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['service']['taskDefinition'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') @mock_ecs @@ -312,8 +342,10 @@ def test_list_services(): cluster='test_ecs_cluster' ) len(response['serviceArns']).should.equal(2) - response['serviceArns'][0].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service1') - response['serviceArns'][1].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2') + response['serviceArns'][0].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service1') + response['serviceArns'][1].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2') @mock_ecs @@ -359,12 +391,15 @@ def test_describe_services(): ) response = client.describe_services( cluster='test_ecs_cluster', - services=['test_ecs_service1', 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2'] + services=['test_ecs_service1', + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2'] ) len(response['services']).should.equal(2) - response['services'][0]['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service1') + response['services'][0]['serviceArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service1') response['services'][0]['serviceName'].should.equal('test_ecs_service1') - response['services'][1]['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2') + response['services'][1]['serviceArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2') response['services'][1]['serviceName'].should.equal('test_ecs_service2') @@ -446,16 +481,20 @@ def test_delete_service(): cluster='test_ecs_cluster', service='test_ecs_service' ) - response['service']['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['service']['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') response['service']['desiredCount'].should.equal(0) len(response['service']['events']).should.equal(0) len(response['service']['loadBalancers']).should.equal(0) response['service']['pendingCount'].should.equal(0) response['service']['runningCount'].should.equal(0) - response['service']['serviceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') + response['service']['serviceArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service') response['service']['serviceName'].should.equal('test_ecs_service') response['service']['status'].should.equal('ACTIVE') - response['service']['taskDefinition'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['service']['taskDefinition'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + @mock_ec2 @mock_ecs @@ -484,18 +523,23 @@ def test_register_container_instance(): instanceIdentityDocument=instance_id_document ) - response['containerInstance']['ec2InstanceId'].should.equal(test_instance.id) + response['containerInstance'][ + 'ec2InstanceId'].should.equal(test_instance.id) full_arn = response['containerInstance']['containerInstanceArn'] arn_part = full_arn.split('/') - arn_part[0].should.equal('arn:aws:ecs:us-east-1:012345678910:container-instance') + arn_part[0].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:container-instance') arn_part[1].should.equal(str(UUID(arn_part[1]))) response['containerInstance']['status'].should.equal('ACTIVE') len(response['containerInstance']['registeredResources']).should.equal(0) len(response['containerInstance']['remainingResources']).should.equal(0) response['containerInstance']['agentConnected'].should.equal(True) - response['containerInstance']['versionInfo']['agentVersion'].should.equal('1.0.0') - response['containerInstance']['versionInfo']['agentHash'].should.equal('4023248') - response['containerInstance']['versionInfo']['dockerVersion'].should.equal('DockerVersion: 1.5.0') + response['containerInstance']['versionInfo'][ + 'agentVersion'].should.equal('1.0.0') + response['containerInstance']['versionInfo'][ + 'agentHash'].should.equal('4023248') + response['containerInstance']['versionInfo'][ + 'dockerVersion'].should.equal('DockerVersion: 1.5.0') @mock_ec2 @@ -526,7 +570,8 @@ def test_list_container_instances(): cluster=test_cluster_name, instanceIdentityDocument=instance_id_document) - test_instance_arns.append(response['containerInstance']['containerInstanceArn']) + test_instance_arns.append(response['containerInstance'][ + 'containerInstanceArn']) response = ecs_client.list_container_instances(cluster=test_cluster_name) @@ -563,13 +608,17 @@ def test_describe_container_instances(): cluster=test_cluster_name, instanceIdentityDocument=instance_id_document) - test_instance_arns.append(response['containerInstance']['containerInstanceArn']) + test_instance_arns.append(response['containerInstance'][ + 'containerInstanceArn']) - test_instance_ids = list(map((lambda x: x.split('/')[1]), test_instance_arns)) - response = ecs_client.describe_container_instances(cluster=test_cluster_name, containerInstances=test_instance_ids) + test_instance_ids = list( + map((lambda x: x.split('/')[1]), test_instance_arns)) + response = ecs_client.describe_container_instances( + cluster=test_cluster_name, containerInstances=test_instance_ids) len(response['failures']).should.equal(0) len(response['containerInstances']).should.equal(instance_to_create) - response_arns = [ci['containerInstanceArn'] for ci in response['containerInstances']] + response_arns = [ci['containerInstanceArn'] + for ci in response['containerInstances']] for arn in test_instance_arns: response_arns.should.contain(arn) @@ -626,10 +675,14 @@ def test_run_task(): startedBy='moto' ) len(response['tasks']).should.equal(2) - response['tasks'][0]['taskArn'].should.contain('arn:aws:ecs:us-east-1:012345678910:task/') - response['tasks'][0]['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') - response['tasks'][0]['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') - response['tasks'][0]['containerInstanceArn'].should.contain('arn:aws:ecs:us-east-1:012345678910:container-instance/') + response['tasks'][0]['taskArn'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:task/') + response['tasks'][0]['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['tasks'][0]['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['tasks'][0]['containerInstanceArn'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:container-instance/') response['tasks'][0]['overrides'].should.equal({}) response['tasks'][0]['lastStatus'].should.equal("RUNNING") response['tasks'][0]['desiredStatus'].should.equal("RUNNING") @@ -664,8 +717,10 @@ def test_start_task(): instanceIdentityDocument=instance_id_document ) - container_instances = client.list_container_instances(cluster=test_cluster_name) - container_instance_id = container_instances['containerInstanceArns'][0].split('/')[-1] + container_instances = client.list_container_instances( + cluster=test_cluster_name) + container_instance_id = container_instances[ + 'containerInstanceArns'][0].split('/')[-1] _ = client.register_task_definition( family='test_ecs_task', @@ -694,10 +749,14 @@ def test_start_task(): ) len(response['tasks']).should.equal(1) - response['tasks'][0]['taskArn'].should.contain('arn:aws:ecs:us-east-1:012345678910:task/') - response['tasks'][0]['clusterArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') - response['tasks'][0]['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') - response['tasks'][0]['containerInstanceArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:container-instance/{0}'.format(container_instance_id)) + response['tasks'][0]['taskArn'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:task/') + response['tasks'][0]['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['tasks'][0]['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['tasks'][0]['containerInstanceArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:container-instance/{0}'.format(container_instance_id)) response['tasks'][0]['overrides'].should.equal({}) response['tasks'][0]['lastStatus'].should.equal("RUNNING") response['tasks'][0]['desiredStatus'].should.equal("RUNNING") @@ -732,8 +791,10 @@ def test_list_tasks(): instanceIdentityDocument=instance_id_document ) - container_instances = client.list_container_instances(cluster=test_cluster_name) - container_instance_id = container_instances['containerInstanceArns'][0].split('/')[-1] + container_instances = client.list_container_instances( + cluster=test_cluster_name) + container_instance_id = container_instances[ + 'containerInstanceArns'][0].split('/')[-1] _ = client.register_task_definition( family='test_ecs_task', @@ -770,7 +831,8 @@ def test_list_tasks(): ) assert len(client.list_tasks()['taskArns']).should.equal(2) - assert len(client.list_tasks(cluster='test_ecs_cluster')['taskArns']).should.equal(2) + assert len(client.list_tasks(cluster='test_ecs_cluster') + ['taskArns']).should.equal(2) assert len(client.list_tasks(startedBy='foo')['taskArns']).should.equal(1) @@ -819,7 +881,7 @@ def test_describe_tasks(): ] ) tasks_arns = [ - task['taskArn'] for task in client.run_task( + task['taskArn'] for task in client.run_task( cluster='test_ecs_cluster', overrides={}, taskDefinition='test_ecs_task', @@ -833,7 +895,8 @@ def test_describe_tasks(): ) len(response['tasks']).should.equal(2) - set([response['tasks'][0]['taskArn'], response['tasks'][1]['taskArn']]).should.equal(set(tasks_arns)) + set([response['tasks'][0]['taskArn'], response['tasks'] + [1]['taskArn']]).should.equal(set(tasks_arns)) @mock_ecs @@ -858,9 +921,11 @@ def describe_task_definition(): family = task_definition['family'] task = client.describe_task_definition(taskDefinition=family) task['containerDefinitions'][0].should.equal(container_definition) - task['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task2:1') + task['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task2:1') task['volumes'].should.equal([]) + @mock_ec2 @mock_ecs def test_stop_task(): @@ -918,7 +983,8 @@ def test_stop_task(): reason='moto testing' ) - stop_response['task']['taskArn'].should.equal(run_response['tasks'][0].get('taskArn')) + stop_response['task']['taskArn'].should.equal( + run_response['tasks'][0].get('taskArn')) stop_response['task']['lastStatus'].should.equal('STOPPED') stop_response['task']['desiredStatus'].should.equal('STOPPED') stop_response['task']['stoppedReason'].should.equal('moto testing') @@ -967,7 +1033,8 @@ def test_update_cluster_name_through_cloudformation_should_trigger_a_replacement } } template2 = deepcopy(template1) - template2['Resources']['testCluster']['Properties']['ClusterName'] = 'testcluster2' + template2['Resources']['testCluster'][ + 'Properties']['ClusterName'] = 'testcluster2' template1_json = json.dumps(template1) cfn_conn = boto3.client('cloudformation', region_name='us-west-1') stack_resp = cfn_conn.create_stack( @@ -994,18 +1061,18 @@ def test_create_task_definition_through_cloudformation(): "Description": "ECS Cluster Test CloudFormation", "Resources": { "testTaskDefinition": { - "Type" : "AWS::ECS::TaskDefinition", - "Properties" : { - "ContainerDefinitions" : [ + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ { "Name": "ecs-sample", - "Image":"amazon/amazon-ecs-sample", + "Image": "amazon/amazon-ecs-sample", "Cpu": "200", "Memory": "500", "Essential": "true" } ], - "Volumes" : [], + "Volumes": [], } } } @@ -1030,19 +1097,19 @@ def test_update_task_definition_family_through_cloudformation_should_trigger_a_r "Description": "ECS Cluster Test CloudFormation", "Resources": { "testTaskDefinition": { - "Type" : "AWS::ECS::TaskDefinition", - "Properties" : { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { "Family": "testTaskDefinition1", - "ContainerDefinitions" : [ + "ContainerDefinitions": [ { "Name": "ecs-sample", - "Image":"amazon/amazon-ecs-sample", + "Image": "amazon/amazon-ecs-sample", "Cpu": "200", "Memory": "500", "Essential": "true" } ], - "Volumes" : [], + "Volumes": [], } } } @@ -1055,7 +1122,8 @@ def test_update_task_definition_family_through_cloudformation_should_trigger_a_r ) template2 = deepcopy(template1) - template2['Resources']['testTaskDefinition']['Properties']['Family'] = 'testTaskDefinition2' + template2['Resources']['testTaskDefinition'][ + 'Properties']['Family'] = 'testTaskDefinition2' template2_json = json.dumps(template2) cfn_conn.update_stack( StackName="test_stack", @@ -1065,7 +1133,8 @@ def test_update_task_definition_family_through_cloudformation_should_trigger_a_r ecs_conn = boto3.client('ecs', region_name='us-west-1') resp = ecs_conn.list_task_definitions(familyPrefix='testTaskDefinition') len(resp['taskDefinitionArns']).should.equal(1) - resp['taskDefinitionArns'][0].endswith('testTaskDefinition2:1').should.be.true + resp['taskDefinitionArns'][0].endswith( + 'testTaskDefinition2:1').should.be.true @mock_ecs @@ -1082,18 +1151,18 @@ def test_create_service_through_cloudformation(): } }, "testTaskDefinition": { - "Type" : "AWS::ECS::TaskDefinition", - "Properties" : { - "ContainerDefinitions" : [ + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ { "Name": "ecs-sample", - "Image":"amazon/amazon-ecs-sample", + "Image": "amazon/amazon-ecs-sample", "Cpu": "200", "Memory": "500", "Essential": "true" } ], - "Volumes" : [], + "Volumes": [], } }, "testService": { @@ -1132,18 +1201,18 @@ def test_update_service_through_cloudformation_should_trigger_replacement(): } }, "testTaskDefinition": { - "Type" : "AWS::ECS::TaskDefinition", - "Properties" : { - "ContainerDefinitions" : [ + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ { "Name": "ecs-sample", - "Image":"amazon/amazon-ecs-sample", + "Image": "amazon/amazon-ecs-sample", "Cpu": "200", "Memory": "500", "Essential": "true" } ], - "Volumes" : [], + "Volumes": [], } }, "testService": { diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index fa13fc23b..4b5d59d6d 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -20,6 +20,7 @@ import sure # noqa from moto import mock_elb, mock_ec2, mock_elb_deprecated, mock_ec2_deprecated + @mock_elb_deprecated def test_create_load_balancer(): conn = boto.connect_elb() @@ -32,7 +33,8 @@ def test_create_load_balancer(): balancer = balancers[0] balancer.name.should.equal("my-lb") balancer.scheme.should.equal("internal") - set(balancer.availability_zones).should.equal(set(['us-east-1a', 'us-east-1b'])) + set(balancer.availability_zones).should.equal( + set(['us-east-1a', 'us-east-1b'])) listener1 = balancer.listeners[0] listener1.load_balancer_port.should.equal(80) listener1.instance_port.should.equal(8080) @@ -46,7 +48,8 @@ def test_create_load_balancer(): @mock_elb_deprecated def test_getting_missing_elb(): conn = boto.connect_elb() - conn.get_all_load_balancers.when.called_with(load_balancer_names='aaa').should.throw(BotoServerError) + conn.get_all_load_balancers.when.called_with( + load_balancer_names='aaa').should.throw(BotoServerError) @mock_elb_deprecated @@ -63,12 +66,14 @@ def test_create_elb_in_multiple_region(): list(west1_conn.get_all_load_balancers()).should.have.length_of(1) list(west2_conn.get_all_load_balancers()).should.have.length_of(1) + @mock_elb_deprecated def test_create_load_balancer_with_certificate(): conn = boto.connect_elb() zones = ['us-east-1a'] - ports = [(443, 8443, 'https', 'arn:aws:iam:123456789012:server-certificate/test-cert')] + ports = [ + (443, 8443, 'https', 'arn:aws:iam:123456789012:server-certificate/test-cert')] conn.create_load_balancer('my-lb', zones, ports) balancers = conn.get_all_load_balancers() @@ -80,7 +85,8 @@ def test_create_load_balancer_with_certificate(): listener.load_balancer_port.should.equal(443) listener.instance_port.should.equal(8443) listener.protocol.should.equal("HTTPS") - listener.ssl_certificate_id.should.equal('arn:aws:iam:123456789012:server-certificate/test-cert') + listener.ssl_certificate_id.should.equal( + 'arn:aws:iam:123456789012:server-certificate/test-cert') @mock_elb @@ -89,15 +95,19 @@ def test_create_and_delete_boto3_support(): client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'tcp', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) - list(client.describe_load_balancers()['LoadBalancerDescriptions']).should.have.length_of(1) + list(client.describe_load_balancers()[ + 'LoadBalancerDescriptions']).should.have.length_of(1) client.delete_load_balancer( LoadBalancerName='my-lb' ) - list(client.describe_load_balancers()['LoadBalancerDescriptions']).should.have.length_of(0) + list(client.describe_load_balancers()[ + 'LoadBalancerDescriptions']).should.have.length_of(0) + @mock_elb_deprecated def test_add_listener(): @@ -142,23 +152,32 @@ def test_create_and_delete_listener_boto3_support(): client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'http', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[{'Protocol': 'http', + 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) - list(client.describe_load_balancers()['LoadBalancerDescriptions']).should.have.length_of(1) + list(client.describe_load_balancers()[ + 'LoadBalancerDescriptions']).should.have.length_of(1) client.create_load_balancer_listeners( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'tcp', 'LoadBalancerPort':443, 'InstancePort':8443}] + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 443, 'InstancePort': 8443}] ) balancer = client.describe_load_balancers()['LoadBalancerDescriptions'][0] list(balancer['ListenerDescriptions']).should.have.length_of(2) - balancer['ListenerDescriptions'][0]['Listener']['Protocol'].should.equal('HTTP') - balancer['ListenerDescriptions'][0]['Listener']['LoadBalancerPort'].should.equal(80) - balancer['ListenerDescriptions'][0]['Listener']['InstancePort'].should.equal(8080) - balancer['ListenerDescriptions'][1]['Listener']['Protocol'].should.equal('TCP') - balancer['ListenerDescriptions'][1]['Listener']['LoadBalancerPort'].should.equal(443) - balancer['ListenerDescriptions'][1]['Listener']['InstancePort'].should.equal(8443) + balancer['ListenerDescriptions'][0][ + 'Listener']['Protocol'].should.equal('HTTP') + balancer['ListenerDescriptions'][0]['Listener'][ + 'LoadBalancerPort'].should.equal(80) + balancer['ListenerDescriptions'][0]['Listener'][ + 'InstancePort'].should.equal(8080) + balancer['ListenerDescriptions'][1][ + 'Listener']['Protocol'].should.equal('TCP') + balancer['ListenerDescriptions'][1]['Listener'][ + 'LoadBalancerPort'].should.equal(443) + balancer['ListenerDescriptions'][1]['Listener'][ + 'InstancePort'].should.equal(8443) @mock_elb_deprecated @@ -189,8 +208,10 @@ def test_get_load_balancers_by_name(): conn.create_load_balancer('my-lb3', zones, ports) conn.get_all_load_balancers().should.have.length_of(3) - conn.get_all_load_balancers(load_balancer_names=['my-lb1']).should.have.length_of(1) - conn.get_all_load_balancers(load_balancer_names=['my-lb1', 'my-lb2']).should.have.length_of(2) + conn.get_all_load_balancers( + load_balancer_names=['my-lb1']).should.have.length_of(1) + conn.get_all_load_balancers( + load_balancer_names=['my-lb1', 'my-lb2']).should.have.length_of(2) @mock_elb_deprecated @@ -240,7 +261,8 @@ def test_create_health_check_boto3(): client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'http', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[{'Protocol': 'http', + 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) client.configure_health_check( @@ -285,14 +307,16 @@ def test_register_instances(): @mock_elb def test_register_instances_boto3(): ec2 = boto3.resource('ec2', region_name='us-east-1') - response = ec2.create_instances(ImageId='ami-1234abcd', MinCount=2, MaxCount=2) + response = ec2.create_instances( + ImageId='ami-1234abcd', MinCount=2, MaxCount=2) instance_id1 = response[0].id instance_id2 = response[1].id client = boto3.client('elb', region_name='us-east-1') client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'http', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[{'Protocol': 'http', + 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) client.register_instances_with_load_balancer( @@ -303,7 +327,8 @@ def test_register_instances_boto3(): ] ) balancer = client.describe_load_balancers()['LoadBalancerDescriptions'][0] - instance_ids = [instance['InstanceId'] for instance in balancer['Instances']] + instance_ids = [instance['InstanceId'] + for instance in balancer['Instances']] set(instance_ids).should.equal(set([instance_id1, instance_id2])) @@ -328,18 +353,21 @@ def test_deregister_instances(): balancer.instances.should.have.length_of(1) balancer.instances[0].id.should.equal(instance_id2) + @mock_ec2 @mock_elb def test_deregister_instances_boto3(): ec2 = boto3.resource('ec2', region_name='us-east-1') - response = ec2.create_instances(ImageId='ami-1234abcd', MinCount=2, MaxCount=2) + response = ec2.create_instances( + ImageId='ami-1234abcd', MinCount=2, MaxCount=2) instance_id1 = response[0].id instance_id2 = response[1].id client = boto3.client('elb', region_name='us-east-1') client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'http', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[{'Protocol': 'http', + 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) client.register_instances_with_load_balancer( @@ -403,18 +431,21 @@ def test_connection_draining_attribute(): connection_draining.enabled = True connection_draining.timeout = 60 - conn.modify_lb_attribute("my-lb", "ConnectionDraining", connection_draining) + conn.modify_lb_attribute( + "my-lb", "ConnectionDraining", connection_draining) attributes = lb.get_attributes(force=True) attributes.connection_draining.enabled.should.be.true attributes.connection_draining.timeout.should.equal(60) connection_draining.timeout = 30 - conn.modify_lb_attribute("my-lb", "ConnectionDraining", connection_draining) + conn.modify_lb_attribute( + "my-lb", "ConnectionDraining", connection_draining) attributes = lb.get_attributes(force=True) attributes.connection_draining.timeout.should.equal(30) connection_draining.enabled = False - conn.modify_lb_attribute("my-lb", "ConnectionDraining", connection_draining) + conn.modify_lb_attribute( + "my-lb", "ConnectionDraining", connection_draining) attributes = lb.get_attributes(force=True) attributes.connection_draining.enabled.should.be.false @@ -453,15 +484,18 @@ def test_connection_settings_attribute(): connection_settings = ConnectionSettingAttribute(conn) connection_settings.idle_timeout = 120 - conn.modify_lb_attribute("my-lb", "ConnectingSettings", connection_settings) + conn.modify_lb_attribute( + "my-lb", "ConnectingSettings", connection_settings) attributes = lb.get_attributes(force=True) attributes.connecting_settings.idle_timeout.should.equal(120) connection_settings.idle_timeout = 60 - conn.modify_lb_attribute("my-lb", "ConnectingSettings", connection_settings) + conn.modify_lb_attribute( + "my-lb", "ConnectingSettings", connection_settings) attributes = lb.get_attributes(force=True) attributes.connecting_settings.idle_timeout.should.equal(60) + @mock_elb_deprecated def test_create_lb_cookie_stickiness_policy(): conn = boto.connect_elb() @@ -478,9 +512,13 @@ def test_create_lb_cookie_stickiness_policy(): # documentation to be a long numeric. # # To work around that, this value is converted to an int and checked. - cookie_expiration_period_response_str = lb.policies.lb_cookie_stickiness_policies[0].cookie_expiration_period - int(cookie_expiration_period_response_str).should.equal(cookie_expiration_period) - lb.policies.lb_cookie_stickiness_policies[0].policy_name.should.equal(policy_name) + cookie_expiration_period_response_str = lb.policies.lb_cookie_stickiness_policies[ + 0].cookie_expiration_period + int(cookie_expiration_period_response_str).should.equal( + cookie_expiration_period) + lb.policies.lb_cookie_stickiness_policies[ + 0].policy_name.should.equal(policy_name) + @mock_elb_deprecated def test_create_lb_cookie_stickiness_policy_no_expiry(): @@ -492,8 +530,11 @@ def test_create_lb_cookie_stickiness_policy_no_expiry(): lb.create_cookie_stickiness_policy(None, policy_name) lb = conn.get_all_load_balancers()[0] - lb.policies.lb_cookie_stickiness_policies[0].cookie_expiration_period.should.be.none - lb.policies.lb_cookie_stickiness_policies[0].policy_name.should.equal(policy_name) + lb.policies.lb_cookie_stickiness_policies[ + 0].cookie_expiration_period.should.be.none + lb.policies.lb_cookie_stickiness_policies[ + 0].policy_name.should.equal(policy_name) + @mock_elb_deprecated def test_create_app_cookie_stickiness_policy(): @@ -506,8 +547,11 @@ def test_create_app_cookie_stickiness_policy(): lb.create_app_cookie_stickiness_policy(cookie_name, policy_name) lb = conn.get_all_load_balancers()[0] - lb.policies.app_cookie_stickiness_policies[0].cookie_name.should.equal(cookie_name) - lb.policies.app_cookie_stickiness_policies[0].policy_name.should.equal(policy_name) + lb.policies.app_cookie_stickiness_policies[ + 0].cookie_name.should.equal(cookie_name) + lb.policies.app_cookie_stickiness_policies[ + 0].policy_name.should.equal(policy_name) + @mock_elb_deprecated def test_create_lb_policy(): @@ -516,11 +560,13 @@ def test_create_lb_policy(): lb = conn.create_load_balancer('my-lb', [], ports) policy_name = "ProxyPolicy" - lb.create_lb_policy(policy_name, 'ProxyProtocolPolicyType', {'ProxyProtocol': True}) + lb.create_lb_policy(policy_name, 'ProxyProtocolPolicyType', { + 'ProxyProtocol': True}) lb = conn.get_all_load_balancers()[0] lb.policies.other_policies[0].policy_name.should.equal(policy_name) + @mock_elb_deprecated def test_set_policies_of_listener(): conn = boto.connect_elb() @@ -543,6 +589,7 @@ def test_set_policies_of_listener(): # by contrast to a backend, a listener stores only policy name strings listener.policy_names[0].should.equal(policy_name) + @mock_elb_deprecated def test_set_policies_of_backend_server(): conn = boto.connect_elb() @@ -553,7 +600,8 @@ def test_set_policies_of_backend_server(): # in a real flow, it is necessary first to create a policy, # then to set that policy to the backend - lb.create_lb_policy(policy_name, 'ProxyProtocolPolicyType', {'ProxyProtocol': True}) + lb.create_lb_policy(policy_name, 'ProxyProtocolPolicyType', { + 'ProxyProtocol': True}) lb.set_policies_of_backend_server(instance_port, [policy_name]) lb = conn.get_all_load_balancers()[0] @@ -562,6 +610,7 @@ def test_set_policies_of_backend_server(): # by contrast to a listener, a backend stores OtherPolicy objects backend.policies[0].policy_name.should.equal(policy_name) + @mock_ec2_deprecated @mock_elb_deprecated def test_describe_instance_health(): @@ -583,7 +632,8 @@ def test_describe_instance_health(): instances_health = conn.describe_instance_health('my-lb') instances_health.should.have.length_of(2) for instance_health in instances_health: - instance_health.instance_id.should.be.within([instance_id1, instance_id2]) + instance_health.instance_id.should.be.within( + [instance_id1, instance_id2]) instance_health.state.should.equal('InService') instances_health = conn.describe_instance_health('my-lb', [instance_id1]) @@ -597,76 +647,78 @@ def test_add_remove_tags(): client = boto3.client('elb', region_name='us-east-1') client.add_tags.when.called_with(LoadBalancerNames=['my-lb'], - Tags=[{ - 'Key': 'a', - 'Value': 'b' - }]).should.throw(botocore.exceptions.ClientError) - + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }]).should.throw(botocore.exceptions.ClientError) client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'tcp', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) - list(client.describe_load_balancers()['LoadBalancerDescriptions']).should.have.length_of(1) + list(client.describe_load_balancers()[ + 'LoadBalancerDescriptions']).should.have.length_of(1) client.add_tags(LoadBalancerNames=['my-lb'], Tags=[{ - 'Key': 'a', - 'Value': 'b' + 'Key': 'a', + 'Value': 'b' }]) - tags = dict([(d['Key'], d['Value']) for d in client.describe_tags(LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']]) + tags = dict([(d['Key'], d['Value']) for d in client.describe_tags( + LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']]) tags.should.have.key('a').which.should.equal('b') client.add_tags(LoadBalancerNames=['my-lb'], Tags=[{ - 'Key': 'a', - 'Value': 'b' + 'Key': 'a', + 'Value': 'b' }, { - 'Key': 'b', - 'Value': 'b' + 'Key': 'b', + 'Value': 'b' }, { - 'Key': 'c', - 'Value': 'b' + 'Key': 'c', + 'Value': 'b' }, { - 'Key': 'd', - 'Value': 'b' + 'Key': 'd', + 'Value': 'b' }, { - 'Key': 'e', - 'Value': 'b' + 'Key': 'e', + 'Value': 'b' }, { - 'Key': 'f', - 'Value': 'b' + 'Key': 'f', + 'Value': 'b' }, { - 'Key': 'g', - 'Value': 'b' + 'Key': 'g', + 'Value': 'b' }, { - 'Key': 'h', - 'Value': 'b' + 'Key': 'h', + 'Value': 'b' }, { - 'Key': 'i', - 'Value': 'b' + 'Key': 'i', + 'Value': 'b' }, { - 'Key': 'j', - 'Value': 'b' + 'Key': 'j', + 'Value': 'b' }]) client.add_tags.when.called_with(LoadBalancerNames=['my-lb'], - Tags=[{ - 'Key': 'k', - 'Value': 'b' - }]).should.throw(botocore.exceptions.ClientError) + Tags=[{ + 'Key': 'k', + 'Value': 'b' + }]).should.throw(botocore.exceptions.ClientError) client.add_tags(LoadBalancerNames=['my-lb'], Tags=[{ - 'Key': 'j', - 'Value': 'c' + 'Key': 'j', + 'Value': 'c' }]) - - tags = dict([(d['Key'], d['Value']) for d in client.describe_tags(LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']]) + tags = dict([(d['Key'], d['Value']) for d in client.describe_tags( + LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']]) tags.should.have.key('a').which.should.equal('b') tags.should.have.key('b').which.should.equal('b') @@ -681,11 +733,12 @@ def test_add_remove_tags(): tags.shouldnt.have.key('k') client.remove_tags(LoadBalancerNames=['my-lb'], - Tags=[{ - 'Key': 'a' - }]) + Tags=[{ + 'Key': 'a' + }]) - tags = dict([(d['Key'], d['Value']) for d in client.describe_tags(LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']]) + tags = dict([(d['Key'], d['Value']) for d in client.describe_tags( + LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']]) tags.shouldnt.have.key('a') tags.should.have.key('b').which.should.equal('b') @@ -698,17 +751,17 @@ def test_add_remove_tags(): tags.should.have.key('i').which.should.equal('b') tags.should.have.key('j').which.should.equal('c') - client.create_load_balancer( LoadBalancerName='other-lb', - Listeners=[{'Protocol':'tcp', 'LoadBalancerPort':433, 'InstancePort':8433}], + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 433, 'InstancePort': 8433}], AvailabilityZones=['us-east-1a', 'us-east-1b'] ) client.add_tags(LoadBalancerNames=['other-lb'], Tags=[{ - 'Key': 'other', - 'Value': 'something' + 'Key': 'other', + 'Value': 'something' }]) lb_tags = dict([(l['LoadBalancerName'], dict([(d['Key'], d['Value']) for d in l['Tags']])) @@ -718,7 +771,8 @@ def test_add_remove_tags(): lb_tags.should.have.key('other-lb') lb_tags['my-lb'].shouldnt.have.key('other') - lb_tags['other-lb'].should.have.key('other').which.should.equal('something') + lb_tags[ + 'other-lb'].should.have.key('other').which.should.equal('something') @mock_elb @@ -727,15 +781,17 @@ def test_create_with_tags(): client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'tcp', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 80, 'InstancePort': 8080}], AvailabilityZones=['us-east-1a', 'us-east-1b'], Tags=[{ - 'Key': 'k', - 'Value': 'v' + 'Key': 'k', + 'Value': 'v' }] ) - tags = dict((d['Key'], d['Value']) for d in client.describe_tags(LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']) + tags = dict((d['Key'], d['Value']) for d in client.describe_tags( + LoadBalancerNames=['my-lb'])['TagDescriptions'][0]['Tags']) tags.should.have.key('k').which.should.equal('v') @@ -754,7 +810,8 @@ def test_subnets(): client = boto3.client('elb', region_name='us-east-1') client.create_load_balancer( LoadBalancerName='my-lb', - Listeners=[{'Protocol':'tcp', 'LoadBalancerPort':80, 'InstancePort':8080}], + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 80, 'InstancePort': 8080}], Subnets=[subnet.id] ) @@ -770,5 +827,5 @@ def test_create_load_balancer_duplicate(): conn = boto.connect_elb() ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] conn.create_load_balancer('my-lb', [], ports) - conn.create_load_balancer.when.called_with('my-lb', [], ports).should.throw(BotoServerError) - + conn.create_load_balancer.when.called_with( + 'my-lb', [], ports).should.throw(BotoServerError) diff --git a/tests/test_emr/test_emr.py b/tests/test_emr/test_emr.py index 4b06d7516..4acd7067c 100644 --- a/tests/test_emr/test_emr.py +++ b/tests/test_emr/test_emr.py @@ -100,7 +100,8 @@ def test_describe_cluster(): # cluster.status.timeline.enddatetime.should.be.a(six.string_types) # cluster.status.timeline.readydatetime.should.be.a(six.string_types) - dict((item.key, item.value) for item in cluster.tags).should.equal(input_tags) + dict((item.key, item.value) + for item in cluster.tags).should.equal(input_tags) cluster.terminationprotected.should.equal('false') cluster.visibletoallusers.should.equal('true') @@ -285,7 +286,8 @@ def test_list_clusters(): y = expected[x.id] x.id.should.equal(y['id']) x.name.should.equal(y['name']) - x.normalizedinstancehours.should.equal(y['normalizedinstancehours']) + x.normalizedinstancehours.should.equal( + y['normalizedinstancehours']) x.status.state.should.equal(y['state']) x.status.timeline.creationdatetime.should.be.a(six.string_types) if y['state'] == 'TERMINATED': @@ -371,11 +373,13 @@ def test_run_jobflow_with_instance_groups(): job_id = conn.run_jobflow(instance_groups=input_instance_groups, **run_jobflow_args) job_flow = conn.describe_jobflow(job_id) - int(job_flow.instancecount).should.equal(sum(g.num_instances for g in input_instance_groups)) + int(job_flow.instancecount).should.equal( + sum(g.num_instances for g in input_instance_groups)) for instance_group in job_flow.instancegroups: expected = input_groups[instance_group.name] instance_group.should.have.property('instancegroupid') - int(instance_group.instancerunningcount).should.equal(expected.num_instances) + int(instance_group.instancerunningcount).should.equal( + expected.num_instances) instance_group.instancerole.should.equal(expected.role) instance_group.instancetype.should.equal(expected.type) instance_group.market.should.equal(expected.market) @@ -483,7 +487,8 @@ def test_instance_groups(): conn.add_instance_groups(job_id, input_instance_groups[2:]) jf = conn.describe_jobflow(job_id) - int(jf.instancecount).should.equal(sum(g.num_instances for g in input_instance_groups)) + int(jf.instancecount).should.equal( + sum(g.num_instances for g in input_instance_groups)) for x in jf.instancegroups: y = input_groups[x.name] if hasattr(y, 'bidprice'): @@ -572,7 +577,8 @@ def test_steps(): list(arg.value for arg in step.args).should.have.length_of(8) step.creationdatetime.should.be.a(six.string_types) # step.enddatetime.should.be.a(six.string_types) - step.jar.should.equal('/home/hadoop/contrib/streaming/hadoop-streaming.jar') + step.jar.should.equal( + '/home/hadoop/contrib/streaming/hadoop-streaming.jar') step.laststatechangereason.should.be.a(six.string_types) step.mainclass.should.equal('') step.name.should.be.a(six.string_types) @@ -592,7 +598,8 @@ def test_steps(): '-input', y.input, '-output', y.output, ]) - x.config.jar.should.equal('/home/hadoop/contrib/streaming/hadoop-streaming.jar') + x.config.jar.should.equal( + '/home/hadoop/contrib/streaming/hadoop-streaming.jar') x.config.mainclass.should.equal('') # properties x.should.have.property('id').should.be.a(six.string_types) @@ -610,7 +617,8 @@ def test_steps(): '-input', y.input, '-output', y.output, ]) - x.config.jar.should.equal('/home/hadoop/contrib/streaming/hadoop-streaming.jar') + x.config.jar.should.equal( + '/home/hadoop/contrib/streaming/hadoop-streaming.jar') x.config.mainclass.should.equal('') # properties x.should.have.property('id').should.be.a(six.string_types) diff --git a/tests/test_emr/test_emr_boto3.py b/tests/test_emr/test_emr_boto3.py index 4fb5c3d79..4999935c5 100644 --- a/tests/test_emr/test_emr_boto3.py +++ b/tests/test_emr/test_emr_boto3.py @@ -88,15 +88,20 @@ def test_describe_cluster(): config['Properties'].should.equal(args['Configurations'][0]['Properties']) attrs = cl['Ec2InstanceAttributes'] - attrs['AdditionalMasterSecurityGroups'].should.equal(args['Instances']['AdditionalMasterSecurityGroups']) - attrs['AdditionalSlaveSecurityGroups'].should.equal(args['Instances']['AdditionalSlaveSecurityGroups']) + attrs['AdditionalMasterSecurityGroups'].should.equal( + args['Instances']['AdditionalMasterSecurityGroups']) + attrs['AdditionalSlaveSecurityGroups'].should.equal( + args['Instances']['AdditionalSlaveSecurityGroups']) attrs['Ec2AvailabilityZone'].should.equal('us-east-1a') attrs['Ec2KeyName'].should.equal(args['Instances']['Ec2KeyName']) attrs['Ec2SubnetId'].should.equal(args['Instances']['Ec2SubnetId']) - attrs['EmrManagedMasterSecurityGroup'].should.equal(args['Instances']['EmrManagedMasterSecurityGroup']) - attrs['EmrManagedSlaveSecurityGroup'].should.equal(args['Instances']['EmrManagedSlaveSecurityGroup']) + attrs['EmrManagedMasterSecurityGroup'].should.equal( + args['Instances']['EmrManagedMasterSecurityGroup']) + attrs['EmrManagedSlaveSecurityGroup'].should.equal( + args['Instances']['EmrManagedSlaveSecurityGroup']) attrs['IamInstanceProfile'].should.equal(args['JobFlowRole']) - attrs['ServiceAccessSecurityGroup'].should.equal(args['Instances']['ServiceAccessSecurityGroup']) + attrs['ServiceAccessSecurityGroup'].should.equal( + args['Instances']['ServiceAccessSecurityGroup']) cl['Id'].should.equal(cluster_id) cl['LogUri'].should.equal(args['LogUri']) cl['MasterPublicDnsName'].should.be.a(six.string_types) @@ -222,11 +227,14 @@ def test_describe_job_flow(): ig['State'].should.equal('RUNNING') attrs['KeepJobFlowAliveWhenNoSteps'].should.equal(True) # attrs['MasterInstanceId'].should.be.a(six.string_types) - attrs['MasterInstanceType'].should.equal(args['Instances']['MasterInstanceType']) + attrs['MasterInstanceType'].should.equal( + args['Instances']['MasterInstanceType']) attrs['MasterPublicDnsName'].should.be.a(six.string_types) attrs['NormalizedInstanceHours'].should.equal(0) - attrs['Placement']['AvailabilityZone'].should.equal(args['Instances']['Placement']['AvailabilityZone']) - attrs['SlaveInstanceType'].should.equal(args['Instances']['SlaveInstanceType']) + attrs['Placement']['AvailabilityZone'].should.equal( + args['Instances']['Placement']['AvailabilityZone']) + attrs['SlaveInstanceType'].should.equal( + args['Instances']['SlaveInstanceType']) attrs['TerminationProtected'].should.equal(False) jf['JobFlowId'].should.equal(cluster_id) jf['JobFlowRole'].should.equal(args['JobFlowRole']) @@ -282,14 +290,18 @@ def test_list_clusters(): y = expected[x['Id']] x['Id'].should.equal(y['Id']) x['Name'].should.equal(y['Name']) - x['NormalizedInstanceHours'].should.equal(y['NormalizedInstanceHours']) + x['NormalizedInstanceHours'].should.equal( + y['NormalizedInstanceHours']) x['Status']['State'].should.equal(y['State']) - x['Status']['Timeline']['CreationDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'CreationDateTime'].should.be.a('datetime.datetime') if y['State'] == 'TERMINATED': - x['Status']['Timeline']['EndDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'EndDateTime'].should.be.a('datetime.datetime') else: x['Status']['Timeline'].shouldnt.have.key('EndDateTime') - x['Status']['Timeline']['ReadyDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'ReadyDateTime'].should.be.a('datetime.datetime') marker = resp.get('Marker') if marker is None: break @@ -316,8 +328,10 @@ def test_run_job_flow(): resp['ExecutionStatusDetail']['State'].should.equal('WAITING') resp['JobFlowId'].should.equal(cluster_id) resp['Name'].should.equal(args['Name']) - resp['Instances']['MasterInstanceType'].should.equal(args['Instances']['MasterInstanceType']) - resp['Instances']['SlaveInstanceType'].should.equal(args['Instances']['SlaveInstanceType']) + resp['Instances']['MasterInstanceType'].should.equal( + args['Instances']['MasterInstanceType']) + resp['Instances']['SlaveInstanceType'].should.equal( + args['Instances']['SlaveInstanceType']) resp['LogUri'].should.equal(args['LogUri']) resp['VisibleToAllUsers'].should.equal(args['VisibleToAllUsers']) resp['Instances']['NormalizedInstanceHours'].should.equal(0) @@ -333,7 +347,8 @@ def test_run_job_flow_with_invalid_params(): args['AmiVersion'] = '2.4' args['ReleaseLabel'] = 'emr-5.0.0' client.run_job_flow(**args) - ex.exception.response['Error']['Message'].should.contain('ValidationException') + ex.exception.response['Error'][ + 'Message'].should.contain('ValidationException') @mock_emr @@ -378,7 +393,8 @@ def test_run_job_flow_with_instance_groups(): args = deepcopy(run_job_flow_args) args['Instances'] = {'InstanceGroups': input_instance_groups} cluster_id = client.run_job_flow(**args)['JobFlowId'] - groups = client.list_instance_groups(ClusterId=cluster_id)['InstanceGroups'] + groups = client.list_instance_groups(ClusterId=cluster_id)[ + 'InstanceGroups'] for x in groups: y = input_groups[x['Name']] x.should.have.key('Id') @@ -484,10 +500,12 @@ def test_instance_groups(): jf = client.describe_job_flows(JobFlowIds=[cluster_id])['JobFlows'][0] base_instance_count = jf['Instances']['InstanceCount'] - client.add_instance_groups(JobFlowId=cluster_id, InstanceGroups=input_instance_groups[2:]) + client.add_instance_groups( + JobFlowId=cluster_id, InstanceGroups=input_instance_groups[2:]) jf = client.describe_job_flows(JobFlowIds=[cluster_id])['JobFlows'][0] - jf['Instances']['InstanceCount'].should.equal(sum(g['InstanceCount'] for g in input_instance_groups)) + jf['Instances']['InstanceCount'].should.equal( + sum(g['InstanceCount'] for g in input_instance_groups)) for x in jf['Instances']['InstanceGroups']: y = input_groups[x['Name']] if hasattr(y, 'BidPrice'): @@ -506,7 +524,8 @@ def test_instance_groups(): x['StartDateTime'].should.be.a('datetime.datetime') x['State'].should.equal('RUNNING') - groups = client.list_instance_groups(ClusterId=cluster_id)['InstanceGroups'] + groups = client.list_instance_groups(ClusterId=cluster_id)[ + 'InstanceGroups'] for x in groups: y = input_groups[x['Name']] if hasattr(y, 'BidPrice'): @@ -525,9 +544,11 @@ def test_instance_groups(): x['Status']['State'].should.equal('RUNNING') x['Status']['StateChangeReason']['Code'].should.be.a(six.string_types) # x['Status']['StateChangeReason']['Message'].should.be.a(six.string_types) - x['Status']['Timeline']['CreationDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'CreationDateTime'].should.be.a('datetime.datetime') # x['Status']['Timeline']['EndDateTime'].should.be.a('datetime.datetime') - x['Status']['Timeline']['ReadyDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'ReadyDateTime'].should.be.a('datetime.datetime') igs = dict((g['Name'], g) for g in groups) client.modify_instance_groups( @@ -592,14 +613,19 @@ def test_steps(): # x['ExecutionStatusDetail'].should.have.key('EndDateTime') # x['ExecutionStatusDetail'].should.have.key('LastStateChangeReason') # x['ExecutionStatusDetail'].should.have.key('StartDateTime') - x['ExecutionStatusDetail']['State'].should.equal('STARTING' if idx == 0 else 'PENDING') + x['ExecutionStatusDetail']['State'].should.equal( + 'STARTING' if idx == 0 else 'PENDING') x['StepConfig']['ActionOnFailure'].should.equal('TERMINATE_CLUSTER') - x['StepConfig']['HadoopJarStep']['Args'].should.equal(y['HadoopJarStep']['Args']) - x['StepConfig']['HadoopJarStep']['Jar'].should.equal(y['HadoopJarStep']['Jar']) + x['StepConfig']['HadoopJarStep'][ + 'Args'].should.equal(y['HadoopJarStep']['Args']) + x['StepConfig']['HadoopJarStep'][ + 'Jar'].should.equal(y['HadoopJarStep']['Jar']) if 'MainClass' in y['HadoopJarStep']: - x['StepConfig']['HadoopJarStep']['MainClass'].should.equal(y['HadoopJarStep']['MainClass']) + x['StepConfig']['HadoopJarStep']['MainClass'].should.equal( + y['HadoopJarStep']['MainClass']) if 'Properties' in y['HadoopJarStep']: - x['StepConfig']['HadoopJarStep']['Properties'].should.equal(y['HadoopJarStep']['Properties']) + x['StepConfig']['HadoopJarStep']['Properties'].should.equal( + y['HadoopJarStep']['Properties']) x['StepConfig']['Name'].should.equal(y['Name']) expected = dict((s['Name'], s) for s in input_steps) @@ -617,7 +643,8 @@ def test_steps(): x['Name'].should.equal(y['Name']) x['Status']['State'].should.be.within(['STARTING', 'PENDING']) # StateChangeReason - x['Status']['Timeline']['CreationDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'CreationDateTime'].should.be.a('datetime.datetime') # x['Status']['Timeline']['EndDateTime'].should.be.a('datetime.datetime') # x['Status']['Timeline']['StartDateTime'].should.be.a('datetime.datetime') @@ -631,7 +658,8 @@ def test_steps(): x['Name'].should.equal(y['Name']) x['Status']['State'].should.be.within(['STARTING', 'PENDING']) # StateChangeReason - x['Status']['Timeline']['CreationDateTime'].should.be.a('datetime.datetime') + x['Status']['Timeline'][ + 'CreationDateTime'].should.be.a('datetime.datetime') # x['Status']['Timeline']['EndDateTime'].should.be.a('datetime.datetime') # x['Status']['Timeline']['StartDateTime'].should.be.a('datetime.datetime') @@ -640,7 +668,8 @@ def test_steps(): steps.should.have.length_of(1) steps[0]['Id'].should.equal(step_id) - steps = client.list_steps(ClusterId=cluster_id, StepStates=['STARTING'])['Steps'] + steps = client.list_steps(ClusterId=cluster_id, + StepStates=['STARTING'])['Steps'] steps.should.have.length_of(1) steps[0]['Id'].should.equal(step_id) @@ -656,8 +685,10 @@ def test_tags(): client.add_tags(ResourceId=cluster_id, Tags=input_tags) resp = client.describe_cluster(ClusterId=cluster_id)['Cluster'] resp['Tags'].should.have.length_of(2) - dict((t['Key'], t['Value']) for t in resp['Tags']).should.equal(dict((t['Key'], t['Value']) for t in input_tags)) + dict((t['Key'], t['Value']) for t in resp['Tags']).should.equal( + dict((t['Key'], t['Value']) for t in input_tags)) - client.remove_tags(ResourceId=cluster_id, TagKeys=[t['Key'] for t in input_tags]) + client.remove_tags(ResourceId=cluster_id, TagKeys=[ + t['Key'] for t in input_tags]) resp = client.describe_cluster(ClusterId=cluster_id)['Cluster'] resp['Tags'].should.equal([]) diff --git a/tests/test_glacier/test_glacier_jobs.py b/tests/test_glacier/test_glacier_jobs.py index ef4a00b75..66780f681 100644 --- a/tests/test_glacier/test_glacier_jobs.py +++ b/tests/test_glacier/test_glacier_jobs.py @@ -13,14 +13,16 @@ def test_init_glacier_job(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" conn.create_vault(vault_name) - archive_id = conn.upload_archive(vault_name, "some stuff", "", "", "some description") + archive_id = conn.upload_archive( + vault_name, "some stuff", "", "", "some description") job_response = conn.initiate_job(vault_name, { "ArchiveId": archive_id, "Type": "archive-retrieval", }) job_id = job_response['JobId'] - job_response['Location'].should.equal("//vaults/my_vault/jobs/{0}".format(job_id)) + job_response['Location'].should.equal( + "//vaults/my_vault/jobs/{0}".format(job_id)) @mock_glacier_deprecated @@ -28,7 +30,8 @@ def test_describe_job(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" conn.create_vault(vault_name) - archive_id = conn.upload_archive(vault_name, "some stuff", "", "", "some description") + archive_id = conn.upload_archive( + vault_name, "some stuff", "", "", "some description") job_response = conn.initiate_job(vault_name, { "ArchiveId": archive_id, "Type": "archive-retrieval", @@ -61,8 +64,10 @@ def test_list_glacier_jobs(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" conn.create_vault(vault_name) - archive_id1 = conn.upload_archive(vault_name, "some stuff", "", "", "some description")['ArchiveId'] - archive_id2 = conn.upload_archive(vault_name, "some other stuff", "", "", "some description")['ArchiveId'] + archive_id1 = conn.upload_archive( + vault_name, "some stuff", "", "", "some description")['ArchiveId'] + archive_id2 = conn.upload_archive( + vault_name, "some other stuff", "", "", "some description")['ArchiveId'] conn.initiate_job(vault_name, { "ArchiveId": archive_id1, @@ -82,7 +87,8 @@ def test_get_job_output(): conn = Layer1(region_name="us-west-2") vault_name = "my_vault" conn.create_vault(vault_name) - archive_response = conn.upload_archive(vault_name, "some stuff", "", "", "some description") + archive_response = conn.upload_archive( + vault_name, "some stuff", "", "", "some description") archive_id = archive_response['ArchiveId'] job_response = conn.initiate_job(vault_name, { "ArchiveId": archive_id, diff --git a/tests/test_glacier/test_glacier_server.py b/tests/test_glacier/test_glacier_server.py index d3e09015f..fd8034421 100644 --- a/tests/test_glacier/test_glacier_server.py +++ b/tests/test_glacier/test_glacier_server.py @@ -18,4 +18,5 @@ def test_list_vaults(): res = test_client.get('/1234bcd/vaults') - json.loads(res.data.decode("utf-8")).should.equal({u'Marker': None, u'VaultList': []}) + json.loads(res.data.decode("utf-8") + ).should.equal({u'Marker': None, u'VaultList': []}) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 6504a5483..076f33916 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -19,11 +19,13 @@ def test_get_all_server_certs(): conn = boto.connect_iam() conn.upload_server_cert("certname", "certbody", "privatekey") - certs = conn.get_all_server_certs()['list_server_certificates_response']['list_server_certificates_result']['server_certificate_metadata_list'] + certs = conn.get_all_server_certs()['list_server_certificates_response'][ + 'list_server_certificates_result']['server_certificate_metadata_list'] certs.should.have.length_of(1) cert1 = certs[0] cert1.server_certificate_name.should.equal("certname") - cert1.arn.should.equal("arn:aws:iam::123456789012:server-certificate/certname") + cert1.arn.should.equal( + "arn:aws:iam::123456789012:server-certificate/certname") @mock_iam_deprecated() @@ -41,7 +43,8 @@ def test_get_server_cert(): conn.upload_server_cert("certname", "certbody", "privatekey") cert = conn.get_server_certificate("certname") cert.server_certificate_name.should.equal("certname") - cert.arn.should.equal("arn:aws:iam::123456789012:server-certificate/certname") + cert.arn.should.equal( + "arn:aws:iam::123456789012:server-certificate/certname") @mock_iam_deprecated() @@ -51,7 +54,8 @@ def test_upload_server_cert(): conn.upload_server_cert("certname", "certbody", "privatekey") cert = conn.get_server_certificate("certname") cert.server_certificate_name.should.equal("certname") - cert.arn.should.equal("arn:aws:iam::123456789012:server-certificate/certname") + cert.arn.should.equal( + "arn:aws:iam::123456789012:server-certificate/certname") @mock_iam_deprecated() @@ -74,7 +78,8 @@ def test_get_instance_profile__should_throw__when_instance_profile_does_not_exis def test_create_role_and_instance_profile(): conn = boto.connect_iam() conn.create_instance_profile("my-profile", path="my-path") - conn.create_role("my-role", assume_role_policy_document="some policy", path="my-path") + conn.create_role( + "my-role", assume_role_policy_document="some policy", path="my-path") conn.add_role_to_instance_profile("my-profile", "my-role") @@ -95,7 +100,8 @@ def test_create_role_and_instance_profile(): def test_remove_role_from_instance_profile(): conn = boto.connect_iam() conn.create_instance_profile("my-profile", path="my-path") - conn.create_role("my-role", assume_role_policy_document="some policy", path="my-path") + conn.create_role( + "my-role", assume_role_policy_document="some policy", path="my-path") conn.add_role_to_instance_profile("my-profile", "my-role") profile = conn.get_instance_profile("my-profile") @@ -127,29 +133,37 @@ def test_list_instance_profiles(): def test_list_instance_profiles_for_role(): conn = boto.connect_iam() - conn.create_role(role_name="my-role", assume_role_policy_document="some policy", path="my-path") - conn.create_role(role_name="my-role2", assume_role_policy_document="some policy2", path="my-path2") + conn.create_role(role_name="my-role", + assume_role_policy_document="some policy", path="my-path") + conn.create_role(role_name="my-role2", + assume_role_policy_document="some policy2", path="my-path2") profile_name_list = ['my-profile', 'my-profile2'] profile_path_list = ['my-path', 'my-path2'] for profile_count in range(0, 2): - conn.create_instance_profile(profile_name_list[profile_count], path=profile_path_list[profile_count]) + conn.create_instance_profile( + profile_name_list[profile_count], path=profile_path_list[profile_count]) for profile_count in range(0, 2): - conn.add_role_to_instance_profile(profile_name_list[profile_count], "my-role") + conn.add_role_to_instance_profile( + profile_name_list[profile_count], "my-role") profile_dump = conn.list_instance_profiles_for_role(role_name="my-role") - profile_list = profile_dump['list_instance_profiles_for_role_response']['list_instance_profiles_for_role_result']['instance_profiles'] + profile_list = profile_dump['list_instance_profiles_for_role_response'][ + 'list_instance_profiles_for_role_result']['instance_profiles'] for profile_count in range(0, len(profile_list)): - profile_name_list.remove(profile_list[profile_count]["instance_profile_name"]) + profile_name_list.remove(profile_list[profile_count][ + "instance_profile_name"]) profile_path_list.remove(profile_list[profile_count]["path"]) - profile_list[profile_count]["roles"]["member"]["role_name"].should.equal("my-role") + profile_list[profile_count]["roles"]["member"][ + "role_name"].should.equal("my-role") len(profile_name_list).should.equal(0) len(profile_path_list).should.equal(0) profile_dump2 = conn.list_instance_profiles_for_role(role_name="my-role2") - profile_list = profile_dump2['list_instance_profiles_for_role_response']['list_instance_profiles_for_role_result']['instance_profiles'] + profile_list = profile_dump2['list_instance_profiles_for_role_response'][ + 'list_instance_profiles_for_role_result']['instance_profiles'] len(profile_list).should.equal(0) @@ -165,9 +179,11 @@ def test_list_role_policies(): @mock_iam_deprecated() def test_put_role_policy(): conn = boto.connect_iam() - conn.create_role("my-role", assume_role_policy_document="some policy", path="my-path") + conn.create_role( + "my-role", assume_role_policy_document="some policy", path="my-path") conn.put_role_policy("my-role", "test policy", "my policy") - policy = conn.get_role_policy("my-role", "test policy")['get_role_policy_response']['get_role_policy_result']['policy_name'] + policy = conn.get_role_policy( + "my-role", "test policy")['get_role_policy_response']['get_role_policy_result']['policy_name'] policy.should.equal("test policy") @@ -246,13 +262,15 @@ def test_get_all_access_keys(): conn.create_user('my-user') response = conn.get_all_access_keys('my-user') assert_equals( - response['list_access_keys_response']['list_access_keys_result']['access_key_metadata'], + response['list_access_keys_response'][ + 'list_access_keys_result']['access_key_metadata'], [] ) conn.create_access_key('my-user') response = conn.get_all_access_keys('my-user') assert_not_equals( - response['list_access_keys_response']['list_access_keys_result']['access_key_metadata'], + response['list_access_keys_response'][ + 'list_access_keys_result']['access_key_metadata'], [] ) @@ -261,7 +279,8 @@ def test_get_all_access_keys(): def test_delete_access_key(): conn = boto.connect_iam() conn.create_user('my-user') - access_key_id = conn.create_access_key('my-user')['create_access_key_response']['create_access_key_result']['access_key']['access_key_id'] + access_key_id = conn.create_access_key('my-user')['create_access_key_response'][ + 'create_access_key_result']['access_key']['access_key_id'] conn.delete_access_key(access_key_id, 'my-user') @@ -278,9 +297,11 @@ def test_delete_user(): def test_generate_credential_report(): conn = boto.connect_iam() result = conn.generate_credential_report() - result['generate_credential_report_response']['generate_credential_report_result']['state'].should.equal('STARTED') + result['generate_credential_report_response'][ + 'generate_credential_report_result']['state'].should.equal('STARTED') result = conn.generate_credential_report() - result['generate_credential_report_response']['generate_credential_report_result']['state'].should.equal('COMPLETE') + result['generate_credential_report_response'][ + 'generate_credential_report_result']['state'].should.equal('COMPLETE') @mock_iam_deprecated() @@ -293,7 +314,8 @@ def test_get_credential_report(): while result['generate_credential_report_response']['generate_credential_report_result']['state'] != 'COMPLETE': result = conn.generate_credential_report() result = conn.get_credential_report() - report = base64.b64decode(result['get_credential_report_response']['get_credential_report_result']['content'].encode('ascii')).decode('ascii') + report = base64.b64decode(result['get_credential_report_response'][ + 'get_credential_report_result']['content'].encode('ascii')).decode('ascii') report.should.match(r'.*my-user.*') @@ -307,23 +329,31 @@ def test_managed_policy(): path='/mypolicy/', description='my user managed policy') - aws_policies = conn.list_policies(scope='AWS')['list_policies_response']['list_policies_result']['policies'] - set(p.name for p in aws_managed_policies).should.equal(set(p['policy_name'] for p in aws_policies)) + aws_policies = conn.list_policies(scope='AWS')['list_policies_response'][ + 'list_policies_result']['policies'] + set(p.name for p in aws_managed_policies).should.equal( + set(p['policy_name'] for p in aws_policies)) - user_policies = conn.list_policies(scope='Local')['list_policies_response']['list_policies_result']['policies'] - set(['UserManagedPolicy']).should.equal(set(p['policy_name'] for p in user_policies)) + user_policies = conn.list_policies(scope='Local')['list_policies_response'][ + 'list_policies_result']['policies'] + set(['UserManagedPolicy']).should.equal( + set(p['policy_name'] for p in user_policies)) - all_policies = conn.list_policies()['list_policies_response']['list_policies_result']['policies'] - set(p['policy_name'] for p in aws_policies + user_policies).should.equal(set(p['policy_name'] for p in all_policies)) + all_policies = conn.list_policies()['list_policies_response'][ + 'list_policies_result']['policies'] + set(p['policy_name'] for p in aws_policies + + user_policies).should.equal(set(p['policy_name'] for p in all_policies)) role_name = 'my-role' - conn.create_role(role_name, assume_role_policy_document={'policy': 'test'}, path="my-path") + conn.create_role(role_name, assume_role_policy_document={ + 'policy': 'test'}, path="my-path") for policy_name in ['AmazonElasticMapReduceRole', 'AmazonElasticMapReduceforEC2Role']: policy_arn = 'arn:aws:iam::aws:policy/service-role/' + policy_name conn.attach_role_policy(policy_arn, role_name) - rows = conn.list_policies(only_attached=True)['list_policies_response']['list_policies_result']['policies'] + rows = conn.list_policies(only_attached=True)['list_policies_response'][ + 'list_policies_result']['policies'] rows.should.have.length_of(2) for x in rows: int(x['attachment_count']).should.be.greater_than(0) @@ -332,7 +362,8 @@ def test_managed_policy(): resp = conn.get_response('ListAttachedRolePolicies', {'RoleName': role_name}, list_marker='AttachedPolicies') - resp['list_attached_role_policies_response']['list_attached_role_policies_result']['attached_policies'].should.have.length_of(2) + resp['list_attached_role_policies_response']['list_attached_role_policies_result'][ + 'attached_policies'].should.have.length_of(2) @mock_iam diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index 6fd0f47dd..a13d6de0b 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -29,7 +29,8 @@ def test_get_all_groups(): conn = boto.connect_iam() conn.create_group('my-group1') conn.create_group('my-group2') - groups = conn.get_all_groups()['list_groups_response']['list_groups_result']['groups'] + groups = conn.get_all_groups()['list_groups_response'][ + 'list_groups_result']['groups'] groups.should.have.length_of(2) @@ -68,5 +69,6 @@ def test_get_groups_for_user(): conn.add_user_to_group('my-group1', 'my-user') conn.add_user_to_group('my-group2', 'my-user') - groups = conn.get_groups_for_user('my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups'] + groups = conn.get_groups_for_user( + 'my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups'] groups.should.have.length_of(2) diff --git a/tests/test_iam/test_server.py b/tests/test_iam/test_server.py index 1b1c3bfe3..59aaf1462 100644 --- a/tests/test_iam/test_server.py +++ b/tests/test_iam/test_server.py @@ -16,10 +16,11 @@ def test_iam_server_get(): backend = server.create_backend_app("iam") test_client = backend.test_client() - group_data = test_client.action_data("CreateGroup", GroupName="test group", Path="/") + group_data = test_client.action_data( + "CreateGroup", GroupName="test group", Path="/") group_id = re.search("(.*)", group_data).groups()[0] groups_data = test_client.action_data("ListGroups") groups_ids = re.findall("(.*)", groups_data) - assert group_id in groups_ids \ No newline at end of file + assert group_id in groups_ids diff --git a/tests/test_kinesis/test_firehose.py b/tests/test_kinesis/test_firehose.py index 371be253b..6ab46c6f9 100644 --- a/tests/test_kinesis/test_firehose.py +++ b/tests/test_kinesis/test_firehose.py @@ -132,11 +132,13 @@ def test_create_stream_without_redshift(): "HasMoreDestinations": False, }) + @mock_kinesis def test_deescribe_non_existant_stream(): client = boto3.client('firehose', region_name='us-east-1') - client.describe_delivery_stream.when.called_with(DeliveryStreamName='not-a-stream').should.throw(ClientError) + client.describe_delivery_stream.when.called_with( + DeliveryStreamName='not-a-stream').should.throw(ClientError) @mock_kinesis @@ -146,11 +148,13 @@ def test_list_and_delete_stream(): create_stream(client, 'stream1') create_stream(client, 'stream2') - set(client.list_delivery_streams()['DeliveryStreamNames']).should.equal(set(['stream1', 'stream2'])) + set(client.list_delivery_streams()['DeliveryStreamNames']).should.equal( + set(['stream1', 'stream2'])) client.delete_delivery_stream(DeliveryStreamName='stream1') - set(client.list_delivery_streams()['DeliveryStreamNames']).should.equal(set(['stream2'])) + set(client.list_delivery_streams()[ + 'DeliveryStreamNames']).should.equal(set(['stream2'])) @mock_kinesis diff --git a/tests/test_kinesis/test_kinesis.py b/tests/test_kinesis/test_kinesis.py index a86bce44c..5b2f9ccf3 100644 --- a/tests/test_kinesis/test_kinesis.py +++ b/tests/test_kinesis/test_kinesis.py @@ -18,7 +18,8 @@ def test_create_cluster(): stream = stream_response["StreamDescription"] stream["StreamName"].should.equal("my_stream") stream["HasMoreShards"].should.equal(False) - stream["StreamARN"].should.equal("arn:aws:kinesis:us-west-2:123456789012:my_stream") + stream["StreamARN"].should.equal( + "arn:aws:kinesis:us-west-2:123456789012:my_stream") stream["StreamStatus"].should.equal("ACTIVE") shards = stream['Shards'] @@ -28,7 +29,8 @@ def test_create_cluster(): @mock_kinesis_deprecated def test_describe_non_existant_stream(): conn = boto.kinesis.connect_to_region("us-east-1") - conn.describe_stream.when.called_with("not-a-stream").should.throw(ResourceNotFoundException) + conn.describe_stream.when.called_with( + "not-a-stream").should.throw(ResourceNotFoundException) @mock_kinesis_deprecated @@ -45,7 +47,8 @@ def test_list_and_delete_stream(): conn.list_streams()['StreamNames'].should.have.length_of(1) # Delete invalid id - conn.delete_stream.when.called_with("not-a-stream").should.throw(ResourceNotFoundException) + conn.delete_stream.when.called_with( + "not-a-stream").should.throw(ResourceNotFoundException) @mock_kinesis_deprecated @@ -73,7 +76,8 @@ def test_get_invalid_shard_iterator(): stream_name = "my_stream" conn.create_stream(stream_name, 1) - conn.get_shard_iterator.when.called_with(stream_name, "123", 'TRIM_HORIZON').should.throw(ResourceNotFoundException) + conn.get_shard_iterator.when.called_with( + stream_name, "123", 'TRIM_HORIZON').should.throw(ResourceNotFoundException) @mock_kinesis_deprecated @@ -138,7 +142,8 @@ def test_get_records_limit(): @mock_kinesis_deprecated def test_get_records_at_sequence_number(): - # AT_SEQUENCE_NUMBER - Start reading exactly from the position denoted by a specific sequence number. + # AT_SEQUENCE_NUMBER - Start reading exactly from the position denoted by + # a specific sequence number. conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" conn.create_stream(stream_name, 1) @@ -158,7 +163,8 @@ def test_get_records_at_sequence_number(): second_sequence_id = response['Records'][1]['SequenceNumber'] # Then get a new iterator starting at that id - response = conn.get_shard_iterator(stream_name, shard_id, 'AT_SEQUENCE_NUMBER', second_sequence_id) + response = conn.get_shard_iterator( + stream_name, shard_id, 'AT_SEQUENCE_NUMBER', second_sequence_id) shard_iterator = response['ShardIterator'] response = conn.get_records(shard_iterator) @@ -169,7 +175,8 @@ def test_get_records_at_sequence_number(): @mock_kinesis_deprecated def test_get_records_after_sequence_number(): - # AFTER_SEQUENCE_NUMBER - Start reading right after the position denoted by a specific sequence number. + # AFTER_SEQUENCE_NUMBER - Start reading right after the position denoted + # by a specific sequence number. conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" conn.create_stream(stream_name, 1) @@ -189,7 +196,8 @@ def test_get_records_after_sequence_number(): second_sequence_id = response['Records'][1]['SequenceNumber'] # Then get a new iterator starting after that id - response = conn.get_shard_iterator(stream_name, shard_id, 'AFTER_SEQUENCE_NUMBER', second_sequence_id) + response = conn.get_shard_iterator( + stream_name, shard_id, 'AFTER_SEQUENCE_NUMBER', second_sequence_id) shard_iterator = response['ShardIterator'] response = conn.get_records(shard_iterator) @@ -199,7 +207,8 @@ def test_get_records_after_sequence_number(): @mock_kinesis_deprecated def test_get_records_latest(): - # LATEST - Start reading just after the most recent record in the shard, so that you always read the most recent data in the shard. + # LATEST - Start reading just after the most recent record in the shard, + # so that you always read the most recent data in the shard. conn = boto.kinesis.connect_to_region("us-west-2") stream_name = "my_stream" conn.create_stream(stream_name, 1) @@ -219,7 +228,8 @@ def test_get_records_latest(): second_sequence_id = response['Records'][1]['SequenceNumber'] # Then get a new iterator starting after that id - response = conn.get_shard_iterator(stream_name, shard_id, 'LATEST', second_sequence_id) + response = conn.get_shard_iterator( + stream_name, shard_id, 'LATEST', second_sequence_id) shard_iterator = response['ShardIterator'] # Write some more data @@ -251,10 +261,10 @@ def test_add_tags(): conn.create_stream(stream_name, 1) conn.describe_stream(stream_name) - conn.add_tags_to_stream(stream_name, {'tag1':'val1'}) - conn.add_tags_to_stream(stream_name, {'tag2':'val2'}) - conn.add_tags_to_stream(stream_name, {'tag1':'val3'}) - conn.add_tags_to_stream(stream_name, {'tag2':'val4'}) + conn.add_tags_to_stream(stream_name, {'tag1': 'val1'}) + conn.add_tags_to_stream(stream_name, {'tag2': 'val2'}) + conn.add_tags_to_stream(stream_name, {'tag1': 'val3'}) + conn.add_tags_to_stream(stream_name, {'tag2': 'val4'}) @mock_kinesis_deprecated @@ -264,17 +274,21 @@ def test_list_tags(): conn.create_stream(stream_name, 1) conn.describe_stream(stream_name) - conn.add_tags_to_stream(stream_name, {'tag1':'val1'}) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + conn.add_tags_to_stream(stream_name, {'tag1': 'val1'}) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag1').should.equal('val1') - conn.add_tags_to_stream(stream_name, {'tag2':'val2'}) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + conn.add_tags_to_stream(stream_name, {'tag2': 'val2'}) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag2').should.equal('val2') - conn.add_tags_to_stream(stream_name, {'tag1':'val3'}) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + conn.add_tags_to_stream(stream_name, {'tag1': 'val3'}) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag1').should.equal('val3') - conn.add_tags_to_stream(stream_name, {'tag2':'val4'}) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + conn.add_tags_to_stream(stream_name, {'tag2': 'val4'}) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag2').should.equal('val4') @@ -285,18 +299,22 @@ def test_remove_tags(): conn.create_stream(stream_name, 1) conn.describe_stream(stream_name) - conn.add_tags_to_stream(stream_name, {'tag1':'val1'}) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + conn.add_tags_to_stream(stream_name, {'tag1': 'val1'}) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag1').should.equal('val1') conn.remove_tags_from_stream(stream_name, ['tag1']) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag1').should.equal(None) - conn.add_tags_to_stream(stream_name, {'tag2':'val2'}) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + conn.add_tags_to_stream(stream_name, {'tag2': 'val2'}) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag2').should.equal('val2') conn.remove_tags_from_stream(stream_name, ['tag2']) - tags = dict([(tag['Key'], tag['Value']) for tag in conn.list_tags_for_stream(stream_name)['Tags']]) + tags = dict([(tag['Key'], tag['Value']) + for tag in conn.list_tags_for_stream(stream_name)['Tags']]) tags.get('tag2').should.equal(None) @@ -316,10 +334,12 @@ def test_split_shard(): stream = stream_response["StreamDescription"] shards = stream['Shards'] shards.should.have.length_of(2) - sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) + sum([shard['SequenceNumberRange']['EndingSequenceNumber'] + for shard in shards]).should.equal(99) shard_range = shards[0]['HashKeyRange'] - new_starting_hash = (int(shard_range['EndingHashKey'])+int(shard_range['StartingHashKey'])) // 2 + new_starting_hash = ( + int(shard_range['EndingHashKey']) + int(shard_range['StartingHashKey'])) // 2 conn.split_shard("my_stream", shards[0]['ShardId'], str(new_starting_hash)) stream_response = conn.describe_stream(stream_name) @@ -327,10 +347,12 @@ def test_split_shard(): stream = stream_response["StreamDescription"] shards = stream['Shards'] shards.should.have.length_of(3) - sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) + sum([shard['SequenceNumberRange']['EndingSequenceNumber'] + for shard in shards]).should.equal(99) shard_range = shards[2]['HashKeyRange'] - new_starting_hash = (int(shard_range['EndingHashKey'])+int(shard_range['StartingHashKey'])) // 2 + new_starting_hash = ( + int(shard_range['EndingHashKey']) + int(shard_range['StartingHashKey'])) // 2 conn.split_shard("my_stream", shards[2]['ShardId'], str(new_starting_hash)) stream_response = conn.describe_stream(stream_name) @@ -338,7 +360,8 @@ def test_split_shard(): stream = stream_response["StreamDescription"] shards = stream['Shards'] shards.should.have.length_of(4) - sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) + sum([shard['SequenceNumberRange']['EndingSequenceNumber'] + for shard in shards]).should.equal(99) @mock_kinesis_deprecated @@ -358,28 +381,34 @@ def test_merge_shards(): shards = stream['Shards'] shards.should.have.length_of(4) - conn.merge_shards.when.called_with(stream_name, 'shardId-000000000000', 'shardId-000000000002').should.throw(InvalidArgumentException) + conn.merge_shards.when.called_with( + stream_name, 'shardId-000000000000', 'shardId-000000000002').should.throw(InvalidArgumentException) stream_response = conn.describe_stream(stream_name) stream = stream_response["StreamDescription"] shards = stream['Shards'] shards.should.have.length_of(4) - sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) + sum([shard['SequenceNumberRange']['EndingSequenceNumber'] + for shard in shards]).should.equal(99) - conn.merge_shards(stream_name, 'shardId-000000000000', 'shardId-000000000001') + conn.merge_shards(stream_name, 'shardId-000000000000', + 'shardId-000000000001') stream_response = conn.describe_stream(stream_name) stream = stream_response["StreamDescription"] shards = stream['Shards'] shards.should.have.length_of(3) - sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) - conn.merge_shards(stream_name, 'shardId-000000000002', 'shardId-000000000000') + sum([shard['SequenceNumberRange']['EndingSequenceNumber'] + for shard in shards]).should.equal(99) + conn.merge_shards(stream_name, 'shardId-000000000002', + 'shardId-000000000000') stream_response = conn.describe_stream(stream_name) stream = stream_response["StreamDescription"] shards = stream['Shards'] shards.should.have.length_of(2) - sum([shard['SequenceNumberRange']['EndingSequenceNumber'] for shard in shards]).should.equal(99) + sum([shard['SequenceNumberRange']['EndingSequenceNumber'] + for shard in shards]).should.equal(99) diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index 27850d4ad..e1468cce0 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -8,11 +8,13 @@ import sure # noqa from moto import mock_kms_deprecated from nose.tools import assert_raises + @mock_kms_deprecated def test_create_key(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key['KeyMetadata']['Description'].should.equal("my key") key['KeyMetadata']['KeyUsage'].should.equal("ENCRYPT_DECRYPT") @@ -22,7 +24,8 @@ def test_create_key(): @mock_kms_deprecated def test_describe_key(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] key = conn.describe_key(key_id) @@ -33,8 +36,10 @@ def test_describe_key(): @mock_kms_deprecated def test_describe_key_via_alias(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') - conn.create_alias(alias_name='alias/my-key-alias', target_key_id=key['KeyMetadata']['KeyId']) + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') + conn.create_alias(alias_name='alias/my-key-alias', + target_key_id=key['KeyMetadata']['KeyId']) alias_key = conn.describe_key('alias/my-key-alias') alias_key['KeyMetadata']['Description'].should.equal("my key") @@ -45,16 +50,20 @@ def test_describe_key_via_alias(): @mock_kms_deprecated def test_describe_key_via_alias_not_found(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') - conn.create_alias(alias_name='alias/my-key-alias', target_key_id=key['KeyMetadata']['KeyId']) + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') + conn.create_alias(alias_name='alias/my-key-alias', + target_key_id=key['KeyMetadata']['KeyId']) - conn.describe_key.when.called_with('alias/not-found-alias').should.throw(JSONResponseError) + conn.describe_key.when.called_with( + 'alias/not-found-alias').should.throw(JSONResponseError) @mock_kms_deprecated def test_describe_key_via_arn(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') arn = key['KeyMetadata']['Arn'] the_key = conn.describe_key(arn) @@ -66,15 +75,18 @@ def test_describe_key_via_arn(): @mock_kms_deprecated def test_describe_missing_key(): conn = boto.kms.connect_to_region("us-west-2") - conn.describe_key.when.called_with("not-a-key").should.throw(JSONResponseError) + conn.describe_key.when.called_with( + "not-a-key").should.throw(JSONResponseError) @mock_kms_deprecated def test_list_keys(): conn = boto.kms.connect_to_region("us-west-2") - conn.create_key(policy="my policy", description="my key1", key_usage='ENCRYPT_DECRYPT') - conn.create_key(policy="my policy", description="my key2", key_usage='ENCRYPT_DECRYPT') + conn.create_key(policy="my policy", description="my key1", + key_usage='ENCRYPT_DECRYPT') + conn.create_key(policy="my policy", description="my key2", + key_usage='ENCRYPT_DECRYPT') keys = conn.list_keys() keys['Keys'].should.have.length_of(2) @@ -84,56 +96,67 @@ def test_list_keys(): def test_enable_key_rotation(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] conn.enable_key_rotation(key_id) - conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(True) + conn.get_key_rotation_status( + key_id)['KeyRotationEnabled'].should.equal(True) + @mock_kms_deprecated def test_enable_key_rotation_via_arn(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['Arn'] conn.enable_key_rotation(key_id) - conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(True) - + conn.get_key_rotation_status( + key_id)['KeyRotationEnabled'].should.equal(True) @mock_kms_deprecated def test_enable_key_rotation_with_missing_key(): conn = boto.kms.connect_to_region("us-west-2") - conn.enable_key_rotation.when.called_with("not-a-key").should.throw(JSONResponseError) + conn.enable_key_rotation.when.called_with( + "not-a-key").should.throw(JSONResponseError) @mock_kms_deprecated def test_enable_key_rotation_with_alias_name_should_fail(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') - conn.create_alias(alias_name='alias/my-key-alias', target_key_id=key['KeyMetadata']['KeyId']) + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') + conn.create_alias(alias_name='alias/my-key-alias', + target_key_id=key['KeyMetadata']['KeyId']) alias_key = conn.describe_key('alias/my-key-alias') alias_key['KeyMetadata']['Arn'].should.equal(key['KeyMetadata']['Arn']) - conn.enable_key_rotation.when.called_with('alias/my-alias').should.throw(JSONResponseError) + conn.enable_key_rotation.when.called_with( + 'alias/my-alias').should.throw(JSONResponseError) @mock_kms_deprecated def test_disable_key_rotation(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] conn.enable_key_rotation(key_id) - conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(True) + conn.get_key_rotation_status( + key_id)['KeyRotationEnabled'].should.equal(True) conn.disable_key_rotation(key_id) - conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) + conn.get_key_rotation_status( + key_id)['KeyRotationEnabled'].should.equal(False) @mock_kms_deprecated @@ -157,59 +180,70 @@ def test_decrypt(): @mock_kms_deprecated def test_disable_key_rotation_with_missing_key(): conn = boto.kms.connect_to_region("us-west-2") - conn.disable_key_rotation.when.called_with("not-a-key").should.throw(JSONResponseError) + conn.disable_key_rotation.when.called_with( + "not-a-key").should.throw(JSONResponseError) @mock_kms_deprecated def test_get_key_rotation_status_with_missing_key(): conn = boto.kms.connect_to_region("us-west-2") - conn.get_key_rotation_status.when.called_with("not-a-key").should.throw(JSONResponseError) + conn.get_key_rotation_status.when.called_with( + "not-a-key").should.throw(JSONResponseError) @mock_kms_deprecated def test_get_key_rotation_status(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] - conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) + conn.get_key_rotation_status( + key_id)['KeyRotationEnabled'].should.equal(False) @mock_kms_deprecated def test_create_key_defaults_key_rotation(): conn = boto.kms.connect_to_region("us-west-2") - key = conn.create_key(policy="my policy", description="my key", key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy="my policy", + description="my key", key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] - conn.get_key_rotation_status(key_id)['KeyRotationEnabled'].should.equal(False) + conn.get_key_rotation_status( + key_id)['KeyRotationEnabled'].should.equal(False) @mock_kms_deprecated def test_get_key_policy(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] policy = conn.get_key_policy(key_id, 'default') policy['Policy'].should.equal('my policy') + @mock_kms_deprecated def test_get_key_policy_via_arn(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') policy = conn.get_key_policy(key['KeyMetadata']['Arn'], 'default') policy['Policy'].should.equal('my policy') + @mock_kms_deprecated def test_put_key_policy(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] conn.put_key_policy(key_id, 'default', 'new policy') @@ -221,7 +255,8 @@ def test_put_key_policy(): def test_put_key_policy_via_arn(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['Arn'] conn.put_key_policy(key_id, 'default', 'new policy') @@ -233,10 +268,13 @@ def test_put_key_policy_via_arn(): def test_put_key_policy_via_alias_should_not_update(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') - conn.create_alias(alias_name='alias/my-key-alias', target_key_id=key['KeyMetadata']['KeyId']) + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') + conn.create_alias(alias_name='alias/my-key-alias', + target_key_id=key['KeyMetadata']['KeyId']) - conn.put_key_policy.when.called_with('alias/my-key-alias', 'default', 'new policy').should.throw(JSONResponseError) + conn.put_key_policy.when.called_with( + 'alias/my-key-alias', 'default', 'new policy').should.throw(JSONResponseError) policy = conn.get_key_policy(key['KeyMetadata']['KeyId'], 'default') policy['Policy'].should.equal('my policy') @@ -246,7 +284,8 @@ def test_put_key_policy_via_alias_should_not_update(): def test_put_key_policy(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') conn.put_key_policy(key['KeyMetadata']['Arn'], 'default', 'new policy') policy = conn.get_key_policy(key['KeyMetadata']['KeyId'], 'default') @@ -257,7 +296,8 @@ def test_put_key_policy(): def test_list_key_policies(): conn = boto.kms.connect_to_region('us-west-2') - key = conn.create_key(policy='my policy', description='my key1', key_usage='ENCRYPT_DECRYPT') + key = conn.create_key(policy='my policy', + description='my key1', key_usage='ENCRYPT_DECRYPT') key_id = key['KeyMetadata']['KeyId'] policies = conn.list_key_policies(key_id) @@ -323,7 +363,8 @@ def test__create_alias__raises_if_wrong_prefix(): ex = err.exception ex.error_message.should.equal('Invalid identifier') ex.error_code.should.equal('ValidationException') - ex.body.should.equal({'message': 'Invalid identifier', '__type': 'ValidationException'}) + ex.body.should.equal({'message': 'Invalid identifier', + '__type': 'ValidationException'}) ex.reason.should.equal('Bad Request') ex.status.should.equal(400) @@ -371,16 +412,19 @@ def test__create_alias__raises_if_alias_has_restricted_characters(): kms.create_alias(alias_name, key_id) ex = err.exception ex.body['__type'].should.equal('ValidationException') - ex.body['message'].should.equal("1 validation error detected: Value '{alias_name}' at 'aliasName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9:/_-]+$".format(**locals())) + ex.body['message'].should.equal( + "1 validation error detected: Value '{alias_name}' at 'aliasName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9:/_-]+$".format(**locals())) ex.error_code.should.equal('ValidationException') - ex.message.should.equal("1 validation error detected: Value '{alias_name}' at 'aliasName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9:/_-]+$".format(**locals())) + ex.message.should.equal( + "1 validation error detected: Value '{alias_name}' at 'aliasName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9:/_-]+$".format(**locals())) ex.reason.should.equal('Bad Request') ex.status.should.equal(400) @mock_kms_deprecated def test__create_alias__raises_if_alias_has_colon_character(): - # For some reason, colons are not accepted for an alias, even though they are accepted by regex ^[a-zA-Z0-9:/_-]+$ + # For some reason, colons are not accepted for an alias, even though they + # are accepted by regex ^[a-zA-Z0-9:/_-]+$ kms = boto.connect_kms() create_resp = kms.create_key() key_id = create_resp['KeyMetadata']['KeyId'] @@ -394,9 +438,11 @@ def test__create_alias__raises_if_alias_has_colon_character(): kms.create_alias(alias_name, key_id) ex = err.exception ex.body['__type'].should.equal('ValidationException') - ex.body['message'].should.equal("{alias_name} contains invalid characters for an alias".format(**locals())) + ex.body['message'].should.equal( + "{alias_name} contains invalid characters for an alias".format(**locals())) ex.error_code.should.equal('ValidationException') - ex.message.should.equal("{alias_name} contains invalid characters for an alias".format(**locals())) + ex.message.should.equal( + "{alias_name} contains invalid characters for an alias".format(**locals())) ex.reason.should.equal('Bad Request') ex.status.should.equal(400) @@ -481,10 +527,12 @@ def test__delete_alias__raises_if_alias_is_not_found(): ex = err.exception ex.body['__type'].should.equal('NotFoundException') - ex.body['message'].should.match(r'Alias arn:aws:kms:{region}:\d{{12}}:{alias_name} is not found.'.format(**locals())) + ex.body['message'].should.match( + r'Alias arn:aws:kms:{region}:\d{{12}}:{alias_name} is not found.'.format(**locals())) ex.box_usage.should.be.none ex.error_code.should.be.none - ex.message.should.match(r'Alias arn:aws:kms:{region}:\d{{12}}:{alias_name} is not found.'.format(**locals())) + ex.message.should.match( + r'Alias arn:aws:kms:{region}:\d{{12}}:{alias_name} is not found.'.format(**locals())) ex.reason.should.equal('Bad Request') ex.request_id.should.be.none ex.status.should.equal(400) @@ -527,7 +575,8 @@ def test__list_aliases(): len([alias for alias in aliases if has_correct_arn(alias) and 'alias/my-alias2' == alias['AliasName']]).should.equal(1) - len([alias for alias in aliases if 'TargetKeyId' in alias and key_id == alias['TargetKeyId']]).should.equal(3) + len([alias for alias in aliases if 'TargetKeyId' in alias and key_id == + alias['TargetKeyId']]).should.equal(3) len(aliases).should.equal(7) @@ -537,13 +586,17 @@ def test__assert_valid_key_id(): from moto.kms.responses import _assert_valid_key_id import uuid - _assert_valid_key_id.when.called_with("not-a-key").should.throw(JSONResponseError) - _assert_valid_key_id.when.called_with(str(uuid.uuid4())).should_not.throw(JSONResponseError) + _assert_valid_key_id.when.called_with( + "not-a-key").should.throw(JSONResponseError) + _assert_valid_key_id.when.called_with( + str(uuid.uuid4())).should_not.throw(JSONResponseError) @mock_kms_deprecated def test__assert_default_policy(): from moto.kms.responses import _assert_default_policy - _assert_default_policy.when.called_with("not-default").should.throw(JSONResponseError) - _assert_default_policy.when.called_with("default").should_not.throw(JSONResponseError) + _assert_default_policy.when.called_with( + "not-default").should.throw(JSONResponseError) + _assert_default_policy.when.called_with( + "default").should_not.throw(JSONResponseError) diff --git a/tests/test_opsworks/test_instances.py b/tests/test_opsworks/test_instances.py index e24486a2f..9c9e20878 100644 --- a/tests/test_opsworks/test_instances.py +++ b/tests/test_opsworks/test_instances.py @@ -102,7 +102,8 @@ def test_describe_instances(): S1L1_i1.should.be.within([i["InstanceId"] for i in response]) S1L1_i2.should.be.within([i["InstanceId"] for i in response]) - response2 = client.describe_instances(InstanceIds=[S1L1_i1, S1L1_i2])['Instances'] + response2 = client.describe_instances( + InstanceIds=[S1L1_i1, S1L1_i2])['Instances'] sorted(response2, key=lambda d: d['InstanceId']).should.equal( sorted(response, key=lambda d: d['InstanceId'])) @@ -168,9 +169,8 @@ def test_ec2_integration(): reservations = ec2.describe_instances()['Reservations'] reservations[0]['Instances'].should.have.length_of(1) instance = reservations[0]['Instances'][0] - opsworks_instance = opsworks.describe_instances(StackId=stack_id)['Instances'][0] + opsworks_instance = opsworks.describe_instances(StackId=stack_id)[ + 'Instances'][0] instance['InstanceId'].should.equal(opsworks_instance['Ec2InstanceId']) instance['PrivateIpAddress'].should.equal(opsworks_instance['PrivateIp']) - - diff --git a/tests/test_opsworks/test_layers.py b/tests/test_opsworks/test_layers.py index dc268bbe5..31fdeae8c 100644 --- a/tests/test_opsworks/test_layers.py +++ b/tests/test_opsworks/test_layers.py @@ -43,7 +43,8 @@ def test_create_layer_response(): Name="_", Shortname="TestLayerShortName" ).should.throw( - Exception, re.compile(r'already a layer with shortname "TestLayerShortName"') + Exception, re.compile( + r'already a layer with shortname "TestLayerShortName"') ) @@ -69,4 +70,3 @@ def test_describe_layers(): rv1['Layers'].should.equal(rv2['Layers']) rv1['Layers'][0]['Name'].should.equal("TestLayer") - diff --git a/tests/test_opsworks/test_stack.py b/tests/test_opsworks/test_stack.py index 8d86e4207..5913ce6d5 100644 --- a/tests/test_opsworks/test_stack.py +++ b/tests/test_opsworks/test_stack.py @@ -44,5 +44,3 @@ def test_describe_stacks(): client.describe_stacks.when.called_with(StackIds=["foo"]).should.throw( Exception, re.compile(r'foo') ) - - diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index 7a6cab633..090147d11 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -15,14 +15,15 @@ def test_create_database(): conn = boto.rds.connect_to_region("us-west-2") database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2', - security_groups=["my_sg"]) + security_groups=["my_sg"]) database.status.should.equal('available') database.id.should.equal("db-master-1") database.allocated_storage.should.equal(10) database.instance_class.should.equal("db.m1.small") database.master_username.should.equal("root") - database.endpoint.should.equal(('db-master-1.aaaaaaaaaa.us-west-2.rds.amazonaws.com', 3306)) + database.endpoint.should.equal( + ('db-master-1.aaaaaaaaaa.us-west-2.rds.amazonaws.com', 3306)) database.security_groups[0].name.should.equal('my_sg') @@ -47,7 +48,8 @@ def test_get_databases(): @mock_rds_deprecated def test_describe_non_existant_database(): conn = boto.rds.connect_to_region("us-west-2") - conn.get_all_dbinstances.when.called_with("not-a-db").should.throw(BotoServerError) + conn.get_all_dbinstances.when.called_with( + "not-a-db").should.throw(BotoServerError) @disable_on_py3() @@ -66,7 +68,8 @@ def test_delete_database(): @mock_rds_deprecated def test_delete_non_existant_database(): conn = boto.rds.connect_to_region("us-west-2") - conn.delete_dbinstance.when.called_with("not-a-db").should.throw(BotoServerError) + conn.delete_dbinstance.when.called_with( + "not-a-db").should.throw(BotoServerError) @mock_rds_deprecated @@ -99,7 +102,8 @@ def test_get_security_groups(): @mock_rds_deprecated def test_get_non_existant_security_group(): conn = boto.rds.connect_to_region("us-west-2") - conn.get_all_dbsecurity_groups.when.called_with("not-a-sg").should.throw(BotoServerError) + conn.get_all_dbsecurity_groups.when.called_with( + "not-a-sg").should.throw(BotoServerError) @mock_rds_deprecated @@ -116,7 +120,8 @@ def test_delete_database_security_group(): @mock_rds_deprecated def test_delete_non_existant_security_group(): conn = boto.rds.connect_to_region("us-west-2") - conn.delete_dbsecurity_group.when.called_with("not-a-db").should.throw(BotoServerError) + conn.delete_dbsecurity_group.when.called_with( + "not-a-db").should.throw(BotoServerError) @disable_on_py3() @@ -137,7 +142,8 @@ def test_security_group_authorize(): def test_add_security_group_to_database(): conn = boto.rds.connect_to_region("us-west-2") - database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + database = conn.create_dbinstance( + "db-master-1", 10, 'db.m1.small', 'root', 'hunter2') security_group = conn.create_dbsecurity_group('db_sg', 'DB Security Group') database.modify(security_groups=[security_group]) @@ -157,7 +163,8 @@ def test_add_database_subnet_group(): subnet_ids = [subnet1.id, subnet2.id] conn = boto.rds.connect_to_region("us-west-2") - subnet_group = conn.create_db_subnet_group("db_subnet", "my db subnet", subnet_ids) + subnet_group = conn.create_db_subnet_group( + "db_subnet", "my db subnet", subnet_ids) subnet_group.name.should.equal('db_subnet') subnet_group.description.should.equal("my db subnet") list(subnet_group.subnet_ids).should.equal(subnet_ids) @@ -177,7 +184,8 @@ def test_describe_database_subnet_group(): list(conn.get_all_db_subnet_groups()).should.have.length_of(2) list(conn.get_all_db_subnet_groups("db_subnet1")).should.have.length_of(1) - conn.get_all_db_subnet_groups.when.called_with("not-a-subnet").should.throw(BotoServerError) + conn.get_all_db_subnet_groups.when.called_with( + "not-a-subnet").should.throw(BotoServerError) @mock_ec2_deprecated @@ -194,7 +202,8 @@ def test_delete_database_subnet_group(): conn.delete_db_subnet_group("db_subnet1") list(conn.get_all_db_subnet_groups()).should.have.length_of(0) - conn.delete_db_subnet_group.when.called_with("db_subnet1").should.throw(BotoServerError) + conn.delete_db_subnet_group.when.called_with( + "db_subnet1").should.throw(BotoServerError) @disable_on_py3() @@ -209,7 +218,7 @@ def test_create_database_in_subnet_group(): conn.create_db_subnet_group("db_subnet1", "my db subnet", [subnet.id]) database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', - 'root', 'hunter2', db_subnet_group_name="db_subnet1") + 'root', 'hunter2', db_subnet_group_name="db_subnet1") database = conn.get_all_dbinstances("db-master-1")[0] database.subnet_group.name.should.equal("db_subnet1") @@ -220,9 +229,11 @@ def test_create_database_in_subnet_group(): def test_create_database_replica(): conn = boto.rds.connect_to_region("us-west-2") - primary = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + primary = conn.create_dbinstance( + "db-master-1", 10, 'db.m1.small', 'root', 'hunter2') - replica = conn.create_dbinstance_read_replica("replica", "db-master-1", "db.m1.small") + replica = conn.create_dbinstance_read_replica( + "replica", "db-master-1", "db.m1.small") replica.id.should.equal("replica") replica.instance_class.should.equal("db.m1.small") status_info = replica.status_infos[0] @@ -238,13 +249,15 @@ def test_create_database_replica(): primary = conn.get_all_dbinstances("db-master-1")[0] list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) + @disable_on_py3() @mock_rds_deprecated def test_create_cross_region_database_replica(): west_1_conn = boto.rds.connect_to_region("us-west-1") west_2_conn = boto.rds.connect_to_region("us-west-2") - primary = west_1_conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2') + primary = west_1_conn.create_dbinstance( + "db-master-1", 10, 'db.m1.small', 'root', 'hunter2') primary_arn = "arn:aws:rds:us-west-1:1234567890:db:db-master-1" replica = west_2_conn.create_dbinstance_read_replica( @@ -274,14 +287,15 @@ def test_connecting_to_us_east_1(): conn = boto.rds.connect_to_region("us-east-1") database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2', - security_groups=["my_sg"]) + security_groups=["my_sg"]) database.status.should.equal('available') database.id.should.equal("db-master-1") database.allocated_storage.should.equal(10) database.instance_class.should.equal("db.m1.small") database.master_username.should.equal("root") - database.endpoint.should.equal(('db-master-1.aaaaaaaaaa.us-east-1.rds.amazonaws.com', 3306)) + database.endpoint.should.equal( + ('db-master-1.aaaaaaaaaa.us-east-1.rds.amazonaws.com', 3306)) database.security_groups[0].name.should.equal('my_sg') @@ -290,7 +304,8 @@ def test_connecting_to_us_east_1(): def test_create_database_with_iops(): conn = boto.rds.connect_to_region("us-west-2") - database = conn.create_dbinstance("db-master-1", 10, 'db.m1.small', 'root', 'hunter2', iops=6000) + database = conn.create_dbinstance( + "db-master-1", 10, 'db.m1.small', 'root', 'hunter2', iops=6000) database.status.should.equal('available') database.iops.should.equal(6000) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 581209655..731bc75c1 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -24,7 +24,8 @@ def test_create_database(): database['DBInstance']['AllocatedStorage'].should.equal(10) database['DBInstance']['DBInstanceClass'].should.equal("db.m1.small") database['DBInstance']['MasterUsername'].should.equal("root") - database['DBInstance']['DBSecurityGroups'][0]['DBSecurityGroupName'].should.equal('my_sg') + database['DBInstance']['DBSecurityGroups'][0][ + 'DBSecurityGroupName'].should.equal('my_sg') @disable_on_py3() @@ -56,14 +57,16 @@ def test_get_databases(): instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") list(instances['DBInstances']).should.have.length_of(1) - instances['DBInstances'][0]['DBInstanceIdentifier'].should.equal("db-master-1") + instances['DBInstances'][0][ + 'DBInstanceIdentifier'].should.equal("db-master-1") @disable_on_py3() @mock_rds2 def test_describe_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') - conn.describe_db_instances.when.called_with(DBInstanceIdentifier="not-a-db").should.throw(ClientError) + conn.describe_db_instances.when.called_with( + DBInstanceIdentifier="not-a-db").should.throw(ClientError) @disable_on_py3() @@ -95,6 +98,7 @@ def test_modify_non_existant_database(): AllocatedStorage=20, ApplyImmediately=True).should.throw(ClientError) + @disable_on_py3() @mock_rds2 def test_reboot_db_instance(): @@ -115,7 +119,8 @@ def test_reboot_db_instance(): @mock_rds2 def test_reboot_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') - conn.reboot_db_instance.when.called_with(DBInstanceIdentifier="not-a-db").should.throw(ClientError) + conn.reboot_db_instance.when.called_with( + DBInstanceIdentifier="not-a-db").should.throw(ClientError) @disable_on_py3() @@ -144,7 +149,8 @@ def test_delete_database(): @mock_rds2 def test_delete_non_existant_database(): conn = boto3.client('rds2', region_name="us-west-2") - conn.delete_db_instance.when.called_with(DBInstanceIdentifier="not-a-db").should.throw(ClientError) + conn.delete_db_instance.when.called_with( + DBInstanceIdentifier="not-a-db").should.throw(ClientError) @disable_on_py3() @@ -157,7 +163,8 @@ def test_create_option_group(): OptionGroupDescription='test option group') option_group['OptionGroup']['OptionGroupName'].should.equal('test') option_group['OptionGroup']['EngineName'].should.equal('mysql') - option_group['OptionGroup']['OptionGroupDescription'].should.equal('test option group') + option_group['OptionGroup'][ + 'OptionGroupDescription'].should.equal('test option group') option_group['OptionGroup']['MajorEngineVersion'].should.equal('5.6') @@ -214,14 +221,16 @@ def test_describe_option_group(): MajorEngineVersion='5.6', OptionGroupDescription='test option group') option_groups = conn.describe_option_groups(OptionGroupName='test') - option_groups['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') + option_groups['OptionGroupsList'][0][ + 'OptionGroupName'].should.equal('test') @disable_on_py3() @mock_rds2 def test_describe_non_existant_option_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.describe_option_groups.when.called_with(OptionGroupName="not-a-option-group").should.throw(ClientError) + conn.describe_option_groups.when.called_with( + OptionGroupName="not-a-option-group").should.throw(ClientError) @disable_on_py3() @@ -233,41 +242,51 @@ def test_delete_option_group(): MajorEngineVersion='5.6', OptionGroupDescription='test option group') option_groups = conn.describe_option_groups(OptionGroupName='test') - option_groups['OptionGroupsList'][0]['OptionGroupName'].should.equal('test') + option_groups['OptionGroupsList'][0][ + 'OptionGroupName'].should.equal('test') conn.delete_option_group(OptionGroupName='test') - conn.describe_option_groups.when.called_with(OptionGroupName='test').should.throw(ClientError) + conn.describe_option_groups.when.called_with( + OptionGroupName='test').should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_delete_non_existant_option_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.delete_option_group.when.called_with(OptionGroupName='non-existant').should.throw(ClientError) + conn.delete_option_group.when.called_with( + OptionGroupName='non-existant').should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_describe_option_group_options(): conn = boto3.client('rds', region_name='us-west-2') - option_group_options = conn.describe_option_group_options(EngineName='sqlserver-ee') + option_group_options = conn.describe_option_group_options( + EngineName='sqlserver-ee') len(option_group_options['OptionGroupOptions']).should.equal(4) - option_group_options = conn.describe_option_group_options(EngineName='sqlserver-ee', MajorEngineVersion='11.00') + option_group_options = conn.describe_option_group_options( + EngineName='sqlserver-ee', MajorEngineVersion='11.00') len(option_group_options['OptionGroupOptions']).should.equal(2) - option_group_options = conn.describe_option_group_options(EngineName='mysql', MajorEngineVersion='5.6') + option_group_options = conn.describe_option_group_options( + EngineName='mysql', MajorEngineVersion='5.6') len(option_group_options['OptionGroupOptions']).should.equal(1) - conn.describe_option_group_options.when.called_with(EngineName='non-existent').should.throw(ClientError) - conn.describe_option_group_options.when.called_with(EngineName='mysql', MajorEngineVersion='non-existent').should.throw(ClientError) + conn.describe_option_group_options.when.called_with( + EngineName='non-existent').should.throw(ClientError) + conn.describe_option_group_options.when.called_with( + EngineName='mysql', MajorEngineVersion='non-existent').should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_modify_option_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.create_option_group(OptionGroupName='test', EngineName='mysql', MajorEngineVersion='5.6', OptionGroupDescription='test option group') + conn.create_option_group(OptionGroupName='test', EngineName='mysql', + MajorEngineVersion='5.6', OptionGroupDescription='test option group') # TODO: create option and validate before deleting. # if Someone can tell me how the hell to use this function # to add options to an option_group, I can finish coding this. - result = conn.modify_option_group(OptionGroupName='test', OptionsToInclude=[], OptionsToRemove=['MEMCACHED'], ApplyImmediately=True) + result = conn.modify_option_group(OptionGroupName='test', OptionsToInclude=[ + ], OptionsToRemove=['MEMCACHED'], ApplyImmediately=True) result['OptionGroup']['EngineName'].should.equal('mysql') result['OptionGroup']['Options'].should.equal([]) result['OptionGroup']['OptionGroupName'].should.equal('test') @@ -277,36 +296,42 @@ def test_modify_option_group(): @mock_rds2 def test_modify_option_group_no_options(): conn = boto3.client('rds', region_name='us-west-2') - conn.create_option_group(OptionGroupName='test', EngineName='mysql', MajorEngineVersion='5.6', OptionGroupDescription='test option group') - conn.modify_option_group.when.called_with(OptionGroupName='test').should.throw(ClientError) + conn.create_option_group(OptionGroupName='test', EngineName='mysql', + MajorEngineVersion='5.6', OptionGroupDescription='test option group') + conn.modify_option_group.when.called_with( + OptionGroupName='test').should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_modify_non_existant_option_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.modify_option_group.when.called_with(OptionGroupName='non-existant', OptionsToInclude=[('OptionName', 'Port', 'DBSecurityGroupMemberships', 'VpcSecurityGroupMemberships', 'OptionSettings')]).should.throw(ParamValidationError) + conn.modify_option_group.when.called_with(OptionGroupName='non-existant', OptionsToInclude=[( + 'OptionName', 'Port', 'DBSecurityGroupMemberships', 'VpcSecurityGroupMemberships', 'OptionSettings')]).should.throw(ParamValidationError) @disable_on_py3() @mock_rds2 def test_delete_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') - conn.delete_db_instance.when.called_with(DBInstanceIdentifier="not-a-db").should.throw(ClientError) + conn.delete_db_instance.when.called_with( + DBInstanceIdentifier="not-a-db").should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_list_tags_invalid_arn(): conn = boto3.client('rds', region_name='us-west-2') - conn.list_tags_for_resource.when.called_with(ResourceName='arn:aws:rds:bad-arn').should.throw(ClientError) + conn.list_tags_for_resource.when.called_with( + ResourceName='arn:aws:rds:bad-arn').should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_list_tags_db(): conn = boto3.client('rds', region_name='us-west-2') - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:foo') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:foo') result['TagList'].should.equal([]) conn.create_db_instance(DBInstanceIdentifier='db-with-tags', AllocatedStorage=10, @@ -326,11 +351,12 @@ def test_list_tags_db(): 'Value': 'bar1', }, ]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}, - {'Value': 'bar1', - 'Key': 'foo1'}]) + {'Value': 'bar1', + 'Key': 'foo1'}]) @disable_on_py3() @@ -355,7 +381,8 @@ def test_add_tags_db(): 'Value': 'bar1', }, ]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-without-tags') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-without-tags') list(result['TagList']).should.have.length_of(2) conn.add_tags_to_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-without-tags', Tags=[ @@ -368,7 +395,8 @@ def test_add_tags_db(): 'Value': 'bar2', }, ]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-without-tags') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-without-tags') list(result['TagList']).should.have.length_of(3) @@ -394,10 +422,13 @@ def test_remove_tags_db(): 'Value': 'bar1', }, ]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') list(result['TagList']).should.have.length_of(2) - conn.remove_tags_from_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags', TagKeys=['foo']) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + conn.remove_tags_from_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags', TagKeys=['foo']) + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') len(result['TagList']).should.equal(1) @@ -409,7 +440,8 @@ def test_add_tags_option_group(): EngineName='mysql', MajorEngineVersion='5.6', OptionGroupDescription='test option group') - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') list(result['TagList']).should.have.length_of(0) conn.add_tags_to_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test', Tags=[ @@ -421,7 +453,8 @@ def test_add_tags_option_group(): 'Key': 'foo2', 'Value': 'bar2', }]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') list(result['TagList']).should.have.length_of(2) @@ -433,7 +466,8 @@ def test_remove_tags_option_group(): EngineName='mysql', MajorEngineVersion='5.6', OptionGroupDescription='test option group') - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') conn.add_tags_to_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test', Tags=[ { @@ -444,11 +478,13 @@ def test_remove_tags_option_group(): 'Key': 'foo2', 'Value': 'bar2', }]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') list(result['TagList']).should.have.length_of(2) conn.remove_tags_from_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test', TagKeys=['foo']) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:og:test') list(result['TagList']).should.have.length_of(1) @@ -457,9 +493,11 @@ def test_remove_tags_option_group(): def test_create_database_security_group(): conn = boto3.client('rds', region_name='us-west-2') - result = conn.create_db_security_group(DBSecurityGroupName='db_sg', DBSecurityGroupDescription='DB Security Group') + result = conn.create_db_security_group( + DBSecurityGroupName='db_sg', DBSecurityGroupDescription='DB Security Group') result['DBSecurityGroup']['DBSecurityGroupName'].should.equal("db_sg") - result['DBSecurityGroup']['DBSecurityGroupDescription'].should.equal("DB Security Group") + result['DBSecurityGroup'][ + 'DBSecurityGroupDescription'].should.equal("DB Security Group") result['DBSecurityGroup']['IPRanges'].should.equal([]) @@ -471,8 +509,10 @@ def test_get_security_groups(): result = conn.describe_db_security_groups() result['DBSecurityGroups'].should.have.length_of(0) - conn.create_db_security_group(DBSecurityGroupName='db_sg1', DBSecurityGroupDescription='DB Security Group') - conn.create_db_security_group(DBSecurityGroupName='db_sg2', DBSecurityGroupDescription='DB Security Group') + conn.create_db_security_group( + DBSecurityGroupName='db_sg1', DBSecurityGroupDescription='DB Security Group') + conn.create_db_security_group( + DBSecurityGroupName='db_sg2', DBSecurityGroupDescription='DB Security Group') result = conn.describe_db_security_groups() result['DBSecurityGroups'].should.have.length_of(2) @@ -486,14 +526,16 @@ def test_get_security_groups(): @mock_rds2 def test_get_non_existant_security_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.describe_db_security_groups.when.called_with(DBSecurityGroupName="not-a-sg").should.throw(ClientError) + conn.describe_db_security_groups.when.called_with( + DBSecurityGroupName="not-a-sg").should.throw(ClientError) @disable_on_py3() @mock_rds2 def test_delete_database_security_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.create_db_security_group(DBSecurityGroupName='db_sg', DBSecurityGroupDescription='DB Security Group') + conn.create_db_security_group( + DBSecurityGroupName='db_sg', DBSecurityGroupDescription='DB Security Group') result = conn.describe_db_security_groups() result['DBSecurityGroups'].should.have.length_of(1) @@ -507,7 +549,8 @@ def test_delete_database_security_group(): @mock_rds2 def test_delete_non_existant_security_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.delete_db_security_group.when.called_with(DBSecurityGroupName="not-a-db").should.throw(ClientError) + conn.delete_db_security_group.when.called_with( + DBSecurityGroupName="not-a-db").should.throw(ClientError) @disable_on_py3() @@ -518,13 +561,13 @@ def test_security_group_authorize(): DBSecurityGroupDescription='DB Security Group') security_group['DBSecurityGroup']['IPRanges'].should.equal([]) - conn.authorize_db_security_group_ingress(DBSecurityGroupName='db_sg', CIDRIP='10.3.2.45/32') result = conn.describe_db_security_groups(DBSecurityGroupName="db_sg") result['DBSecurityGroups'][0]['IPRanges'].should.have.length_of(1) - result['DBSecurityGroups'][0]['IPRanges'].should.equal([{'Status': 'authorized', 'CIDRIP': '10.3.2.45/32'}]) + result['DBSecurityGroups'][0]['IPRanges'].should.equal( + [{'Status': 'authorized', 'CIDRIP': '10.3.2.45/32'}]) conn.authorize_db_security_group_ingress(DBSecurityGroupName='db_sg', CIDRIP='10.3.2.46/32') @@ -554,9 +597,10 @@ def test_add_security_group_to_database(): conn.create_db_security_group(DBSecurityGroupName='db_sg', DBSecurityGroupDescription='DB Security Group') conn.modify_db_instance(DBInstanceIdentifier='db-master-1', - DBSecurityGroups=['db_sg']) + DBSecurityGroups=['db_sg']) result = conn.describe_db_instances() - result['DBInstances'][0]['DBSecurityGroups'][0]['DBSecurityGroupName'].should.equal('db_sg') + result['DBInstances'][0]['DBSecurityGroups'][0][ + 'DBSecurityGroupName'].should.equal('db_sg') @disable_on_py3() @@ -572,12 +616,13 @@ def test_list_tags_security_group(): 'Key': 'foo'}, {'Value': 'bar1', 'Key': 'foo1'}])['DBSecurityGroup']['DBSecurityGroupName'] - resource = 'arn:aws:rds:us-west-2:1234567890:secgrp:{0}'.format(security_group) + resource = 'arn:aws:rds:us-west-2:1234567890:secgrp:{0}'.format( + security_group) result = conn.list_tags_for_resource(ResourceName=resource) result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}, - {'Value': 'bar1', - 'Key': 'foo1'}]) + {'Value': 'bar1', + 'Key': 'foo1'}]) @disable_on_py3() @@ -590,7 +635,8 @@ def test_add_tags_security_group(): security_group = conn.create_db_security_group(DBSecurityGroupName="db_sg", DBSecurityGroupDescription='DB Security Group')['DBSecurityGroup']['DBSecurityGroupName'] - resource = 'arn:aws:rds:us-west-2:1234567890:secgrp:{0}'.format(security_group) + resource = 'arn:aws:rds:us-west-2:1234567890:secgrp:{0}'.format( + security_group) conn.add_tags_to_resource(ResourceName=resource, Tags=[{'Value': 'bar', 'Key': 'foo'}, @@ -600,8 +646,9 @@ def test_add_tags_security_group(): result = conn.list_tags_for_resource(ResourceName=resource) result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}, - {'Value': 'bar1', - 'Key': 'foo1'}]) + {'Value': 'bar1', + 'Key': 'foo1'}]) + @disable_on_py3() @mock_rds2 @@ -617,7 +664,8 @@ def test_remove_tags_security_group(): {'Value': 'bar1', 'Key': 'foo1'}])['DBSecurityGroup']['DBSecurityGroupName'] - resource = 'arn:aws:rds:us-west-2:1234567890:secgrp:{0}'.format(security_group) + resource = 'arn:aws:rds:us-west-2:1234567890:secgrp:{0}'.format( + security_group) conn.remove_tags_from_resource(ResourceName=resource, TagKeys=['foo']) result = conn.list_tags_for_resource(ResourceName=resource) @@ -630,8 +678,10 @@ def test_remove_tags_security_group(): def test_create_database_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet1 = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] - subnet2 = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/26')['Subnet'] + subnet1 = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet2 = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/26')['Subnet'] subnet_ids = [subnet1['SubnetId'], subnet2['SubnetId']] conn = boto3.client('rds', region_name='us-west-2') @@ -639,9 +689,11 @@ def test_create_database_subnet_group(): DBSubnetGroupDescription='my db subnet', SubnetIds=subnet_ids) result['DBSubnetGroup']['DBSubnetGroupName'].should.equal("db_subnet") - result['DBSubnetGroup']['DBSubnetGroupDescription'].should.equal("my db subnet") + result['DBSubnetGroup'][ + 'DBSubnetGroupDescription'].should.equal("my db subnet") subnets = result['DBSubnetGroup']['Subnets'] - subnet_group_ids = [subnets[0]['SubnetIdentifier'], subnets[1]['SubnetIdentifier']] + subnet_group_ids = [subnets[0]['SubnetIdentifier'], + subnets[1]['SubnetIdentifier']] list(subnet_group_ids).should.equal(subnet_ids) @@ -651,7 +703,8 @@ def test_create_database_subnet_group(): def test_create_database_in_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] conn = boto3.client('rds', region_name='us-west-2') conn.create_db_subnet_group(DBSubnetGroupName='db_subnet1', @@ -666,7 +719,8 @@ def test_create_database_in_subnet_group(): Port=1234, DBSubnetGroupName='db_subnet1') result = conn.describe_db_instances(DBInstanceIdentifier='db-master-1') - result['DBInstances'][0]['DBSubnetGroup']['DBSubnetGroupName'].should.equal('db_subnet1') + result['DBInstances'][0]['DBSubnetGroup'][ + 'DBSubnetGroupName'].should.equal('db_subnet1') @disable_on_py3() @@ -675,7 +729,8 @@ def test_create_database_in_subnet_group(): def test_describe_database_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] conn = boto3.client('rds', region_name='us-west-2') conn.create_db_subnet_group(DBSubnetGroupName="db_subnet1", @@ -691,9 +746,11 @@ def test_describe_database_subnet_group(): subnets = resp['DBSubnetGroups'][0]['Subnets'] subnets.should.have.length_of(1) - list(conn.describe_db_subnet_groups(DBSubnetGroupName="db_subnet1")['DBSubnetGroups']).should.have.length_of(1) + list(conn.describe_db_subnet_groups(DBSubnetGroupName="db_subnet1") + ['DBSubnetGroups']).should.have.length_of(1) - conn.describe_db_subnet_groups.when.called_with(DBSubnetGroupName="not-a-subnet").should.throw(ClientError) + conn.describe_db_subnet_groups.when.called_with( + DBSubnetGroupName="not-a-subnet").should.throw(ClientError) @disable_on_py3() @@ -702,7 +759,8 @@ def test_describe_database_subnet_group(): def test_delete_database_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] conn = boto3.client('rds', region_name='us-west-2') result = conn.describe_db_subnet_groups() @@ -718,7 +776,8 @@ def test_delete_database_subnet_group(): result = conn.describe_db_subnet_groups() result['DBSubnetGroups'].should.have.length_of(0) - conn.delete_db_subnet_group.when.called_with(DBSubnetGroupName="db_subnet1").should.throw(ClientError) + conn.delete_db_subnet_group.when.called_with( + DBSubnetGroupName="db_subnet1").should.throw(ClientError) @disable_on_py3() @@ -727,7 +786,8 @@ def test_delete_database_subnet_group(): def test_list_tags_database_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] conn = boto3.client('rds', region_name='us-west-2') result = conn.describe_db_subnet_groups() @@ -740,11 +800,13 @@ def test_list_tags_database_subnet_group(): 'Key': 'foo'}, {'Value': 'bar1', 'Key': 'foo1'}])['DBSubnetGroup']['DBSubnetGroupName'] - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:subgrp:{0}'.format(subnet)) + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:subgrp:{0}'.format(subnet)) result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}, - {'Value': 'bar1', - 'Key': 'foo1'}]) + {'Value': 'bar1', + 'Key': 'foo1'}]) + @disable_on_py3() @mock_ec2 @@ -752,7 +814,8 @@ def test_list_tags_database_subnet_group(): def test_add_tags_database_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] conn = boto3.client('rds', region_name='us-west-2') result = conn.describe_db_subnet_groups() @@ -773,8 +836,9 @@ def test_add_tags_database_subnet_group(): result = conn.list_tags_for_resource(ResourceName=resource) result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}, - {'Value': 'bar1', - 'Key': 'foo1'}]) + {'Value': 'bar1', + 'Key': 'foo1'}]) + @disable_on_py3() @mock_ec2 @@ -782,7 +846,8 @@ def test_add_tags_database_subnet_group(): def test_remove_tags_database_subnet_group(): vpc_conn = boto3.client('ec2', 'us-west-2') vpc = vpc_conn.create_vpc(CidrBlock='10.0.0.0/16')['Vpc'] - subnet = vpc_conn.create_subnet(VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] + subnet = vpc_conn.create_subnet( + VpcId=vpc['VpcId'], CidrBlock='10.1.0.0/24')['Subnet'] conn = boto3.client('rds', region_name='us-west-2') result = conn.describe_db_subnet_groups() @@ -820,17 +885,22 @@ def test_create_database_replica(): replica = conn.create_db_instance_read_replica(DBInstanceIdentifier="db-replica-1", SourceDBInstanceIdentifier="db-master-1", DBInstanceClass="db.m1.small") - replica['DBInstance']['ReadReplicaSourceDBInstanceIdentifier'].should.equal('db-master-1') + replica['DBInstance'][ + 'ReadReplicaSourceDBInstanceIdentifier'].should.equal('db-master-1') replica['DBInstance']['DBInstanceClass'].should.equal('db.m1.small') replica['DBInstance']['DBInstanceIdentifier'].should.equal('db-replica-1') master = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") - master['DBInstances'][0]['ReadReplicaDBInstanceIdentifiers'].should.equal(['db-replica-1']) + master['DBInstances'][0]['ReadReplicaDBInstanceIdentifiers'].should.equal([ + 'db-replica-1']) - conn.delete_db_instance(DBInstanceIdentifier="db-replica-1", SkipFinalSnapshot=True) + conn.delete_db_instance( + DBInstanceIdentifier="db-replica-1", SkipFinalSnapshot=True) master = conn.describe_db_instances(DBInstanceIdentifier="db-master-1") - master['DBInstances'][0]['ReadReplicaDBInstanceIdentifiers'].should.equal([]) + master['DBInstances'][0][ + 'ReadReplicaDBInstanceIdentifiers'].should.equal([]) + @disable_on_py3() @mock_rds2 @@ -854,19 +924,25 @@ def test_create_database_with_encrypted_storage(): KmsKeyId=key['KeyMetadata']['KeyId']) database['DBInstance']['StorageEncrypted'].should.equal(True) - database['DBInstance']['KmsKeyId'].should.equal(key['KeyMetadata']['KeyId']) + database['DBInstance']['KmsKeyId'].should.equal( + key['KeyMetadata']['KeyId']) + @disable_on_py3() @mock_rds2 def test_create_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') db_parameter_group = conn.create_db_parameter_group(DBParameterGroupName='test', - DBParameterGroupFamily='mysql5.6', - Description='test parameter group') + DBParameterGroupFamily='mysql5.6', + Description='test parameter group') + + db_parameter_group['DBParameterGroup'][ + 'DBParameterGroupName'].should.equal('test') + db_parameter_group['DBParameterGroup'][ + 'DBParameterGroupFamily'].should.equal('mysql5.6') + db_parameter_group['DBParameterGroup'][ + 'Description'].should.equal('test parameter group') - db_parameter_group['DBParameterGroup']['DBParameterGroupName'].should.equal('test') - db_parameter_group['DBParameterGroup']['DBParameterGroupFamily'].should.equal('mysql5.6') - db_parameter_group['DBParameterGroup']['Description'].should.equal('test parameter group') @disable_on_py3() @mock_rds2 @@ -886,8 +962,11 @@ def test_create_db_instance_with_parameter_group(): Port=1234) len(database['DBInstance']['DBParameterGroups']).should.equal(1) - database['DBInstance']['DBParameterGroups'][0]['DBParameterGroupName'].should.equal('test') - database['DBInstance']['DBParameterGroups'][0]['ParameterApplyStatus'].should.equal('in-sync') + database['DBInstance']['DBParameterGroups'][0][ + 'DBParameterGroupName'].should.equal('test') + database['DBInstance']['DBParameterGroups'][0][ + 'ParameterApplyStatus'].should.equal('in-sync') + @disable_on_py3() @mock_rds2 @@ -902,8 +981,10 @@ def test_modify_db_instance_with_parameter_group(): Port=1234) len(database['DBInstance']['DBParameterGroups']).should.equal(1) - database['DBInstance']['DBParameterGroups'][0]['DBParameterGroupName'].should.equal('default.mysql5.6') - database['DBInstance']['DBParameterGroups'][0]['ParameterApplyStatus'].should.equal('in-sync') + database['DBInstance']['DBParameterGroups'][0][ + 'DBParameterGroupName'].should.equal('default.mysql5.6') + database['DBInstance']['DBParameterGroups'][0][ + 'ParameterApplyStatus'].should.equal('in-sync') db_parameter_group = conn.create_db_parameter_group(DBParameterGroupName='test', DBParameterGroupFamily='mysql5.6', @@ -912,10 +993,13 @@ def test_modify_db_instance_with_parameter_group(): DBParameterGroupName='test', ApplyImmediately=True) - database = conn.describe_db_instances(DBInstanceIdentifier='db-master-1')['DBInstances'][0] + database = conn.describe_db_instances( + DBInstanceIdentifier='db-master-1')['DBInstances'][0] len(database['DBParameterGroups']).should.equal(1) - database['DBParameterGroups'][0]['DBParameterGroupName'].should.equal('test') - database['DBParameterGroups'][0]['ParameterApplyStatus'].should.equal('in-sync') + database['DBParameterGroups'][0][ + 'DBParameterGroupName'].should.equal('test') + database['DBParameterGroups'][0][ + 'ParameterApplyStatus'].should.equal('in-sync') @disable_on_py3() @@ -946,15 +1030,18 @@ def test_describe_db_parameter_group(): conn.create_db_parameter_group(DBParameterGroupName='test', DBParameterGroupFamily='mysql5.6', Description='test parameter group') - db_parameter_groups = conn.describe_db_parameter_groups(DBParameterGroupName='test') - db_parameter_groups['DBParameterGroups'][0]['DBParameterGroupName'].should.equal('test') + db_parameter_groups = conn.describe_db_parameter_groups( + DBParameterGroupName='test') + db_parameter_groups['DBParameterGroups'][0][ + 'DBParameterGroupName'].should.equal('test') @disable_on_py3() @mock_rds2 def test_describe_non_existant_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') - db_parameter_groups = conn.describe_db_parameter_groups(DBParameterGroupName='test') + db_parameter_groups = conn.describe_db_parameter_groups( + DBParameterGroupName='test') len(db_parameter_groups['DBParameterGroups']).should.equal(0) @@ -963,14 +1050,18 @@ def test_describe_non_existant_db_parameter_group(): def test_delete_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') conn.create_db_parameter_group(DBParameterGroupName='test', - DBParameterGroupFamily='mysql5.6', - Description='test parameter group') - db_parameter_groups = conn.describe_db_parameter_groups(DBParameterGroupName='test') - db_parameter_groups['DBParameterGroups'][0]['DBParameterGroupName'].should.equal('test') + DBParameterGroupFamily='mysql5.6', + Description='test parameter group') + db_parameter_groups = conn.describe_db_parameter_groups( + DBParameterGroupName='test') + db_parameter_groups['DBParameterGroups'][0][ + 'DBParameterGroupName'].should.equal('test') conn.delete_db_parameter_group(DBParameterGroupName='test') - db_parameter_groups = conn.describe_db_parameter_groups(DBParameterGroupName='test') + db_parameter_groups = conn.describe_db_parameter_groups( + DBParameterGroupName='test') len(db_parameter_groups['DBParameterGroups']).should.equal(0) + @disable_on_py3() @mock_rds2 def test_modify_db_parameter_group(): @@ -986,7 +1077,7 @@ def test_modify_db_parameter_group(): 'Description': 'test param', 'ApplyMethod': 'immediate' }] - ) + ) modify_result['DBParameterGroupName'].should.equal('test') @@ -1001,7 +1092,9 @@ def test_modify_db_parameter_group(): @mock_rds2 def test_delete_non_existant_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') - conn.delete_db_parameter_group.when.called_with(DBParameterGroupName='non-existant').should.throw(ClientError) + conn.delete_db_parameter_group.when.called_with( + DBParameterGroupName='non-existant').should.throw(ClientError) + @disable_on_py3() @mock_rds2 @@ -1011,8 +1104,9 @@ def test_create_parameter_group_with_tags(): DBParameterGroupFamily='mysql5.6', Description='test parameter group', Tags=[{ - 'Key': 'foo', - 'Value': 'bar', + 'Key': 'foo', + 'Value': 'bar', }]) - result = conn.list_tags_for_resource(ResourceName='arn:aws:rds:us-west-2:1234567890:pg:test') + result = conn.list_tags_for_resource( + ResourceName='arn:aws:rds:us-west-2:1234567890:pg:test') result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}]) diff --git a/tests/test_rds2/test_server.py b/tests/test_rds2/test_server.py index 19c2b6e9f..f9489e054 100644 --- a/tests/test_rds2/test_server.py +++ b/tests/test_rds2/test_server.py @@ -11,7 +11,7 @@ Test the different server responses #@mock_rds2 -#def test_list_databases(): +# def test_list_databases(): # backend = server.create_backend_app("rds2") # test_client = backend.test_client() # diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 13acf6d7c..41be8f022 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -35,18 +35,21 @@ def test_create_cluster(): ) cluster_response = conn.describe_clusters(cluster_identifier) - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] cluster['ClusterIdentifier'].should.equal(cluster_identifier) cluster['NodeType'].should.equal("dw.hs1.xlarge") cluster['MasterUsername'].should.equal("username") cluster['DBName'].should.equal("my_db") - cluster['ClusterSecurityGroups'][0]['ClusterSecurityGroupName'].should.equal("Default") + cluster['ClusterSecurityGroups'][0][ + 'ClusterSecurityGroupName'].should.equal("Default") cluster['VpcSecurityGroups'].should.equal([]) cluster['ClusterSubnetGroupName'].should.equal(None) cluster['AvailabilityZone'].should.equal("us-east-1d") cluster['PreferredMaintenanceWindow'].should.equal("Mon:03:00-Mon:11:00") - cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("default.redshift-1.0") + cluster['ClusterParameterGroups'][0][ + 'ParameterGroupName'].should.equal("default.redshift-1.0") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(10) cluster['Port'].should.equal(1234) cluster['ClusterVersion'].should.equal("1.0") @@ -69,7 +72,8 @@ def test_create_single_node_cluster(): ) cluster_response = conn.describe_clusters(cluster_identifier) - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] cluster['ClusterIdentifier'].should.equal(cluster_identifier) cluster['NodeType'].should.equal("dw.hs1.xlarge") @@ -91,13 +95,15 @@ def test_default_cluster_attibutes(): ) cluster_response = conn.describe_clusters(cluster_identifier) - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] cluster['DBName'].should.equal("dev") cluster['ClusterSubnetGroupName'].should.equal(None) assert "us-east-" in cluster['AvailabilityZone'] cluster['PreferredMaintenanceWindow'].should.equal("Mon:03:00-Mon:03:30") - cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("default.redshift-1.0") + cluster['ClusterParameterGroups'][0][ + 'ParameterGroupName'].should.equal("default.redshift-1.0") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(1) cluster['Port'].should.equal(5439) cluster['ClusterVersion'].should.equal("1.0") @@ -127,7 +133,8 @@ def test_create_cluster_in_subnet_group(): ) cluster_response = redshift_conn.describe_clusters("my_cluster") - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] cluster['ClusterSubnetGroupName'].should.equal('my_subnet_group') @@ -153,8 +160,10 @@ def test_create_cluster_with_security_group(): ) cluster_response = conn.describe_clusters(cluster_identifier) - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - group_names = [group['ClusterSecurityGroupName'] for group in cluster['ClusterSecurityGroups']] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] + group_names = [group['ClusterSecurityGroupName'] + for group in cluster['ClusterSecurityGroups']] set(group_names).should.equal(set(["security_group1", "security_group2"])) @@ -165,7 +174,8 @@ def test_create_cluster_with_vpc_security_groups(): ec2_conn = boto.connect_ec2() redshift_conn = boto.connect_redshift() vpc = vpc_conn.create_vpc("10.0.0.0/16") - security_group = ec2_conn.create_security_group("vpc_security_group", "a group", vpc_id=vpc.id) + security_group = ec2_conn.create_security_group( + "vpc_security_group", "a group", vpc_id=vpc.id) redshift_conn.create_cluster( "my_cluster", @@ -176,8 +186,10 @@ def test_create_cluster_with_vpc_security_groups(): ) cluster_response = redshift_conn.describe_clusters("my_cluster") - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - group_ids = [group['VpcSecurityGroupId'] for group in cluster['VpcSecurityGroups']] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] + group_ids = [group['VpcSecurityGroupId'] + for group in cluster['VpcSecurityGroups']] list(group_ids).should.equal([security_group.id]) @@ -199,14 +211,17 @@ def test_create_cluster_with_parameter_group(): ) cluster_response = conn.describe_clusters("my_cluster") - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("my_parameter_group") + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] + cluster['ClusterParameterGroups'][0][ + 'ParameterGroupName'].should.equal("my_parameter_group") @mock_redshift_deprecated def test_describe_non_existant_cluster(): conn = boto.redshift.connect_to_region("us-east-1") - conn.describe_clusters.when.called_with("not-a-cluster").should.throw(ClusterNotFound) + conn.describe_clusters.when.called_with( + "not-a-cluster").should.throw(ClusterNotFound) @mock_redshift_deprecated @@ -221,16 +236,19 @@ def test_delete_cluster(): master_user_password="password", ) - clusters = conn.describe_clusters()['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + clusters = conn.describe_clusters()['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'] list(clusters).should.have.length_of(1) conn.delete_cluster(cluster_identifier) - clusters = conn.describe_clusters()['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + clusters = conn.describe_clusters()['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'] list(clusters).should.have.length_of(0) # Delete invalid id - conn.delete_cluster.when.called_with("not-a-cluster").should.throw(ClusterNotFound) + conn.delete_cluster.when.called_with( + "not-a-cluster").should.throw(ClusterNotFound) @mock_redshift_deprecated @@ -269,13 +287,16 @@ def test_modify_cluster(): ) cluster_response = conn.describe_clusters("new_identifier") - cluster = cluster_response['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + cluster = cluster_response['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'][0] cluster['ClusterIdentifier'].should.equal("new_identifier") cluster['NodeType'].should.equal("dw.hs1.xlarge") - cluster['ClusterSecurityGroups'][0]['ClusterSecurityGroupName'].should.equal("security_group") + cluster['ClusterSecurityGroups'][0][ + 'ClusterSecurityGroupName'].should.equal("security_group") cluster['PreferredMaintenanceWindow'].should.equal("Tue:03:00-Tue:11:00") - cluster['ClusterParameterGroups'][0]['ParameterGroupName'].should.equal("my_parameter_group") + cluster['ClusterParameterGroups'][0][ + 'ParameterGroupName'].should.equal("my_parameter_group") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(7) cluster['AllowVersionUpgrade'].should.equal(False) cluster['NumberOfNodes'].should.equal(2) @@ -297,12 +318,15 @@ def test_create_cluster_subnet_group(): subnet_ids=[subnet1.id, subnet2.id], ) - subnets_response = redshift_conn.describe_cluster_subnet_groups("my_subnet") - my_subnet = subnets_response['DescribeClusterSubnetGroupsResponse']['DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'][0] + subnets_response = redshift_conn.describe_cluster_subnet_groups( + "my_subnet") + my_subnet = subnets_response['DescribeClusterSubnetGroupsResponse'][ + 'DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'][0] my_subnet['ClusterSubnetGroupName'].should.equal("my_subnet") my_subnet['Description'].should.equal("This is my subnet group") - subnet_ids = [subnet['SubnetIdentifier'] for subnet in my_subnet['Subnets']] + subnet_ids = [subnet['SubnetIdentifier'] + for subnet in my_subnet['Subnets']] set(subnet_ids).should.equal(set([subnet1.id, subnet2.id])) @@ -320,7 +344,8 @@ def test_create_invalid_cluster_subnet_group(): @mock_redshift_deprecated def test_describe_non_existant_subnet_group(): conn = boto.redshift.connect_to_region("us-east-1") - conn.describe_cluster_subnet_groups.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) + conn.describe_cluster_subnet_groups.when.called_with( + "not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) @mock_redshift_deprecated @@ -338,17 +363,20 @@ def test_delete_cluster_subnet_group(): ) subnets_response = redshift_conn.describe_cluster_subnet_groups() - subnets = subnets_response['DescribeClusterSubnetGroupsResponse']['DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'] + subnets = subnets_response['DescribeClusterSubnetGroupsResponse'][ + 'DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'] subnets.should.have.length_of(1) redshift_conn.delete_cluster_subnet_group("my_subnet") subnets_response = redshift_conn.describe_cluster_subnet_groups() - subnets = subnets_response['DescribeClusterSubnetGroupsResponse']['DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'] + subnets = subnets_response['DescribeClusterSubnetGroupsResponse'][ + 'DescribeClusterSubnetGroupsResult']['ClusterSubnetGroups'] subnets.should.have.length_of(0) # Delete invalid id - redshift_conn.delete_cluster_subnet_group.when.called_with("not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) + redshift_conn.delete_cluster_subnet_group.when.called_with( + "not-a-subnet-group").should.throw(ClusterSubnetGroupNotFound) @mock_redshift_deprecated @@ -359,8 +387,10 @@ def test_create_cluster_security_group(): "This is my security group", ) - groups_response = conn.describe_cluster_security_groups("my_security_group") - my_group = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'][0] + groups_response = conn.describe_cluster_security_groups( + "my_security_group") + my_group = groups_response['DescribeClusterSecurityGroupsResponse'][ + 'DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'][0] my_group['ClusterSecurityGroupName'].should.equal("my_security_group") my_group['Description'].should.equal("This is my security group") @@ -370,7 +400,8 @@ def test_create_cluster_security_group(): @mock_redshift_deprecated def test_describe_non_existant_security_group(): conn = boto.redshift.connect_to_region("us-east-1") - conn.describe_cluster_security_groups.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) + conn.describe_cluster_security_groups.when.called_with( + "not-a-security-group").should.throw(ClusterSecurityGroupNotFound) @mock_redshift_deprecated @@ -382,17 +413,20 @@ def test_delete_cluster_security_group(): ) groups_response = conn.describe_cluster_security_groups() - groups = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] + groups = groups_response['DescribeClusterSecurityGroupsResponse'][ + 'DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] groups.should.have.length_of(2) # The default group already exists conn.delete_cluster_security_group("my_security_group") groups_response = conn.describe_cluster_security_groups() - groups = groups_response['DescribeClusterSecurityGroupsResponse']['DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] + groups = groups_response['DescribeClusterSecurityGroupsResponse'][ + 'DescribeClusterSecurityGroupsResult']['ClusterSecurityGroups'] groups.should.have.length_of(1) # Delete invalid id - conn.delete_cluster_security_group.when.called_with("not-a-security-group").should.throw(ClusterSecurityGroupNotFound) + conn.delete_cluster_security_group.when.called_with( + "not-a-security-group").should.throw(ClusterSecurityGroupNotFound) @mock_redshift_deprecated @@ -404,8 +438,10 @@ def test_create_cluster_parameter_group(): "This is my parameter group", ) - groups_response = conn.describe_cluster_parameter_groups("my_parameter_group") - my_group = groups_response['DescribeClusterParameterGroupsResponse']['DescribeClusterParameterGroupsResult']['ParameterGroups'][0] + groups_response = conn.describe_cluster_parameter_groups( + "my_parameter_group") + my_group = groups_response['DescribeClusterParameterGroupsResponse'][ + 'DescribeClusterParameterGroupsResult']['ParameterGroups'][0] my_group['ParameterGroupName'].should.equal("my_parameter_group") my_group['ParameterGroupFamily'].should.equal("redshift-1.0") @@ -415,7 +451,8 @@ def test_create_cluster_parameter_group(): @mock_redshift_deprecated def test_describe_non_existant_parameter_group(): conn = boto.redshift.connect_to_region("us-east-1") - conn.describe_cluster_parameter_groups.when.called_with("not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) + conn.describe_cluster_parameter_groups.when.called_with( + "not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) @mock_redshift_deprecated @@ -428,14 +465,17 @@ def test_delete_cluster_parameter_group(): ) groups_response = conn.describe_cluster_parameter_groups() - groups = groups_response['DescribeClusterParameterGroupsResponse']['DescribeClusterParameterGroupsResult']['ParameterGroups'] + groups = groups_response['DescribeClusterParameterGroupsResponse'][ + 'DescribeClusterParameterGroupsResult']['ParameterGroups'] groups.should.have.length_of(2) # The default group already exists conn.delete_cluster_parameter_group("my_parameter_group") groups_response = conn.describe_cluster_parameter_groups() - groups = groups_response['DescribeClusterParameterGroupsResponse']['DescribeClusterParameterGroupsResult']['ParameterGroups'] + groups = groups_response['DescribeClusterParameterGroupsResponse'][ + 'DescribeClusterParameterGroupsResult']['ParameterGroups'] groups.should.have.length_of(1) # Delete invalid id - conn.delete_cluster_parameter_group.when.called_with("not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) + conn.delete_cluster_parameter_group.when.called_with( + "not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) diff --git a/tests/test_redshift/test_server.py b/tests/test_redshift/test_server.py index a6bdc93f3..ba407ab4c 100644 --- a/tests/test_redshift/test_server.py +++ b/tests/test_redshift/test_server.py @@ -19,5 +19,6 @@ def test_describe_clusters(): res = test_client.get('/?Action=DescribeClusters') json_data = json.loads(res.data.decode("utf-8")) - clusters = json_data['DescribeClustersResponse']['DescribeClustersResult']['Clusters'] + clusters = json_data['DescribeClustersResponse'][ + 'DescribeClustersResult']['Clusters'] list(clusters).should.equal([]) diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index f376375a0..ea8609556 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -23,15 +23,18 @@ def test_hosted_zone(): zones = conn.get_all_hosted_zones() len(zones["ListHostedZonesResponse"]["HostedZones"]).should.equal(2) - id1 = firstzone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + id1 = firstzone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] zone = conn.get_hosted_zone(id1) - zone["GetHostedZoneResponse"]["HostedZone"]["Name"].should.equal("testdns.aws.com.") + zone["GetHostedZoneResponse"]["HostedZone"][ + "Name"].should.equal("testdns.aws.com.") conn.delete_hosted_zone(id1) zones = conn.get_all_hosted_zones() len(zones["ListHostedZonesResponse"]["HostedZones"]).should.equal(1) - conn.get_hosted_zone.when.called_with("abcd").should.throw(boto.route53.exception.DNSServerError, "404 Not Found") + conn.get_hosted_zone.when.called_with("abcd").should.throw( + boto.route53.exception.DNSServerError, "404 Not Found") @mock_route53_deprecated @@ -42,7 +45,8 @@ def test_rrset(): boto.route53.exception.DNSServerError, "404 Not Found") zone = conn.create_hosted_zone("testdns.aws.com") - zoneid = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + zoneid = zone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] changes = ResourceRecordSets(conn, zoneid) change = changes.add_change("CREATE", "foo.bar.testdns.aws.com", "A") @@ -105,15 +109,18 @@ def test_rrset(): rrsets = conn.get_all_rrsets(zoneid, type="A") rrsets.should.have.length_of(2) - rrsets = conn.get_all_rrsets(zoneid, name="foo.bar.testdns.aws.com", type="A") + rrsets = conn.get_all_rrsets( + zoneid, name="foo.bar.testdns.aws.com", type="A") rrsets.should.have.length_of(1) rrsets[0].resource_records[0].should.equal('1.2.3.4') - rrsets = conn.get_all_rrsets(zoneid, name="bar.foo.testdns.aws.com", type="A") + rrsets = conn.get_all_rrsets( + zoneid, name="bar.foo.testdns.aws.com", type="A") rrsets.should.have.length_of(1) rrsets[0].resource_records[0].should.equal('5.6.7.8') - rrsets = conn.get_all_rrsets(zoneid, name="foo.foo.testdns.aws.com", type="A") + rrsets = conn.get_all_rrsets( + zoneid, name="foo.foo.testdns.aws.com", type="A") rrsets.should.have.length_of(0) @@ -121,7 +128,8 @@ def test_rrset(): def test_rrset_with_multiple_values(): conn = boto.connect_route53('the_key', 'the_secret') zone = conn.create_hosted_zone("testdns.aws.com") - zoneid = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + zoneid = zone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] changes = ResourceRecordSets(conn, zoneid) change = changes.add_change("CREATE", "foo.bar.testdns.aws.com", "A") @@ -138,11 +146,14 @@ def test_rrset_with_multiple_values(): def test_alias_rrset(): conn = boto.connect_route53('the_key', 'the_secret') zone = conn.create_hosted_zone("testdns.aws.com") - zoneid = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + zoneid = zone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] changes = ResourceRecordSets(conn, zoneid) - changes.add_change("CREATE", "foo.alias.testdns.aws.com", "A", alias_hosted_zone_id="Z3DG6IL3SJCGPX", alias_dns_name="foo.testdns.aws.com") - changes.add_change("CREATE", "bar.alias.testdns.aws.com", "CNAME", alias_hosted_zone_id="Z3DG6IL3SJCGPX", alias_dns_name="bar.testdns.aws.com") + changes.add_change("CREATE", "foo.alias.testdns.aws.com", "A", + alias_hosted_zone_id="Z3DG6IL3SJCGPX", alias_dns_name="foo.testdns.aws.com") + changes.add_change("CREATE", "bar.alias.testdns.aws.com", "CNAME", + alias_hosted_zone_id="Z3DG6IL3SJCGPX", alias_dns_name="bar.testdns.aws.com") changes.commit() rrsets = conn.get_all_rrsets(zoneid, type="A") @@ -169,7 +180,8 @@ def test_create_health_check(): ) conn.create_health_check(check) - checks = conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + checks = conn.get_list_health_checks()['ListHealthChecksResponse'][ + 'HealthChecks'] list(checks).should.have.length_of(1) check = checks[0] config = check['HealthCheckConfig'] @@ -195,12 +207,14 @@ def test_delete_health_check(): ) conn.create_health_check(check) - checks = conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + checks = conn.get_list_health_checks()['ListHealthChecksResponse'][ + 'HealthChecks'] list(checks).should.have.length_of(1) health_check_id = checks[0]['Id'] conn.delete_health_check(health_check_id) - checks = conn.get_list_health_checks()['ListHealthChecksResponse']['HealthChecks'] + checks = conn.get_list_health_checks()['ListHealthChecksResponse'][ + 'HealthChecks'] list(checks).should.have.length_of(0) @@ -214,14 +228,17 @@ def test_use_health_check_in_resource_record_set(): hc_type="HTTP", resource_path="/", ) - check = conn.create_health_check(check)['CreateHealthCheckResponse']['HealthCheck'] + check = conn.create_health_check( + check)['CreateHealthCheckResponse']['HealthCheck'] check_id = check['Id'] zone = conn.create_hosted_zone("testdns.aws.com") - zone_id = zone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + zone_id = zone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] changes = ResourceRecordSets(conn, zone_id) - change = changes.add_change("CREATE", "foo.bar.testdns.aws.com", "A", health_check=check_id) + change = changes.add_change( + "CREATE", "foo.bar.testdns.aws.com", "A", health_check=check_id) change.add_value("1.2.3.4") changes.commit() @@ -233,14 +250,18 @@ def test_use_health_check_in_resource_record_set(): def test_hosted_zone_comment_preserved(): conn = boto.connect_route53('the_key', 'the_secret') - firstzone = conn.create_hosted_zone("testdns.aws.com.", comment="test comment") - zone_id = firstzone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + firstzone = conn.create_hosted_zone( + "testdns.aws.com.", comment="test comment") + zone_id = firstzone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] hosted_zone = conn.get_hosted_zone(zone_id) - hosted_zone["GetHostedZoneResponse"]["HostedZone"]["Config"]["Comment"].should.equal("test comment") + hosted_zone["GetHostedZoneResponse"]["HostedZone"][ + "Config"]["Comment"].should.equal("test comment") hosted_zones = conn.get_all_hosted_zones() - hosted_zones["ListHostedZonesResponse"]["HostedZones"][0]["Config"]["Comment"].should.equal("test comment") + hosted_zones["ListHostedZonesResponse"]["HostedZones"][ + 0]["Config"]["Comment"].should.equal("test comment") zone = conn.get_zone("testdns.aws.com.") zone.config["Comment"].should.equal("test comment") @@ -253,16 +274,20 @@ def test_deleting_weighted_route(): conn.create_hosted_zone("testdns.aws.com.") zone = conn.get_zone("testdns.aws.com.") - zone.add_cname("cname.testdns.aws.com", "example.com", identifier=('success-test-foo', '50')) - zone.add_cname("cname.testdns.aws.com", "example.com", identifier=('success-test-bar', '50')) + zone.add_cname("cname.testdns.aws.com", "example.com", + identifier=('success-test-foo', '50')) + zone.add_cname("cname.testdns.aws.com", "example.com", + identifier=('success-test-bar', '50')) cnames = zone.get_cname('cname.testdns.aws.com.', all=True) cnames.should.have.length_of(2) - foo_cname = [cname for cname in cnames if cname.identifier == 'success-test-foo'][0] + foo_cname = [cname for cname in cnames if cname.identifier == + 'success-test-foo'][0] zone.delete_record(foo_cname) cname = zone.get_cname('cname.testdns.aws.com.', all=True) - # When get_cname only had one result, it returns just that result instead of a list. + # When get_cname only had one result, it returns just that result instead + # of a list. cname.identifier.should.equal('success-test-bar') @@ -273,17 +298,21 @@ def test_deleting_latency_route(): conn.create_hosted_zone("testdns.aws.com.") zone = conn.get_zone("testdns.aws.com.") - zone.add_cname("cname.testdns.aws.com", "example.com", identifier=('success-test-foo', 'us-west-2')) - zone.add_cname("cname.testdns.aws.com", "example.com", identifier=('success-test-bar', 'us-west-1')) + zone.add_cname("cname.testdns.aws.com", "example.com", + identifier=('success-test-foo', 'us-west-2')) + zone.add_cname("cname.testdns.aws.com", "example.com", + identifier=('success-test-bar', 'us-west-1')) cnames = zone.get_cname('cname.testdns.aws.com.', all=True) cnames.should.have.length_of(2) - foo_cname = [cname for cname in cnames if cname.identifier == 'success-test-foo'][0] + foo_cname = [cname for cname in cnames if cname.identifier == + 'success-test-foo'][0] foo_cname.region.should.equal('us-west-2') zone.delete_record(foo_cname) cname = zone.get_cname('cname.testdns.aws.com.', all=True) - # When get_cname only had one result, it returns just that result instead of a list. + # When get_cname only had one result, it returns just that result instead + # of a list. cname.identifier.should.equal('success-test-bar') cname.region.should.equal('us-west-1') @@ -292,15 +321,19 @@ def test_deleting_latency_route(): def test_hosted_zone_private_zone_preserved(): conn = boto.connect_route53('the_key', 'the_secret') - firstzone = conn.create_hosted_zone("testdns.aws.com.", private_zone=True, vpc_id='vpc-fake', vpc_region='us-east-1') - zone_id = firstzone["CreateHostedZoneResponse"]["HostedZone"]["Id"].split("/")[-1] + firstzone = conn.create_hosted_zone( + "testdns.aws.com.", private_zone=True, vpc_id='vpc-fake', vpc_region='us-east-1') + zone_id = firstzone["CreateHostedZoneResponse"][ + "HostedZone"]["Id"].split("/")[-1] hosted_zone = conn.get_hosted_zone(zone_id) # in (original) boto, these bools returned as strings. - hosted_zone["GetHostedZoneResponse"]["HostedZone"]["Config"]["PrivateZone"].should.equal('True') + hosted_zone["GetHostedZoneResponse"]["HostedZone"][ + "Config"]["PrivateZone"].should.equal('True') hosted_zones = conn.get_all_hosted_zones() - hosted_zones["ListHostedZonesResponse"]["HostedZones"][0]["Config"]["PrivateZone"].should.equal('True') + hosted_zones["ListHostedZonesResponse"]["HostedZones"][ + 0]["Config"]["PrivateZone"].should.equal('True') zone = conn.get_zone("testdns.aws.com.") zone.config["PrivateZone"].should.equal('True') @@ -331,6 +364,7 @@ def test_hosted_zone_private_zone_preserved_boto3(): # zone = conn.list_hosted_zones_by_name(DNSName="testdns.aws.com.") # zone.config["PrivateZone"].should.equal(True) + @mock_route53 def test_list_or_change_tags_for_resource_request(): conn = boto3.client('route53', region_name='us-east-1') @@ -359,7 +393,8 @@ def test_list_or_change_tags_for_resource_request(): ) # Check to make sure that the response has the 'ResourceTagSet' key - response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response = conn.list_tags_for_resource( + ResourceType='healthcheck', ResourceId=healthcheck_id) response.should.contain('ResourceTagSet') # Validate that each key was added @@ -376,7 +411,8 @@ def test_list_or_change_tags_for_resource_request(): ) # Check to make sure that the response has the 'ResourceTagSet' key - response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response = conn.list_tags_for_resource( + ResourceType='healthcheck', ResourceId=healthcheck_id) response.should.contain('ResourceTagSet') response['ResourceTagSet']['Tags'].should_not.contain(tag1) response['ResourceTagSet']['Tags'].should.contain(tag2) @@ -388,7 +424,8 @@ def test_list_or_change_tags_for_resource_request(): RemoveTagKeys=[tag2['Key']] ) - response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response = conn.list_tags_for_resource( + ResourceType='healthcheck', ResourceId=healthcheck_id) response['ResourceTagSet']['Tags'].should_not.contain(tag2) # Re-add the tags @@ -405,5 +442,6 @@ def test_list_or_change_tags_for_resource_request(): RemoveTagKeys=[tag1['Key'], tag2['Key']] ) - response = conn.list_tags_for_resource(ResourceType='healthcheck', ResourceId=healthcheck_id) + response = conn.list_tags_for_resource( + ResourceType='healthcheck', ResourceId=healthcheck_id) response['ResourceTagSet']['Tags'].should.be.empty diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index e424ba6a3..32b772abe 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -48,6 +48,7 @@ def reduced_min_part_size(f): class MyModel(object): + def __init__(self, name, value): self.name = name self.value = value @@ -67,7 +68,8 @@ def test_my_model_save(): model_instance = MyModel('steve', 'is awesome') model_instance.save() - body = conn.Object('mybucket', 'steve').get()['Body'].read().decode("utf-8") + body = conn.Object('mybucket', 'steve').get()[ + 'Body'].read().decode("utf-8") assert body == b'is awesome' @@ -110,7 +112,8 @@ def test_multipart_upload(): multipart.upload_part_from_file(BytesIO(part2), 2) multipart.complete_upload() # we should get both parts as the key contents - bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) + bucket.get_key( + "the-key").get_contents_as_string().should.equal(part1 + part2) @mock_s3_deprecated @@ -127,7 +130,8 @@ def test_multipart_upload_out_of_order(): multipart.upload_part_from_file(BytesIO(part1), 2) multipart.complete_upload() # we should get both parts as the key contents - bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) + bucket.get_key( + "the-key").get_contents_as_string().should.equal(part1 + part2) @mock_s3_deprecated @@ -136,7 +140,8 @@ def test_multipart_upload_with_headers(): conn = boto.connect_s3('the_key', 'the_secret') bucket = conn.create_bucket("foobar") - multipart = bucket.initiate_multipart_upload("the-key", metadata={"foo": "bar"}) + multipart = bucket.initiate_multipart_upload( + "the-key", metadata={"foo": "bar"}) part1 = b'0' * 10 multipart.upload_part_from_file(BytesIO(part1), 1) multipart.complete_upload() @@ -159,7 +164,8 @@ def test_multipart_upload_with_copy_key(): multipart.upload_part_from_file(BytesIO(part1), 1) multipart.copy_part_from_key("foobar", "original-key", 2, 0, 3) multipart.complete_upload() - bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + b"key_") + bucket.get_key( + "the-key").get_contents_as_string().should.equal(part1 + b"key_") @mock_s3_deprecated @@ -229,7 +235,8 @@ def test_multipart_duplicate_upload(): multipart.upload_part_from_file(BytesIO(part2), 2) multipart.complete_upload() # We should get only one copy of part 1. - bucket.get_key("the-key").get_contents_as_string().should.equal(part1 + part2) + bucket.get_key( + "the-key").get_contents_as_string().should.equal(part1 + part2) @mock_s3_deprecated @@ -260,7 +267,8 @@ def test_key_save_to_missing_bucket(): key = Key(bucket) key.key = "the-key" - key.set_contents_from_string.when.called_with("foobar").should.throw(S3ResponseError) + key.set_contents_from_string.when.called_with( + "foobar").should.throw(S3ResponseError) @mock_s3_deprecated @@ -275,7 +283,8 @@ def test_missing_key_urllib2(): conn = boto.connect_s3('the_key', 'the_secret') conn.create_bucket("foobar") - urlopen.when.called_with("http://foobar.s3.amazonaws.com/the-key").should.throw(HTTPError) + urlopen.when.called_with( + "http://foobar.s3.amazonaws.com/the-key").should.throw(HTTPError) @mock_s3_deprecated @@ -315,7 +324,8 @@ def test_large_key_save(): key.key = "the-key" key.set_contents_from_string("foobar" * 100000) - bucket.get_key("the-key").get_contents_as_string().should.equal(b'foobar' * 100000) + bucket.get_key( + "the-key").get_contents_as_string().should.equal(b'foobar' * 100000) @mock_s3_deprecated @@ -328,8 +338,10 @@ def test_copy_key(): bucket.copy_key('new-key', 'foobar', 'the-key') - bucket.get_key("the-key").get_contents_as_string().should.equal(b"some value") - bucket.get_key("new-key").get_contents_as_string().should.equal(b"some value") + bucket.get_key( + "the-key").get_contents_as_string().should.equal(b"some value") + bucket.get_key( + "new-key").get_contents_as_string().should.equal(b"some value") @mock_s3_deprecated @@ -344,8 +356,10 @@ def test_copy_key_with_version(): bucket.copy_key('new-key', 'foobar', 'the-key', src_version_id='0') - bucket.get_key("the-key").get_contents_as_string().should.equal(b"another value") - bucket.get_key("new-key").get_contents_as_string().should.equal(b"some value") + bucket.get_key( + "the-key").get_contents_as_string().should.equal(b"another value") + bucket.get_key( + "new-key").get_contents_as_string().should.equal(b"some value") @mock_s3_deprecated @@ -373,7 +387,8 @@ def test_copy_key_replace_metadata(): metadata={'momd': 'Mometadatastring'}) bucket.get_key("new-key").get_metadata('md').should.be.none - bucket.get_key("new-key").get_metadata('momd').should.equal('Mometadatastring') + bucket.get_key( + "new-key").get_metadata('momd').should.equal('Mometadatastring') @freeze_time("2012-01-01 12:00:00") @@ -389,7 +404,8 @@ def test_last_modified(): rs = bucket.get_all_keys() rs[0].last_modified.should.equal('2012-01-01T12:00:00.000Z') - bucket.get_key("the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') + bucket.get_key( + "the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') @mock_s3_deprecated @@ -401,7 +417,8 @@ def test_missing_bucket(): @mock_s3_deprecated def test_bucket_with_dash(): conn = boto.connect_s3('the_key', 'the_secret') - conn.get_bucket.when.called_with('mybucket-test').should.throw(S3ResponseError) + conn.get_bucket.when.called_with( + 'mybucket-test').should.throw(S3ResponseError) @mock_s3_deprecated @@ -432,7 +449,8 @@ def test_create_existing_bucket_in_us_east_1(): @mock_s3_deprecated def test_other_region(): - conn = S3Connection('key', 'secret', host='s3-website-ap-southeast-2.amazonaws.com') + conn = S3Connection( + 'key', 'secret', host='s3-website-ap-southeast-2.amazonaws.com') conn.create_bucket("foobar") list(conn.get_bucket("foobar").get_all_keys()).should.equal([]) @@ -613,7 +631,8 @@ def test_bucket_key_listing_order(): delimiter = None keys = [x.name for x in bucket.list(prefix + 'x', delimiter)] - keys.should.equal([u'toplevel/x/key', u'toplevel/x/y/key', u'toplevel/x/y/z/key']) + keys.should.equal( + [u'toplevel/x/key', u'toplevel/x/y/key', u'toplevel/x/y/z/key']) delimiter = '/' keys = [x.name for x in bucket.list(prefix + 'x', delimiter)] @@ -640,7 +659,8 @@ def test_copy_key_reduced_redundancy(): key.key = "the-key" key.set_contents_from_string("some value") - bucket.copy_key('new-key', 'foobar', 'the-key', storage_class='REDUCED_REDUNDANCY') + bucket.copy_key('new-key', 'foobar', 'the-key', + storage_class='REDUCED_REDUNDANCY') # we use the bucket iterator because of: # https:/github.com/boto/boto/issues/1173 @@ -886,34 +906,54 @@ def test_ranged_get(): key.set_contents_from_string(rep * 10) # Implicitly bounded range requests. - key.get_contents_as_string(headers={'Range': 'bytes=0-'}).should.equal(rep * 10) - key.get_contents_as_string(headers={'Range': 'bytes=50-'}).should.equal(rep * 5) - key.get_contents_as_string(headers={'Range': 'bytes=99-'}).should.equal(b'9') + key.get_contents_as_string( + headers={'Range': 'bytes=0-'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=50-'}).should.equal(rep * 5) + key.get_contents_as_string( + headers={'Range': 'bytes=99-'}).should.equal(b'9') # Explicitly bounded range requests starting from the first byte. - key.get_contents_as_string(headers={'Range': 'bytes=0-0'}).should.equal(b'0') - key.get_contents_as_string(headers={'Range': 'bytes=0-49'}).should.equal(rep * 5) - key.get_contents_as_string(headers={'Range': 'bytes=0-99'}).should.equal(rep * 10) - key.get_contents_as_string(headers={'Range': 'bytes=0-100'}).should.equal(rep * 10) - key.get_contents_as_string(headers={'Range': 'bytes=0-700'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=0-0'}).should.equal(b'0') + key.get_contents_as_string( + headers={'Range': 'bytes=0-49'}).should.equal(rep * 5) + key.get_contents_as_string( + headers={'Range': 'bytes=0-99'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=0-100'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=0-700'}).should.equal(rep * 10) # Explicitly bounded range requests starting from the / a middle byte. - key.get_contents_as_string(headers={'Range': 'bytes=50-54'}).should.equal(rep[:5]) - key.get_contents_as_string(headers={'Range': 'bytes=50-99'}).should.equal(rep * 5) - key.get_contents_as_string(headers={'Range': 'bytes=50-100'}).should.equal(rep * 5) - key.get_contents_as_string(headers={'Range': 'bytes=50-700'}).should.equal(rep * 5) + key.get_contents_as_string( + headers={'Range': 'bytes=50-54'}).should.equal(rep[:5]) + key.get_contents_as_string( + headers={'Range': 'bytes=50-99'}).should.equal(rep * 5) + key.get_contents_as_string( + headers={'Range': 'bytes=50-100'}).should.equal(rep * 5) + key.get_contents_as_string( + headers={'Range': 'bytes=50-700'}).should.equal(rep * 5) # Explicitly bounded range requests starting from the last byte. - key.get_contents_as_string(headers={'Range': 'bytes=99-99'}).should.equal(b'9') - key.get_contents_as_string(headers={'Range': 'bytes=99-100'}).should.equal(b'9') - key.get_contents_as_string(headers={'Range': 'bytes=99-700'}).should.equal(b'9') + key.get_contents_as_string( + headers={'Range': 'bytes=99-99'}).should.equal(b'9') + key.get_contents_as_string( + headers={'Range': 'bytes=99-100'}).should.equal(b'9') + key.get_contents_as_string( + headers={'Range': 'bytes=99-700'}).should.equal(b'9') # Suffix range requests. - key.get_contents_as_string(headers={'Range': 'bytes=-1'}).should.equal(b'9') - key.get_contents_as_string(headers={'Range': 'bytes=-60'}).should.equal(rep * 6) - key.get_contents_as_string(headers={'Range': 'bytes=-100'}).should.equal(rep * 10) - key.get_contents_as_string(headers={'Range': 'bytes=-101'}).should.equal(rep * 10) - key.get_contents_as_string(headers={'Range': 'bytes=-700'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=-1'}).should.equal(b'9') + key.get_contents_as_string( + headers={'Range': 'bytes=-60'}).should.equal(rep * 6) + key.get_contents_as_string( + headers={'Range': 'bytes=-100'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=-101'}).should.equal(rep * 10) + key.get_contents_as_string( + headers={'Range': 'bytes=-700'}).should.equal(rep * 10) key.size.should.equal(100) @@ -1006,6 +1046,7 @@ def test_boto3_key_etag(): resp = s3.get_object(Bucket='mybucket', Key='steve') resp['ETag'].should.equal('"d32bda93738f7e03adb22e66c90fbc04"') + @mock_s3 def test_boto3_list_keys_xml_escaped(): s3 = boto3.client('s3', region_name='us-east-1') @@ -1045,13 +1086,13 @@ def test_boto3_list_objects_v2_truncated_response(): assert resp['IsTruncated'] == True assert 'Delimiter' not in resp assert 'StartAfter' not in resp - assert 'Owner' not in listed_object # owner info was not requested + assert 'Owner' not in listed_object # owner info was not requested next_token = resp['NextContinuationToken'] - # Second list - resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, ContinuationToken=next_token) + resp = s3.list_objects_v2( + Bucket='mybucket', MaxKeys=1, ContinuationToken=next_token) listed_object = resp['Contents'][0] assert listed_object['Key'] == 'three' @@ -1065,9 +1106,9 @@ def test_boto3_list_objects_v2_truncated_response(): next_token = resp['NextContinuationToken'] - # Third list - resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, ContinuationToken=next_token) + resp = s3.list_objects_v2( + Bucket='mybucket', MaxKeys=1, ContinuationToken=next_token) listed_object = resp['Contents'][0] assert listed_object['Key'] == 'two' @@ -1107,7 +1148,7 @@ def test_boto3_list_objects_v2_truncated_response_start_after(): # Second list # The ContinuationToken must take precedence over StartAfter. resp = s3.list_objects_v2(Bucket='mybucket', MaxKeys=1, StartAfter='one', - ContinuationToken=next_token) + ContinuationToken=next_token) listed_object = resp['Contents'][0] assert listed_object['Key'] == 'two' @@ -1143,7 +1184,8 @@ def test_boto3_bucket_create(): s3.Object('blah', 'hello.txt').put(Body="some text") - s3.Object('blah', 'hello.txt').get()['Body'].read().decode("utf-8").should.equal("some text") + s3.Object('blah', 'hello.txt').get()['Body'].read().decode( + "utf-8").should.equal("some text") @mock_s3 @@ -1153,7 +1195,8 @@ def test_boto3_bucket_create_eu_central(): s3.Object('blah', 'hello.txt').put(Body="some text") - s3.Object('blah', 'hello.txt').get()['Body'].read().decode("utf-8").should.equal("some text") + s3.Object('blah', 'hello.txt').get()['Body'].read().decode( + "utf-8").should.equal("some text") @mock_s3 @@ -1163,10 +1206,12 @@ def test_boto3_head_object(): s3.Object('blah', 'hello.txt').put(Body="some text") - s3.Object('blah', 'hello.txt').meta.client.head_object(Bucket='blah', Key='hello.txt') + s3.Object('blah', 'hello.txt').meta.client.head_object( + Bucket='blah', Key='hello.txt') with assert_raises(ClientError): - s3.Object('blah', 'hello2.txt').meta.client.head_object(Bucket='blah', Key='hello_bad.txt') + s3.Object('blah', 'hello2.txt').meta.client.head_object( + Bucket='blah', Key='hello_bad.txt') @mock_s3 @@ -1176,7 +1221,8 @@ def test_boto3_get_object(): s3.Object('blah', 'hello.txt').put(Body="some text") - s3.Object('blah', 'hello.txt').meta.client.head_object(Bucket='blah', Key='hello.txt') + s3.Object('blah', 'hello.txt').meta.client.head_object( + Bucket='blah', Key='hello.txt') with assert_raises(ClientError) as e: s3.Object('blah', 'hello2.txt').get() diff --git a/tests/test_s3/test_s3_lifecycle.py b/tests/test_s3/test_s3_lifecycle.py index f0a70bc6f..5cae8f790 100644 --- a/tests/test_s3/test_s3_lifecycle.py +++ b/tests/test_s3/test_s3_lifecycle.py @@ -56,9 +56,9 @@ def test_lifecycle_multi(): lifecycle.add_rule("2", "2/", "Enabled", Expiration(days=2)) lifecycle.add_rule("3", "3/", "Enabled", Expiration(date=date)) lifecycle.add_rule("4", "4/", "Enabled", None, - Transition(days=4, storage_class=sc)) + Transition(days=4, storage_class=sc)) lifecycle.add_rule("5", "5/", "Enabled", None, - Transition(date=date, storage_class=sc)) + Transition(date=date, storage_class=sc)) bucket.configure_lifecycle(lifecycle) # read the lifecycle back diff --git a/tests/test_s3/test_s3_utils.py b/tests/test_s3/test_s3_utils.py index 3b1d4a01a..b4f56d89a 100644 --- a/tests/test_s3/test_s3_utils.py +++ b/tests/test_s3/test_s3_utils.py @@ -8,11 +8,14 @@ def test_base_url(): def test_localhost_bucket(): - expect(bucket_name_from_url('https://wfoobar.localhost:5000/abc')).should.equal("wfoobar") + expect(bucket_name_from_url('https://wfoobar.localhost:5000/abc') + ).should.equal("wfoobar") def test_localhost_without_bucket(): - expect(bucket_name_from_url('https://www.localhost:5000/def')).should.equal(None) + expect(bucket_name_from_url( + 'https://www.localhost:5000/def')).should.equal(None) + def test_versioned_key_store(): d = _VersionedKeyStore() diff --git a/tests/test_s3/test_server.py b/tests/test_s3/test_server.py index 303224541..f6b8f889c 100644 --- a/tests/test_s3/test_server.py +++ b/tests/test_s3/test_server.py @@ -31,7 +31,8 @@ def test_s3_server_bucket_create(): res.status_code.should.equal(200) res.data.should.contain(b"ListBucketResult") - res = test_client.put('/bar', 'http://foobaz.localhost:5000/', data='test value') + res = test_client.put( + '/bar', 'http://foobaz.localhost:5000/', data='test value') res.status_code.should.equal(200) res = test_client.get('/bar', 'http://foobaz.localhost:5000/') @@ -45,7 +46,8 @@ def test_s3_server_bucket_versioning(): # Just enough XML to enable versioning body = 'Enabled' - res = test_client.put('/?versioning', 'http://foobaz.localhost:5000', data=body) + res = test_client.put( + '/?versioning', 'http://foobaz.localhost:5000', data=body) res.status_code.should.equal(200) diff --git a/tests/test_s3bucket_path/test_bucket_path_server.py b/tests/test_s3bucket_path/test_bucket_path_server.py index adc5de532..c67a2bcaa 100644 --- a/tests/test_s3bucket_path/test_bucket_path_server.py +++ b/tests/test_s3bucket_path/test_bucket_path_server.py @@ -44,7 +44,8 @@ def test_s3_server_bucket_create(): res = test_client.get('/missing-bucket', 'http://localhost:5000') res.status_code.should.equal(404) - res = test_client.put('/foobar/bar', 'http://localhost:5000', data='test value') + res = test_client.put( + '/foobar/bar', 'http://localhost:5000', data='test value') res.status_code.should.equal(200) res = test_client.get('/foobar/bar', 'http://localhost:5000') diff --git a/tests/test_s3bucket_path/test_s3bucket_path.py b/tests/test_s3bucket_path/test_s3bucket_path.py index 528c75368..21d786c61 100644 --- a/tests/test_s3bucket_path/test_s3bucket_path.py +++ b/tests/test_s3bucket_path/test_s3bucket_path.py @@ -20,6 +20,7 @@ def create_connection(key=None, secret=None): class MyModel(object): + def __init__(self, name, value): self.name = name self.value = value @@ -42,7 +43,8 @@ def test_my_model_save(): model_instance = MyModel('steve', 'is awesome') model_instance.save() - conn.get_bucket('mybucket').get_key('steve').get_contents_as_string().should.equal(b'is awesome') + conn.get_bucket('mybucket').get_key( + 'steve').get_contents_as_string().should.equal(b'is awesome') @mock_s3_deprecated @@ -57,7 +59,8 @@ def test_missing_key_urllib2(): conn = create_connection('the_key', 'the_secret') conn.create_bucket("foobar") - urlopen.when.called_with("http://s3.amazonaws.com/foobar/the-key").should.throw(HTTPError) + urlopen.when.called_with( + "http://s3.amazonaws.com/foobar/the-key").should.throw(HTTPError) @mock_s3_deprecated @@ -93,7 +96,8 @@ def test_large_key_save(): key.key = "the-key" key.set_contents_from_string("foobar" * 100000) - bucket.get_key("the-key").get_contents_as_string().should.equal(b'foobar' * 100000) + bucket.get_key( + "the-key").get_contents_as_string().should.equal(b'foobar' * 100000) @mock_s3_deprecated @@ -106,8 +110,10 @@ def test_copy_key(): bucket.copy_key('new-key', 'foobar', 'the-key') - bucket.get_key("the-key").get_contents_as_string().should.equal(b"some value") - bucket.get_key("new-key").get_contents_as_string().should.equal(b"some value") + bucket.get_key( + "the-key").get_contents_as_string().should.equal(b"some value") + bucket.get_key( + "new-key").get_contents_as_string().should.equal(b"some value") @mock_s3_deprecated @@ -135,7 +141,8 @@ def test_last_modified(): rs = bucket.get_all_keys() rs[0].last_modified.should.equal('2012-01-01T12:00:00.000Z') - bucket.get_key("the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') + bucket.get_key( + "the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') @mock_s3_deprecated @@ -147,7 +154,8 @@ def test_missing_bucket(): @mock_s3_deprecated def test_bucket_with_dash(): conn = create_connection('the_key', 'the_secret') - conn.get_bucket.when.called_with('mybucket-test').should.throw(S3ResponseError) + conn.get_bucket.when.called_with( + 'mybucket-test').should.throw(S3ResponseError) @mock_s3_deprecated @@ -268,7 +276,8 @@ def test_bucket_key_listing_order(): delimiter = None keys = [x.name for x in bucket.list(prefix + 'x', delimiter)] - keys.should.equal(['toplevel/x/key', 'toplevel/x/y/key', 'toplevel/x/y/z/key']) + keys.should.equal( + ['toplevel/x/key', 'toplevel/x/y/key', 'toplevel/x/y/z/key']) delimiter = '/' keys = [x.name for x in bucket.list(prefix + 'x', delimiter)] diff --git a/tests/test_s3bucket_path/test_s3bucket_path_utils.py b/tests/test_s3bucket_path/test_s3bucket_path_utils.py index 8497f8184..c607ea2ec 100644 --- a/tests/test_s3bucket_path/test_s3bucket_path_utils.py +++ b/tests/test_s3bucket_path/test_s3bucket_path_utils.py @@ -8,7 +8,8 @@ def test_base_url(): def test_localhost_bucket(): - expect(bucket_name_from_url('https://localhost:5000/wfoobar/abc')).should.equal("wfoobar") + expect(bucket_name_from_url('https://localhost:5000/wfoobar/abc') + ).should.equal("wfoobar") def test_localhost_without_bucket(): diff --git a/tests/test_ses/test_ses.py b/tests/test_ses/test_ses.py index 7771b9a65..431d42e1d 100644 --- a/tests/test_ses/test_ses.py +++ b/tests/test_ses/test_ses.py @@ -15,7 +15,8 @@ def test_verify_email_identity(): conn.verify_email_identity("test@example.com") identities = conn.list_identities() - address = identities['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'][0] + address = identities['ListIdentitiesResponse'][ + 'ListIdentitiesResult']['Identities'][0] address.should.equal('test@example.com') @@ -27,7 +28,8 @@ def test_domain_verify(): conn.verify_domain_identity("domain2.com") identities = conn.list_identities() - domains = list(identities['ListIdentitiesResponse']['ListIdentitiesResult']['Identities']) + domains = list(identities['ListIdentitiesResponse'][ + 'ListIdentitiesResult']['Identities']) domains.should.equal(['domain1.com', 'domain2.com']) @@ -36,9 +38,11 @@ def test_delete_identity(): conn = boto.connect_ses('the_key', 'the_secret') conn.verify_email_identity("test@example.com") - conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'].should.have.length_of(1) + conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult'][ + 'Identities'].should.have.length_of(1) conn.delete_identity("test@example.com") - conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult']['Identities'].should.have.length_of(0) + conn.list_identities()['ListIdentitiesResponse']['ListIdentitiesResult'][ + 'Identities'].should.have.length_of(0) @mock_ses_deprecated @@ -50,12 +54,15 @@ def test_send_email(): "test body", "test_to@example.com").should.throw(BotoServerError) conn.verify_email_identity("test@example.com") - conn.send_email("test@example.com", "test subject", "test body", "test_to@example.com") + conn.send_email("test@example.com", "test subject", + "test body", "test_to@example.com") send_quota = conn.get_send_quota() - sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours']) + sent_count = int(send_quota['GetSendQuotaResponse'][ + 'GetSendQuotaResult']['SentLast24Hours']) sent_count.should.equal(1) + @mock_ses_deprecated def test_send_html_email(): conn = boto.connect_ses('the_key', 'the_secret') @@ -65,12 +72,15 @@ def test_send_html_email(): "test body", "test_to@example.com", format="html").should.throw(BotoServerError) conn.verify_email_identity("test@example.com") - conn.send_email("test@example.com", "test subject", "test body", "test_to@example.com", format="html") + conn.send_email("test@example.com", "test subject", + "test body", "test_to@example.com", format="html") send_quota = conn.get_send_quota() - sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours']) + sent_count = int(send_quota['GetSendQuotaResponse'][ + 'GetSendQuotaResult']['SentLast24Hours']) sent_count.should.equal(1) + @mock_ses_deprecated def test_send_raw_email(): conn = boto.connect_ses('the_key', 'the_secret') @@ -101,5 +111,6 @@ def test_send_raw_email(): ) send_quota = conn.get_send_quota() - sent_count = int(send_quota['GetSendQuotaResponse']['GetSendQuotaResult']['SentLast24Hours']) + sent_count = int(send_quota['GetSendQuotaResponse'][ + 'GetSendQuotaResult']['SentLast24Hours']) sent_count.should.equal(1) diff --git a/tests/test_sns/test_application.py b/tests/test_sns/test_application.py index 31db73f62..613b11af5 100644 --- a/tests/test_sns/test_application.py +++ b/tests/test_sns/test_application.py @@ -17,8 +17,10 @@ def test_create_platform_application(): "PlatformPrincipal": "platform_principal", }, ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] - application_arn.should.equal('arn:aws:sns:us-east-1:123456789012:app/APNS/my-application') + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn.should.equal( + 'arn:aws:sns:us-east-1:123456789012:app/APNS/my-application') @mock_sns_deprecated @@ -32,8 +34,10 @@ def test_get_platform_application_attributes(): "PlatformPrincipal": "platform_principal", }, ) - arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] - attributes = conn.get_platform_application_attributes(arn)['GetPlatformApplicationAttributesResponse']['GetPlatformApplicationAttributesResult']['Attributes'] + arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] + attributes = conn.get_platform_application_attributes(arn)['GetPlatformApplicationAttributesResponse'][ + 'GetPlatformApplicationAttributesResult']['Attributes'] attributes.should.equal({ "PlatformCredential": "platform_credential", "PlatformPrincipal": "platform_principal", @@ -43,7 +47,8 @@ def test_get_platform_application_attributes(): @mock_sns_deprecated def test_get_missing_platform_application_attributes(): conn = boto.connect_sns() - conn.get_platform_application_attributes.when.called_with("a-fake-arn").should.throw(BotoServerError) + conn.get_platform_application_attributes.when.called_with( + "a-fake-arn").should.throw(BotoServerError) @mock_sns_deprecated @@ -57,11 +62,13 @@ def test_set_platform_application_attributes(): "PlatformPrincipal": "platform_principal", }, ) - arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] conn.set_platform_application_attributes(arn, - {"PlatformPrincipal": "other"} - ) - attributes = conn.get_platform_application_attributes(arn)['GetPlatformApplicationAttributesResponse']['GetPlatformApplicationAttributesResult']['Attributes'] + {"PlatformPrincipal": "other"} + ) + attributes = conn.get_platform_application_attributes(arn)['GetPlatformApplicationAttributesResponse'][ + 'GetPlatformApplicationAttributesResult']['Attributes'] attributes.should.equal({ "PlatformCredential": "platform_credential", "PlatformPrincipal": "other", @@ -81,7 +88,8 @@ def test_list_platform_applications(): ) applications_repsonse = conn.list_platform_applications() - applications = applications_repsonse['ListPlatformApplicationsResponse']['ListPlatformApplicationsResult']['PlatformApplications'] + applications = applications_repsonse['ListPlatformApplicationsResponse'][ + 'ListPlatformApplicationsResult']['PlatformApplications'] applications.should.have.length_of(2) @@ -98,14 +106,16 @@ def test_delete_platform_application(): ) applications_repsonse = conn.list_platform_applications() - applications = applications_repsonse['ListPlatformApplicationsResponse']['ListPlatformApplicationsResult']['PlatformApplications'] + applications = applications_repsonse['ListPlatformApplicationsResponse'][ + 'ListPlatformApplicationsResult']['PlatformApplications'] applications.should.have.length_of(2) application_arn = applications[0]['PlatformApplicationArn'] conn.delete_platform_application(application_arn) applications_repsonse = conn.list_platform_applications() - applications = applications_repsonse['ListPlatformApplicationsResponse']['ListPlatformApplicationsResult']['PlatformApplications'] + applications = applications_repsonse['ListPlatformApplicationsResponse'][ + 'ListPlatformApplicationsResult']['PlatformApplications'] applications.should.have.length_of(1) @@ -116,7 +126,8 @@ def test_create_platform_endpoint(): name="my-application", platform="APNS", ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] endpoint = conn.create_platform_endpoint( platform_application_arn=application_arn, @@ -127,8 +138,10 @@ def test_create_platform_endpoint(): }, ) - endpoint_arn = endpoint['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn'] - endpoint_arn.should.contain("arn:aws:sns:us-east-1:123456789012:endpoint/APNS/my-application/") + endpoint_arn = endpoint['CreatePlatformEndpointResponse'][ + 'CreatePlatformEndpointResult']['EndpointArn'] + endpoint_arn.should.contain( + "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/my-application/") @mock_sns_deprecated @@ -138,7 +151,8 @@ def test_get_list_endpoints_by_platform_application(): name="my-application", platform="APNS", ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] endpoint = conn.create_platform_endpoint( platform_application_arn=application_arn, @@ -148,7 +162,8 @@ def test_get_list_endpoints_by_platform_application(): "CustomUserData": "some data", }, ) - endpoint_arn = endpoint['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn'] + endpoint_arn = endpoint['CreatePlatformEndpointResponse'][ + 'CreatePlatformEndpointResult']['EndpointArn'] endpoint_list = conn.list_endpoints_by_platform_application( platform_application_arn=application_arn @@ -166,7 +181,8 @@ def test_get_endpoint_attributes(): name="my-application", platform="APNS", ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] endpoint = conn.create_platform_endpoint( platform_application_arn=application_arn, @@ -177,9 +193,11 @@ def test_get_endpoint_attributes(): "CustomUserData": "some data", }, ) - endpoint_arn = endpoint['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn'] + endpoint_arn = endpoint['CreatePlatformEndpointResponse'][ + 'CreatePlatformEndpointResult']['EndpointArn'] - attributes = conn.get_endpoint_attributes(endpoint_arn)['GetEndpointAttributesResponse']['GetEndpointAttributesResult']['Attributes'] + attributes = conn.get_endpoint_attributes(endpoint_arn)['GetEndpointAttributesResponse'][ + 'GetEndpointAttributesResult']['Attributes'] attributes.should.equal({ "Token": "some_unique_id", "Enabled": 'False', @@ -190,7 +208,8 @@ def test_get_endpoint_attributes(): @mock_sns_deprecated def test_get_missing_endpoint_attributes(): conn = boto.connect_sns() - conn.get_endpoint_attributes.when.called_with("a-fake-arn").should.throw(BotoServerError) + conn.get_endpoint_attributes.when.called_with( + "a-fake-arn").should.throw(BotoServerError) @mock_sns_deprecated @@ -200,7 +219,8 @@ def test_set_endpoint_attributes(): name="my-application", platform="APNS", ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] endpoint = conn.create_platform_endpoint( platform_application_arn=application_arn, @@ -211,12 +231,14 @@ def test_set_endpoint_attributes(): "CustomUserData": "some data", }, ) - endpoint_arn = endpoint['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn'] + endpoint_arn = endpoint['CreatePlatformEndpointResponse'][ + 'CreatePlatformEndpointResult']['EndpointArn'] conn.set_endpoint_attributes(endpoint_arn, - {"CustomUserData": "other data"} - ) - attributes = conn.get_endpoint_attributes(endpoint_arn)['GetEndpointAttributesResponse']['GetEndpointAttributesResult']['Attributes'] + {"CustomUserData": "other data"} + ) + attributes = conn.get_endpoint_attributes(endpoint_arn)['GetEndpointAttributesResponse'][ + 'GetEndpointAttributesResult']['Attributes'] attributes.should.equal({ "Token": "some_unique_id", "Enabled": 'False', @@ -231,7 +253,8 @@ def test_delete_endpoint(): name="my-application", platform="APNS", ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] endpoint = conn.create_platform_endpoint( platform_application_arn=application_arn, @@ -242,7 +265,8 @@ def test_delete_endpoint(): "CustomUserData": "some data", }, ) - endpoint_arn = endpoint['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn'] + endpoint_arn = endpoint['CreatePlatformEndpointResponse'][ + 'CreatePlatformEndpointResult']['EndpointArn'] endpoint_list = conn.list_endpoints_by_platform_application( platform_application_arn=application_arn @@ -265,7 +289,8 @@ def test_publish_to_platform_endpoint(): name="my-application", platform="APNS", ) - application_arn = platform_application['CreatePlatformApplicationResponse']['CreatePlatformApplicationResult']['PlatformApplicationArn'] + application_arn = platform_application['CreatePlatformApplicationResponse'][ + 'CreatePlatformApplicationResult']['PlatformApplicationArn'] endpoint = conn.create_platform_endpoint( platform_application_arn=application_arn, @@ -276,6 +301,8 @@ def test_publish_to_platform_endpoint(): }, ) - endpoint_arn = endpoint['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn'] + endpoint_arn = endpoint['CreatePlatformEndpointResponse'][ + 'CreatePlatformEndpointResult']['EndpointArn'] - conn.publish(message="some message", message_structure="json", target_arn=endpoint_arn) + conn.publish(message="some message", message_structure="json", + target_arn=endpoint_arn) diff --git a/tests/test_sns/test_application_boto3.py b/tests/test_sns/test_application_boto3.py index 251d1cf1d..968240b15 100644 --- a/tests/test_sns/test_application_boto3.py +++ b/tests/test_sns/test_application_boto3.py @@ -18,7 +18,8 @@ def test_create_platform_application(): }, ) application_arn = response['PlatformApplicationArn'] - application_arn.should.equal('arn:aws:sns:us-east-1:123456789012:app/APNS/my-application') + application_arn.should.equal( + 'arn:aws:sns:us-east-1:123456789012:app/APNS/my-application') @mock_sns @@ -33,7 +34,8 @@ def test_get_platform_application_attributes(): }, ) arn = platform_application['PlatformApplicationArn'] - attributes = conn.get_platform_application_attributes(PlatformApplicationArn=arn)['Attributes'] + attributes = conn.get_platform_application_attributes( + PlatformApplicationArn=arn)['Attributes'] attributes.should.equal({ "PlatformCredential": "platform_credential", "PlatformPrincipal": "platform_principal", @@ -43,7 +45,8 @@ def test_get_platform_application_attributes(): @mock_sns def test_get_missing_platform_application_attributes(): conn = boto3.client('sns', region_name='us-east-1') - conn.get_platform_application_attributes.when.called_with(PlatformApplicationArn="a-fake-arn").should.throw(ClientError) + conn.get_platform_application_attributes.when.called_with( + PlatformApplicationArn="a-fake-arn").should.throw(ClientError) @mock_sns @@ -59,9 +62,11 @@ def test_set_platform_application_attributes(): ) arn = platform_application['PlatformApplicationArn'] conn.set_platform_application_attributes(PlatformApplicationArn=arn, - Attributes={"PlatformPrincipal": "other"} - ) - attributes = conn.get_platform_application_attributes(PlatformApplicationArn=arn)['Attributes'] + Attributes={ + "PlatformPrincipal": "other"} + ) + attributes = conn.get_platform_application_attributes( + PlatformApplicationArn=arn)['Attributes'] attributes.should.equal({ "PlatformCredential": "platform_credential", "PlatformPrincipal": "other", @@ -133,7 +138,8 @@ def test_create_platform_endpoint(): ) endpoint_arn = endpoint['EndpointArn'] - endpoint_arn.should.contain("arn:aws:sns:us-east-1:123456789012:endpoint/APNS/my-application/") + endpoint_arn.should.contain( + "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/my-application/") @mock_sns @@ -186,7 +192,8 @@ def test_get_endpoint_attributes(): ) endpoint_arn = endpoint['EndpointArn'] - attributes = conn.get_endpoint_attributes(EndpointArn=endpoint_arn)['Attributes'] + attributes = conn.get_endpoint_attributes( + EndpointArn=endpoint_arn)['Attributes'] attributes.should.equal({ "Token": "some_unique_id", "Enabled": 'false', @@ -197,7 +204,8 @@ def test_get_endpoint_attributes(): @mock_sns def test_get_missing_endpoint_attributes(): conn = boto3.client('sns', region_name='us-east-1') - conn.get_endpoint_attributes.when.called_with(EndpointArn="a-fake-arn").should.throw(ClientError) + conn.get_endpoint_attributes.when.called_with( + EndpointArn="a-fake-arn").should.throw(ClientError) @mock_sns @@ -222,9 +230,10 @@ def test_set_endpoint_attributes(): endpoint_arn = endpoint['EndpointArn'] conn.set_endpoint_attributes(EndpointArn=endpoint_arn, - Attributes={"CustomUserData": "other data"} - ) - attributes = conn.get_endpoint_attributes(EndpointArn=endpoint_arn)['Attributes'] + Attributes={"CustomUserData": "other data"} + ) + attributes = conn.get_endpoint_attributes( + EndpointArn=endpoint_arn)['Attributes'] attributes.should.equal({ "Token": "some_unique_id", "Enabled": 'false', @@ -253,4 +262,5 @@ def test_publish_to_platform_endpoint(): endpoint_arn = endpoint['EndpointArn'] - conn.publish(Message="some message", MessageStructure="json", TargetArn=endpoint_arn) + conn.publish(Message="some message", + MessageStructure="json", TargetArn=endpoint_arn) diff --git a/tests/test_sns/test_publishing.py b/tests/test_sns/test_publishing.py index dab2a569b..718bce5c4 100644 --- a/tests/test_sns/test_publishing.py +++ b/tests/test_sns/test_publishing.py @@ -15,12 +15,14 @@ def test_publish_to_sqs(): conn = boto.connect_sns() conn.create_topic("some-topic") topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] sqs_conn = boto.connect_sqs() sqs_conn.create_queue("test-queue") - conn.subscribe(topic_arn, "sqs", "arn:aws:sqs:us-east-1:123456789012:test-queue") + conn.subscribe(topic_arn, "sqs", + "arn:aws:sqs:us-east-1:123456789012:test-queue") conn.publish(topic=topic_arn, message="my message") @@ -35,12 +37,14 @@ def test_publish_to_sqs_in_different_region(): conn = boto.sns.connect_to_region("us-west-1") conn.create_topic("some-topic") topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] sqs_conn = boto.sqs.connect_to_region("us-west-2") sqs_conn.create_queue("test-queue") - conn.subscribe(topic_arn, "sqs", "arn:aws:sqs:us-west-2:123456789012:test-queue") + conn.subscribe(topic_arn, "sqs", + "arn:aws:sqs:us-west-2:123456789012:test-queue") conn.publish(topic=topic_arn, message="my message") @@ -61,9 +65,11 @@ def test_publish_to_http(): conn = boto.connect_sns() conn.create_topic("some-topic") topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] conn.subscribe(topic_arn, "http", "http://example.com/foobar") - response = conn.publish(topic=topic_arn, message="my message", subject="my subject") + response = conn.publish( + topic=topic_arn, message="my message", subject="my subject") message_id = response['PublishResponse']['PublishResult']['MessageId'] diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index edf2948fb..cda9fed60 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -70,5 +70,6 @@ def test_publish_to_http(): Protocol="http", Endpoint="http://example.com/foobar") - response = conn.publish(TopicArn=topic_arn, Message="my message", Subject="my subject") + response = conn.publish( + TopicArn=topic_arn, Message="my message", Subject="my subject") message_id = response['MessageId'] diff --git a/tests/test_sns/test_server.py b/tests/test_sns/test_server.py index 422763dac..ce505278f 100644 --- a/tests/test_sns/test_server.py +++ b/tests/test_sns/test_server.py @@ -15,8 +15,10 @@ def test_sns_server_get(): topic_data = test_client.action_data("CreateTopic", Name="test topic") topic_data.should.contain("CreateTopicResult") - topic_data.should.contain("arn:aws:sns:us-east-1:123456789012:test topic") + topic_data.should.contain( + "arn:aws:sns:us-east-1:123456789012:test topic") topics_data = test_client.action_data("ListTopics") topics_data.should.contain("ListTopicsResult") - topic_data.should.contain("arn:aws:sns:us-east-1:123456789012:test topic") + topic_data.should.contain( + "arn:aws:sns:us-east-1:123456789012:test topic") diff --git a/tests/test_sns/test_subscriptions.py b/tests/test_sns/test_subscriptions.py index e141c503a..c521bb428 100644 --- a/tests/test_sns/test_subscriptions.py +++ b/tests/test_sns/test_subscriptions.py @@ -12,11 +12,13 @@ def test_creating_subscription(): conn = boto.connect_sns() conn.create_topic("some-topic") topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] conn.subscribe(topic_arn, "http", "http://example.com/") - subscriptions = conn.get_all_subscriptions()["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["Subscriptions"] + subscriptions = conn.get_all_subscriptions()["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["Subscriptions"] subscriptions.should.have.length_of(1) subscription = subscriptions[0] subscription["TopicArn"].should.equal(topic_arn) @@ -28,7 +30,8 @@ def test_creating_subscription(): conn.unsubscribe(subscription["SubscriptionArn"]) # And there should be zero subscriptions left - subscriptions = conn.get_all_subscriptions()["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["Subscriptions"] + subscriptions = conn.get_all_subscriptions()["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["Subscriptions"] subscriptions.should.have.length_of(0) @@ -46,7 +49,8 @@ def test_getting_subscriptions_by_topic(): conn.subscribe(topic1_arn, "http", "http://example1.com/") conn.subscribe(topic2_arn, "http", "http://example2.com/") - topic1_subscriptions = conn.get_all_subscriptions_by_topic(topic1_arn)["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"]["Subscriptions"] + topic1_subscriptions = conn.get_all_subscriptions_by_topic(topic1_arn)[ + "ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"]["Subscriptions"] topic1_subscriptions.should.have.length_of(1) topic1_subscriptions[0]['Endpoint'].should.equal("http://example1.com/") @@ -63,25 +67,36 @@ def test_subscription_paging(): topic2_arn = topics[1]['TopicArn'] for index in range(DEFAULT_PAGE_SIZE + int(DEFAULT_PAGE_SIZE / 3)): - conn.subscribe(topic1_arn, 'email', 'email_' + str(index) + '@test.com') - conn.subscribe(topic2_arn, 'email', 'email_' + str(index) + '@test.com') + conn.subscribe(topic1_arn, 'email', 'email_' + + str(index) + '@test.com') + conn.subscribe(topic2_arn, 'email', 'email_' + + str(index) + '@test.com') all_subscriptions = conn.get_all_subscriptions() - all_subscriptions["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["Subscriptions"].should.have.length_of(DEFAULT_PAGE_SIZE) - next_token = all_subscriptions["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["NextToken"] + all_subscriptions["ListSubscriptionsResponse"]["ListSubscriptionsResult"][ + "Subscriptions"].should.have.length_of(DEFAULT_PAGE_SIZE) + next_token = all_subscriptions["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["NextToken"] next_token.should.equal(DEFAULT_PAGE_SIZE) all_subscriptions = conn.get_all_subscriptions(next_token=next_token * 2) - all_subscriptions["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["Subscriptions"].should.have.length_of(int(DEFAULT_PAGE_SIZE * 2 / 3)) - next_token = all_subscriptions["ListSubscriptionsResponse"]["ListSubscriptionsResult"]["NextToken"] + all_subscriptions["ListSubscriptionsResponse"]["ListSubscriptionsResult"][ + "Subscriptions"].should.have.length_of(int(DEFAULT_PAGE_SIZE * 2 / 3)) + next_token = all_subscriptions["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["NextToken"] next_token.should.equal(None) topic1_subscriptions = conn.get_all_subscriptions_by_topic(topic1_arn) - topic1_subscriptions["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"]["Subscriptions"].should.have.length_of(DEFAULT_PAGE_SIZE) - next_token = topic1_subscriptions["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"]["NextToken"] + topic1_subscriptions["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"][ + "Subscriptions"].should.have.length_of(DEFAULT_PAGE_SIZE) + next_token = topic1_subscriptions["ListSubscriptionsByTopicResponse"][ + "ListSubscriptionsByTopicResult"]["NextToken"] next_token.should.equal(DEFAULT_PAGE_SIZE) - topic1_subscriptions = conn.get_all_subscriptions_by_topic(topic1_arn, next_token=next_token) - topic1_subscriptions["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"]["Subscriptions"].should.have.length_of(int(DEFAULT_PAGE_SIZE / 3)) - next_token = topic1_subscriptions["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"]["NextToken"] + topic1_subscriptions = conn.get_all_subscriptions_by_topic( + topic1_arn, next_token=next_token) + topic1_subscriptions["ListSubscriptionsByTopicResponse"]["ListSubscriptionsByTopicResult"][ + "Subscriptions"].should.have.length_of(int(DEFAULT_PAGE_SIZE / 3)) + next_token = topic1_subscriptions["ListSubscriptionsByTopicResponse"][ + "ListSubscriptionsByTopicResult"]["NextToken"] next_token.should.equal(None) diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py index b884ca54d..906c483f7 100644 --- a/tests/test_sns/test_subscriptions_boto3.py +++ b/tests/test_sns/test_subscriptions_boto3.py @@ -52,7 +52,8 @@ def test_getting_subscriptions_by_topic(): Protocol="http", Endpoint="http://example2.com/") - topic1_subscriptions = conn.list_subscriptions_by_topic(TopicArn=topic1_arn)["Subscriptions"] + topic1_subscriptions = conn.list_subscriptions_by_topic(TopicArn=topic1_arn)[ + "Subscriptions"] topic1_subscriptions.should.have.length_of(1) topic1_subscriptions[0]['Endpoint'].should.equal("http://example1.com/") @@ -77,14 +78,19 @@ def test_subscription_paging(): next_token.should.equal(str(DEFAULT_PAGE_SIZE)) all_subscriptions = conn.list_subscriptions(NextToken=next_token) - all_subscriptions["Subscriptions"].should.have.length_of(int(DEFAULT_PAGE_SIZE / 3)) + all_subscriptions["Subscriptions"].should.have.length_of( + int(DEFAULT_PAGE_SIZE / 3)) all_subscriptions.shouldnt.have("NextToken") - topic1_subscriptions = conn.list_subscriptions_by_topic(TopicArn=topic1_arn) - topic1_subscriptions["Subscriptions"].should.have.length_of(DEFAULT_PAGE_SIZE) + topic1_subscriptions = conn.list_subscriptions_by_topic( + TopicArn=topic1_arn) + topic1_subscriptions["Subscriptions"].should.have.length_of( + DEFAULT_PAGE_SIZE) next_token = topic1_subscriptions["NextToken"] next_token.should.equal(str(DEFAULT_PAGE_SIZE)) - topic1_subscriptions = conn.list_subscriptions_by_topic(TopicArn=topic1_arn, NextToken=next_token) - topic1_subscriptions["Subscriptions"].should.have.length_of(int(DEFAULT_PAGE_SIZE / 3)) + topic1_subscriptions = conn.list_subscriptions_by_topic( + TopicArn=topic1_arn, NextToken=next_token) + topic1_subscriptions["Subscriptions"].should.have.length_of( + int(DEFAULT_PAGE_SIZE / 3)) topic1_subscriptions.shouldnt.have("NextToken") diff --git a/tests/test_sns/test_topics.py b/tests/test_sns/test_topics.py index ab2f06382..79b85f709 100644 --- a/tests/test_sns/test_topics.py +++ b/tests/test_sns/test_topics.py @@ -34,7 +34,8 @@ def test_create_and_delete_topic(): @mock_sns_deprecated def test_get_missing_topic(): conn = boto.connect_sns() - conn.get_topic_attributes.when.called_with("a-fake-arn").should.throw(BotoServerError) + conn.get_topic_attributes.when.called_with( + "a-fake-arn").should.throw(BotoServerError) @mock_sns_deprecated @@ -42,7 +43,9 @@ def test_create_topic_in_multiple_regions(): for region in ['us-west-1', 'us-west-2']: conn = boto.sns.connect_to_region(region) conn.create_topic("some-topic") - list(conn.get_all_topics()["ListTopicsResponse"]["ListTopicsResult"]["Topics"]).should.have.length_of(1) + list(conn.get_all_topics()["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"]).should.have.length_of(1) + @mock_sns_deprecated def test_topic_corresponds_to_region(): @@ -50,8 +53,11 @@ def test_topic_corresponds_to_region(): conn = boto.sns.connect_to_region(region) conn.create_topic("some-topic") topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] - topic_arn.should.equal("arn:aws:sns:{0}:123456789012:some-topic".format(region)) + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] + topic_arn.should.equal( + "arn:aws:sns:{0}:123456789012:some-topic".format(region)) + @mock_sns_deprecated def test_topic_attributes(): @@ -59,9 +65,11 @@ def test_topic_attributes(): conn.create_topic("some-topic") topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"][0]['TopicArn'] + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] - attributes = conn.get_topic_attributes(topic_arn)['GetTopicAttributesResponse']['GetTopicAttributesResult']['Attributes'] + attributes = conn.get_topic_attributes(topic_arn)['GetTopicAttributesResponse'][ + 'GetTopicAttributesResult']['Attributes'] attributes["TopicArn"].should.equal( "arn:aws:sns:{0}:123456789012:some-topic" .format(conn.region.name) @@ -73,7 +81,8 @@ def test_topic_attributes(): attributes["SubscriptionsConfirmed"].should.equal(0) attributes["SubscriptionsDeleted"].should.equal(0) attributes["DeliveryPolicy"].should.equal("") - attributes["EffectiveDeliveryPolicy"].should.equal(DEFAULT_EFFECTIVE_DELIVERY_POLICY) + attributes["EffectiveDeliveryPolicy"].should.equal( + DEFAULT_EFFECTIVE_DELIVERY_POLICY) # boto can't handle prefix-mandatory strings: # i.e. unicode on Python 2 -- u"foobar" @@ -90,10 +99,13 @@ def test_topic_attributes(): conn.set_topic_attributes(topic_arn, "DisplayName", displayname) conn.set_topic_attributes(topic_arn, "DeliveryPolicy", delivery) - attributes = conn.get_topic_attributes(topic_arn)['GetTopicAttributesResponse']['GetTopicAttributesResult']['Attributes'] + attributes = conn.get_topic_attributes(topic_arn)['GetTopicAttributesResponse'][ + 'GetTopicAttributesResult']['Attributes'] attributes["Policy"].should.equal("{'foo': 'bar'}") attributes["DisplayName"].should.equal("My display name") - attributes["DeliveryPolicy"].should.equal("{'http': {'defaultHealthyRetryPolicy': {'numRetries': 5}}}") + attributes["DeliveryPolicy"].should.equal( + "{'http': {'defaultHealthyRetryPolicy': {'numRetries': 5}}}") + @mock_sns_deprecated def test_topic_paging(): @@ -102,15 +114,19 @@ def test_topic_paging(): conn.create_topic("some-topic_" + str(index)) topics_json = conn.get_all_topics() - topics_list = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"] - next_token = topics_json["ListTopicsResponse"]["ListTopicsResult"]["NextToken"] + topics_list = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"] + next_token = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["NextToken"] len(topics_list).should.equal(DEFAULT_PAGE_SIZE) next_token.should.equal(DEFAULT_PAGE_SIZE) topics_json = conn.get_all_topics(next_token=next_token) - topics_list = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"] - next_token = topics_json["ListTopicsResponse"]["ListTopicsResult"]["NextToken"] + topics_list = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"] + next_token = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["NextToken"] topics_list.should.have.length_of(int(DEFAULT_PAGE_SIZE / 2)) next_token.should.equal(None) diff --git a/tests/test_sns/test_topics_boto3.py b/tests/test_sns/test_topics_boto3.py index b757a3750..55d03afff 100644 --- a/tests/test_sns/test_topics_boto3.py +++ b/tests/test_sns/test_topics_boto3.py @@ -35,7 +35,8 @@ def test_create_and_delete_topic(): @mock_sns def test_get_missing_topic(): conn = boto3.client("sns", region_name="us-east-1") - conn.get_topic_attributes.when.called_with(TopicArn="a-fake-arn").should.throw(ClientError) + conn.get_topic_attributes.when.called_with( + TopicArn="a-fake-arn").should.throw(ClientError) @mock_sns @@ -53,7 +54,8 @@ def test_topic_corresponds_to_region(): conn.create_topic(Name="some-topic") topics_json = conn.list_topics() topic_arn = topics_json["Topics"][0]['TopicArn'] - topic_arn.should.equal("arn:aws:sns:{0}:123456789012:some-topic".format(region)) + topic_arn.should.equal( + "arn:aws:sns:{0}:123456789012:some-topic".format(region)) @mock_sns @@ -76,7 +78,8 @@ def test_topic_attributes(): attributes["SubscriptionsConfirmed"].should.equal('0') attributes["SubscriptionsDeleted"].should.equal('0') attributes["DeliveryPolicy"].should.equal("") - attributes["EffectiveDeliveryPolicy"].should.equal(DEFAULT_EFFECTIVE_DELIVERY_POLICY) + attributes["EffectiveDeliveryPolicy"].should.equal( + DEFAULT_EFFECTIVE_DELIVERY_POLICY) # boto can't handle prefix-mandatory strings: # i.e. unicode on Python 2 -- u"foobar" @@ -84,11 +87,13 @@ def test_topic_attributes(): if six.PY2: policy = json.dumps({b"foo": b"bar"}) displayname = b"My display name" - delivery = json.dumps({b"http": {b"defaultHealthyRetryPolicy": {b"numRetries": 5}}}) + delivery = json.dumps( + {b"http": {b"defaultHealthyRetryPolicy": {b"numRetries": 5}}}) else: policy = json.dumps({u"foo": u"bar"}) displayname = u"My display name" - delivery = json.dumps({u"http": {u"defaultHealthyRetryPolicy": {u"numRetries": 5}}}) + delivery = json.dumps( + {u"http": {u"defaultHealthyRetryPolicy": {u"numRetries": 5}}}) conn.set_topic_attributes(TopicArn=topic_arn, AttributeName="Policy", AttributeValue=policy) @@ -102,7 +107,8 @@ def test_topic_attributes(): attributes = conn.get_topic_attributes(TopicArn=topic_arn)['Attributes'] attributes["Policy"].should.equal('{"foo": "bar"}') attributes["DisplayName"].should.equal("My display name") - attributes["DeliveryPolicy"].should.equal('{"http": {"defaultHealthyRetryPolicy": {"numRetries": 5}}}') + attributes["DeliveryPolicy"].should.equal( + '{"http": {"defaultHealthyRetryPolicy": {"numRetries": 5}}}') @mock_sns diff --git a/tests/test_sqs/test_server.py b/tests/test_sqs/test_server.py index c7411193a..b7a43ab90 100644 --- a/tests/test_sqs/test_server.py +++ b/tests/test_sqs/test_server.py @@ -31,7 +31,8 @@ def test_sqs_list_identities(): res = test_client.get( '/123/testqueue?Action=ReceiveMessage&MaxNumberOfMessages=1') - message = re.search("(.*?)", res.data.decode('utf-8')).groups()[0] + message = re.search("(.*?)", + res.data.decode('utf-8')).groups()[0] message.should.equal('test-message') @@ -58,7 +59,8 @@ def test_messages_polling(): msg_res = test_client.get( '/123/testqueue?Action=ReceiveMessage&MaxNumberOfMessages=1&WaitTimeSeconds=5' ) - new_msgs = re.findall("(.*?)", msg_res.data.decode('utf-8')) + new_msgs = re.findall("(.*?)", + msg_res.data.decode('utf-8')) count += len(new_msgs) messages.append(new_msgs) @@ -71,5 +73,6 @@ def test_messages_polling(): get_messages_thread.join() insert_messages_thread.join() - # got each message in a separate call to ReceiveMessage, despite the long WaitTimeSeconds + # got each message in a separate call to ReceiveMessage, despite the long + # WaitTimeSeconds assert len(messages) == 5 diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 89ea7413d..653963122 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -34,7 +34,8 @@ def test_create_queue(): @mock_sqs def test_get_inexistent_queue(): sqs = boto3.resource('sqs', region_name='us-east-1') - sqs.get_queue_by_name.when.called_with(QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) + sqs.get_queue_by_name.when.called_with( + QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) @mock_sqs @@ -43,8 +44,10 @@ def test_message_send(): queue = sqs.create_queue(QueueName="blah") msg = queue.send_message(MessageBody="derp") - msg.get('MD5OfMessageBody').should.equal('58fd9edd83341c29f1aebba81c31e257') - msg.get('ResponseMetadata', {}).get('RequestId').should.equal('27daac76-34dd-47df-bd01-1f6e873584a0') + msg.get('MD5OfMessageBody').should.equal( + '58fd9edd83341c29f1aebba81c31e257') + msg.get('ResponseMetadata', {}).get('RequestId').should.equal( + '27daac76-34dd-47df-bd01-1f6e873584a0') msg.get('MessageId').should_not.contain(' \n') messages = queue.receive_messages() @@ -73,7 +76,8 @@ def test_create_queues_in_multiple_region(): list(west1_conn.list_queues()['QueueUrls']).should.have.length_of(1) list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) - west1_conn.list_queues()['QueueUrls'][0].should.equal('http://sqs.us-west-1.amazonaws.com/123456789012/blah') + west1_conn.list_queues()['QueueUrls'][0].should.equal( + 'http://sqs.us-west-1.amazonaws.com/123456789012/blah') @mock_sqs @@ -87,14 +91,16 @@ def test_get_queue_with_prefix(): queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue.should.have.length_of(1) - queue[0].should.equal("http://sqs.us-west-1.amazonaws.com/123456789012/test-queue") + queue[0].should.equal( + "http://sqs.us-west-1.amazonaws.com/123456789012/test-queue") @mock_sqs def test_delete_queue(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') - conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": "60"}) + conn.create_queue(QueueName="test-queue", + Attributes={"VisibilityTimeout": "60"}) queue = sqs.Queue('test-queue') conn.list_queues()['QueueUrls'].should.have.length_of(1) @@ -110,7 +116,8 @@ def test_delete_queue(): def test_set_queue_attribute(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') - conn.create_queue(QueueName="test-queue", Attributes={"VisibilityTimeout": '60'}) + conn.create_queue(QueueName="test-queue", + Attributes={"VisibilityTimeout": '60'}) queue = sqs.Queue("test-queue") queue.attributes['VisibilityTimeout'].should.equal('60') @@ -133,7 +140,8 @@ def test_send_message(): response = queue.send_message(MessageBody=body_one) response = queue.send_message(MessageBody=body_two) - messages = conn.receive_message(QueueUrl=queue.url, MaxNumberOfMessages=2)['Messages'] + messages = conn.receive_message( + QueueUrl=queue.url, MaxNumberOfMessages=2)['Messages'] messages[0]['Body'].should.equal(body_one) messages[1]['Body'].should.equal(body_two) @@ -244,13 +252,15 @@ def test_receive_message_with_explicit_visibility_timeout(): queue.write(queue.new_message(body_one)) queue.count().should.equal(1) - messages = conn.receive_message(queue, number_messages=1, visibility_timeout=0) + messages = conn.receive_message( + queue, number_messages=1, visibility_timeout=0) assert len(messages) == 1 # Message should remain visible queue.count().should.equal(1) + @mock_sqs_deprecated def test_change_message_visibility(): conn = boto.connect_sqs('the_key', 'the_secret') @@ -381,7 +391,8 @@ def test_send_batch_operation_with_message_attributes(): queue = conn.create_queue("test-queue", visibility_timeout=60) queue.set_message_class(RawMessage) - message_tuple = ("my_first_message", 'test message 1', 0, {'name1': {'data_type': 'String', 'string_value': 'foo'}}) + message_tuple = ("my_first_message", 'test message 1', 0, { + 'name1': {'data_type': 'String', 'string_value': 'foo'}}) queue.write_batch([message_tuple]) messages = queue.get_messages() @@ -415,7 +426,8 @@ def test_queue_attributes(): queue_name = 'test-queue' visibility_timeout = 60 - queue = conn.create_queue(queue_name, visibility_timeout=visibility_timeout) + queue = conn.create_queue( + queue_name, visibility_timeout=visibility_timeout) attributes = queue.get_attributes() @@ -462,7 +474,8 @@ def test_change_message_visibility_on_invalid_receipt(): assert len(messages) == 1 - original_message.change_visibility.when.called_with(100).should.throw(SQSError) + original_message.change_visibility.when.called_with( + 100).should.throw(SQSError) @mock_sqs_deprecated @@ -485,7 +498,8 @@ def test_change_message_visibility_on_visible_message(): queue.count().should.equal(1) - original_message.change_visibility.when.called_with(100).should.throw(SQSError) + original_message.change_visibility.when.called_with( + 100).should.throw(SQSError) @mock_sqs_deprecated @@ -505,7 +519,8 @@ def test_purge_action(): def test_delete_message_after_visibility_timeout(): VISIBILITY_TIMEOUT = 1 conn = boto.sqs.connect_to_region("us-east-1") - new_queue = conn.create_queue('new-queue', visibility_timeout=VISIBILITY_TIMEOUT) + new_queue = conn.create_queue( + 'new-queue', visibility_timeout=VISIBILITY_TIMEOUT) m1 = Message() m1.set_body('Message 1!') diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py index 19865ca77..4e0e52606 100644 --- a/tests/test_sts/test_sts.py +++ b/tests/test_sts/test_sts.py @@ -16,7 +16,8 @@ def test_get_session_token(): token = conn.get_session_token(duration=123) token.expiration.should.equal('2012-01-01T12:02:03.000Z') - token.session_token.should.equal("AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE") + token.session_token.should.equal( + "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE") token.access_key.should.equal("AKIAIOSFODNN7EXAMPLE") token.secret_key.should.equal("wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") @@ -28,10 +29,13 @@ def test_get_federation_token(): token = conn.get_federation_token(duration=123, name="Bob") token.credentials.expiration.should.equal('2012-01-01T12:02:03.000Z') - token.credentials.session_token.should.equal("AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==") + token.credentials.session_token.should.equal( + "AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==") token.credentials.access_key.should.equal("AKIAIOSFODNN7EXAMPLE") - token.credentials.secret_key.should.equal("wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") - token.federated_user_arn.should.equal("arn:aws:sts::123456789012:federated-user/Bob") + token.credentials.secret_key.should.equal( + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") + token.federated_user_arn.should.equal( + "arn:aws:sts::123456789012:federated-user/Bob") token.federated_user_id.should.equal("123456789012:Bob") @@ -55,20 +59,25 @@ def test_assume_role(): ] }) s3_role = "arn:aws:iam::123456789012:role/test-role" - role = conn.assume_role(s3_role, "session-name", policy, duration_seconds=123) + role = conn.assume_role(s3_role, "session-name", + policy, duration_seconds=123) credentials = role.credentials credentials.expiration.should.equal('2012-01-01T12:02:03.000Z') - credentials.session_token.should.equal("BQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE") + credentials.session_token.should.equal( + "BQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE") credentials.access_key.should.equal("AKIAIOSFODNN7EXAMPLE") - credentials.secret_key.should.equal("aJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") + credentials.secret_key.should.equal( + "aJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") role.user.arn.should.equal("arn:aws:iam::123456789012:role/test-role") role.user.assume_role_id.should.contain("session-name") + @mock_sts def test_get_caller_identity(): - identity = boto3.client("sts", region_name='us-east-1').get_caller_identity() + identity = boto3.client( + "sts", region_name='us-east-1').get_caller_identity() identity['Arn'].should.equal('arn:aws:sts::123456789012:user/moto') identity['UserId'].should.equal('AKIAIOSFODNN7EXAMPLE') diff --git a/tests/test_swf/models/test_activity_task.py b/tests/test_swf/models/test_activity_task.py index 0885c4b1e..5dddab975 100644 --- a/tests/test_swf/models/test_activity_task.py +++ b/tests/test_swf/models/test_activity_task.py @@ -147,6 +147,7 @@ def test_activity_task_cannot_change_state_on_closed_workflow_execution(): ) wfe.complete(123) - task.timeout.when.called_with(Timeout(task, 0, "foo")).should.throw(SWFWorkflowExecutionClosedError) + task.timeout.when.called_with(Timeout(task, 0, "foo")).should.throw( + SWFWorkflowExecutionClosedError) task.complete.when.called_with().should.throw(SWFWorkflowExecutionClosedError) task.fail.when.called_with().should.throw(SWFWorkflowExecutionClosedError) diff --git a/tests/test_swf/models/test_decision_task.py b/tests/test_swf/models/test_decision_task.py index fdb53d28a..b5e23eaca 100644 --- a/tests/test_swf/models/test_decision_task.py +++ b/tests/test_swf/models/test_decision_task.py @@ -75,5 +75,6 @@ def test_decision_task_cannot_change_state_on_closed_workflow_execution(): wfe.complete(123) - task.timeout.when.called_with(Timeout(task, 0, "foo")).should.throw(SWFWorkflowExecutionClosedError) + task.timeout.when.called_with(Timeout(task, 0, "foo")).should.throw( + SWFWorkflowExecutionClosedError) task.complete.when.called_with().should.throw(SWFWorkflowExecutionClosedError) diff --git a/tests/test_swf/models/test_domain.py b/tests/test_swf/models/test_domain.py index ce3ed0f13..57f66c830 100644 --- a/tests/test_swf/models/test_domain.py +++ b/tests/test_swf/models/test_domain.py @@ -15,7 +15,8 @@ WorkflowExecution = namedtuple( def test_domain_short_dict_representation(): domain = Domain("foo", "52") - domain.to_short_dict().should.equal({"name": "foo", "status": "REGISTERED"}) + domain.to_short_dict().should.equal( + {"name": "foo", "status": "REGISTERED"}) domain.description = "foo bar" domain.to_short_dict()["description"].should.equal("foo bar") @@ -67,16 +68,23 @@ def test_domain_decision_tasks(): def test_domain_get_workflow_execution(): domain = Domain("my-domain", "60") - wfe1 = WorkflowExecution(workflow_id="wf-id-1", run_id="run-id-1", execution_status="OPEN", open=True) - wfe2 = WorkflowExecution(workflow_id="wf-id-1", run_id="run-id-2", execution_status="CLOSED", open=False) - wfe3 = WorkflowExecution(workflow_id="wf-id-2", run_id="run-id-3", execution_status="OPEN", open=True) - wfe4 = WorkflowExecution(workflow_id="wf-id-3", run_id="run-id-4", execution_status="CLOSED", open=False) + wfe1 = WorkflowExecution( + workflow_id="wf-id-1", run_id="run-id-1", execution_status="OPEN", open=True) + wfe2 = WorkflowExecution( + workflow_id="wf-id-1", run_id="run-id-2", execution_status="CLOSED", open=False) + wfe3 = WorkflowExecution( + workflow_id="wf-id-2", run_id="run-id-3", execution_status="OPEN", open=True) + wfe4 = WorkflowExecution( + workflow_id="wf-id-3", run_id="run-id-4", execution_status="CLOSED", open=False) domain.workflow_executions = [wfe1, wfe2, wfe3, wfe4] # get workflow execution through workflow_id and run_id - domain.get_workflow_execution("wf-id-1", run_id="run-id-1").should.equal(wfe1) - domain.get_workflow_execution("wf-id-1", run_id="run-id-2").should.equal(wfe2) - domain.get_workflow_execution("wf-id-3", run_id="run-id-4").should.equal(wfe4) + domain.get_workflow_execution( + "wf-id-1", run_id="run-id-1").should.equal(wfe1) + domain.get_workflow_execution( + "wf-id-1", run_id="run-id-2").should.equal(wfe2) + domain.get_workflow_execution( + "wf-id-3", run_id="run-id-4").should.equal(wfe4) domain.get_workflow_execution.when.called_with( "wf-id-1", run_id="non-existent" @@ -98,7 +106,8 @@ def test_domain_get_workflow_execution(): ) # raise_if_closed attribute - domain.get_workflow_execution("wf-id-1", run_id="run-id-1", raise_if_closed=True).should.equal(wfe1) + domain.get_workflow_execution( + "wf-id-1", run_id="run-id-1", raise_if_closed=True).should.equal(wfe1) domain.get_workflow_execution.when.called_with( "wf-id-3", run_id="run-id-4", raise_if_closed=True ).should.throw( diff --git a/tests/test_swf/models/test_generic_type.py b/tests/test_swf/models/test_generic_type.py index 692c66a47..d7410f395 100644 --- a/tests/test_swf/models/test_generic_type.py +++ b/tests/test_swf/models/test_generic_type.py @@ -3,6 +3,7 @@ from moto.swf.models import GenericType # Tests for GenericType (ActivityType, WorkflowType) class FooType(GenericType): + @property def kind(self): return "foo" @@ -38,10 +39,12 @@ def test_type_full_dict_representation(): _type.to_full_dict()["configuration"].should.equal({}) _type.task_list = "foo" - _type.to_full_dict()["configuration"]["defaultTaskList"].should.equal({"name": "foo"}) + _type.to_full_dict()["configuration"][ + "defaultTaskList"].should.equal({"name": "foo"}) _type.just_an_example_timeout = "60" - _type.to_full_dict()["configuration"]["justAnExampleTimeout"].should.equal("60") + _type.to_full_dict()["configuration"][ + "justAnExampleTimeout"].should.equal("60") _type.non_whitelisted_property = "34" keys = _type.to_full_dict()["configuration"].keys() @@ -50,4 +53,5 @@ def test_type_full_dict_representation(): def test_type_string_representation(): _type = FooType("test-foo", "v1.0") - str(_type).should.equal("FooType(name: test-foo, version: v1.0, status: REGISTERED)") + str(_type).should.equal( + "FooType(name: test-foo, version: v1.0, status: REGISTERED)") diff --git a/tests/test_swf/models/test_workflow_execution.py b/tests/test_swf/models/test_workflow_execution.py index f6a69f8d7..45b91c86a 100644 --- a/tests/test_swf/models/test_workflow_execution.py +++ b/tests/test_swf/models/test_workflow_execution.py @@ -240,8 +240,10 @@ def test_workflow_execution_schedule_activity_task(): wfe.open_counts["openActivityTasks"].should.equal(1) last_event = wfe.events()[-1] last_event.event_type.should.equal("ActivityTaskScheduled") - last_event.event_attributes["decisionTaskCompletedEventId"].should.equal(123) - last_event.event_attributes["taskList"]["name"].should.equal("task-list-name") + last_event.event_attributes[ + "decisionTaskCompletedEventId"].should.equal(123) + last_event.event_attributes["taskList"][ + "name"].should.equal("task-list-name") wfe.activity_tasks.should.have.length_of(1) task = wfe.activity_tasks[0] @@ -288,43 +290,50 @@ def test_workflow_execution_schedule_activity_task_should_fail_if_wrong_attribut wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("ACTIVITY_TYPE_DOES_NOT_EXIST") + last_event.event_attributes["cause"].should.equal( + "ACTIVITY_TYPE_DOES_NOT_EXIST") hsh["activityType"]["name"] = "test-activity" wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("ACTIVITY_TYPE_DEPRECATED") + last_event.event_attributes["cause"].should.equal( + "ACTIVITY_TYPE_DEPRECATED") hsh["activityType"]["version"] = "v1.2" wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("DEFAULT_TASK_LIST_UNDEFINED") + last_event.event_attributes["cause"].should.equal( + "DEFAULT_TASK_LIST_UNDEFINED") hsh["taskList"] = {"name": "foobar"} wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("DEFAULT_SCHEDULE_TO_START_TIMEOUT_UNDEFINED") + last_event.event_attributes["cause"].should.equal( + "DEFAULT_SCHEDULE_TO_START_TIMEOUT_UNDEFINED") hsh["scheduleToStartTimeout"] = "600" wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("DEFAULT_SCHEDULE_TO_CLOSE_TIMEOUT_UNDEFINED") + last_event.event_attributes["cause"].should.equal( + "DEFAULT_SCHEDULE_TO_CLOSE_TIMEOUT_UNDEFINED") hsh["scheduleToCloseTimeout"] = "600" wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("DEFAULT_START_TO_CLOSE_TIMEOUT_UNDEFINED") + last_event.event_attributes["cause"].should.equal( + "DEFAULT_START_TO_CLOSE_TIMEOUT_UNDEFINED") hsh["startToCloseTimeout"] = "600" wfe.schedule_activity_task(123, hsh) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("DEFAULT_HEARTBEAT_TIMEOUT_UNDEFINED") + last_event.event_attributes["cause"].should.equal( + "DEFAULT_HEARTBEAT_TIMEOUT_UNDEFINED") wfe.open_counts["openActivityTasks"].should.equal(0) wfe.activity_tasks.should.have.length_of(0) @@ -393,7 +402,8 @@ def test_workflow_execution_schedule_activity_task_with_same_activity_id(): wfe.open_counts["openActivityTasks"].should.equal(1) last_event = wfe.events()[-1] last_event.event_type.should.equal("ScheduleActivityTaskFailed") - last_event.event_attributes["cause"].should.equal("ACTIVITY_ID_ALREADY_IN_USE") + last_event.event_attributes["cause"].should.equal( + "ACTIVITY_ID_ALREADY_IN_USE") def test_workflow_execution_start_activity_task(): @@ -456,7 +466,8 @@ def test_first_timeout(): wfe.first_timeout().should.be.a(Timeout) -# See moto/swf/models/workflow_execution.py "_process_timeouts()" for more details +# See moto/swf/models/workflow_execution.py "_process_timeouts()" for more +# details def test_timeouts_are_processed_in_order_and_reevaluated(): # Let's make a Workflow Execution with the following properties: # - execution start to close timeout of 8 mins diff --git a/tests/test_swf/responses/test_activity_tasks.py b/tests/test_swf/responses/test_activity_tasks.py index e6671e9e9..3511d4e56 100644 --- a/tests/test_swf/responses/test_activity_tasks.py +++ b/tests/test_swf/responses/test_activity_tasks.py @@ -11,15 +11,18 @@ from ..utils import setup_workflow, SCHEDULE_ACTIVITY_TASK_DECISION @mock_swf_deprecated def test_poll_for_activity_task_when_one(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - resp = conn.poll_for_activity_task("test-domain", "activity-task-list", identity="surprise") + resp = conn.poll_for_activity_task( + "test-domain", "activity-task-list", identity="surprise") resp["activityId"].should.equal("my-activity-001") resp["taskToken"].should_not.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") resp["events"][-1]["eventType"].should.equal("ActivityTaskStarted") resp["events"][-1]["activityTaskStartedEventAttributes"].should.equal( {"identity": "surprise", "scheduledEventId": 5} @@ -44,12 +47,14 @@ def test_poll_for_activity_task_on_non_existent_queue(): @mock_swf_deprecated def test_count_pending_activity_tasks(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - resp = conn.count_pending_activity_tasks("test-domain", "activity-task-list") + resp = conn.count_pending_activity_tasks( + "test-domain", "activity-task-list") resp.should.equal({"count": 1, "truncated": False}) @@ -64,16 +69,20 @@ def test_count_pending_decision_tasks_on_non_existent_task_list(): @mock_swf_deprecated def test_respond_activity_task_completed(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - activity_token = conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] + activity_token = conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] - resp = conn.respond_activity_task_completed(activity_token, result="result of the task") + resp = conn.respond_activity_task_completed( + activity_token, result="result of the task") resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") resp["events"][-2]["eventType"].should.equal("ActivityTaskCompleted") resp["events"][-2]["activityTaskCompletedEventAttributes"].should.equal( {"result": "result of the task", "scheduledEventId": 5, "startedEventId": 6} @@ -83,13 +92,16 @@ def test_respond_activity_task_completed(): @mock_swf_deprecated def test_respond_activity_task_completed_on_closed_workflow_execution(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - activity_token = conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] + activity_token = conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] - # bad: we're closing workflow execution manually, but endpoints are not coded for now.. + # bad: we're closing workflow execution manually, but endpoints are not + # coded for now.. wfe = swf_backend.domains[0].workflow_executions[-1] wfe.execution_status = "CLOSED" # /bad @@ -102,11 +114,13 @@ def test_respond_activity_task_completed_on_closed_workflow_execution(): @mock_swf_deprecated def test_respond_activity_task_completed_with_task_already_completed(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - activity_token = conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] + activity_token = conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] conn.respond_activity_task_completed(activity_token) @@ -119,18 +133,21 @@ def test_respond_activity_task_completed_with_task_already_completed(): @mock_swf_deprecated def test_respond_activity_task_failed(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - activity_token = conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] + activity_token = conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] resp = conn.respond_activity_task_failed(activity_token, reason="short reason", details="long details") resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") resp["events"][-2]["eventType"].should.equal("ActivityTaskFailed") resp["events"][-2]["activityTaskFailedEventAttributes"].should.equal( {"reason": "short reason", "details": "long details", @@ -144,7 +161,8 @@ def test_respond_activity_task_completed_with_wrong_token(): # because the safeguards are shared with RespondActivityTaskCompleted, so # no need to retest everything end-to-end. conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) @@ -158,11 +176,13 @@ def test_respond_activity_task_completed_with_wrong_token(): @mock_swf_deprecated def test_record_activity_task_heartbeat(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - activity_token = conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] + activity_token = conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] resp = conn.record_activity_task_heartbeat(activity_token) resp.should.equal({"cancelRequested": False}) @@ -171,11 +191,13 @@ def test_record_activity_task_heartbeat(): @mock_swf_deprecated def test_record_activity_task_heartbeat_with_wrong_token(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] + conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] conn.record_activity_task_heartbeat.when.called_with( "bad-token", details="some progress details" @@ -185,17 +207,21 @@ def test_record_activity_task_heartbeat_with_wrong_token(): @mock_swf_deprecated def test_record_activity_task_heartbeat_sets_details_in_case_of_timeout(): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) with freeze_time("2015-01-01 12:00:00"): - activity_token = conn.poll_for_activity_task("test-domain", "activity-task-list")["taskToken"] - conn.record_activity_task_heartbeat(activity_token, details="some progress details") + activity_token = conn.poll_for_activity_task( + "test-domain", "activity-task-list")["taskToken"] + conn.record_activity_task_heartbeat( + activity_token, details="some progress details") with freeze_time("2015-01-01 12:05:30"): # => Activity Task Heartbeat timeout reached!! - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") resp["events"][-2]["eventType"].should.equal("ActivityTaskTimedOut") attrs = resp["events"][-2]["activityTaskTimedOutEventAttributes"] attrs["details"].should.equal("some progress details") diff --git a/tests/test_swf/responses/test_activity_types.py b/tests/test_swf/responses/test_activity_types.py index 20c44dc5f..b283d3448 100644 --- a/tests/test_swf/responses/test_activity_types.py +++ b/tests/test_swf/responses/test_activity_types.py @@ -48,8 +48,10 @@ def test_list_activity_types(): conn.register_activity_type("test-domain", "c-test-activity", "v1.0") all_activity_types = conn.list_activity_types("test-domain", "REGISTERED") - names = [activity_type["activityType"]["name"] for activity_type in all_activity_types["typeInfos"]] - names.should.equal(["a-test-activity", "b-test-activity", "c-test-activity"]) + names = [activity_type["activityType"]["name"] + for activity_type in all_activity_types["typeInfos"]] + names.should.equal( + ["a-test-activity", "b-test-activity", "c-test-activity"]) @mock_swf_deprecated @@ -62,8 +64,10 @@ def test_list_activity_types_reverse_order(): all_activity_types = conn.list_activity_types("test-domain", "REGISTERED", reverse_order=True) - names = [activity_type["activityType"]["name"] for activity_type in all_activity_types["typeInfos"]] - names.should.equal(["c-test-activity", "b-test-activity", "a-test-activity"]) + names = [activity_type["activityType"]["name"] + for activity_type in all_activity_types["typeInfos"]] + names.should.equal( + ["c-test-activity", "b-test-activity", "a-test-activity"]) # DeprecateActivityType endpoint @@ -110,7 +114,8 @@ def test_describe_activity_type(): conn.register_activity_type("test-domain", "test-activity", "v1.0", task_list="foo", default_task_heartbeat_timeout="32") - actype = conn.describe_activity_type("test-domain", "test-activity", "v1.0") + actype = conn.describe_activity_type( + "test-domain", "test-activity", "v1.0") actype["configuration"]["defaultTaskList"]["name"].should.equal("foo") infos = actype["typeInfo"] infos["activityType"]["name"].should.equal("test-activity") diff --git a/tests/test_swf/responses/test_decision_tasks.py b/tests/test_swf/responses/test_decision_tasks.py index b552723cb..466e1a2ae 100644 --- a/tests/test_swf/responses/test_decision_tasks.py +++ b/tests/test_swf/responses/test_decision_tasks.py @@ -12,15 +12,19 @@ from ..utils import setup_workflow def test_poll_for_decision_task_when_one(): conn = setup_workflow() - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") types = [evt["eventType"] for evt in resp["events"]] types.should.equal(["WorkflowExecutionStarted", "DecisionTaskScheduled"]) - resp = conn.poll_for_decision_task("test-domain", "queue", identity="srv01") + resp = conn.poll_for_decision_task( + "test-domain", "queue", identity="srv01") types = [evt["eventType"] for evt in resp["events"]] - types.should.equal(["WorkflowExecutionStarted", "DecisionTaskScheduled", "DecisionTaskStarted"]) + types.should.equal(["WorkflowExecutionStarted", + "DecisionTaskScheduled", "DecisionTaskStarted"]) - resp["events"][-1]["decisionTaskStartedEventAttributes"]["identity"].should.equal("srv01") + resp[ + "events"][-1]["decisionTaskStartedEventAttributes"]["identity"].should.equal("srv01") @mock_swf_deprecated @@ -44,9 +48,11 @@ def test_poll_for_decision_task_on_non_existent_queue(): @mock_swf_deprecated def test_poll_for_decision_task_with_reverse_order(): conn = setup_workflow() - resp = conn.poll_for_decision_task("test-domain", "queue", reverse_order=True) + resp = conn.poll_for_decision_task( + "test-domain", "queue", reverse_order=True) types = [evt["eventType"] for evt in resp["events"]] - types.should.equal(["DecisionTaskStarted", "DecisionTaskScheduled", "WorkflowExecutionStarted"]) + types.should.equal( + ["DecisionTaskStarted", "DecisionTaskScheduled", "WorkflowExecutionStarted"]) # CountPendingDecisionTasks endpoint @@ -89,7 +95,8 @@ def test_respond_decision_task_completed_with_no_decision(): ) resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") types = [evt["eventType"] for evt in resp["events"]] types.should.equal([ "WorkflowExecutionStarted", @@ -104,7 +111,8 @@ def test_respond_decision_task_completed_with_no_decision(): "startedEventId": 3, }) - resp = conn.describe_workflow_execution("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.describe_workflow_execution( + "test-domain", conn.run_id, "uid-abcd1234") resp["latestExecutionContext"].should.equal("free-form context") @@ -123,7 +131,8 @@ def test_respond_decision_task_completed_on_close_workflow_execution(): resp = conn.poll_for_decision_task("test-domain", "queue") task_token = resp["taskToken"] - # bad: we're closing workflow execution manually, but endpoints are not coded for now.. + # bad: we're closing workflow execution manually, but endpoints are not + # coded for now.. wfe = swf_backend.domains[0].workflow_executions[-1] wfe.execution_status = "CLOSED" # /bad @@ -155,10 +164,12 @@ def test_respond_decision_task_completed_with_complete_workflow_execution(): "decisionType": "CompleteWorkflowExecution", "completeWorkflowExecutionDecisionAttributes": {"result": "foo bar"} }] - resp = conn.respond_decision_task_completed(task_token, decisions=decisions) + resp = conn.respond_decision_task_completed( + task_token, decisions=decisions) resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") types = [evt["eventType"] for evt in resp["events"]] types.should.equal([ "WorkflowExecutionStarted", @@ -167,7 +178,8 @@ def test_respond_decision_task_completed_with_complete_workflow_execution(): "DecisionTaskCompleted", "WorkflowExecutionCompleted", ]) - resp["events"][-1]["workflowExecutionCompletedEventAttributes"]["result"].should.equal("foo bar") + resp["events"][-1]["workflowExecutionCompletedEventAttributes"][ + "result"].should.equal("foo bar") @mock_swf_deprecated @@ -255,10 +267,12 @@ def test_respond_decision_task_completed_with_fail_workflow_execution(): "decisionType": "FailWorkflowExecution", "failWorkflowExecutionDecisionAttributes": {"reason": "my rules", "details": "foo"} }] - resp = conn.respond_decision_task_completed(task_token, decisions=decisions) + resp = conn.respond_decision_task_completed( + task_token, decisions=decisions) resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") types = [evt["eventType"] for evt in resp["events"]] types.should.equal([ "WorkflowExecutionStarted", @@ -294,10 +308,12 @@ def test_respond_decision_task_completed_with_schedule_activity_task(): }, } }] - resp = conn.respond_decision_task_completed(task_token, decisions=decisions) + resp = conn.respond_decision_task_completed( + task_token, decisions=decisions) resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") types = [evt["eventType"] for evt in resp["events"]] types.should.equal([ "WorkflowExecutionStarted", @@ -320,5 +336,6 @@ def test_respond_decision_task_completed_with_schedule_activity_task(): }, }) - resp = conn.describe_workflow_execution("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.describe_workflow_execution( + "test-domain", conn.run_id, "uid-abcd1234") resp["latestActivityTaskTimestamp"].should.equal(1420113600.0) diff --git a/tests/test_swf/responses/test_domains.py b/tests/test_swf/responses/test_domains.py index 1f785095c..3fa12d665 100644 --- a/tests/test_swf/responses/test_domains.py +++ b/tests/test_swf/responses/test_domains.py @@ -102,7 +102,8 @@ def test_describe_domain(): conn.register_domain("test-domain", "60", description="A test domain") domain = conn.describe_domain("test-domain") - domain["configuration"]["workflowExecutionRetentionPeriodInDays"].should.equal("60") + domain["configuration"][ + "workflowExecutionRetentionPeriodInDays"].should.equal("60") domain["domainInfo"]["description"].should.equal("A test domain") domain["domainInfo"]["name"].should.equal("test-domain") domain["domainInfo"]["status"].should.equal("REGISTERED") diff --git a/tests/test_swf/responses/test_timeouts.py b/tests/test_swf/responses/test_timeouts.py index 726410e76..5bd0ead96 100644 --- a/tests/test_swf/responses/test_timeouts.py +++ b/tests/test_swf/responses/test_timeouts.py @@ -11,19 +11,23 @@ from ..utils import setup_workflow, SCHEDULE_ACTIVITY_TASK_DECISION def test_activity_task_heartbeat_timeout(): with freeze_time("2015-01-01 12:00:00"): conn = setup_workflow() - decision_token = conn.poll_for_decision_task("test-domain", "queue")["taskToken"] + decision_token = conn.poll_for_decision_task( + "test-domain", "queue")["taskToken"] conn.respond_decision_task_completed(decision_token, decisions=[ SCHEDULE_ACTIVITY_TASK_DECISION ]) - conn.poll_for_activity_task("test-domain", "activity-task-list", identity="surprise") + conn.poll_for_activity_task( + "test-domain", "activity-task-list", identity="surprise") with freeze_time("2015-01-01 12:04:30"): - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") resp["events"][-1]["eventType"].should.equal("ActivityTaskStarted") with freeze_time("2015-01-01 12:05:30"): # => Activity Task Heartbeat timeout reached!! - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") resp["events"][-2]["eventType"].should.equal("ActivityTaskTimedOut") attrs = resp["events"][-2]["activityTaskTimedOutEventAttributes"] @@ -44,7 +48,8 @@ def test_decision_task_start_to_close_timeout(): conn.poll_for_decision_task("test-domain", "queue")["taskToken"] with freeze_time("2015-01-01 12:04:30"): - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") event_types = [evt["eventType"] for evt in resp["events"]] event_types.should.equal( @@ -53,7 +58,8 @@ def test_decision_task_start_to_close_timeout(): with freeze_time("2015-01-01 12:05:30"): # => Decision Task Start to Close timeout reached!! - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") event_types = [evt["eventType"] for evt in resp["events"]] event_types.should.equal( @@ -77,7 +83,8 @@ def test_workflow_execution_start_to_close_timeout(): conn = setup_workflow() with freeze_time("2015-01-01 13:59:30"): - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") event_types = [evt["eventType"] for evt in resp["events"]] event_types.should.equal( @@ -86,11 +93,13 @@ def test_workflow_execution_start_to_close_timeout(): with freeze_time("2015-01-01 14:00:30"): # => Workflow Execution Start to Close timeout reached!! - resp = conn.get_workflow_execution_history("test-domain", conn.run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", conn.run_id, "uid-abcd1234") event_types = [evt["eventType"] for evt in resp["events"]] event_types.should.equal( - ["WorkflowExecutionStarted", "DecisionTaskScheduled", "WorkflowExecutionTimedOut"] + ["WorkflowExecutionStarted", "DecisionTaskScheduled", + "WorkflowExecutionTimedOut"] ) attrs = resp["events"][-1]["workflowExecutionTimedOutEventAttributes"] attrs.should.equal({ diff --git a/tests/test_swf/responses/test_workflow_executions.py b/tests/test_swf/responses/test_workflow_executions.py index d5dc44a38..5c97c778b 100644 --- a/tests/test_swf/responses/test_workflow_executions.py +++ b/tests/test_swf/responses/test_workflow_executions.py @@ -30,14 +30,16 @@ def setup_swf_environment(): def test_start_workflow_execution(): conn = setup_swf_environment() - wf = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + wf = conn.start_workflow_execution( + "test-domain", "uid-abcd1234", "test-workflow", "v1.0") wf.should.contain("runId") @mock_swf_deprecated def test_start_already_started_workflow_execution(): conn = setup_swf_environment() - conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + conn.start_workflow_execution( + "test-domain", "uid-abcd1234", "test-workflow", "v1.0") conn.start_workflow_execution.when.called_with( "test-domain", "uid-abcd1234", "test-workflow", "v1.0" @@ -58,11 +60,14 @@ def test_start_workflow_execution_on_deprecated_type(): @mock_swf_deprecated def test_describe_workflow_execution(): conn = setup_swf_environment() - hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + hsh = conn.start_workflow_execution( + "test-domain", "uid-abcd1234", "test-workflow", "v1.0") run_id = hsh["runId"] - wfe = conn.describe_workflow_execution("test-domain", run_id, "uid-abcd1234") - wfe["executionInfo"]["execution"]["workflowId"].should.equal("uid-abcd1234") + wfe = conn.describe_workflow_execution( + "test-domain", run_id, "uid-abcd1234") + wfe["executionInfo"]["execution"][ + "workflowId"].should.equal("uid-abcd1234") wfe["executionInfo"]["executionStatus"].should.equal("OPEN") @@ -79,10 +84,12 @@ def test_describe_non_existent_workflow_execution(): @mock_swf_deprecated def test_get_workflow_execution_history(): conn = setup_swf_environment() - hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + hsh = conn.start_workflow_execution( + "test-domain", "uid-abcd1234", "test-workflow", "v1.0") run_id = hsh["runId"] - resp = conn.get_workflow_execution_history("test-domain", run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", run_id, "uid-abcd1234") types = [evt["eventType"] for evt in resp["events"]] types.should.equal(["WorkflowExecutionStarted", "DecisionTaskScheduled"]) @@ -90,7 +97,8 @@ def test_get_workflow_execution_history(): @mock_swf_deprecated def test_get_workflow_execution_history_with_reverse_order(): conn = setup_swf_environment() - hsh = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + hsh = conn.start_workflow_execution( + "test-domain", "uid-abcd1234", "test-workflow", "v1.0") run_id = hsh["runId"] resp = conn.get_workflow_execution_history("test-domain", run_id, "uid-abcd1234", @@ -191,7 +199,8 @@ def test_terminate_workflow_execution(): run_id=run_id) resp.should.be.none - resp = conn.get_workflow_execution_history("test-domain", run_id, "uid-abcd1234") + resp = conn.get_workflow_execution_history( + "test-domain", run_id, "uid-abcd1234") evt = resp["events"][-1] evt["eventType"].should.equal("WorkflowExecutionTerminated") attrs = evt["workflowExecutionTerminatedEventAttributes"] diff --git a/tests/test_swf/responses/test_workflow_types.py b/tests/test_swf/responses/test_workflow_types.py index 1e838c2ee..9e097a873 100644 --- a/tests/test_swf/responses/test_workflow_types.py +++ b/tests/test_swf/responses/test_workflow_types.py @@ -49,8 +49,10 @@ def test_list_workflow_types(): conn.register_workflow_type("test-domain", "c-test-workflow", "v1.0") all_workflow_types = conn.list_workflow_types("test-domain", "REGISTERED") - names = [activity_type["workflowType"]["name"] for activity_type in all_workflow_types["typeInfos"]] - names.should.equal(["a-test-workflow", "b-test-workflow", "c-test-workflow"]) + names = [activity_type["workflowType"]["name"] + for activity_type in all_workflow_types["typeInfos"]] + names.should.equal( + ["a-test-workflow", "b-test-workflow", "c-test-workflow"]) @mock_swf_deprecated @@ -63,8 +65,10 @@ def test_list_workflow_types_reverse_order(): all_workflow_types = conn.list_workflow_types("test-domain", "REGISTERED", reverse_order=True) - names = [activity_type["workflowType"]["name"] for activity_type in all_workflow_types["typeInfos"]] - names.should.equal(["c-test-workflow", "b-test-workflow", "a-test-workflow"]) + names = [activity_type["workflowType"]["name"] + for activity_type in all_workflow_types["typeInfos"]] + names.should.equal( + ["c-test-workflow", "b-test-workflow", "a-test-workflow"]) # DeprecateWorkflowType endpoint @@ -111,10 +115,12 @@ def test_describe_workflow_type(): conn.register_workflow_type("test-domain", "test-workflow", "v1.0", task_list="foo", default_child_policy="TERMINATE") - actype = conn.describe_workflow_type("test-domain", "test-workflow", "v1.0") + actype = conn.describe_workflow_type( + "test-domain", "test-workflow", "v1.0") actype["configuration"]["defaultTaskList"]["name"].should.equal("foo") actype["configuration"]["defaultChildPolicy"].should.equal("TERMINATE") - actype["configuration"].keys().should_not.contain("defaultTaskStartToCloseTimeout") + actype["configuration"].keys().should_not.contain( + "defaultTaskStartToCloseTimeout") infos = actype["typeInfo"] infos["workflowType"]["name"].should.equal("test-workflow") infos["workflowType"]["version"].should.equal("v1.0") diff --git a/tests/test_swf/utils.py b/tests/test_swf/utils.py index 756d17c27..2197b71df 100644 --- a/tests/test_swf/utils.py +++ b/tests/test_swf/utils.py @@ -29,7 +29,8 @@ SCHEDULE_ACTIVITY_TASK_DECISION = { } } for key, value in ACTIVITY_TASK_TIMEOUTS.items(): - SCHEDULE_ACTIVITY_TASK_DECISION["scheduleActivityTaskDecisionAttributes"][key] = value + SCHEDULE_ACTIVITY_TASK_DECISION[ + "scheduleActivityTaskDecisionAttributes"][key] = value # A test Domain @@ -86,7 +87,8 @@ def setup_workflow(): default_task_schedule_to_start_timeout="600", default_task_start_to_close_timeout="600", ) - wfe = conn.start_workflow_execution("test-domain", "uid-abcd1234", "test-workflow", "v1.0") + wfe = conn.start_workflow_execution( + "test-domain", "uid-abcd1234", "test-workflow", "v1.0") conn.run_id = wfe["runId"] return conn From 0dda687762d44f58fa643372cb79037bf0c122a5 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 23 Feb 2017 21:41:05 -0500 Subject: [PATCH 033/274] Fix urlparse for py3. --- moto/compat.py | 5 ----- moto/emr/responses.py | 2 +- moto/instance_metadata/responses.py | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/moto/compat.py b/moto/compat.py index 2dd2d879e..a92a5f67b 100644 --- a/moto/compat.py +++ b/moto/compat.py @@ -3,8 +3,3 @@ try: except ImportError: # python 2.6 or earlier, use backport from ordereddict import OrderedDict # flake8: noqa - -try: - from urlparse import urlparse # flake8: noqa -except ImportError: - from urllib.parse import urlparse # flake8: noqa diff --git a/moto/emr/responses.py b/moto/emr/responses.py index 91dc8cc11..3919d8b3e 100644 --- a/moto/emr/responses.py +++ b/moto/emr/responses.py @@ -6,7 +6,7 @@ from functools import wraps import pytz -from moto.compat import urlparse +from six.moves.urllib.parse import urlparse from moto.core.responses import AWSServiceSpec from moto.core.responses import BaseResponse from moto.core.responses import xml_to_json_response diff --git a/moto/instance_metadata/responses.py b/moto/instance_metadata/responses.py index 2ea9aa9a8..460e65aca 100644 --- a/moto/instance_metadata/responses.py +++ b/moto/instance_metadata/responses.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import datetime import json -from urlparse import urlparse +from six.moves.urllib.parse import urlparse from moto.core.responses import BaseResponse From 3c0c4c29960cf290b346c6dbb11b348776f60373 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 23 Feb 2017 22:28:09 -0500 Subject: [PATCH 034/274] Fix tests for py3. --- moto/awslambda/responses.py | 2 +- moto/backends.py | 4 ++-- moto/cloudformation/parsing.py | 6 ++---- moto/emr/responses.py | 2 +- moto/server.py | 4 ++-- tests/test_cloudformation/test_stack_parsing.py | 4 ++-- tests/test_ec2/test_spot_instances.py | 4 ++-- tests/test_emr/test_emr_boto3.py | 3 +-- tests/test_events/test_events.py | 5 ++--- tests/test_s3/test_s3.py | 2 +- 10 files changed, 16 insertions(+), 20 deletions(-) diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index b7664c314..d145f4760 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -57,7 +57,7 @@ class LambdaResponse(BaseResponse): def _create_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - spec = json.loads(self.body.decode('utf-8')) + spec = json.loads(self.body) try: fn = lambda_backend.create_function(spec) except ValueError as e: diff --git a/moto/backends.py b/moto/backends.py index 5b1695e3b..94c7f4849 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -62,10 +62,10 @@ BACKENDS = { } -def get_model(name, region): +def get_model(name, region_name): for backends in BACKENDS.values(): for region, backend in backends.items(): - if region == region: + if region == region_name: models = getattr(backend.__class__, '__models__', {}) if name in models: return list(getattr(backend, models[name])()) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index f2ba08522..9dcbdae29 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -21,9 +21,8 @@ from moto.s3 import models as s3_models from moto.sns import models as sns_models from moto.sqs import models as sqs_models from .utils import random_suffix -from .exceptions import MissingParameterError, UnformattedGetAttTemplateException +from .exceptions import MissingParameterError, UnformattedGetAttTemplateException, ValidationError from boto.cloudformation.stack import Output -from boto.exception import BotoServerError MODEL_MAP = { "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, @@ -137,8 +136,7 @@ def clean_json(resource_json, resources_map): logger.warning(n.message.format( resource_json['Fn::GetAtt'][0])) except UnformattedGetAttTemplateException: - raise BotoServerError( - UnformattedGetAttTemplateException.status_code, + raise ValidationError( 'Bad Request', UnformattedGetAttTemplateException.description.format( resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1])) diff --git a/moto/emr/responses.py b/moto/emr/responses.py index 3919d8b3e..8442e4010 100644 --- a/moto/emr/responses.py +++ b/moto/emr/responses.py @@ -259,7 +259,7 @@ class ElasticMapReduceResponse(BaseResponse): 'Provided AMI: {0}, release label: {1}.').format( ami_version, release_label) raise EmrError(error_type="ValidationException", - message=message, template='single_error') + message=message, template='error_json') else: if ami_version: kwargs['requested_ami_version'] = ami_version diff --git a/moto/server.py b/moto/server.py index c7e7f18fb..fcc91ac6c 100644 --- a/moto/server.py +++ b/moto/server.py @@ -39,7 +39,7 @@ class DomainDispatcherApplication(object): return host for backend_name, backend in BACKENDS.items(): - for url_base in backend.values()[0].url_bases: + for url_base in list(backend.values())[0].url_bases: if re.match(url_base, 'http://%s' % host): return backend_name @@ -118,7 +118,7 @@ def create_backend_app(service): backend_app.view_functions = {} backend_app.url_map = Map() backend_app.url_map.converters['regex'] = RegexConverter - backend = BACKENDS[service].values()[0] + backend = list(BACKENDS[service].values())[0] for url_path, handler in backend.flask_paths.items(): if handler.__name__ == 'dispatch': endpoint = '{0}.dispatch'.format(handler.__self__.__name__) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index be459eff1..c2af6363a 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -4,12 +4,12 @@ import json from mock import patch import sure # noqa +from moto.cloudformation.exceptions import ValidationError from moto.cloudformation.models import FakeStack from moto.cloudformation.parsing import resource_class_from_type, parse_condition from moto.sqs.models import Queue from moto.s3.models import FakeBucket from boto.cloudformation.stack import Output -from boto.exception import BotoServerError dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -158,7 +158,7 @@ def test_parse_stack_with_get_attribute_outputs(): def test_parse_stack_with_bad_get_attribute_outputs(): FakeStack.when.called_with( - "test_id", "test_stack", bad_output_template_json, {}, "us-west-1").should.throw(BotoServerError) + "test_id", "test_stack", bad_output_template_json, {}, "us-west-1").should.throw(ValidationError) def test_parse_equals_condition(): diff --git a/tests/test_ec2/test_spot_instances.py b/tests/test_ec2/test_spot_instances.py index 5c3bdff12..05f8ee88f 100644 --- a/tests/test_ec2/test_spot_instances.py +++ b/tests/test_ec2/test_spot_instances.py @@ -39,7 +39,7 @@ def test_request_spot_instances(): "ImageId": 'ami-abcd1234', "KeyName": "test", "SecurityGroups": ['group1', 'group2'], - "UserData": b"some test data", + "UserData": "some test data", "InstanceType": 'm1.small', "Placement": { "AvailabilityZone": 'us-east-1c', @@ -67,7 +67,7 @@ def test_request_spot_instances(): "ImageId": 'ami-abcd1234', "KeyName": "test", "SecurityGroups": ['group1', 'group2'], - "UserData": b"some test data", + "UserData": "some test data", "InstanceType": 'm1.small', "Placement": { "AvailabilityZone": 'us-east-1c', diff --git a/tests/test_emr/test_emr_boto3.py b/tests/test_emr/test_emr_boto3.py index 4999935c5..b2877c7f5 100644 --- a/tests/test_emr/test_emr_boto3.py +++ b/tests/test_emr/test_emr_boto3.py @@ -347,8 +347,7 @@ def test_run_job_flow_with_invalid_params(): args['AmiVersion'] = '2.4' args['ReleaseLabel'] = 'emr-5.0.0' client.run_job_flow(**args) - ex.exception.response['Error'][ - 'Message'].should.contain('ValidationException') + ex.exception.response['Error']['Code'].should.equal('ValidationException') @mock_emr diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index a2d5a5d47..537b741f2 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -49,7 +49,6 @@ def get_random_rule(): return RULES[random.randint(0, len(RULES) - 1)] -@mock_events def generate_environment(): client = boto3.client('events', 'us-west-2') @@ -115,12 +114,12 @@ def test_list_rule_names_by_target(): client = generate_environment() rules = client.list_rule_names_by_target(TargetArn=test_1_target['Arn']) - assert(len(rules) == len(test_1_target['Rules'])) + assert(len(rules['RuleNames']) == len(test_1_target['Rules'])) for rule in rules['RuleNames']: assert(rule in test_1_target['Rules']) rules = client.list_rule_names_by_target(TargetArn=test_2_target['Arn']) - assert(len(rules) == len(test_2_target['Rules'])) + assert(len(rules['RuleNames']) == len(test_2_target['Rules'])) for rule in rules['RuleNames']: assert(rule in test_2_target['Rules']) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 32b772abe..36d4bdbc4 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -71,7 +71,7 @@ def test_my_model_save(): body = conn.Object('mybucket', 'steve').get()[ 'Body'].read().decode("utf-8") - assert body == b'is awesome' + assert body == 'is awesome' @mock_s3 From b73360c1873612ca1e59e708fc8e42ab6377254b Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 23 Feb 2017 22:34:43 -0500 Subject: [PATCH 035/274] Fix api gateway callback. --- moto/apigateway/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 6585d19f5..bfcfdbfa6 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -339,9 +339,11 @@ class RestAPI(object): return status_code, {}, response def update_integration_mocks(self, stage_name): - stage_url = STAGE_URL.format(api_id=self.id.upper(), + stage_url = STAGE_URL.format(api_id=self.id, region_name=self.region_name, stage_name=stage_name) - responses.add_callback(responses.GET, stage_url, + responses.add_callback(responses.GET, stage_url.upper(), + callback=self.resource_callback) + responses.add_callback(responses.GET, stage_url.lower(), callback=self.resource_callback) def create_stage(self, name, deployment_id, variables=None, description='', cacheClusterEnabled=None, cacheClusterSize=None): From 5324638573e18e51b895b39ec0d32ac2133a37d0 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 26 Feb 2017 19:55:19 -0500 Subject: [PATCH 036/274] Add docs on contributing and code of conduct. --- CODE_OF_CONDUCT.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 4 +++ ISSUE_TEMPLATE.md | 33 +++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 ISSUE_TEMPLATE.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..8f2d40361 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project maintainer at spulec@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1266d508e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +### Contributing code + +If you have improvements to Moto, send us your pull requests! For those +just getting started, Github has a [howto](https://help.github.com/articles/using-pull-requests/). diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..c3d7d3f65 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,33 @@ +## Reporting Bugs + +Please be aware of the following things when filing bug reports: + +1. Avoid raising duplicate issues. *Please* use the GitHub issue search feature + to check whether your bug report or feature request has been mentioned in + the past. +2. When filing bug reports about exceptions or tracebacks, please include the + *complete* traceback. Partial tracebacks, or just the exception text, are + not helpful. +3. Make sure you provide a suitable amount of information to work with. This + means you should provide: + + - Guidance on **how to reproduce the issue**. Ideally, this should be a + *small* code sample that can be run immediately by the maintainers. + Failing that, let us know what you're doing, how often it happens, what + environment you're using, etc. Be thorough: it prevents us needing to ask + further questions. + - Tell us **what you expected to happen**. When we run your example code, + what are we expecting to happen? What does "success" look like for your + code? + - Tell us **what actually happens**. It's not helpful for you to say "it + doesn't work" or "it fails". Tell us *how* it fails: do you get an + exception? A hang? How was the actual result different from your expected + result? + - Tell us **what version of Moto you're using**, and + **how you installed it**. Tell us whether you're using standalone server + mode or the Python mocks. If you are using the Python mocks, include the + version of boto/boto3/botocore. + + + If you do not provide all of these things, it will take us much longer to + fix your problem. From e5bcafd22f297dd0b5af4b2b840e379819c009b5 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 26 Feb 2017 23:40:54 -0500 Subject: [PATCH 037/274] Cleanup travis. --- .travis.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35506f2dc..9c867f237 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,11 @@ sudo: false python: - 2.6 - 2.7 + - 3.3 env: - matrix: - - BOTO_VERSION=2.45.0 -matrix: - include: - - python: "3.3" - env: BOTO_VERSION=2.45.0 + - TEST_SERVER_MODE=false install: - - travis_retry pip install boto==$BOTO_VERSION + - travis_retry pip install boto==2.45.0 - travis_retry pip install boto3 - travis_retry pip install . - travis_retry pip install -r requirements-dev.txt From 089b2a66d259bbe0b5c162cf2bbdf1f9e64f71fb Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 26 Feb 2017 23:56:50 -0500 Subject: [PATCH 038/274] Add server-mode tests. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9c867f237..8a2e8ce6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - 3.3 env: - TEST_SERVER_MODE=false + - TEST_SERVER_MODE=true install: - travis_retry pip install boto==2.45.0 - travis_retry pip install boto3 From e841c0d2f5cb04b825097b7966c3a69c135670a0 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 27 Feb 2017 00:02:23 -0500 Subject: [PATCH 039/274] Need to run moto_server... --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8a2e8ce6f..bf1bece4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,10 @@ install: - travis_retry pip install . - travis_retry pip install -r requirements-dev.txt - travis_retry pip install coveralls + - | + if [ "$TEST_SERVER_MODE" = "true" ]; then + moto_server -p 8086& + fi script: - make test after_success: From b63618b97589f2f865142cbdc84ca12aa9fbb6b1 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 27 Feb 2017 00:17:01 -0500 Subject: [PATCH 040/274] Add keys for server mode. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index bf1bece4b..85180bcab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ install: - | if [ "$TEST_SERVER_MODE" = "true" ]; then moto_server -p 8086& + export AWS_SECRET_ACCESS_KEY=foobar_secret + export AWS_ACCESS_KEY_ID=foobar_key fi script: - make test From 5a56b3a049c648a9720ce742bc18a021eb0e3411 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 27 Feb 2017 08:54:33 -0500 Subject: [PATCH 041/274] Set credentials for server too. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 85180bcab..d0c8ce45b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ install: - travis_retry pip install coveralls - | if [ "$TEST_SERVER_MODE" = "true" ]; then - moto_server -p 8086& + AWS_SECRET_ACCESS_KEY=server_secret AWS_ACCESS_KEY_ID=server_key moto_server -p 8086& export AWS_SECRET_ACCESS_KEY=foobar_secret export AWS_ACCESS_KEY_ID=foobar_key fi From a22caf27ab698eb13da0f030dc67b108cd9b47ef Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 27 Feb 2017 10:20:53 -0500 Subject: [PATCH 042/274] Cleanup sns default topic. --- moto/sns/models.py | 6 +++--- tests/test_sns/test_topics.py | 3 ++- tests/test_sns/test_topics_boto3.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 0ad00928d..4fa72a7d0 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -26,7 +26,7 @@ class Topic(object): self.sns_backend = sns_backend self.account_id = DEFAULT_ACCOUNT_ID self.display_name = "" - self.policy = DEFAULT_TOPIC_POLICY + self.policy = json.dumps(DEFAULT_TOPIC_POLICY) self.delivery_policy = "" self.effective_delivery_policy = DEFAULT_EFFECTIVE_DELIVERY_POLICY self.arn = make_arn_for_topic( @@ -288,7 +288,7 @@ for region in boto.sns.regions(): sns_backends[region.name] = SNSBackend(region.name) -DEFAULT_TOPIC_POLICY = json.dumps({ +DEFAULT_TOPIC_POLICY = { "Version": "2008-10-17", "Id": "us-east-1/698519295917/test__default_policy_ID", "Statement": [{ @@ -315,7 +315,7 @@ DEFAULT_TOPIC_POLICY = json.dumps({ } } }] -}) +} DEFAULT_EFFECTIVE_DELIVERY_POLICY = json.dumps({ 'http': { diff --git a/tests/test_sns/test_topics.py b/tests/test_sns/test_topics.py index 79b85f709..cbb4849c8 100644 --- a/tests/test_sns/test_topics.py +++ b/tests/test_sns/test_topics.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import boto +import json import six import sure # noqa @@ -75,7 +76,7 @@ def test_topic_attributes(): .format(conn.region.name) ) attributes["Owner"].should.equal(123456789012) - attributes["Policy"].should.equal(DEFAULT_TOPIC_POLICY) + json.loads(attributes["Policy"]).should.equal(DEFAULT_TOPIC_POLICY) attributes["DisplayName"].should.equal("") attributes["SubscriptionsPending"].should.equal(0) attributes["SubscriptionsConfirmed"].should.equal(0) diff --git a/tests/test_sns/test_topics_boto3.py b/tests/test_sns/test_topics_boto3.py index 55d03afff..bfa9b5d1f 100644 --- a/tests/test_sns/test_topics_boto3.py +++ b/tests/test_sns/test_topics_boto3.py @@ -72,7 +72,7 @@ def test_topic_attributes(): .format(conn._client_config.region_name) ) attributes["Owner"].should.equal('123456789012') - attributes["Policy"].should.equal(DEFAULT_TOPIC_POLICY) + json.loads(attributes["Policy"]).should.equal(DEFAULT_TOPIC_POLICY) attributes["DisplayName"].should.equal("") attributes["SubscriptionsPending"].should.equal('0') attributes["SubscriptionsConfirmed"].should.equal('0') From 1287d53817b83dc6509a1ff5d568267970421175 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 27 Feb 2017 20:53:57 -0500 Subject: [PATCH 043/274] Fix tests for py26 and py3. --- moto/sns/models.py | 6 +++--- tests/test_sns/test_publishing.py | 3 +-- tests/test_sns/test_topics.py | 2 +- tests/test_sns/test_topics_boto3.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 4fa72a7d0..0ccf60ea9 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -28,7 +28,7 @@ class Topic(object): self.display_name = "" self.policy = json.dumps(DEFAULT_TOPIC_POLICY) self.delivery_policy = "" - self.effective_delivery_policy = DEFAULT_EFFECTIVE_DELIVERY_POLICY + self.effective_delivery_policy = json.dumps(DEFAULT_EFFECTIVE_DELIVERY_POLICY) self.arn = make_arn_for_topic( self.account_id, name, sns_backend.region_name) @@ -317,7 +317,7 @@ DEFAULT_TOPIC_POLICY = { }] } -DEFAULT_EFFECTIVE_DELIVERY_POLICY = json.dumps({ +DEFAULT_EFFECTIVE_DELIVERY_POLICY = { 'http': { 'disableSubscriptionOverrides': False, 'defaultHealthyRetryPolicy': { @@ -330,4 +330,4 @@ DEFAULT_EFFECTIVE_DELIVERY_POLICY = json.dumps({ 'backoffFunction': 'linear' } } -}) +} diff --git a/tests/test_sns/test_publishing.py b/tests/test_sns/test_publishing.py index 718bce5c4..51042675f 100644 --- a/tests/test_sns/test_publishing.py +++ b/tests/test_sns/test_publishing.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import sure # noqa from moto.packages.responses import responses -from moto import mock_sns, mock_sns_deprecated, mock_sqs_deprecated +from moto import mock_sns_deprecated, mock_sqs_deprecated @mock_sqs_deprecated @@ -54,7 +54,6 @@ def test_publish_to_sqs_in_different_region(): @freeze_time("2013-01-01") -@mock_sns @mock_sns_deprecated def test_publish_to_http(): responses.add( diff --git a/tests/test_sns/test_topics.py b/tests/test_sns/test_topics.py index cbb4849c8..1b039c51d 100644 --- a/tests/test_sns/test_topics.py +++ b/tests/test_sns/test_topics.py @@ -82,7 +82,7 @@ def test_topic_attributes(): attributes["SubscriptionsConfirmed"].should.equal(0) attributes["SubscriptionsDeleted"].should.equal(0) attributes["DeliveryPolicy"].should.equal("") - attributes["EffectiveDeliveryPolicy"].should.equal( + json.loads(attributes["EffectiveDeliveryPolicy"]).should.equal( DEFAULT_EFFECTIVE_DELIVERY_POLICY) # boto can't handle prefix-mandatory strings: diff --git a/tests/test_sns/test_topics_boto3.py b/tests/test_sns/test_topics_boto3.py index bfa9b5d1f..4702744c3 100644 --- a/tests/test_sns/test_topics_boto3.py +++ b/tests/test_sns/test_topics_boto3.py @@ -78,7 +78,7 @@ def test_topic_attributes(): attributes["SubscriptionsConfirmed"].should.equal('0') attributes["SubscriptionsDeleted"].should.equal('0') attributes["DeliveryPolicy"].should.equal("") - attributes["EffectiveDeliveryPolicy"].should.equal( + json.loads(attributes["EffectiveDeliveryPolicy"]).should.equal( DEFAULT_EFFECTIVE_DELIVERY_POLICY) # boto can't handle prefix-mandatory strings: From 3be1b16eb90635dbd2f6e34c7b82b8b0bcf10593 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 27 Feb 2017 21:24:34 -0500 Subject: [PATCH 044/274] Drop py26. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d0c8ce45b..87ee121e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python sudo: false python: - - 2.6 - 2.7 - 3.3 env: From d530bcf4a7a2fdce06474b8bec452e07d1f25aed Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 28 Feb 2017 21:29:28 -0500 Subject: [PATCH 045/274] remove py26. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ee9c07aed..d34715554 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,6 @@ setup( test_suite="tests", classifiers=[ "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", From bcc3e57949831b31661c1c91d739128e8bbd56d9 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:26:23 +1100 Subject: [PATCH 046/274] Cloudformation ResourceMaps incorrectly share namespaces for Conditions and Resources (#828) * add tests to check CF's conditions and resources have distinct namespace * separate the resource and condition namespaces for CF --- moto/cloudformation/parsing.py | 14 ++--- .../test_cloudformation_stack_crud.py | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 521658cee..fdc569dc1 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -143,7 +143,7 @@ def clean_json(resource_json, resources_map): if 'Fn::If' in resource_json: condition_name, true_value, false_value = resource_json['Fn::If'] - if resources_map[condition_name]: + if resources_map.lazy_condition_map[condition_name]: return clean_json(true_value, resources_map) else: return clean_json(false_value, resources_map) @@ -206,7 +206,7 @@ def parse_resource(logical_id, resource_json, resources_map): def parse_and_create_resource(logical_id, resource_json, resources_map, region_name): condition = resource_json.get('Condition') - if condition and not resources_map[condition]: + if condition and not resources_map.lazy_condition_map[condition]: # If this has a False condition, don't create the resource return None @@ -352,13 +352,13 @@ class ResourceMap(collections.Mapping): def load_conditions(self): conditions = self._template.get('Conditions', {}) - lazy_condition_map = LazyDict() + self.lazy_condition_map = LazyDict() for condition_name, condition in conditions.items(): - lazy_condition_map[condition_name] = functools.partial(parse_condition, - condition, self._parsed_resources, lazy_condition_map) + self.lazy_condition_map[condition_name] = functools.partial(parse_condition, + condition, self._parsed_resources, self.lazy_condition_map) - for condition_name in lazy_condition_map: - self._parsed_resources[condition_name] = lazy_condition_map[condition_name] + for condition_name in self.lazy_condition_map: + _ = self.lazy_condition_map[condition_name] def create(self): self.load_mapping() diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index e45dafbfa..0696d5ada 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -279,6 +279,60 @@ def test_cloudformation_params(): param.value.should.equal('testing123') +@mock_cloudformation() +def test_cloudformation_params_conditions_and_resources_are_distinct(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Conditions": { + "FooEnabled": { + "Fn::Equals": [ + { + "Ref": "FooEnabled" + }, + "true" + ] + }, + "FooDisabled": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "FooEnabled" + }, + "true" + ] + } + ] + } + }, + "Parameters": { + "FooEnabled": { + "Type": "String", + "AllowedValues": [ + "true", + "false" + ] + } + }, + "Resources": { + "Bar": { + "Properties": { + "CidrBlock": "192.168.0.0/16", + }, + "Condition": "FooDisabled", + "Type": "AWS::EC2::VPC" + } + } + } + dummy_template_json = json.dumps(dummy_template) + cfn = boto.connect_cloudformation() + cfn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[('FooEnabled', 'true')]) + stack = cfn.describe_stacks('test_stack1')[0] + resources = stack.list_resources() + assert not [resource for resource in resources if resource.logical_resource_id == 'Bar'] + + @mock_cloudformation def test_stack_tags(): conn = boto.connect_cloudformation() From 7d75c3ba189d41f35d23469fc47d1211ff3a3231 Mon Sep 17 00:00:00 2001 From: Guy Templeton Date: Sun, 5 Mar 2017 03:30:36 +0000 Subject: [PATCH 047/274] Feat: ECS container status updating (#831) * Uptick boto3 version to version supporting ECS container instance state changes * Add initial status update * Only place tasks on active instances * PEP8 cleanup --- moto/ecs/models.py | 47 ++++++++++++++++------------ moto/ecs/responses.py | 42 ++++++++++++------------- requirements-dev.txt | 2 +- tests/test_ecs/test_ecs_boto3.py | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 41 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 3ce7be8b5..25fe0ffec 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -61,6 +61,7 @@ class Cluster(BaseObject): # ClusterName is optional in CloudFormation, thus create a random name if necessary cluster_name=properties.get('ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))), ) + @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] @@ -126,6 +127,7 @@ class TaskDefinition(BaseObject): # no-op when nothing changed between old and new resources return original_resource + class Task(BaseObject): def __init__(self, cluster, task_definition, container_instance_arn, overrides={}, started_by=''): self.cluster_arn = cluster.arn @@ -227,10 +229,10 @@ class ContainerInstance(BaseObject): self.remainingResources = [] self.runningTaskCount = 0 self.versionInfo = { - 'agentVersion': "1.0.0", - 'agentHash': '4023248', - 'dockerVersion': 'DockerVersion: 1.5.0' - } + 'agentVersion': "1.0.0", + 'agentHash': '4023248', + 'dockerVersion': 'DockerVersion: 1.5.0' + } @property def response_object(self): @@ -327,20 +329,6 @@ class EC2ContainerServiceBackend(BaseBackend): task_arns.extend([task_definition.arn for task_definition in task_definition_list]) return task_arns - def describe_task_definition(self, task_definition_str): - task_definition_name = task_definition_str.split('/')[-1] - if ':' in task_definition_name: - family, revision = task_definition_name.split(':') - revision = int(revision) - else: - family = task_definition_name - revision = len(self.task_definitions.get(family, [])) - - if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): - return self.task_definitions[family][revision-1] - else: - raise Exception("{0} is not a task_definition".format(task_definition_name)) - def deregister_task_definition(self, task_definition_str): task_definition_name = task_definition_str.split('/')[-1] family, revision = task_definition_name.split(':') @@ -363,9 +351,11 @@ class EC2ContainerServiceBackend(BaseBackend): container_instances = list(self.container_instances.get(cluster_name, {}).keys()) if not container_instances: raise Exception("No instances found in cluster {}".format(cluster_name)) + active_container_instances = [x for x in container_instances if + self.container_instances[cluster_name][x].status == 'ACTIVE'] for _ in range(count or 1): container_instance_arn = self.container_instances[cluster_name][ - container_instances[randint(0, len(container_instances) - 1)] + active_container_instances[randint(0, len(active_container_instances) - 1)] ].containerInstanceArn task = Task(cluster, task_definition, container_instance_arn, overrides or {}, started_by or '') tasks.append(task) @@ -537,6 +527,25 @@ class EC2ContainerServiceBackend(BaseBackend): return container_instance_objects, failures + def update_container_instances_state(self, cluster_str, list_container_instance_ids, status): + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) + status = status.upper() + if status not in ['ACTIVE', 'DRAINING']: + raise Exception("An error occurred (InvalidParameterException) when calling the UpdateContainerInstancesState operation: Container instances status should be one of [ACTIVE,DRAINING]") + failures = [] + container_instance_objects = [] + for container_instance_id in list_container_instance_ids: + container_instance = self.container_instances[cluster_name].get(container_instance_id, None) + if container_instance is not None: + container_instance.status = status + container_instance_objects.append(container_instance) + else: + failures.append(ContainerInstanceFailure('MISSING', container_instance_id)) + + return container_instance_objects, failures + def deregister_container_instance(self, cluster_str, container_instance_str): pass diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index ce90de379..d61b7dd15 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals import json -import uuid from moto.core.responses import BaseResponse from .models import ecs_backends @@ -34,8 +33,8 @@ class EC2ContainerServiceResponse(BaseResponse): cluster_arns = self.ecs_backend.list_clusters() return json.dumps({ 'clusterArns': cluster_arns - #, - #'nextToken': str(uuid.uuid1()) + # , + # 'nextToken': str(uuid.uuid1()) }) def describe_clusters(self): @@ -66,15 +65,8 @@ class EC2ContainerServiceResponse(BaseResponse): task_definition_arns = self.ecs_backend.list_task_definitions() return json.dumps({ 'taskDefinitionArns': task_definition_arns - #, - #'nextToken': str(uuid.uuid1()) - }) - - def describe_task_definition(self): - task_definition_str = self._get_param('taskDefinition') - task_definition = self.ecs_backend.describe_task_definition(task_definition_str) - return json.dumps({ - 'taskDefinition': task_definition.response_object + # , + # 'nextToken': str(uuid.uuid1()) }) def deregister_task_definition(self): @@ -94,7 +86,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({ 'tasks': [task.response_object for task in tasks], 'failures': [] - }) + }) def describe_tasks(self): cluster = self._get_param('cluster') @@ -123,7 +115,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({ 'tasks': [task.response_object for task in tasks], 'failures': [] - }) + }) def list_tasks(self): cluster_str = self._get_param('cluster') @@ -135,8 +127,7 @@ class EC2ContainerServiceResponse(BaseResponse): task_arns = self.ecs_backend.list_tasks(cluster_str, container_instance, family, started_by, service_name, desiredStatus) return json.dumps({ 'taskArns': task_arns - }) - + }) def stop_task(self): cluster_str = self._get_param('cluster') @@ -145,8 +136,7 @@ class EC2ContainerServiceResponse(BaseResponse): task = self.ecs_backend.stop_task(cluster_str, task, reason) return json.dumps({ 'task': task.response_object - }) - + }) def create_service(self): cluster_str = self._get_param('cluster') @@ -201,7 +191,7 @@ class EC2ContainerServiceResponse(BaseResponse): ec2_instance_id = instance_identity_document["instanceId"] container_instance = self.ecs_backend.register_container_instance(cluster_str, ec2_instance_id) return json.dumps({ - 'containerInstance' : container_instance.response_object + 'containerInstance': container_instance.response_object }) def list_container_instances(self): @@ -216,6 +206,16 @@ class EC2ContainerServiceResponse(BaseResponse): list_container_instance_arns = self._get_param('containerInstances') container_instances, failures = self.ecs_backend.describe_container_instances(cluster_str, list_container_instance_arns) return json.dumps({ - 'failures': [ci.response_object for ci in failures], - 'containerInstances': [ci.response_object for ci in container_instances] + 'failures': [ci.response_object for ci in failures], + 'containerInstances': [ci.response_object for ci in container_instances] + }) + + def update_container_instances_state(self): + cluster_str = self._get_param('cluster') + list_container_instance_arns = self._get_param('containerInstances') + status_str = self._get_param('status') + container_instances, failures = self.ecs_backend.update_container_instances_state(cluster_str, list_container_instance_arns, status_str) + return json.dumps({ + 'failures': [ci.response_object for ci in failures], + 'containerInstances': [ci.response_object for ci in container_instances] }) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9bdccc6e4..554834a51 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,6 @@ sure==1.2.24 coverage freezegun flask -boto3>=1.3.1 +boto3>=1.4.4 botocore>=1.4.28 six diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index f073628a9..bbb86dbe3 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -573,6 +573,58 @@ def test_describe_container_instances(): for arn in test_instance_arns: response_arns.should.contain(arn) +@mock_ec2 +@mock_ecs +def test_update_container_instances_state(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + instance_to_create = 3 + test_instance_arns = [] + for i in range(0, instance_to_create): + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document) + + test_instance_arns.append(response['containerInstance']['containerInstanceArn']) + + test_instance_ids = list(map((lambda x: x.split('/')[1]), test_instance_arns)) + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING') + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_statuses = [ci['status'] for ci in response['containerInstances']] + for status in response_statuses: + status.should.equal('DRAINING') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING') + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_statuses = [ci['status'] for ci in response['containerInstances']] + for status in response_statuses: + status.should.equal('DRAINING') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='ACTIVE') + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_statuses = [ci['status'] for ci in response['containerInstances']] + for status in response_statuses: + status.should.equal('ACTIVE') + ecs_client.update_container_instances_state.when.called_with(cluster=test_cluster_name, containerInstances=test_instance_ids, status='test_status').should.throw(Exception) + + @mock_ec2 @mock_ecs @@ -861,6 +913,7 @@ def describe_task_definition(): task['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task2:1') task['volumes'].should.equal([]) + @mock_ec2 @mock_ecs def test_stop_task(): From 8d737eb59d031b2e00a4dd32b5c3a8ac6af3af69 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:31:45 +1100 Subject: [PATCH 048/274] Route53: allow hosted zone id as well when creating record sets (#833) * add test that creates r53 record set from hosted zone id (not name) * pass test to enable creating record sets by hosted zone ids --- moto/route53/models.py | 7 ++- .../test_cloudformation_stack_crud.py | 53 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 6b293a1ca..552deebdf 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -213,8 +213,11 @@ class RecordSetGroup(object): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] - zone_name = properties["HostedZoneName"] - hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) + zone_name = properties.get("HostedZoneName") + if zone_name: + hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) + else: + hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"]) record_sets = properties["RecordSets"] for record_set in record_sets: hosted_zone.add_rrset(record_set) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 0696d5ada..a2b5a06f5 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -12,7 +12,7 @@ import sure # noqa import tests.backport_assert_raises # noqa from nose.tools import assert_raises -from moto import mock_cloudformation, mock_s3 +from moto import mock_cloudformation, mock_s3, mock_route53 from moto.cloudformation import cloudformation_backends dummy_template = { @@ -69,6 +69,57 @@ def test_create_stack(): }) +@mock_cloudformation +@mock_route53 +def test_create_stack_hosted_zone_by_id(): + conn = boto.connect_cloudformation() + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Parameters": { + }, + "Resources": { + "Bar": { + "Type" : "AWS::Route53::HostedZone", + "Properties" : { + "Name" : "foo.bar.baz", + } + }, + }, + } + dummy_template2 = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 2", + "Parameters": { + "ZoneId": { "Type": "String" } + }, + "Resources": { + "Foo": { + "Properties": { + "HostedZoneId": {"Ref": "ZoneId"}, + "RecordSets": [] + }, + "Type": "AWS::Route53::RecordSetGroup" + } + }, + } + conn.create_stack( + "test_stack", + template_body=json.dumps(dummy_template), + parameters={}.items() + ) + r53_conn = boto.connect_route53() + zone_id = r53_conn.get_zones()[0].id + conn.create_stack( + "test_stack", + template_body=json.dumps(dummy_template2), + parameters={"ZoneId": zone_id}.items() + ) + + stack = conn.describe_stacks()[0] + assert stack.list_resources() + + @mock_cloudformation def test_creating_stacks_across_regions(): west1_conn = boto.cloudformation.connect_to_region("us-west-1") From 1b6007e2b2b8cf44ec8a3bf799cd51925bd9659d Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:36:25 +1100 Subject: [PATCH 049/274] Correct IAM list_server_certs template that was based off incorrect docs (#836) The documentation for this method is here https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListServerCertificates.html The docs say the return type is this ServerCertificateMetadataList.member.N but the sample response incorrectly include a . I've sent feedback to the AWS docs telling them to fix their stuff but this also needs to be fixed. I haven't checked other templates with tags in them, as they may be prone to this same problem. --- moto/iam/responses.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 223691e1e..6884c2025 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -578,18 +578,16 @@ LIST_SERVER_CERTIFICATES_TEMPLATE = """ {% for certificate in server_certificates %} - - {{ certificate.cert_name }} - {% if certificate.path %} - {{ certificate.path }} - arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} - {% else %} - arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} - {% endif %} - 2010-05-08T01:02:03.004Z - ASCACKCEVSQ6C2EXAMPLE - 2012-05-08T01:02:03.004Z - + {{ certificate.cert_name }} + {% if certificate.path %} + {{ certificate.path }} + arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} + {% else %} + arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} + {% endif %} + 2010-05-08T01:02:03.004Z + ASCACKCEVSQ6C2EXAMPLE + 2012-05-08T01:02:03.004Z {% endfor %} From a30ba2b597764d5132c818084933398d9fcf755c Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Sat, 4 Mar 2017 19:37:53 -0800 Subject: [PATCH 050/274] EC2 tags specified in CloudFormation should be applied to the instances (#840) Fixes #839. --- moto/ec2/models.py | 7 +++++-- .../fixtures/vpc_single_instance_in_subnet.py | 4 ++++ .../test_cloudformation_stack_integration.py | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 30769fd7e..35c7bd878 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -336,7 +336,7 @@ class NetworkInterfaceBackend(object): return generic_filter(filters, enis) -class Instance(BotoInstance, TaggedEC2Resource): +class Instance(TaggedEC2Resource, BotoInstance): def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs): super(Instance, self).__init__() self.ec2_backend = ec2_backend @@ -441,7 +441,10 @@ class Instance(BotoInstance, TaggedEC2Resource): key_name=properties.get("KeyName"), private_ip=properties.get('PrivateIpAddress'), ) - return reservation.instances[0] + instance = reservation.instances[0] + for tag in properties.get("Tags", []): + instance.add_tag(tag["Key"], tag["Value"]) + return instance @property def physical_resource_id(self): diff --git a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py index 1f296cf0c..177da884e 100644 --- a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py +++ b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py @@ -236,6 +236,10 @@ template = { "Ref": "AWS::StackId" }, "Key": "Application" + }, + { + "Value": "Bar", + "Key": "Foo" } ], "SecurityGroupIds": [ diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 4237bee19..f76e02a49 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -676,6 +676,7 @@ def test_vpc_single_instance_in_subnet(): ec2_conn = boto.ec2.connect_to_region("us-west-1") reservation = ec2_conn.get_all_instances()[0] instance = reservation.instances[0] + instance.tags["Foo"].should.equal("Bar") # Check that the EIP is attached the the EC2 instance eip = ec2_conn.get_all_addresses()[0] eip.domain.should.equal('vpc') From 783242b696570c641f748ae4f029f86c09332ffe Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Sat, 4 Mar 2017 19:40:43 -0800 Subject: [PATCH 051/274] Elastic IP PhysicalResourceId should always be its public IP (#841) According to the [CloudFormation `Ref` docs][docs], the `Ref` return value (and physical ID of the resource) for an Elastic IP is its public IP address. [docs]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html --- moto/ec2/models.py | 2 +- .../test_cloudformation_stack_integration.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 35c7bd878..c18f8b390 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2836,7 +2836,7 @@ class ElasticAddress(object): @property def physical_resource_id(self): - return self.allocation_id if self.allocation_id else self.public_ip + return self.public_ip def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index f76e02a49..c168ff723 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -694,7 +694,7 @@ def test_vpc_single_instance_in_subnet(): subnet_resource.physical_resource_id.should.equal(subnet.id) eip_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] - eip_resource.physical_resource_id.should.equal(eip.allocation_id) + eip_resource.physical_resource_id.should.equal(eip.public_ip) @mock_cloudformation() @mock_ec2() @@ -991,7 +991,7 @@ def test_vpc_eip(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] - cfn_eip.physical_resource_id.should.equal(eip.allocation_id) + cfn_eip.physical_resource_id.should.equal(eip.public_ip) @mock_ec2() From a9554924df793fe36d11938208617f8e8a3381c7 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:48:51 +1100 Subject: [PATCH 052/274] make cloudformation update stack use parameters provided (#843) --- moto/cloudformation/models.py | 8 ++--- moto/cloudformation/parsing.py | 4 ++- moto/cloudformation/responses.py | 6 ++++ .../test_cloudformation_stack_crud.py | 36 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 1f091251b..a9dda8fdc 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -78,10 +78,10 @@ class FakeStack(object): def stack_outputs(self): return self.output_map.values() - def update(self, template, role_arn=None): + def update(self, template, role_arn=None, parameters=None): self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") self.template = template - self.resource_map.update(json.loads(template)) + self.resource_map.update(json.loads(template), parameters) self.output_map = self._create_output_map() self._add_stack_event("UPDATE_COMPLETE") self.status = "UPDATE_COMPLETE" @@ -157,9 +157,9 @@ class CloudFormationBackend(BaseBackend): if stack.name == name_or_stack_id: return stack - def update_stack(self, name, template, role_arn=None): + def update_stack(self, name, template, role_arn=None, parameters=None): stack = self.get_stack(name) - stack.update(template, role_arn) + stack.update(template, role_arn, parameters=parameters) return stack def list_stack_resources(self, stack_name_or_id): diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index fdc569dc1..06673bd8c 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -374,7 +374,9 @@ class ResourceMap(collections.Mapping): self.tags['aws:cloudformation:logical-id'] = resource ec2_models.ec2_backends[self._region_name].create_tags([self[resource].physical_resource_id], self.tags) - def update(self, template): + def update(self, template, parameters=None): + if parameters: + self.input_parameters = parameters self.load_mapping() self.load_parameters() self.load_conditions() diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index d16b3560c..06d0bbb00 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -138,6 +138,11 @@ class CloudFormationResponse(BaseResponse): stack_body = self.cloudformation_backend.get_stack(stack_name).template else: stack_body = self._get_param('TemplateBody') + parameters = dict([ + (parameter['parameter_key'], parameter['parameter_value']) + for parameter + in self._get_list_prefix("Parameters.member") + ]) stack = self.cloudformation_backend.get_stack(stack_name) if stack.status == 'ROLLBACK_COMPLETE': @@ -147,6 +152,7 @@ class CloudFormationResponse(BaseResponse): name=stack_name, template=stack_body, role_arn=role_arn, + parameters=parameters ) if self.request_json: stack_body = { diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index a2b5a06f5..e145ea283 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -444,6 +444,42 @@ def test_update_stack(): }) +@mock_cloudformation +def test_update_stack_with_parameters(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack", + "Resources": { + "VPC": { + "Properties": { + "CidrBlock": {"Ref": "Bar"} + }, + "Type": "AWS::EC2::VPC" + } + }, + "Parameters": { + "Bar": { + "Type": "String" + } + } + } + dummy_template_json = json.dumps(dummy_template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + parameters=[("Bar", "192.168.0.0/16")] + ) + conn.update_stack( + "test_stack", + template_body=dummy_template_json, + parameters=[("Bar", "192.168.0.1/16")] + ) + + stack = conn.describe_stacks()[0] + assert stack.parameters[0].value == "192.168.0.1/16" + + @mock_cloudformation def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() From f46a24180f9a2a84b61b05efdb57126cffaca113 Mon Sep 17 00:00:00 2001 From: William Richard Date: Sat, 4 Mar 2017 22:51:01 -0500 Subject: [PATCH 053/274] Cast desired capacity for cloudformation asg to int (#846) Cloudformation passes MaxSize, MinSize and DesiredCapacity as strings, but we want to store them as ints. Also includes tests of this fix, to help avoid regression. --- moto/autoscaling/models.py | 1 + .../test_cloudformation_stack_integration.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 53a0f62df..2b5a07c15 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -324,6 +324,7 @@ class AutoScalingBackend(BaseBackend): max_size = make_int(max_size) min_size = make_int(min_size) + desired_capacity = make_int(desired_capacity) default_cooldown = make_int(default_cooldown) if health_check_period is None: health_check_period = 300 diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index c168ff723..f842ffe70 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -535,6 +535,7 @@ def test_autoscaling_group_with_elb(): "LaunchConfigurationName": {"Ref": "my-launch-config"}, "MinSize": "2", "MaxSize": "2", + "DesiredCapacity": "2", "LoadBalancerNames": [{"Ref": "my-elb"}] }, }, @@ -614,6 +615,7 @@ def test_autoscaling_group_update(): "LaunchConfigurationName": {"Ref": "my-launch-config"}, "MinSize": "2", "MaxSize": "2", + "DesiredCapacity": "2" }, }, @@ -638,6 +640,7 @@ def test_autoscaling_group_update(): asg = autoscale_conn.get_all_groups()[0] asg.min_size.should.equal(2) asg.max_size.should.equal(2) + asg.desired_capacity.should.equal(2) asg_template['Resources']['my-as-group']['Properties']['MaxSize'] = 3 asg_template_json = json.dumps(asg_template) @@ -648,6 +651,7 @@ def test_autoscaling_group_update(): asg = autoscale_conn.get_all_groups()[0] asg.min_size.should.equal(2) asg.max_size.should.equal(3) + asg.desired_capacity.should.equal(2) @mock_ec2() From 56f9409ca995b73870316a2ac4ff1d024b3a5cab Mon Sep 17 00:00:00 2001 From: Chris LaRose Date: Sat, 4 Mar 2017 19:53:14 -0800 Subject: [PATCH 054/274] Use request URL to generate SQS queue URLs; fixes #626 (#827) --- moto/sqs/models.py | 5 ++--- moto/sqs/responses.py | 16 ++++++++++------ tests/test_sqs/test_sqs.py | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 13b8c34b6..efe9f9517 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -180,9 +180,8 @@ class Queue(object): result[attribute] = getattr(self, camelcase_to_underscores(attribute)) return result - @property - def url(self): - return "http://sqs.{0}.amazonaws.com/123456789012/{1}".format(self.region, self.name) + def url(self, request_url): + return "{0}://{1}/123456789012/{2}".format(request_url.scheme, request_url.netloc, self.name) @property def messages(self): diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 15c067613..6720a09bd 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from six.moves.urllib.parse import urlparse from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores @@ -57,26 +58,29 @@ class SQSResponse(BaseResponse): return status_code, headers, body def create_queue(self): + request_url = urlparse(self.uri) queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'), wait_time_seconds=self.attribute.get('WaitTimeSeconds')) template = self.response_template(CREATE_QUEUE_RESPONSE) - return template.render(queue=queue) + return template.render(queue=queue, request_url=request_url) def get_queue_url(self): + request_url = urlparse(self.uri) queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.get_queue(queue_name) if queue: template = self.response_template(GET_QUEUE_URL_RESPONSE) - return template.render(queue=queue) + return template.render(queue=queue, request_url=request_url) else: return "", dict(status=404) def list_queues(self): + request_url = urlparse(self.uri) queue_name_prefix = self.querystring.get("QueueNamePrefix", [None])[0] queues = self.sqs_backend.list_queues(queue_name_prefix) template = self.response_template(LIST_QUEUES_RESPONSE) - return template.render(queues=queues) + return template.render(queues=queues, request_url=request_url) def change_message_visibility(self): queue_name = self._get_queue_name() @@ -265,7 +269,7 @@ class SQSResponse(BaseResponse): CREATE_QUEUE_RESPONSE = """ - {{ queue.url }} + {{ queue.url(request_url) }} {{ queue.visibility_timeout }} @@ -275,7 +279,7 @@ CREATE_QUEUE_RESPONSE = """ GET_QUEUE_URL_RESPONSE = """ - {{ queue.url }} + {{ queue.url(request_url) }} 470a6f13-2ed9-4181-ad8a-2fdea142988e @@ -285,7 +289,7 @@ GET_QUEUE_URL_RESPONSE = """ LIST_QUEUES_RESPONSE = """ {% for queue in queues %} - {{ queue.url }} + {{ queue.url(request_url) }} {% endfor %} diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 32b026a46..2ad5f1af1 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -71,7 +71,7 @@ def test_create_queues_in_multiple_region(): list(west1_conn.list_queues()['QueueUrls']).should.have.length_of(1) list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) - west1_conn.list_queues()['QueueUrls'][0].should.equal('http://sqs.us-west-1.amazonaws.com/123456789012/blah') + west1_conn.list_queues()['QueueUrls'][0].should.equal('https://us-west-1.queue.amazonaws.com/123456789012/blah') @mock_sqs @@ -85,7 +85,7 @@ def test_get_queue_with_prefix(): queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue.should.have.length_of(1) - queue[0].should.equal("http://sqs.us-west-1.amazonaws.com/123456789012/test-queue") + queue[0].should.equal("https://us-west-1.queue.amazonaws.com/123456789012/test-queue") @mock_sqs From 9b6d3983d2aa4ae41ea67595a8d744c108796b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Sun, 5 Mar 2017 04:56:36 +0100 Subject: [PATCH 055/274] iam: add group policy methods (#849) Implemented mocks for: * get_all_group_policies * list_group_policies (boto3) * get_group_policy * put_group_policy --- moto/iam/models.py | 31 +++++++++++++++++ moto/iam/responses.py | 56 +++++++++++++++++++++++++++++++ tests/test_iam/test_iam_groups.py | 38 +++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/moto/iam/models.py b/moto/iam/models.py index d27722f33..15a26f663 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -167,6 +167,7 @@ class Group(object): ) self.users = [] + self.policies = {} def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -174,6 +175,24 @@ class Group(object): raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') raise UnformattedGetAttTemplateException() + def get_policy(self, policy_name): + try: + policy_json = self.policies[policy_name] + except KeyError: + raise IAMNotFoundException("Policy {0} not found".format(policy_name)) + + return { + 'policy_name': policy_name, + 'policy_document': policy_json, + 'group_name': self.name, + } + + def put_policy(self, policy_name, policy_json): + self.policies[policy_name] = policy_json + + def list_policies(self): + return self.policies.keys() + class User(object): def __init__(self, name, path=None): @@ -573,6 +592,18 @@ class IAMBackend(BaseBackend): return groups + def put_group_policy(self, group_name, policy_name, policy_json): + group = self.get_group(group_name) + group.put_policy(policy_name, policy_json) + + def list_group_policies(self, group_name, marker=None, max_items=None): + group = self.get_group(group_name) + return group.list_policies() + + def get_group_policy(self, group_name, policy_name): + group = self.get_group(group_name) + return group.get_policy(policy_name) + def create_user(self, user_name, path='/'): if user_name in self.users: raise IAMConflictException("EntityAlreadyExists", "User {0} already exists".format(user_name)) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 6884c2025..c23e9bd8e 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -186,6 +186,32 @@ class IamResponse(BaseResponse): template = self.response_template(LIST_GROUPS_FOR_USER_TEMPLATE) return template.render(groups=groups) + def put_group_policy(self): + group_name = self._get_param('GroupName') + policy_name = self._get_param('PolicyName') + policy_document = self._get_param('PolicyDocument') + iam_backend.put_group_policy(group_name, policy_name, policy_document) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name="PutGroupPolicyResponse") + + def list_group_policies(self): + group_name = self._get_param('GroupName') + marker = self._get_param('Marker') + max_items = self._get_param('MaxItems') + policies = iam_backend.list_group_policies(group_name, + marker=marker, max_items=max_items) + template = self.response_template(LIST_GROUP_POLICIES_TEMPLATE) + return template.render(name="ListGroupPoliciesResponse", + policies=policies, + marker=marker) + + def get_group_policy(self): + group_name = self._get_param('GroupName') + policy_name = self._get_param('PolicyName') + policy_result = iam_backend.get_group_policy(group_name, policy_name) + template = self.response_template(GET_GROUP_POLICY_TEMPLATE) + return template.render(name="GetGroupPolicyResponse", **policy_result) + def create_user(self): user_name = self._get_param('UserName') path = self._get_param('Path') @@ -194,6 +220,7 @@ class IamResponse(BaseResponse): template = self.response_template(USER_TEMPLATE) return template.render(action='Create', user=user) + def get_user(self): user_name = self._get_param('UserName') user = iam_backend.get_user(user_name) @@ -699,6 +726,35 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """ """ +LIST_GROUP_POLICIES_TEMPLATE = """ + + {% if marker is none %} + false + {% else %} + true + {{ marker }} + {% endif %} + + {% for policy in policies %} + {{ policy }} + {% endfor %} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +GET_GROUP_POLICY_TEMPLATE = """ + + {{ policy_name }} + {{ group_name }} + {{ policy_document }} + + + 7e7cd8bc-99ef-11e1-a4c3-27EXAMPLE804 + +""" USER_TEMPLATE = """<{{ action }}UserResponse> <{{ action }}UserResult> diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index 412484a70..ccc802283 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import boto +import boto3 import sure # noqa from nose.tools import assert_raises @@ -70,3 +71,40 @@ def test_get_groups_for_user(): groups = conn.get_groups_for_user('my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups'] groups.should.have.length_of(2) + + +@mock_iam() +def test_put_group_policy(): + conn = boto.connect_iam() + conn.create_group('my-group') + conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}') + + +@mock_iam() +def test_get_group_policy(): + conn = boto.connect_iam() + conn.create_group('my-group') + with assert_raises(BotoServerError): + conn.get_group_policy('my-group', 'my-policy') + + conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}') + policy = conn.get_group_policy('my-group', 'my-policy') + +@mock_iam() +def test_get_all_group_policies(): + conn = boto.connect_iam() + conn.create_group('my-group') + policies = conn.get_all_group_policies('my-group')['list_group_policies_response']['list_group_policies_result']['policy_names'] + assert policies == [] + conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}') + policies = conn.get_all_group_policies('my-group')['list_group_policies_response']['list_group_policies_result']['policy_names'] + assert policies == ['my-policy'] + + +@mock_iam() +def test_list_group_policies(): + conn = boto3.client('iam') + conn.create_group(GroupName='my-group') + policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.be.empty + conn.put_group_policy(GroupName='my-group', PolicyName='my-policy', PolicyDocument='{"some": "json"}') + policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.equal(['my-policy']) From f6465df63077ad2b738c0dd94baaa3cca1a4f2c9 Mon Sep 17 00:00:00 2001 From: Andrew Garrett <2rs2ts@users.noreply.github.com> Date: Sat, 4 Mar 2017 20:00:25 -0800 Subject: [PATCH 056/274] Return CF Stack events in reverse chronological order (#853) This is how the AWS API works: http://boto3.readthedocs.io/en/latest/reference/services/cloudformation.html#CloudFormation.Client.describe_stack_events --- moto/cloudformation/responses.py | 2 +- .../test_cloudformation_stack_crud.py | 11 ++++++++--- .../test_cloudformation_stack_crud_boto3.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 06d0bbb00..695118319 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -292,7 +292,7 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """ DESCRIBE_STACK_EVENTS_RESPONSE = """ - {% for event in stack.events %} + {% for event in stack.events[::-1] %} {{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }} {{ event.resource_status }} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index e145ea283..7eb563c42 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -509,10 +509,15 @@ def test_describe_stack_events_shows_create_update_and_delete(): events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") # testing ordering of stack events without assuming resource events will not exist + # the AWS API returns events in reverse chronological order stack_events_to_look_for = iter([ - ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), - ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), - ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) + ("DELETE_COMPLETE", None), + ("DELETE_IN_PROGRESS", "User Initiated"), + ("UPDATE_COMPLETE", None), + ("UPDATE_IN_PROGRESS", "User Initiated"), + ("CREATE_COMPLETE", None), + ("CREATE_IN_PROGRESS", "User Initiated"), + ]) try: for event in events: event.stack_id.should.equal(stack_id) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 97c3e864a..98ed213e5 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -345,10 +345,15 @@ def test_stack_events(): events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") # testing ordering of stack events without assuming resource events will not exist + # the AWS API returns events in reverse chronological order stack_events_to_look_for = iter([ - ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), - ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), - ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) + ("DELETE_COMPLETE", None), + ("DELETE_IN_PROGRESS", "User Initiated"), + ("UPDATE_COMPLETE", None), + ("UPDATE_IN_PROGRESS", "User Initiated"), + ("CREATE_COMPLETE", None), + ("CREATE_IN_PROGRESS", "User Initiated"), + ]) try: for event in events: event.stack_id.should.equal(stack.stack_id) From e7ea6b350c848c3ecbd6990db4c224250a0a7511 Mon Sep 17 00:00:00 2001 From: Andrew Garrett <2rs2ts@users.noreply.github.com> Date: Sat, 4 Mar 2017 20:01:50 -0800 Subject: [PATCH 057/274] Fix lambda stdout/stderr mocking (#851) Originally, the code was setting sys.stdout and sys.stderr back to the original, official forms, but this breaks idioms like mocking stdout to capture printing output for tests. So instead, we will reset sys.stdout and sys.stderr to what they were before running the lambda function, so that in case someone is mocking stdout or stderr, their tests won't break. --- moto/awslambda/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 069717ca4..c0593bcde 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -135,6 +135,8 @@ class LambdaFunction(object): print("Exception %s", ex) try: + original_stdout = sys.stdout + original_stderr = sys.stderr codeOut = StringIO() codeErr = StringIO() sys.stdout = codeOut @@ -150,8 +152,8 @@ class LambdaFunction(object): finally: codeErr.close() codeOut.close() - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ + sys.stdout = original_stdout + sys.stderr = original_stderr return self.convert(result) def invoke(self, request, headers): From e7735c3ee1661ad4269ac9862c61600714668ff0 Mon Sep 17 00:00:00 2001 From: Andrew Garrett <2rs2ts@users.noreply.github.com> Date: Sat, 4 Mar 2017 20:12:55 -0800 Subject: [PATCH 058/274] Add event IDs to CF Stack events (#852) So that events can be uniquely identified. I tried to match the format documented here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-listing-event-history.html --- moto/cloudformation/models.py | 2 ++ tests/test_cloudformation/test_cloudformation_stack_crud.py | 1 + .../test_cloudformation/test_cloudformation_stack_crud_boto3.py | 1 + 3 files changed, 4 insertions(+) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index a9dda8fdc..e9493b2b6 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import datetime import json +import uuid import boto.cloudformation from moto.core import BaseBackend @@ -105,6 +106,7 @@ class FakeEvent(object): self.resource_status_reason = resource_status_reason self.resource_properties = resource_properties self.timestamp = datetime.utcnow() + self.event_id = uuid.uuid4() class CloudFormationBackend(BaseBackend): diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 7eb563c42..1a2d16e94 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -522,6 +522,7 @@ def test_describe_stack_events_shows_create_update_and_delete(): for event in events: event.stack_id.should.equal(stack_id) event.stack_name.should.equal("test_stack") + event.event_id.should.match(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}") if event.resource_type == "AWS::CloudFormation::Stack": event.logical_resource_id.should.equal("test_stack") diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 98ed213e5..112b8bd04 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -358,6 +358,7 @@ def test_stack_events(): for event in events: event.stack_id.should.equal(stack.stack_id) event.stack_name.should.equal("test_stack") + event.event_id.should.match(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}") if event.resource_type == "AWS::CloudFormation::Stack": event.logical_resource_id.should.equal("test_stack") From 0393c384adec93c8808dbb6503dae5936e3f26e8 Mon Sep 17 00:00:00 2001 From: Matt Chamberlin Date: Sat, 4 Mar 2017 20:17:18 -0800 Subject: [PATCH 059/274] fix etag metadata field name in key response dict (etag --> ETag) (#855) --- moto/s3/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index c41ff3901..c60c49b72 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -90,7 +90,7 @@ class FakeKey(object): @property def response_dict(self): r = { - 'etag': self.etag, + 'Etag': self.etag, 'last-modified': self.last_modified_RFC1123, } if self._storage_class != 'STANDARD': From 896f040fca975865c1d441d90016446c72db3ed4 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 5 Mar 2017 10:09:19 -0500 Subject: [PATCH 060/274] Fix sqs tests for server mode. --- tests/test_iam/test_iam_groups.py | 2 +- tests/test_sqs/test_sqs.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index 7ca8d4ac0..9d5095884 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -105,7 +105,7 @@ def test_get_all_group_policies(): @mock_iam() def test_list_group_policies(): - conn = boto3.client('iam') + conn = boto3.client('iam', region_name='us-east-1') conn.create_group(GroupName='my-group') policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.be.empty conn.put_group_policy(GroupName='my-group', PolicyName='my-policy', PolicyDocument='{"some": "json"}') diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 19aa6d855..30e3e017b 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -10,7 +10,7 @@ import requests import sure # noqa import time -from moto import mock_sqs, mock_sqs_deprecated +from moto import settings, mock_sqs, mock_sqs_deprecated from tests.helpers import requires_boto_gte import tests.backport_assert_raises # noqa from nose.tools import assert_raises @@ -76,8 +76,13 @@ def test_create_queues_in_multiple_region(): list(west1_conn.list_queues()['QueueUrls']).should.have.length_of(1) list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) + if settings.TEST_SERVER_MODE: + base_url = 'http://localhost:8086' + else: + base_url = 'https://us-west-1.queue.amazonaws.com' + west1_conn.list_queues()['QueueUrls'][0].should.equal( - 'https://us-west-1.queue.amazonaws.com/123456789012/blah') + '{base_url}/123456789012/blah'.format(base_url=base_url)) @mock_sqs @@ -91,8 +96,14 @@ def test_get_queue_with_prefix(): queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue.should.have.length_of(1) + + if settings.TEST_SERVER_MODE: + base_url = 'http://localhost:8086' + else: + base_url = 'https://us-west-1.queue.amazonaws.com' + queue[0].should.equal( - "https://us-west-1.queue.amazonaws.com/123456789012/test-queue") + "{base_url}/123456789012/test-queue".format(base_url=base_url)) @mock_sqs From cf771d7f14dc3c3758072878a6b670b747e7cd21 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 6 Mar 2017 21:22:37 -0500 Subject: [PATCH 061/274] Add py26 deprecation to changelog --- CHANGELOG.md | 1 + setup.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 912659875..5938acbb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Latest BACKWARDS INCOMPATIBLE * The normal @mock_ decorators will no longer work with boto. It is suggested that you upgrade to boto3 or use the standalone-server mode. If you would still like to use boto, you must use the @mock__deprecated decorators which will be removed in a future release. * The @mock_s3bucket_path decorator is now deprecated. Use the @mock_s3 decorator instead. + * Drop support for Python 2.6 Added * Reset API: a reset API has been added to flush all of the current data ex: `requests.post("http://motoapi.amazonaws.com/moto-api/reset")` diff --git a/setup.py b/setup.py index d34715554..a09438d69 100644 --- a/setup.py +++ b/setup.py @@ -15,9 +15,6 @@ install_requires = [ ] extras_require = { - # No builtin OrderedDict before 2.7 - ':python_version=="2.6"': ['ordereddict'], - 'server': ['flask'], } From 1068e26e66840d177a296fc9db711f9b03d25d86 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 6 Mar 2017 21:48:22 -0500 Subject: [PATCH 062/274] Bump travis to python 3.6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 87ee121e6..c58ed85f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python sudo: false python: - 2.7 - - 3.3 + - 3.6 env: - TEST_SERVER_MODE=false - TEST_SERVER_MODE=true From cdd6e476cca75e401aa778bd2e777580e757ca4a Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Mar 2017 20:50:24 -0500 Subject: [PATCH 063/274] If using newer dynamodb api, use version 2. --- moto/server.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/moto/server.py b/moto/server.py index fcc91ac6c..b028f62bc 100644 --- a/moto/server.py +++ b/moto/server.py @@ -62,8 +62,14 @@ class DomainDispatcherApplication(object): except ValueError: region = 'us-east-1' service = 's3' - host = "{service}.{region}.amazonaws.com".format( - service=service, region=region) + if service == 'dynamodb': + dynamo_api_version = environ['HTTP_X_AMZ_TARGET'].split("_")[1].split(".")[0] + # If Newer API version, use dynamodb2 + if dynamo_api_version > "20111205": + host = "dynamodb2" + else: + host = "{service}.{region}.amazonaws.com".format( + service=service, region=region) with self.lock: backend = self.get_backend_for_host(host) From b2a360aaf78a42d63f4d54d34a97961bef402919 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Mar 2017 21:03:03 -0500 Subject: [PATCH 064/274] Remove old boto sns test in favor of boto3 test. --- tests/test_sns/test_publishing.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/test_sns/test_publishing.py b/tests/test_sns/test_publishing.py index 51042675f..dd75ff4be 100644 --- a/tests/test_sns/test_publishing.py +++ b/tests/test_sns/test_publishing.py @@ -51,24 +51,3 @@ def test_publish_to_sqs_in_different_region(): queue = sqs_conn.get_queue("test-queue") message = queue.read(1) message.get_body().should.equal('my message') - - -@freeze_time("2013-01-01") -@mock_sns_deprecated -def test_publish_to_http(): - responses.add( - method="POST", - url="http://example.com/foobar", - ) - - conn = boto.connect_sns() - conn.create_topic("some-topic") - topics_json = conn.get_all_topics() - topic_arn = topics_json["ListTopicsResponse"][ - "ListTopicsResult"]["Topics"][0]['TopicArn'] - - conn.subscribe(topic_arn, "http", "http://example.com/foobar") - - response = conn.publish( - topic=topic_arn, message="my message", subject="my subject") - message_id = response['PublishResponse']['PublishResult']['MessageId'] From 1709208872b59e937dee4b82f25c7d1f9bb9c66d Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Mar 2017 22:45:42 -0500 Subject: [PATCH 065/274] First version of dashboard. --- moto/core/models.py | 28 +++++ moto/core/responses.py | 27 +++++ moto/core/urls.py | 2 + moto/core/utils.py | 5 +- moto/ec2/models.py | 6 +- moto/server.py | 2 +- moto/sqs/models.py | 5 +- moto/templates/dashboard.html | 169 +++++++++++++++++++++++++++++++ tests/test_core/test_moto_api.py | 12 +++ 9 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 moto/templates/dashboard.html diff --git a/moto/core/models.py b/moto/core/models.py index 492a0e2ff..055cbbd7e 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals from __future__ import absolute_import +from collections import defaultdict import functools import inspect import re +import six from moto import settings from moto.packages.responses import responses @@ -208,12 +210,38 @@ class Model(type): return dec +model_data = defaultdict(dict) +class InstanceTrackerMeta(type): + def __new__(meta, name, bases, dct): + cls = super(InstanceTrackerMeta, meta).__new__(meta, name, bases, dct) + if name == 'BaseModel': + return cls + + service = cls.__module__.split(".")[1] + if name not in model_data[service]: + model_data[service][name] = cls + cls.instances = [] + return cls + +@six.add_metaclass(InstanceTrackerMeta) +class BaseModel(object): + def __new__(cls, *args, **kwargs): + instance = super(BaseModel, cls).__new__(cls, *args, **kwargs) + cls.instances.append(instance) + return instance + + class BaseBackend(object): def reset(self): self.__dict__ = {} self.__init__() + def get_models(self): + import pdb;pdb.set_trace() + models = getattr(backend.__class__, '__models__', {}) + + @property def _url_module(self): backend_module = self.__class__.__module__ diff --git a/moto/core/responses.py b/moto/core/responses.py index 00e3ba742..ebc4e1743 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -12,6 +12,7 @@ from jinja2 import Environment, DictLoader, TemplateNotFound import six from six.moves.urllib.parse import parse_qs, urlparse +from flask import render_template import xmltodict from pkg_resources import resource_filename from werkzeug.exceptions import HTTPException @@ -350,6 +351,32 @@ class MotoAPIResponse(BaseResponse): return 200, {}, json.dumps({"status": "ok"}) return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"}) + def model_data(self, request, full_url, headers): + from moto.core.models import model_data + + results = {} + for service in sorted(model_data): + models = model_data[service] + results[service] = {} + for name in sorted(models): + model = models[name] + results[service][name] = [] + for instance in model.instances: + inst_result = {} + for attr in dir(instance): + if not attr.startswith("_"): + try: + json.dumps(getattr(instance, attr)) + except TypeError: + pass + else: + inst_result[attr] = getattr(instance, attr) + results[service][name].append(inst_result) + return 200, {"Content-Type": "application/javascript"}, json.dumps(results) + + def dashboard(self, request, full_url, headers): + return render_template('dashboard.html') + class _RecursiveDictRef(object): """Store a recursive reference to dict.""" diff --git a/moto/core/urls.py b/moto/core/urls.py index ece486058..4d4906d77 100644 --- a/moto/core/urls.py +++ b/moto/core/urls.py @@ -8,5 +8,7 @@ url_bases = [ response_instance = MotoAPIResponse() url_paths = { + '{0}/moto-api/$': response_instance.dashboard, + '{0}/moto-api/data.json': response_instance.model_data, '{0}/moto-api/reset': response_instance.reset_response, } diff --git a/moto/core/utils.py b/moto/core/utils.py index d26694014..54622d0d7 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -122,7 +122,10 @@ class convert_flask_to_httpretty_response(object): result = self.callback(request, request.url, {}) # result is a status, headers, response tuple - status, headers, content = result + if len(result) == 3: + status, headers, content = result + else: + status, headers, content = 200, {}, result response = Response(response=content, status=status, headers=headers) if request.method == "HEAD" and 'content-length' in headers: diff --git a/moto/ec2/models.py b/moto/ec2/models.py index c7467feee..a26aac6a4 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -13,7 +13,7 @@ from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest from boto.ec2.launchspecification import LaunchSpecification from moto.core import BaseBackend -from moto.core.models import Model +from moto.core.models import Model, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores from .exceptions import ( EC2ClientError, @@ -129,7 +129,7 @@ class StateReason(object): self.code = code -class TaggedEC2Resource(object): +class TaggedEC2Resource(BaseModel): def get_tags(self, *args, **kwargs): tags = self.ec2_backend.describe_tags( @@ -2612,7 +2612,7 @@ class InternetGatewayBackend(object): return self.describe_internet_gateways(internet_gateway_ids=igw_ids)[0] -class VPCGatewayAttachment(object): +class VPCGatewayAttachment(BaseModel): def __init__(self, gateway_id, vpc_id): self.gateway_id = gateway_id diff --git a/moto/server.py b/moto/server.py index fcc91ac6c..b70bb99bd 100644 --- a/moto/server.py +++ b/moto/server.py @@ -47,7 +47,7 @@ class DomainDispatcherApplication(object): def get_application(self, environ): path_info = environ.get('PATH_INFO', '') - if path_info.startswith("/moto-api"): + if path_info.startswith("/moto-api") or path_info == "/favicon.ico": host = "moto_api" elif path_info.startswith("/latest/meta-data/"): host = "instance_metadata" diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 2a6fc19b1..60258972b 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -7,6 +7,7 @@ from xml.sax.saxutils import escape import boto.sqs from moto.core import BaseBackend +from moto.core.models import BaseModel from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis from .utils import generate_receipt_handle from .exceptions import ( @@ -18,7 +19,7 @@ DEFAULT_ACCOUNT_ID = 123456789012 DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU" -class Message(object): +class Message(BaseModel): def __init__(self, message_id, body): self.id = message_id @@ -93,7 +94,7 @@ class Message(object): return False -class Queue(object): +class Queue(BaseModel): camelcase_attributes = ['ApproximateNumberOfMessages', 'ApproximateNumberOfMessagesDelayed', 'ApproximateNumberOfMessagesNotVisible', diff --git a/moto/templates/dashboard.html b/moto/templates/dashboard.html new file mode 100644 index 000000000..dc0fd880d --- /dev/null +++ b/moto/templates/dashboard.html @@ -0,0 +1,169 @@ + + + + + + + + + Moto + + + + + + + + + + + +
+ +
+

Moto Dashboard

+
+ +
+ + + + + + + + {% raw %} + + + {% endraw %} + + + diff --git a/tests/test_core/test_moto_api.py b/tests/test_core/test_moto_api.py index 3b441a3f1..e65a92ac8 100644 --- a/tests/test_core/test_moto_api.py +++ b/tests/test_core/test_moto_api.py @@ -19,3 +19,15 @@ def test_reset_api(): res.content.should.equal(b'{"status": "ok"}') conn.list_queues().shouldnt.contain('QueueUrls') # No more queues + + +@mock_sqs +def test_data_api(): + conn = boto3.client("sqs", region_name='us-west-1') + conn.create_queue(QueueName="queue1") + + res = requests.post("{base_url}/moto-api/data.json".format(base_url=base_url)) + queues = res.json()['sqs']['Queue'] + len(queues).should.equal(1) + queue = queues[0] + queue['name'].should.equal("queue1") From caea5f441daf63191132d522f177fb1c7dfcee55 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Mar 2017 23:18:58 -0500 Subject: [PATCH 066/274] Fix resetting backends. --- moto/core/models.py | 8 +++----- moto/ec2/models.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/moto/core/models.py b/moto/core/models.py index 055cbbd7e..fd90493b2 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -234,14 +234,12 @@ class BaseModel(object): class BaseBackend(object): def reset(self): + for service, models in model_data.items(): + for model_name, model in models.items(): + model.instances = [] self.__dict__ = {} self.__init__() - def get_models(self): - import pdb;pdb.set_trace() - models = getattr(backend.__class__, '__models__', {}) - - @property def _url_module(self): backend_module = self.__class__.__module__ diff --git a/moto/ec2/models.py b/moto/ec2/models.py index a26aac6a4..0c72ac648 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2633,7 +2633,7 @@ class VPCGatewayAttachment(BaseModel): @property def physical_resource_id(self): - return self.id + return self.vpc_id class VPCGatewayAttachmentBackend(object): From 6d422d1f37d8734dd006d6e3411b4fdb41cb0ccb Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 11 Mar 2017 23:41:12 -0500 Subject: [PATCH 067/274] Add BaseModel to all models. --- moto/apigateway/models.py | 18 +++++++++--------- moto/autoscaling/models.py | 8 ++++---- moto/awslambda/models.py | 4 ++-- moto/cloudformation/models.py | 6 +++--- moto/cloudwatch/models.py | 6 +++--- moto/core/__init__.py | 2 +- moto/datapipeline/models.py | 6 +++--- moto/dynamodb/models.py | 6 +++--- moto/dynamodb2/models.py | 6 +++--- moto/ecs/models.py | 4 ++-- moto/elb/models.py | 10 +++++----- moto/emr/models.py | 12 ++++++------ moto/events/models.py | 4 ++-- moto/glacier/models.py | 6 +++--- moto/iam/models.py | 16 ++++++++-------- moto/kinesis/models.py | 12 ++++++------ moto/kms/models.py | 4 ++-- moto/opsworks/models.py | 8 ++++---- moto/rds/models.py | 8 ++++---- moto/rds2/models.py | 8 ++++---- moto/redshift/models.py | 10 +++++----- moto/route53/models.py | 10 +++++----- moto/s3/models.py | 16 ++++++++-------- moto/ses/models.py | 8 ++++---- moto/sns/models.py | 10 +++++----- moto/sqs/models.py | 3 +-- moto/sts/models.py | 6 +++--- moto/swf/models/activity_task.py | 3 ++- moto/swf/models/decision_task.py | 3 ++- moto/swf/models/domain.py | 3 ++- moto/swf/models/generic_type.py | 3 ++- moto/swf/models/history_event.py | 3 ++- moto/swf/models/timeout.py | 3 ++- moto/swf/models/workflow_execution.py | 3 ++- 34 files changed, 122 insertions(+), 116 deletions(-) diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index d5564fa61..e7ff98119 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -5,7 +5,7 @@ import datetime import requests from moto.packages.responses import responses -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds from .utils import create_id from .exceptions import StageNotFoundException @@ -13,7 +13,7 @@ from .exceptions import StageNotFoundException STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}" -class Deployment(dict): +class Deployment(BaseModel, dict): def __init__(self, deployment_id, name, description=""): super(Deployment, self).__init__() @@ -24,7 +24,7 @@ class Deployment(dict): datetime.datetime.now()) -class IntegrationResponse(dict): +class IntegrationResponse(BaseModel, dict): def __init__(self, status_code, selection_pattern=None): self['responseTemplates'] = {"application/json": None} @@ -33,7 +33,7 @@ class IntegrationResponse(dict): self['selectionPattern'] = selection_pattern -class Integration(dict): +class Integration(BaseModel, dict): def __init__(self, integration_type, uri, http_method, request_templates=None): super(Integration, self).__init__() @@ -58,14 +58,14 @@ class Integration(dict): return self["integrationResponses"].pop(status_code) -class MethodResponse(dict): +class MethodResponse(BaseModel, dict): def __init__(self, status_code): super(MethodResponse, self).__init__() self['statusCode'] = status_code -class Method(dict): +class Method(BaseModel, dict): def __init__(self, method_type, authorization_type): super(Method, self).__init__() @@ -92,7 +92,7 @@ class Method(dict): return self.method_responses.pop(response_code) -class Resource(object): +class Resource(BaseModel): def __init__(self, id, region_name, api_id, path_part, parent_id): self.id = id @@ -165,7 +165,7 @@ class Resource(object): return self.resource_methods[method_type].pop('methodIntegration') -class Stage(dict): +class Stage(BaseModel, dict): def __init__(self, name=None, deployment_id=None, variables=None, description='', cacheClusterEnabled=False, cacheClusterSize=None): @@ -293,7 +293,7 @@ class Stage(dict): raise Exception('Patch operation "%s" not implemented' % op['op']) -class RestAPI(object): +class RestAPI(BaseModel): def __init__(self, id, region_name, name, description): self.id = id diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 3b3a618e2..0fdd82ddb 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends from moto.elb import elb_backends from moto.elb.exceptions import LoadBalancerNotFoundError @@ -16,7 +16,7 @@ class InstanceState(object): self.lifecycle_state = lifecycle_state -class FakeScalingPolicy(object): +class FakeScalingPolicy(BaseModel): def __init__(self, name, policy_type, adjustment_type, as_name, scaling_adjustment, cooldown, autoscaling_backend): @@ -43,7 +43,7 @@ class FakeScalingPolicy(object): self.as_name, self.scaling_adjustment) -class FakeLaunchConfiguration(object): +class FakeLaunchConfiguration(BaseModel): def __init__(self, name, image_id, key_name, ramdisk_id, kernel_id, security_groups, user_data, instance_type, instance_monitoring, instance_profile_name, @@ -142,7 +142,7 @@ class FakeLaunchConfiguration(object): return block_device_map -class FakeAutoScalingGroup(object): +class FakeAutoScalingGroup(BaseModel): def __init__(self, name, availability_zones, desired_capacity, max_size, min_size, launch_config_name, vpc_zone_identifier, diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 7d21ccbe0..477537d10 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -14,12 +14,12 @@ except: from io import StringIO import boto.awslambda -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.s3.models import s3_backend from moto.s3.exceptions import MissingBucket -class LambdaFunction(object): +class LambdaFunction(BaseModel): def __init__(self, spec): # required diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index a565c289c..df9f4a139 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -4,14 +4,14 @@ import json import uuid import boto.cloudformation -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .parsing import ResourceMap, OutputMap from .utils import generate_stack_id from .exceptions import ValidationError -class FakeStack(object): +class FakeStack(BaseModel): def __init__(self, stack_id, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None): self.stack_id = stack_id @@ -99,7 +99,7 @@ class FakeStack(object): self.status = "DELETE_COMPLETE" -class FakeEvent(object): +class FakeEvent(BaseModel): def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None): self.stack_id = stack_id diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index 7257286ba..dd97ddcbb 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -1,4 +1,4 @@ -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel import boto.ec2.cloudwatch import datetime @@ -10,7 +10,7 @@ class Dimension(object): self.value = value -class FakeAlarm(object): +class FakeAlarm(BaseModel): def __init__(self, name, namespace, metric_name, comparison_operator, evaluation_periods, period, threshold, statistic, description, dimensions, alarm_actions, @@ -34,7 +34,7 @@ class FakeAlarm(object): self.configuration_updated_timestamp = datetime.datetime.utcnow() -class MetricDatum(object): +class MetricDatum(BaseModel): def __init__(self, namespace, name, value, dimensions): self.namespace = namespace diff --git a/moto/core/__init__.py b/moto/core/__init__.py index 4f783d46c..9e2c1e70f 100644 --- a/moto/core/__init__.py +++ b/moto/core/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from .models import BaseBackend, moto_api_backend # flake8: noqa +from .models import BaseModel, BaseBackend, moto_api_backend # flake8: noqa moto_api_backends = {"global": moto_api_backend} diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index 0cb33e4ed..77c84924d 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -2,11 +2,11 @@ from __future__ import unicode_literals import datetime import boto.datapipeline -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .utils import get_random_pipeline_id, remove_capitalization_of_dict_keys -class PipelineObject(object): +class PipelineObject(BaseModel): def __init__(self, object_id, name, fields): self.object_id = object_id @@ -21,7 +21,7 @@ class PipelineObject(object): } -class Pipeline(object): +class Pipeline(BaseModel): def __init__(self, name, unique_id): self.name = name diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index db50dbcc6..39bf15fca 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -4,7 +4,7 @@ import datetime import json from moto.compat import OrderedDict -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import unix_time from .comparisons import get_comparison_func @@ -53,7 +53,7 @@ class DynamoType(object): return comparison_func(self.value, *range_values) -class Item(object): +class Item(BaseModel): def __init__(self, hash_key, hash_key_type, range_key, range_key_type, attrs): self.hash_key = hash_key @@ -90,7 +90,7 @@ class Item(object): } -class Table(object): +class Table(BaseModel): def __init__(self, name, hash_key_attr, hash_key_type, range_key_attr=None, range_key_type=None, read_capacity=None, diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 15c30e590..2ee5da203 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -5,7 +5,7 @@ import decimal import json from moto.compat import OrderedDict -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import unix_time from .comparisons import get_comparison_func @@ -76,7 +76,7 @@ class DynamoType(object): return comparison_func(self.cast_value, *range_values) -class Item(object): +class Item(BaseModel): def __init__(self, hash_key, hash_key_type, range_key, range_key_type, attrs): self.hash_key = hash_key @@ -173,7 +173,7 @@ class Item(object): 'ADD not supported for %s' % ', '.join(update_action['Value'].keys())) -class Table(object): +class Table(BaseModel): def __init__(self, table_name, schema=None, attr=None, throughput=None, indexes=None, global_indexes=None): self.name = table_name diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 7efefdbaa..e5a2e9f96 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -2,12 +2,12 @@ from __future__ import unicode_literals import uuid from random import randint, random -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends from copy import copy -class BaseObject(object): +class BaseObject(BaseModel): def camelCase(self, key): words = [] diff --git a/moto/elb/models.py b/moto/elb/models.py index 11559c2e7..41df8a649 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -11,7 +11,7 @@ from boto.ec2.elb.policies import ( Policies, OtherPolicy, ) -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.ec2.models import ec2_backends from .exceptions import ( LoadBalancerNotFoundError, @@ -21,7 +21,7 @@ from .exceptions import ( ) -class FakeHealthCheck(object): +class FakeHealthCheck(BaseModel): def __init__(self, timeout, healthy_threshold, unhealthy_threshold, interval, target): @@ -34,7 +34,7 @@ class FakeHealthCheck(object): raise BadHealthCheckDefinition -class FakeListener(object): +class FakeListener(BaseModel): def __init__(self, load_balancer_port, instance_port, protocol, ssl_certificate_id): self.load_balancer_port = load_balancer_port @@ -47,7 +47,7 @@ class FakeListener(object): return "FakeListener(lbp: %s, inp: %s, pro: %s, cid: %s, policies: %s)" % (self.load_balancer_port, self.instance_port, self.protocol, self.ssl_certificate_id, self.policy_names) -class FakeBackend(object): +class FakeBackend(BaseModel): def __init__(self, instance_port): self.instance_port = instance_port @@ -57,7 +57,7 @@ class FakeBackend(object): return "FakeBackend(inp: %s, policies: %s)" % (self.instance_port, self.policy_names) -class FakeLoadBalancer(object): +class FakeLoadBalancer(BaseModel): def __init__(self, name, zones, ports, scheme='internet-facing', vpc_id=None, subnets=None): self.name = name diff --git a/moto/emr/models.py b/moto/emr/models.py index 94bc45ecc..78bedf574 100644 --- a/moto/emr/models.py +++ b/moto/emr/models.py @@ -5,12 +5,12 @@ from datetime import timedelta import boto.emr import pytz from dateutil.parser import parse as dtparse -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .utils import random_instance_group_id, random_cluster_id, random_step_id -class FakeApplication(object): +class FakeApplication(BaseModel): def __init__(self, name, version, args=None, additional_info=None): self.additional_info = additional_info or {} @@ -19,7 +19,7 @@ class FakeApplication(object): self.version = version -class FakeBootstrapAction(object): +class FakeBootstrapAction(BaseModel): def __init__(self, args, name, script_path): self.args = args or [] @@ -27,7 +27,7 @@ class FakeBootstrapAction(object): self.script_path = script_path -class FakeInstanceGroup(object): +class FakeInstanceGroup(BaseModel): def __init__(self, instance_count, instance_role, instance_type, market='ON_DEMAND', name=None, id=None, bid_price=None): @@ -57,7 +57,7 @@ class FakeInstanceGroup(object): self.num_instances = instance_count -class FakeStep(object): +class FakeStep(BaseModel): def __init__(self, state, @@ -81,7 +81,7 @@ class FakeStep(object): self.state = state -class FakeCluster(object): +class FakeCluster(BaseModel): def __init__(self, emr_backend, diff --git a/moto/events/models.py b/moto/events/models.py index 3cf2c3d7a..faec7b434 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -1,10 +1,10 @@ import os import re -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel -class Rule(object): +class Rule(BaseModel): def _generate_arn(self, name): return 'arn:aws:events:us-west-2:111111111111:rule/' + name diff --git a/moto/glacier/models.py b/moto/glacier/models.py index 8e3286887..1afb1241a 100644 --- a/moto/glacier/models.py +++ b/moto/glacier/models.py @@ -3,12 +3,12 @@ from __future__ import unicode_literals import hashlib import boto.glacier -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .utils import get_job_id -class ArchiveJob(object): +class ArchiveJob(BaseModel): def __init__(self, job_id, archive_id): self.job_id = job_id @@ -35,7 +35,7 @@ class ArchiveJob(object): } -class Vault(object): +class Vault(BaseModel): def __init__(self, vault_name, region): self.vault_name = vault_name diff --git a/moto/iam/models.py b/moto/iam/models.py index f00e02052..ba6985895 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -3,13 +3,13 @@ import base64 from datetime import datetime import pytz -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id -class Policy(object): +class Policy(BaseModel): is_attachable = False @@ -54,7 +54,7 @@ class InlinePolicy(Policy): """TODO: is this needed?""" -class Role(object): +class Role(BaseModel): def __init__(self, role_id, name, assume_role_policy_document, path): self.id = role_id @@ -96,7 +96,7 @@ class Role(object): raise UnformattedGetAttTemplateException() -class InstanceProfile(object): +class InstanceProfile(BaseModel): def __init__(self, instance_profile_id, name, path, roles): self.id = instance_profile_id @@ -126,7 +126,7 @@ class InstanceProfile(object): raise UnformattedGetAttTemplateException() -class Certificate(object): +class Certificate(BaseModel): def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None): self.cert_name = cert_name @@ -140,7 +140,7 @@ class Certificate(object): return self.name -class AccessKey(object): +class AccessKey(BaseModel): def __init__(self, user_name): self.user_name = user_name @@ -159,7 +159,7 @@ class AccessKey(object): raise UnformattedGetAttTemplateException() -class Group(object): +class Group(BaseModel): def __init__(self, name, path='/'): self.name = name @@ -198,7 +198,7 @@ class Group(object): return self.policies.keys() -class User(object): +class User(BaseModel): def __init__(self, name, path=None): self.name = name diff --git a/moto/kinesis/models.py b/moto/kinesis/models.py index 5d80426ae..84cbbb73a 100644 --- a/moto/kinesis/models.py +++ b/moto/kinesis/models.py @@ -11,13 +11,13 @@ from operator import attrgetter from hashlib import md5 from moto.compat import OrderedDict -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .exceptions import StreamNotFoundError, ShardNotFoundError, ResourceInUseError, \ ResourceNotFoundError, InvalidArgumentError from .utils import compose_shard_iterator, compose_new_shard_iterator, decompose_shard_iterator -class Record(object): +class Record(BaseModel): def __init__(self, partition_key, data, sequence_number, explicit_hash_key): self.partition_key = partition_key @@ -33,7 +33,7 @@ class Record(object): } -class Shard(object): +class Shard(BaseModel): def __init__(self, shard_id, starting_hash, ending_hash): self._shard_id = shard_id @@ -94,7 +94,7 @@ class Shard(object): } -class Stream(object): +class Stream(BaseModel): def __init__(self, stream_name, shard_count, region): self.stream_name = stream_name @@ -173,14 +173,14 @@ class Stream(object): } -class FirehoseRecord(object): +class FirehoseRecord(BaseModel): def __init__(self, record_data): self.record_id = 12345678 self.record_data = record_data -class DeliveryStream(object): +class DeliveryStream(BaseModel): def __init__(self, stream_name, **stream_kwargs): self.name = stream_name diff --git a/moto/kms/models.py b/moto/kms/models.py index 37fde9eb8..be8c52162 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals import boto.kms -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .utils import generate_key_id from collections import defaultdict -class Key(object): +class Key(BaseModel): def __init__(self, policy, key_usage, description, region): self.id = generate_key_id() diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index a1b8370dd..3adfd3323 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends import uuid import datetime @@ -8,7 +8,7 @@ from random import choice from .exceptions import ResourceNotFoundException, ValidationException -class OpsworkInstance(object): +class OpsworkInstance(BaseModel): """ opsworks maintains its own set of ec2 instance metadata. This metadata exists before any instance reservations are made, and is @@ -166,7 +166,7 @@ class OpsworkInstance(object): return d -class Layer(object): +class Layer(BaseModel): def __init__(self, stack_id, type, name, shortname, attributes=None, @@ -292,7 +292,7 @@ class Layer(object): return d -class Stack(object): +class Stack(BaseModel): def __init__(self, name, region, service_role_arn, default_instance_profile_arn, vpcid="vpc-1f99bf7a", diff --git a/moto/rds/models.py b/moto/rds/models.py index 4334a9f72..670b0a808 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -6,13 +6,13 @@ import boto.rds from jinja2 import Template from moto.cloudformation.exceptions import UnformattedGetAttTemplateException -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import get_random_hex from moto.ec2.models import ec2_backends from moto.rds2.models import rds2_backends -class Database(object): +class Database(BaseModel): def __init__(self, **kwargs): self.status = "available" @@ -239,7 +239,7 @@ class Database(object): backend.delete_database(self.db_instance_identifier) -class SecurityGroup(object): +class SecurityGroup(BaseModel): def __init__(self, group_name, description): self.group_name = group_name @@ -317,7 +317,7 @@ class SecurityGroup(object): backend.delete_security_group(self.group_name) -class SubnetGroup(object): +class SubnetGroup(BaseModel): def __init__(self, subnet_name, description, subnets): self.subnet_name = subnet_name diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 52cb298cd..f03cf4ad1 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -7,7 +7,7 @@ import boto.rds2 from jinja2 import Template from re import compile as re_compile from moto.cloudformation.exceptions import UnformattedGetAttTemplateException -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import get_random_hex from moto.ec2.models import ec2_backends from .exceptions import (RDSClientError, @@ -17,7 +17,7 @@ from .exceptions import (RDSClientError, DBParameterGroupNotFoundError) -class Database(object): +class Database(BaseModel): def __init__(self, **kwargs): self.status = "available" @@ -372,7 +372,7 @@ class Database(object): backend.delete_database(self.db_instance_identifier) -class SecurityGroup(object): +class SecurityGroup(BaseModel): def __init__(self, group_name, description, tags): self.group_name = group_name @@ -481,7 +481,7 @@ class SecurityGroup(object): backend.delete_security_group(self.group_name) -class SubnetGroup(object): +class SubnetGroup(BaseModel): def __init__(self, subnet_name, description, subnets, tags): self.subnet_name = subnet_name diff --git a/moto/redshift/models.py b/moto/redshift/models.py index af6c6f643..5e64f7a16 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import boto.redshift -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends from .exceptions import ( ClusterNotFoundError, @@ -12,7 +12,7 @@ from .exceptions import ( ) -class Cluster(object): +class Cluster(BaseModel): def __init__(self, redshift_backend, cluster_identifier, node_type, master_username, master_user_password, db_name, cluster_type, cluster_security_groups, @@ -174,7 +174,7 @@ class Cluster(object): } -class SubnetGroup(object): +class SubnetGroup(BaseModel): def __init__(self, ec2_backend, cluster_subnet_group_name, description, subnet_ids): self.ec2_backend = ec2_backend @@ -220,7 +220,7 @@ class SubnetGroup(object): } -class SecurityGroup(object): +class SecurityGroup(BaseModel): def __init__(self, cluster_security_group_name, description): self.cluster_security_group_name = cluster_security_group_name @@ -235,7 +235,7 @@ class SecurityGroup(object): } -class ParameterGroup(object): +class ParameterGroup(BaseModel): def __init__(self, cluster_parameter_group_name, group_family, description): self.cluster_parameter_group_name = cluster_parameter_group_name diff --git a/moto/route53/models.py b/moto/route53/models.py index 6e0ad35c0..15679f0e3 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -5,11 +5,11 @@ from collections import defaultdict import uuid from jinja2 import Template -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import get_random_hex -class HealthCheck(object): +class HealthCheck(BaseModel): def __init__(self, health_check_id, health_check_args): self.id = health_check_id @@ -63,7 +63,7 @@ class HealthCheck(object): return template.render(health_check=self) -class RecordSet(object): +class RecordSet(BaseModel): def __init__(self, kwargs): self.name = kwargs.get('Name') @@ -154,7 +154,7 @@ class RecordSet(object): hosted_zone.delete_rrset_by_name(self.name) -class FakeZone(object): +class FakeZone(BaseModel): def __init__(self, name, id_, private_zone, comment=None): self.name = name @@ -212,7 +212,7 @@ class FakeZone(object): return hosted_zone -class RecordSetGroup(object): +class RecordSetGroup(BaseModel): def __init__(self, hosted_zone_id, record_sets): self.hosted_zone_id = hosted_zone_id diff --git a/moto/s3/models.py b/moto/s3/models.py index 77c6e1a00..04220c142 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -9,7 +9,7 @@ import codecs import six from bisect import insort -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime from .exceptions import BucketAlreadyExists, MissingBucket, MissingKey, InvalidPart, EntityTooSmall from .utils import clean_key_name, _VersionedKeyStore @@ -18,7 +18,7 @@ UPLOAD_ID_BYTES = 43 UPLOAD_PART_MIN_SIZE = 5242880 -class FakeKey(object): +class FakeKey(BaseModel): def __init__(self, name, value, storage="STANDARD", etag=None, is_versioned=False, version_id=0): self.name = name @@ -119,7 +119,7 @@ class FakeKey(object): return self._expiry.strftime("%a, %d %b %Y %H:%M:%S GMT") -class FakeMultipart(object): +class FakeMultipart(BaseModel): def __init__(self, key_name, metadata): self.key_name = key_name @@ -167,7 +167,7 @@ class FakeMultipart(object): yield self.parts[part_id] -class FakeGrantee(object): +class FakeGrantee(BaseModel): def __init__(self, id='', uri='', display_name=''): self.id = id @@ -193,14 +193,14 @@ PERMISSION_WRITE_ACP = 'WRITE_ACP' PERMISSION_READ_ACP = 'READ_ACP' -class FakeGrant(object): +class FakeGrant(BaseModel): def __init__(self, grantees, permissions): self.grantees = grantees self.permissions = permissions -class FakeAcl(object): +class FakeAcl(BaseModel): def __init__(self, grants=[]): self.grants = grants @@ -234,7 +234,7 @@ def get_canned_acl(acl): return FakeAcl(grants=grants) -class LifecycleRule(object): +class LifecycleRule(BaseModel): def __init__(self, id=None, prefix=None, status=None, expiration_days=None, expiration_date=None, transition_days=None, @@ -249,7 +249,7 @@ class LifecycleRule(object): self.storage_class = storage_class -class FakeBucket(object): +class FakeBucket(BaseModel): def __init__(self, name, region_name): self.name = name diff --git a/moto/ses/models.py b/moto/ses/models.py index 3502d6bc7..2f51d1473 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import email -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from .exceptions import MessageRejectedError from .utils import get_random_message_id @@ -10,19 +10,19 @@ from .utils import get_random_message_id RECIPIENT_LIMIT = 50 -class Message(object): +class Message(BaseModel): def __init__(self, message_id): self.id = message_id -class RawMessage(object): +class RawMessage(BaseModel): def __init__(self, message_id): self.id = message_id -class SESQuota(object): +class SESQuota(BaseModel): def __init__(self, sent): self.sent = sent diff --git a/moto/sns/models.py b/moto/sns/models.py index 0ccf60ea9..64352d545 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -9,7 +9,7 @@ import requests import six from moto.compat import OrderedDict -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.sqs import sqs_backends from .exceptions import SNSNotFoundError @@ -19,7 +19,7 @@ DEFAULT_ACCOUNT_ID = 123456789012 DEFAULT_PAGE_SIZE = 100 -class Topic(object): +class Topic(BaseModel): def __init__(self, name, sns_backend): self.name = name @@ -67,7 +67,7 @@ class Topic(object): return topic -class Subscription(object): +class Subscription(BaseModel): def __init__(self, topic, endpoint, protocol): self.topic = topic @@ -99,7 +99,7 @@ class Subscription(object): } -class PlatformApplication(object): +class PlatformApplication(BaseModel): def __init__(self, region, name, platform, attributes): self.region = region @@ -116,7 +116,7 @@ class PlatformApplication(object): ) -class PlatformEndpoint(object): +class PlatformEndpoint(BaseModel): def __init__(self, region, application, custom_user_data, token, attributes): self.region = region diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 60258972b..62b79fdc1 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -6,8 +6,7 @@ from xml.sax.saxutils import escape import boto.sqs -from moto.core import BaseBackend -from moto.core.models import BaseModel +from moto.core import BaseBackend, BaseModel from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis from .utils import generate_receipt_handle from .exceptions import ( diff --git a/moto/sts/models.py b/moto/sts/models.py index f1c6401d2..c7163a335 100644 --- a/moto/sts/models.py +++ b/moto/sts/models.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals import datetime -from moto.core import BaseBackend +from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds -class Token(object): +class Token(BaseModel): def __init__(self, duration, name=None, policy=None): now = datetime.datetime.utcnow() @@ -17,7 +17,7 @@ class Token(object): return iso_8601_datetime_with_milliseconds(self.expiration) -class AssumedRole(object): +class AssumedRole(BaseModel): def __init__(self, role_session_name, role_arn, policy, duration, external_id): self.session_name = role_session_name diff --git a/moto/swf/models/activity_task.py b/moto/swf/models/activity_task.py index e205cc07a..0c1f283ca 100644 --- a/moto/swf/models/activity_task.py +++ b/moto/swf/models/activity_task.py @@ -2,13 +2,14 @@ from __future__ import unicode_literals from datetime import datetime import uuid +from moto.core import BaseModel from moto.core.utils import unix_time from ..exceptions import SWFWorkflowExecutionClosedError from .timeout import Timeout -class ActivityTask(object): +class ActivityTask(BaseModel): def __init__(self, activity_id, activity_type, scheduled_event_id, workflow_execution, timeouts, input=None): diff --git a/moto/swf/models/decision_task.py b/moto/swf/models/decision_task.py index 13bddfd7a..9255dd6f2 100644 --- a/moto/swf/models/decision_task.py +++ b/moto/swf/models/decision_task.py @@ -2,13 +2,14 @@ from __future__ import unicode_literals from datetime import datetime import uuid +from moto.core import BaseModel from moto.core.utils import unix_time from ..exceptions import SWFWorkflowExecutionClosedError from .timeout import Timeout -class DecisionTask(object): +class DecisionTask(BaseModel): def __init__(self, workflow_execution, scheduled_event_id): self.workflow_execution = workflow_execution diff --git a/moto/swf/models/domain.py b/moto/swf/models/domain.py index ed7154067..0aa62f4f0 100644 --- a/moto/swf/models/domain.py +++ b/moto/swf/models/domain.py @@ -1,13 +1,14 @@ from __future__ import unicode_literals from collections import defaultdict +from moto.core import BaseModel from ..exceptions import ( SWFUnknownResourceFault, SWFWorkflowExecutionAlreadyStartedFault, ) -class Domain(object): +class Domain(BaseModel): def __init__(self, name, retention, description=None): self.name = name diff --git a/moto/swf/models/generic_type.py b/moto/swf/models/generic_type.py index 2ae98bb53..a56220ed6 100644 --- a/moto/swf/models/generic_type.py +++ b/moto/swf/models/generic_type.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +from moto.core import BaseModel from moto.core.utils import camelcase_to_underscores -class GenericType(object): +class GenericType(BaseModel): def __init__(self, name, version, **kwargs): self.name = name diff --git a/moto/swf/models/history_event.py b/moto/swf/models/history_event.py index e841ca38e..0dc21a09a 100644 --- a/moto/swf/models/history_event.py +++ b/moto/swf/models/history_event.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from moto.core import BaseModel from moto.core.utils import underscores_to_camelcase, unix_time from ..utils import decapitalize @@ -27,7 +28,7 @@ SUPPORTED_HISTORY_EVENT_TYPES = ( ) -class HistoryEvent(object): +class HistoryEvent(BaseModel): def __init__(self, event_id, event_type, event_timestamp=None, **kwargs): if event_type not in SUPPORTED_HISTORY_EVENT_TYPES: diff --git a/moto/swf/models/timeout.py b/moto/swf/models/timeout.py index 09e0f6772..f26c8a38b 100644 --- a/moto/swf/models/timeout.py +++ b/moto/swf/models/timeout.py @@ -1,7 +1,8 @@ +from moto.core import BaseModel from moto.core.utils import unix_time -class Timeout(object): +class Timeout(BaseModel): def __init__(self, obj, timestamp, kind): self.obj = obj diff --git a/moto/swf/models/workflow_execution.py b/moto/swf/models/workflow_execution.py index 8b8acda4e..2f41c287f 100644 --- a/moto/swf/models/workflow_execution.py +++ b/moto/swf/models/workflow_execution.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import uuid +from moto.core import BaseModel from moto.core.utils import camelcase_to_underscores, unix_time from ..constants import ( @@ -20,7 +21,7 @@ from .timeout import Timeout # TODO: extract decision related logic into a Decision class -class WorkflowExecution(object): +class WorkflowExecution(BaseModel): # NB: the list is ordered exactly as in SWF validation exceptions so we can # mimic error messages closely ; don't reorder it without checking SWF. From 09ac3539b752913e64a85f4f95726d7c0a945997 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Mar 2017 12:34:54 -0400 Subject: [PATCH 068/274] Sort dashboard attributes. --- moto/templates/dashboard.html | 93 ++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/moto/templates/dashboard.html b/moto/templates/dashboard.html index dc0fd880d..9c49904d0 100644 --- a/moto/templates/dashboard.html +++ b/moto/templates/dashboard.html @@ -50,15 +50,7 @@ + @@ -82,13 +74,13 @@
{{#each data}} -
+
{{#each this}} @@ -72,7 +67,7 @@ {% raw %} {% endraw %} + + + From 0e2fdf94f9d5d5b864253c621ba131a6f03a3eb3 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Mar 2017 20:18:49 -0400 Subject: [PATCH 073/274] Cleanup lints. --- Makefile | 3 +++ moto/cloudformation/parsing.py | 2 +- moto/ec2/models.py | 1 - moto/iam/responses.py | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 58b74b2fb..82aef0cd1 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ init: @python setup.py develop @pip install -r requirements.txt +lint: + flake8 moto + test: rm -f .coverage rm -rf cover diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 337de2f2d..fbf34b6f1 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -365,7 +365,7 @@ class ResourceMap(collections.Mapping): condition, self._parsed_resources, self.lazy_condition_map) for condition_name in self.lazy_condition_map: - _ = self.lazy_condition_map[condition_name] + self.lazy_condition_map[condition_name] def create(self): self.load_mapping() diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 6ed6e9af0..87ce61c5b 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -349,7 +349,6 @@ class NetworkInterfaceBackend(object): return generic_filter(filters, enis) - class Instance(TaggedEC2Resource, BotoInstance): def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs): super(Instance, self).__init__() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index cd9ddbf75..318c04f3a 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -232,7 +232,6 @@ class IamResponse(BaseResponse): template = self.response_template(USER_TEMPLATE) return template.render(action='Create', user=user) - def get_user(self): user_name = self._get_param('UserName') user = iam_backend.get_user(user_name) From d2c56619cd770a47732fb53642cacd72ad5cdfd8 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Mar 2017 20:35:45 -0400 Subject: [PATCH 074/274] Add lint to Travis. --- .travis.yml | 1 + moto/core/models.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index c58ed85f8..bdda2b402 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ install: export AWS_ACCESS_KEY_ID=foobar_key fi script: + - make lint - make test after_success: - coveralls diff --git a/moto/core/models.py b/moto/core/models.py index 82e78fdb8..9c2fc6d6b 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -211,6 +211,8 @@ class Model(type): model_data = defaultdict(dict) + + class InstanceTrackerMeta(type): def __new__(meta, name, bases, dct): cls = super(InstanceTrackerMeta, meta).__new__(meta, name, bases, dct) @@ -223,6 +225,7 @@ class InstanceTrackerMeta(type): cls.instances = [] return cls + @six.add_metaclass(InstanceTrackerMeta) class BaseModel(object): def __new__(cls, *args, **kwargs): From 1664e4412fac9efaf6e23ed8ea38e61fae1c5b86 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Mar 2017 20:37:23 -0400 Subject: [PATCH 075/274] Add lint to make test instead. --- .travis.yml | 1 - Makefile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index bdda2b402..c58ed85f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ install: export AWS_ACCESS_KEY_ID=foobar_key fi script: - - make lint - make test after_success: - coveralls diff --git a/Makefile b/Makefile index 82aef0cd1..300067296 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ init: lint: flake8 moto -test: +test: lint rm -f .coverage rm -rf cover @nosetests -sv --with-coverage --cover-html ./tests/ From 689adf7dbc9ded65e34090f9de23d913e5815858 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Mar 2017 20:41:08 -0400 Subject: [PATCH 076/274] Add flake8 to dev dependencies. --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 554834a51..52def6ed0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,7 @@ mock nose sure==1.2.24 coverage +flake8 freezegun flask boto3>=1.4.4 From cda553abfb84ca575f9fe24d546fbd2117f302a8 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 12 Mar 2017 21:04:19 -0400 Subject: [PATCH 077/274] Change tests to use default server port of 5000. --- .travis.yml | 2 +- moto/core/models.py | 6 +++--- other_langs/sqsSample.java | 2 +- other_langs/test.js | 2 +- other_langs/test.rb | 2 +- tests/test_awslambda/test_lambda.py | 2 +- tests/test_core/test_instance_metadata.py | 2 +- tests/test_core/test_moto_api.py | 2 +- tests/test_sqs/test_sqs.py | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index c58ed85f8..4783e13c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: - travis_retry pip install coveralls - | if [ "$TEST_SERVER_MODE" = "true" ]; then - AWS_SECRET_ACCESS_KEY=server_secret AWS_ACCESS_KEY_ID=server_key moto_server -p 8086& + AWS_SECRET_ACCESS_KEY=server_secret AWS_ACCESS_KEY_ID=server_key moto_server -p 5000& export AWS_SECRET_ACCESS_KEY=foobar_secret export AWS_ACCESS_KEY_ID=foobar_key fi diff --git a/moto/core/models.py b/moto/core/models.py index 9c2fc6d6b..a3a343aa7 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -157,7 +157,7 @@ class ServerModeMockAWS(BaseMockAWS): def reset(self): import requests - requests.post("http://localhost:8086/moto-api/reset") + requests.post("http://localhost:5000/moto-api/reset") def enable_patching(self): if self.__class__.nested_count == 1: @@ -169,12 +169,12 @@ class ServerModeMockAWS(BaseMockAWS): def fake_boto3_client(*args, **kwargs): if 'endpoint_url' not in kwargs: - kwargs['endpoint_url'] = "http://localhost:8086" + kwargs['endpoint_url'] = "http://localhost:5000" return real_boto3_client(*args, **kwargs) def fake_boto3_resource(*args, **kwargs): if 'endpoint_url' not in kwargs: - kwargs['endpoint_url'] = "http://localhost:8086" + kwargs['endpoint_url'] = "http://localhost:5000" return real_boto3_resource(*args, **kwargs) self._client_patcher = mock.patch('boto3.client', fake_boto3_client) self._resource_patcher = mock.patch( diff --git a/other_langs/sqsSample.java b/other_langs/sqsSample.java index 23368272c..d303a4d27 100644 --- a/other_langs/sqsSample.java +++ b/other_langs/sqsSample.java @@ -36,7 +36,7 @@ public class S3Sample { AmazonSQS sqs = new AmazonSQSClient(); Region usWest2 = Region.getRegion(Regions.US_WEST_2); sqs.setRegion(usWest2); - sqs.setEndpoint("http://localhost:8086"); + sqs.setEndpoint("http://localhost:5000"); String queueName = "my-first-queue"; sqs.createQueue(queueName); diff --git a/other_langs/test.js b/other_langs/test.js index 65d65ae70..adc738a2d 100644 --- a/other_langs/test.js +++ b/other_langs/test.js @@ -1,6 +1,6 @@ var AWS = require('aws-sdk'); -var s3 = new AWS.S3({endpoint: "http://localhost:8086"}); +var s3 = new AWS.S3({endpoint: "http://localhost:5000"}); var myBucket = 'my.unique.bucket.name'; var myKey = 'myBucketKey'; diff --git a/other_langs/test.rb b/other_langs/test.rb index dc5b7914b..f7d84eb1f 100644 --- a/other_langs/test.rb +++ b/other_langs/test.rb @@ -1,6 +1,6 @@ require 'aws-sdk' -sqs = Aws::SQS::Resource.new(region: 'us-west-2', endpoint: 'http://localhost:8086') +sqs = Aws::SQS::Resource.new(region: 'us-west-2', endpoint: 'http://localhost:5000') my_queue = sqs.create_queue(queue_name: 'my-bucket') puts sqs.client.list_queues() diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 84e8a8f2b..d967c8bad 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -40,7 +40,7 @@ def lambda_handler(event, context): vol = ec2.Volume(volume_id) print('Volume - %s state=%s, size=%s' % (volume_id, vol.state, vol.size)) return event -""".format(base_url="localhost:8086" if settings.TEST_SERVER_MODE else "ec2.us-west-2.amazonaws.com") +""".format(base_url="localhost:5000" if settings.TEST_SERVER_MODE else "ec2.us-west-2.amazonaws.com") return _process_lamda(pfunc) diff --git a/tests/test_core/test_instance_metadata.py b/tests/test_core/test_instance_metadata.py index 69b9052e9..f8bf24814 100644 --- a/tests/test_core/test_instance_metadata.py +++ b/tests/test_core/test_instance_metadata.py @@ -6,7 +6,7 @@ import requests from moto import mock_ec2, settings if settings.TEST_SERVER_MODE: - BASE_URL = 'http://localhost:8086' + BASE_URL = 'http://localhost:5000' else: BASE_URL = 'http://169.254.169.254' diff --git a/tests/test_core/test_moto_api.py b/tests/test_core/test_moto_api.py index e65a92ac8..cb0ca8939 100644 --- a/tests/test_core/test_moto_api.py +++ b/tests/test_core/test_moto_api.py @@ -6,7 +6,7 @@ import requests import boto3 from moto import mock_sqs, settings -base_url = "http://localhost:8086" if settings.TEST_SERVER_MODE else "http://motoapi.amazonaws.com" +base_url = "http://localhost:5000" if settings.TEST_SERVER_MODE else "http://motoapi.amazonaws.com" @mock_sqs diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 30e3e017b..2889e520f 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -77,7 +77,7 @@ def test_create_queues_in_multiple_region(): list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) if settings.TEST_SERVER_MODE: - base_url = 'http://localhost:8086' + base_url = 'http://localhost:5000' else: base_url = 'https://us-west-1.queue.amazonaws.com' @@ -98,7 +98,7 @@ def test_get_queue_with_prefix(): queue.should.have.length_of(1) if settings.TEST_SERVER_MODE: - base_url = 'http://localhost:8086' + base_url = 'http://localhost:5000' else: base_url = 'https://us-west-1.queue.amazonaws.com' From bd2ff89bf127315f398f49c734cdb25eaf96bfdf Mon Sep 17 00:00:00 2001 From: Seamus Cawley Date: Mon, 13 Mar 2017 13:52:57 +0000 Subject: [PATCH 078/274] Ensure SQS property WaitTimeSeconds is an integer --- moto/sqs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 62b79fdc1..61093aa82 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -113,7 +113,7 @@ class Queue(BaseModel): self.region = region # wait_time_seconds will be set to immediate return messages - self.wait_time_seconds = wait_time_seconds or 0 + self.wait_time_seconds = int(wait_time_seconds) if wait_time_seconds else 0 self._messages = [] now = unix_time() From b9ea947aa0572234051d9ba22b77b862daa3b524 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 13 Mar 2017 14:09:51 +0000 Subject: [PATCH 079/274] Add ListHostedZonesByName --- moto/route53/responses.py | 47 +++++++++++++++++++++++++++- moto/route53/urls.py | 1 + tests/test_route53/test_route53.py | 50 ++++++++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/moto/route53/responses.py b/moto/route53/responses.py index 07f6e2303..984f305ab 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -7,7 +7,7 @@ from .models import route53_backend import xmltodict -class Route53 (BaseResponse): +class Route53(BaseResponse): def list_or_create_hostzone_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) @@ -47,6 +47,32 @@ class Route53 (BaseResponse): template = Template(LIST_HOSTED_ZONES_RESPONSE) return 200, headers, template.render(zones=all_zones) + def list_hosted_zones_by_name_response(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + parsed_url = urlparse(full_url) + query_params = parse_qs(parsed_url.query) + dnsname = query_params.get("dnsname") + + if dnsname: + dnsname = dnsname[0] # parse_qs gives us a list, but this parameter doesn't repeat + # return all zones with that name (there can be more than one) + zones = [zone for zone in route53_backend.get_all_hosted_zones() if zone.name == dnsname] + else: + # sort by names, but with domain components reversed + # see http://boto3.readthedocs.io/en/latest/reference/services/route53.html#Route53.Client.list_hosted_zones_by_name + + def sort_key(zone): + domains = zone.name.split(".") + if domains[-1] == "": + domains = domains[-1:] + domains[:-1] + return ".".join(reversed(domains)) + + zones = route53_backend.get_all_hosted_zones() + zones = sorted(zones, key=sort_key) + + template = Template(LIST_HOSTED_ZONES_BY_NAME_RESPONSE) + return 200, headers, template.render(zones=zones) + def get_or_delete_hostzone_response(self, request, full_url, headers): self.setup_class(request, full_url, headers) parsed_url = urlparse(full_url) @@ -289,6 +315,25 @@ LIST_HOSTED_ZONES_RESPONSE = """ + + {% for zone in zones %} + + /hostedzone/{{ zone.id }} + {{ zone.name }} + + {% if zone.comment %} + {{ zone.comment }} + {% endif %} + {{ zone.private_zone }} + + {{ zone.rrsets|count }} + + {% endfor %} + + false +""" + CREATE_HEALTH_CHECK_RESPONSE = """ {{ health_check.to_xml() }} diff --git a/moto/route53/urls.py b/moto/route53/urls.py index 795f7d807..53abf23a2 100644 --- a/moto/route53/urls.py +++ b/moto/route53/urls.py @@ -18,6 +18,7 @@ url_paths = { '{0}/(?P[\d_-]+)/hostedzone$': Route53().list_or_create_hostzone_response, '{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)$': Route53().get_or_delete_hostzone_response, '{0}/(?P[\d_-]+)/hostedzone/(?P[^/]+)/rrset/?$': Route53().rrset_response, + '{0}/(?P[\d_-]+)/hostedzonesbyname': Route53().list_hosted_zones_by_name_response, '{0}/(?P[\d_-]+)/healthcheck': Route53().health_check_response, '{0}/(?P[\d_-]+)/tags/healthcheck/(?P[^/]+)$': tag_response1, '{0}/(?P[\d_-]+)/tags/hostedzone/(?P[^/]+)$': tag_response2, diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index ea8609556..b64c63a30 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -361,8 +361,9 @@ def test_hosted_zone_private_zone_preserved_boto3(): hosted_zones = conn.list_hosted_zones() hosted_zones["HostedZones"][0]["Config"]["PrivateZone"].should.equal(True) - # zone = conn.list_hosted_zones_by_name(DNSName="testdns.aws.com.") - # zone.config["PrivateZone"].should.equal(True) + hosted_zones = conn.list_hosted_zones_by_name(DNSName="testdns.aws.com.") + len(hosted_zones["HostedZones"]).should.equal(1) + hosted_zones["HostedZones"][0]["Config"]["PrivateZone"].should.equal(True) @mock_route53 @@ -445,3 +446,48 @@ def test_list_or_change_tags_for_resource_request(): response = conn.list_tags_for_resource( ResourceType='healthcheck', ResourceId=healthcheck_id) response['ResourceTagSet']['Tags'].should.be.empty + + +@mock_route53 +def test_list_hosted_zones_by_name(): + conn = boto3.client('route53', region_name='us-east-1') + conn.create_hosted_zone( + Name="test.b.com.", + CallerReference=str(hash('foo')), + HostedZoneConfig=dict( + PrivateZone=True, + Comment="test com", + ) + ) + conn.create_hosted_zone( + Name="test.a.org.", + CallerReference=str(hash('bar')), + HostedZoneConfig=dict( + PrivateZone=True, + Comment="test org", + ) + ) + conn.create_hosted_zone( + Name="test.a.org.", + CallerReference=str(hash('bar')), + HostedZoneConfig=dict( + PrivateZone=True, + Comment="test org 2", + ) + ) + + # test lookup + zones = conn.list_hosted_zones_by_name(DNSName="test.b.com.") + len(zones["HostedZones"]).should.equal(1) + zones["HostedZones"][0]["Name"].should.equal("test.b.com.") + zones = conn.list_hosted_zones_by_name(DNSName="test.a.org.") + len(zones["HostedZones"]).should.equal(2) + zones["HostedZones"][0]["Name"].should.equal("test.a.org.") + zones["HostedZones"][1]["Name"].should.equal("test.a.org.") + + # test sort order + zones = conn.list_hosted_zones_by_name() + len(zones["HostedZones"]).should.equal(3) + zones["HostedZones"][0]["Name"].should.equal("test.b.com.") + zones["HostedZones"][1]["Name"].should.equal("test.a.org.") + zones["HostedZones"][2]["Name"].should.equal("test.a.org.") From c5853b48da25e16f26e0611985b1d6bb708223ac Mon Sep 17 00:00:00 2001 From: Adam Stauffer Date: Mon, 13 Mar 2017 16:48:22 -0400 Subject: [PATCH 080/274] update RDS responses to return DBInstanceArn --- AUTHORS.md | 1 + moto/rds/models.py | 6 +++++ moto/rds2/models.py | 9 +++++++- tests/test_rds2/test_rds2.py | 43 ++++++++++++++++++++---------------- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index e85996125..08757d2bb 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -44,3 +44,4 @@ Moto is written by Steve Pulec with contributions from: * [Jean-Baptiste Barth](https://github.com/jbbarth) * [Tom Viner](https://github.com/tomviner) * [Justin Wiley](https://github.com/SectorNine50) +* [Adam Stauffer](https://github.com/adamstauffer) diff --git a/moto/rds/models.py b/moto/rds/models.py index 670b0a808..a499b134d 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -71,6 +71,11 @@ class Database(BaseModel): # DBParameterGroupName # VpcSecurityGroupIds.member.N + @property + def db_instance_arn(self): + return "arn:aws:rds:{0}:1234567890:db:{1}".format( + self.region, self.db_instance_identifier) + @property def physical_resource_id(self): return self.db_instance_identifier @@ -231,6 +236,7 @@ class Database(BaseModel):
{{ database.address }}
{{ database.port }} + {{ database.db_instance_arn }} """) return template.render(database=self) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index f03cf4ad1..eecb608dd 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -95,6 +95,11 @@ class Database(BaseModel): self.character_set_name = kwargs.get('character_set_name', None) self.tags = kwargs.get('tags', []) + @property + def db_instance_arn(self): + return "arn:aws:rds:{0}:1234567890:db:{1}".format( + self.region, self.db_instance_identifier) + @property def physical_resource_id(self): return self.db_instance_identifier @@ -206,6 +211,7 @@ class Database(BaseModel):
{{ database.address }}
{{ database.port }} + {{ database.db_instance_arn }} """) return template.render(database=self) @@ -349,7 +355,8 @@ class Database(BaseModel): "Status": "active", "VpcSecurityGroupId": "sg-123456" } - ] + ], + "DBInstanceArn": "{{ database.db_instance_arn }}" }""") return template.render(database=self) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 731bc75c1..1e2e0abdf 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -26,6 +26,8 @@ def test_create_database(): database['DBInstance']['MasterUsername'].should.equal("root") database['DBInstance']['DBSecurityGroups'][0][ 'DBSecurityGroupName'].should.equal('my_sg') + database['DBInstance']['DBInstanceArn'].should.equal( + 'arn:aws:rds:us-west-2:1234567890:db:db-master-1') @disable_on_py3() @@ -59,6 +61,8 @@ def test_get_databases(): list(instances['DBInstances']).should.have.length_of(1) instances['DBInstances'][0][ 'DBInstanceIdentifier'].should.equal("db-master-1") + instances['DBInstances'][0]['DBInstanceArn'].should.equal( + 'arn:aws:rds:us-west-2:1234567890:db:db-master-1') @disable_on_py3() @@ -333,26 +337,27 @@ def test_list_tags_db(): result = conn.list_tags_for_resource( ResourceName='arn:aws:rds:us-west-2:1234567890:db:foo') result['TagList'].should.equal([]) - conn.create_db_instance(DBInstanceIdentifier='db-with-tags', - AllocatedStorage=10, - DBInstanceClass='postgres', - Engine='db.m1.small', - MasterUsername='root', - MasterUserPassword='hunter2', - Port=1234, - DBSecurityGroups=['my_sg'], - Tags=[ - { - 'Key': 'foo', - 'Value': 'bar', - }, - { - 'Key': 'foo1', - 'Value': 'bar1', - }, - ]) + test_instance = conn.create_db_instance( + DBInstanceIdentifier='db-with-tags', + AllocatedStorage=10, + DBInstanceClass='postgres', + Engine='db.m1.small', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=['my_sg'], + Tags=[ + { + 'Key': 'foo', + 'Value': 'bar', + }, + { + 'Key': 'foo1', + 'Value': 'bar1', + }, + ]) result = conn.list_tags_for_resource( - ResourceName='arn:aws:rds:us-west-2:1234567890:db:db-with-tags') + ResourceName=test_instance['DBInstance']['DBInstanceArn']) result['TagList'].should.equal([{'Value': 'bar', 'Key': 'foo'}, {'Value': 'bar1', From 54c7e0bcf9e3386a7f0f2ec9f08919fad3b3a23d Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Mon, 13 Mar 2017 23:07:18 -0400 Subject: [PATCH 081/274] Update docs to newer sphinx. --- docs/_build/doctrees/ec2_tut.doctree | Bin 7904 -> 8282 bytes docs/_build/doctrees/environment.pickle | Bin 7968 -> 8914 bytes docs/_build/doctrees/getting_started.doctree | Bin 10820 -> 11613 bytes docs/_build/doctrees/index.doctree | Bin 16675 -> 18990 bytes docs/_build/html/.buildinfo | 2 +- docs/_build/html/_sources/ec2_tut.rst.txt | 74 + .../html/_sources/getting_started.rst.txt | 112 + docs/_build/html/_sources/index.rst.txt | 91 + docs/_build/html/_static/alabaster.css | 176 +- docs/_build/html/_static/basic.css | 79 +- docs/_build/html/_static/comment-bright.png | Bin 3500 -> 756 bytes docs/_build/html/_static/comment-close.png | Bin 3578 -> 829 bytes docs/_build/html/_static/comment.png | Bin 3445 -> 641 bytes docs/_build/html/_static/custom.css | 1 + docs/_build/html/_static/doctools.js | 28 +- docs/_build/html/_static/down-pressed.png | Bin 347 -> 222 bytes docs/_build/html/_static/down.png | Bin 347 -> 202 bytes docs/_build/html/_static/file.png | Bin 358 -> 286 bytes docs/_build/html/_static/jquery-3.1.0.js | 10074 ++++++++++++++++ docs/_build/html/_static/jquery.js | 8 +- docs/_build/html/_static/minus.png | Bin 173 -> 90 bytes docs/_build/html/_static/plus.png | Bin 173 -> 90 bytes docs/_build/html/_static/pygments.css | 6 + docs/_build/html/_static/searchtools.js | 166 +- docs/_build/html/_static/up-pressed.png | Bin 345 -> 214 bytes docs/_build/html/_static/up.png | Bin 345 -> 203 bytes docs/_build/html/_static/websupport.js | 4 +- docs/_build/html/ec2_tut.html | 38 +- docs/_build/html/genindex.html | 26 +- docs/_build/html/getting_started.html | 74 +- docs/_build/html/index.html | 40 +- docs/_build/html/objects.inv | 7 +- docs/_build/html/search.html | 19 +- docs/_build/html/searchindex.js | 2 +- 34 files changed, 10849 insertions(+), 178 deletions(-) create mode 100644 docs/_build/html/_sources/ec2_tut.rst.txt create mode 100644 docs/_build/html/_sources/getting_started.rst.txt create mode 100644 docs/_build/html/_sources/index.rst.txt create mode 100644 docs/_build/html/_static/custom.css create mode 100644 docs/_build/html/_static/jquery-3.1.0.js diff --git a/docs/_build/doctrees/ec2_tut.doctree b/docs/_build/doctrees/ec2_tut.doctree index 2d6d78f26bdfa1142833303de5b05379aae3bafa..719a1ed0b0b296abd8f5a0552f3c7da449e67020 100644 GIT binary patch literal 8282 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 diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle index 2f5d88b27e3d1167003b58b79671819b79c26345..a1145484936f5a699ba82d8b41e5a96f5f92bd35 100644 GIT binary patch literal 8914 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 diff --git a/docs/_build/doctrees/getting_started.doctree b/docs/_build/doctrees/getting_started.doctree index fde8e76496d1a08c4083c3262db144f83776d0b6..b34024fc2fdb909814c552a41763971b78a9dd3e 100644 GIT binary patch literal 11613 zcmeHN2Y4M&Ybz8Iy!m$5;eg6Dxoi&3o@kccghU@7|fQ zBw$Dk1S~=cE%eX>A(XH++t|)_wz7@wY-jr}TiNoTb7x+WWUp<=_w9aPT;9ywd+xcX zpIgoYD=XEolEj`Lm4dJ;q5^%^L=YF$WqZ!3m8n`)w0i=#CgSR42*Sud7WEsR6eIWo2i0NR~ZT_am0Gcr~{Ga<8*2u-SB0KEX1e4nP}nbwSq^#HIcDxr4cP^;dN z#9kjIzFID|^f8EuQaB3nVl#yCHuGZQ8l|*Z>>qKFqD?+Uwu?rbG zj8+>)>@vHHgyK~rbw#Q+7GnMwM(wFJG1ty#;%93 zhiuRs4MnjyErxExj~~{_V1|guA(&4zlnKHJOH@lbiq&qrD|V+yX6kCYD~_n$!s{QrC>zT@~MrBE0V%x4X-3Wk!=e_l?`DNc4l_sw6kLc87r< z8dv*er<>TTVIo2uIJOGNGJ94Bo=A`&vx$cn&pQf}FQ~)+ z%Vf`c@QgZ=s^^0&NfTd}l-;B(NpUx&>Si?CE1LAk0V3pNPD~6Bt9zOZ**%R3)r(1PCzj)_JD1kUBADx4M2i{)XLhRY zB|;Z>Vycm(yqHVhi-mUmf;t5r$5M444ebKnj4z_(B}*v*F0$X3f(VMP5P^Cb<{nSg z%YlH6U<^`)O?G87CQ{sm6?L_L0m0W0S#OS zL=a@djA-f>`{>IAX@;th=&vJG&eio zQ!)5RQgu3Kv}XL9?0+^@b42AJs60Ap&BFy|-5_q&Wp3Ekqz-GAiPJFh`6d&RE&CNq zrGDU8Z%*7R$+9U=pJNvFDiE2b>eWQedEhYXEKH8ne}AeTAn3*FKeLn&3?jpRO{@A} ziwR$sst0qa+~2DHK2X}muzEef-;k=aG~<$9Llq1|lIj;y^+p1_8%U_Rxl{>jLs32PMTK)(3C)E{l~){v`g|-AQz8*3^-_I4x11Jvvz0tllu9mV5dFE# z-voBQy)>vdLke$6)mw9+EwCvOqdKvk(!#L%a0`L#6CYWQ zz>PV#9|eKm%5)zCHy=;cCx{!G`ccCTnc!oo`Xq7lGQ=&fRu6S-l_AIi`!KzgqC=$_ z0pl{A$HaW0!0RCE`22A&=NH5BG?s9Ch6@({V2Nuvju&{b;}j#|Pwum5J+#j{?fQwx zfC!NKO4MZ?VF(`G=wN8{@Uzgs96`X0wFMCtsR1CFR(c05PIRcar-87XRAxjB5b7;q zJ?NO0;y`WQybUygX8;Eti>9NcorQ3uNp=JMObHI?@KQO5BZmZs#ioYxnbMMByhY*c zl%Fb6Y79gWM(XheyW4kzsf2bHpC0RkVHwwM^IN-LdQ;NJ|JZT$RHU9rX?+S<86ha~TQx7JS{%qKTF?^Ga;?e4_pLJQW5yD}X-|g%?Qlx! zJ6cSNW5{QDlewZXoo9){go9!RLWKXq*{2y(@nz=~>*N;IO?FfDx zl>bDkev&9BWM6FR^h>GwDVp&ZW;{hF3ogQ3w#&C>u}7#=q{mqR$NmalB2w|mL#%t+ zve(?$qxN|uZUm#M6(-Q&Pq)y_8vL2%XdcY@|5?!d?Iz*p!1d3k>KBM>BIirZ%<$z@ z{UUMw#B!N|7N>biAeInuP0Favv9f?E&a5I^v&c|xYQGFqdq(dTyp`

63{d(Th7JYxCE#|1-lv_CZ{+8_F=zB|U62EPt z?^jdxI}{b@eCF0gN2R|D+xtpu4F0_~+*u56VEla;*>Y(90F3-$s{RPmNJ9D5W+eVv zs{WW}zZ0{cq_v|PHAC?}i?ZxIj%L{NBM&R@i3vLR#Q6-)Ib%VIlVfsg6#kPIsyPb( z>2g%xn#Tfaq-=*sBfr5?V>yV|qIy<`93aIl4<{vQjA5-;DH2wKO4ZZoU_8Y1C zXBu-5iR1jio=D`3$P0F}fyhJhd6)DT60-@>rTFjCac!}ix}WCwo%;~S?|-?7_dKpLtJP82zs{e^mc1H&YnZE8qjNizDfY zOgZ{8v~e)iEAf`Y;GNS3@ifia2&uZjAZrZB*a3uu{CL~-qtM)_S-3I7TUe)CF)X66j-^9xilSKg zmUJ&hw@^$`RQEB)r4*0mV(6zqhc9u)wHOTk*5Q%r^*q(iX+y>|ab)xYHZB4iw2k*f^(t13Lp@~Zah+bH6z?Poy|uCHiO>)gA@(O*h2_@ z=I{l(xX2U{xD){wGk^$aPIu`V1Y1##8Q9VzPV)BhuF=tZa9C5VhaOI9tfMjQM|o|E zD?^;;&=|fS#DX=jJae5=4TFK0?-cLhbN!Ou1G?Hp#F6zHCU>cbyj&f7Y24xebL{K` zSy0O$9;q%d6Fa9f9!U|+m_5WG`wht05vb)V$T$uiQ=wpq%0X+eerW&x{RQh9>qHU+ zbO3{^5E;jH+?^aactdGF{vH@6n4xwVa>P2oxH)3cTDj~-vhA?7ZCgB(3zn8t5bU>1 zkUj)N5XH54r20Ch#keYQ-Zanax&H>!KXwX)j?&>qc8cglWH2JYQ3kH6aVTO8X#nSq z364Uhu=$}w@rj9jR+%oNF%ou1m)6bGD}$<}Z^Rt!Vq#N!E>FGG)atoJh8f^6UkhFo zZTc`qLIlslBh^P3jk#UCC^Vp%gk&~L`uU9PrVN?cG8qw2Si70q7c*lJVB@53K|i$1 z_5itQN=u;dRySM{w5PMz z#M%v_$xzpX)^`Exh~3wy5TXJVeUvA>8^3lp8n`?~ALGUsQscU4zE%!>Cm$8*<7gh& zBhTFBy@-47q24PN4Rsjc#SCzQ0D3AOHr}Xwi1bNr9mOvhkKLOWJJBtOw$Y68 zrUoyK$|!v=4?BflqMk0Oz$e|ZkJ#N_)s*$<`|yo2sJ>QWBmEK{`cfL&m(yV=q#Ke{ zk5kV&9Jza%E{#xIgUg{vznq6T)Y*-z9V#q~^aMUzs&#P7gEFz8TDX_4wW5M{(I4tE zep6i`SbIY*Plrl{z~xM&tLOopmt($AS5XgE2M&0!Cn7D-H)8kd$qELbHZ0M;obR?_(Md?8r@6H+4r$7q#u-A`fqp%&eR}W9@1`)YDs+w7R*FCe^wLx1N*7##>JZ<= z04vGysTxby(RK{V2CFE83)mF2V!(oQ{Wt^D1lD2qW%QXcD;*J>ym7?d4wZ-6p-@+a zHXQRQk(YhE#sIU|5d#qhO3d#=7&@Wmj;bgpQ~ESvve$`PS&+%h`y!G;#CjI(3-%7A z?s^iR3;!G;>Lk)CkO_VCQOQ`4)SNl;JN*i@L91)4Nv-ZQOgPf7L_53@RbO5(8D_w* zGQj<_uBNO-=eWYH>6BqNlG-LcpSADczzVFu-d~MQ6nBXF;O_m`R`vb#xn9iSjunfO zIEgYTK0v5D>`rVyCcPC^c|r98q&U-*Kxb&ZELM{aSRM zwReEkS|c1Z&B)<86a6{>E!eAY8Koa&WQOs2B$6t)toi!&+`LZA)k7FE)<8lB*iGs; zaQg<59_k{wS}l{xS;o1>6pc9KEP8=~`^g_#4Ec@Rx^|Jq^h0R2JCb;E@J9V6dM{+C z$cx@=FnK|%lJ#2%tcQmTMiAFq>3gkH^&+)9Z(B!OSkOIdevC!cc9@|x82`v$@-myt-~I@19ZQO-n+8m zPyGnKKiX_wH&KL{ot;G?300MFgsFvNd)X8D~p4)V)-oN!it59x$@dq)OwnGf4e77F_HcpK*#mx W@oRUIp;jaPBtBC81^l9p=zjoXZ^-lj literal 10820 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 VuUw;)LQQoE;Y{i6Xnxem{0AO%_2mEn diff --git a/docs/_build/doctrees/index.doctree b/docs/_build/doctrees/index.doctree index 3dfe8f0888ddf510511c3b7e49f7d9a787efb1d1..41878354ca955f987b4b6d4ab3e04c8afadd446d 100644 GIT binary patch literal 18990 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 Zc`oSQhMQ2{j;p(QNZ!FRZ-Z~o{{X?28JqwB literal 16675 zcmcIsYm6LMR`z2aJ#Ej7?f4O|J$A*7lgVUzCU({+8SI5g>~sd>&c9O>>OY$1UJA}bK$r9~{etOP<@0SkZF#}80~Kng4P!)nEkpj`=s z5Z}3VZ}siRc)F*xkv!d9b?&+6@tt#T-8v_~H1?|xuj~{5@fpjn#gXHMiw)mmVZw8C zq0Smn^7-WP*OFgJUQ6cr(J-!tkrRQ^Z&WO{5#ln>)ojPJ0@g@gN#^Z+d?GS~P1Z3m zZ$?qzRO1M&iBZmqn+>y$U#}#5JYXA+g-@qpvF(H(bS!Z_S2b&QaW5sx$R}%_8HQqL z%%=i#EA-=_#uA=iTr?_z$8y3aBXg66BzB&Uc}{~RH}~@iMf%v1?bq4TtFad^H3Q#e zwJ2Pw`;or{wZkRBWHAV%gikb0Xci^*n4NnivG?0!aTPjFh8O}PXx6c3p_zRYL?>Ve zewNr3&0^iJ-7R`fH86u#al;RaRa{T(s%Z*=Jr! zVt$HkH!*nymSGUB#;i)URe=Ft16O-GwCDJ~#6E9dm3afzj644f**CX}?(v4g< z4(EDoC#96cLJmwbcT32ajGo9?3rii^^@(Jh|2TsCI+ELV8URr`~i)P(?)o(1;{Q8oJPf9YE zdKFou6w)i^vau5@($Kh>*8v~}d{h}dv$C+~EHY!{G&YMNvSh@pwhUa|GTOH&;1L2> zD4d~x7E1H>a$ok{j+rf`vl(l0HW`!mvzRRL9vTgQ_HSU;q#bWS_0xw>;zrbJV)Z=;uO<8tC7A7|=hU1ixi4Z-Tvuy5(QlbS z1NWxz)$;46(@3Bga*YmNaaZmmvcOOdr5FX{(bwj1iITO%~^s|=?rizs z6DdK$_b-SwOph12O1Mv?{B;RwUpGZeE67Xrew@35kAQM-Vc|Ky-o%c5VIiMiFw$td zfvtDs)C_scc+T@<%ecyFmn(QE`0zRiS8lE1?~P~I@OQO=6pbBfi0^9*j&`D!u^ISr zldjw`H|b*~M&4elnb@Rn<`JUqGpP=X^zleJ&CG)>I)^l&DcEUqb}~)z?p0!!Lzck< z3ifb*V{<@LxG&Pk^X^NjSS7`$#3!X0^kAhhckh5kMY{Vz{OCzB?qytcpCVpenXk)} zO6+-exgVcCqxqwQ`ARc&jpQKP3}j|ECi2v}4@>LT6^C#3sJhDmiLU5_aEBwYBT6ki$MkVy*)`i+m##lF8dGKvY(Z*|7TnFo_TZ) z#L$BK-+R(vjB@$j#XfQ%1M{2|J4cr&C6#b!K+JVn1%1JN2sA4C21oPsP7BU(S&$#r z`9OEE+MJW(UucWpNvErEg@_7nQB#A(Rk>#>$JN1gy0oyc7Wp_4VTReTjGB)jR77q~(*6h4Oqsiu^`fIzstCT;d@#i7InZyg_kyx{(`uJPcwR?gqMxbL^c zO^@FS?swbb=CTQNnZ=lb`~5x1Jk<}S!`YbU6c2^Y$EBsm4gzcGvf8H6ML$IZ1$P!7 zYH8hLIDTMZq0FK!Ke&qrI~kBTT4z_D8ZrE4IvFb!pO7k^mnse(fR|}R7Jv(KJOVJ^ zR-h*YuV@w;F$6P-2gmj8UQ1qAat{=Sd{VMd<||6`l@%L2-zpLvAST@}xIe3@k0+nK zY+7zDEUZ{{r-7r1z@(RHV()?%YvPqElDdm(n^Da>=$T62C_(w2guGmzz994`$f&{Bce{JFht@%`yh9~ zMC$0~GfKOGpy<9WOe0o=OVYl-rZ`}~_rOVYXbL_B_qTN$4IEO3`eSK0Rd~M5MM37W zROw4?m2@gyfv*!u!Tn23fjuVQC$U9gP0w#ppoq9?$4wepLeS^^pe{mh#PFGW;B?9o zACi{%hP1@s!Sk;)A`6~(<#+_o=^eG~37vn_tU6-oWC{$7uGyW<{l3y>pkU!Mot%_y ze^lD`e-*b<52cQ@*)f5%Nqnefevet4GH1i|S~wOCQ)JylwMFRA(e8~~w?_olbO(2( z?9)=&Q=|oUGlK(ZfktFNx**2`NK=^-J-{hyMj8<~O5h=K?xZv)bXV#1suFu3#JFXl zvq-PY(s0i!4p`(Be;poTNWpzc*L+~Ck9E0yIC1$w3|Ef+F)8nQTV8Qnmo=n&1^2Ed z>mHNlMS!hI?=I8bhf_m*GdX`+fu{I*0Pwg;=()gOx_~)g@gG2i>G$Moc8*)6L z{ZJpJdO-e`X1)pOI>;C@S!b&udav#@Z>uR0#fUYJT?mq&&4 zLwz_a9iNptz9)4Y9M134hz!o(mE!^D*)BD7dx^u5?`y^y5z3j^L*S#+o@3)sgC4jC zf->+?&hT+v+HOX%AS~myj*my^Uco($kG|`;dI5hjdqlfQZ<}{`7k?b39}YcuUx7I6K!n>jCI1p0YmuX0vNLD3KUZhKeHN5br zIvi0Q+V?H@>--#5w1udS+eBSeTosjZVKeqvP1M0Ad`#4d@{|3g?=3*!IYNYY4<&6V z$eWY$xiylh;QpKvt+XRY&4nV2H!vi_bn3;S-ie${4Zj+OF*EK2s3m5>dPQHphqpRE z(U+;pc0VZ{(URJ_7QQ^LN?Eh|;Me)Z2U~y1A)!%)EuC;oIH5z6UI(}z!(_troTfjo zO#ewK3}UArpKh~%7UaEVS9xR=$zzAo%h%n{(@dghlq%PXLWv^IcF6Kt9nUekYlyo> z`)-;;_%*7)>{U0dz}y+H%3_EJD#-jP=?D?C6cK6N_KuRfO7fFJ@-zb>1*%}xq!$!M zN`WM(QAiDzGYxuTem5-$M>E{~Y7bt#5O*t5&Xoy+T+ ztRdO}*0nVKj;_~WY5G5CNSdZ^ORs*1E)gid*^?UdS{X>uRZsb5pIRAyFpR98WPo~^ zoKip(%t$e)U`C1YJtYPTqMU|kk?2!(*8OUhYG?irBhdj7S%{F0L{!nF{N3>h;mIE; z)_8}NiJ^I19#&5nW}F@TsfTq^lmLOFw*9DFuY9q!{EX zB}PGt;ZER74`}+i=n4qCT$J)vE-EU`;G%5$ZWmnx(TFaZNuw!bW>B-&_0@OyVB*sq z%+#uTM>^}4Qt5{pR}-l!!d2@uV_H+Jg!maL#Ne*FixFL}O8F{R)s$W(R4xiT>W=^- zmO_y9b(C7SZ;KMs9Qq%ce%!G7ZVI~!5&3N7*P?*2;uiXBnzV$g+Qd(gL%8bef6VW%sur=NpO1r{zF7|C?fylB=8B7HdX! za`S*}WPvTuyPqZHjGG^osQMKBL<-O|l(H(;DD@T37wYRczVf3c%33#kKWg|9qh2i` z@bR!!ulio4=f*K?gc>1wBI*@%#Ziv}(PJY&6ShK>?W(K&Y}@W{8~4y^;pv@jXzl8D z>sVT^Kek$a{zXEuQ2L8f%fTK~X$X!m3htk%VI}^=3uprfu#DU_(z<=)f=J32u+uYO z1niYmzgkCYkHGKOz$e2mpGNb5ID$*Xs}5>iVlOccX0z{ zy(oM_FX6;TO^bA4fBMez#itXD1#7Bt5mWQC>PaHk(KdZ+YiqIDiflApkvJB+w*<-%B8i|y8(@W! zky)cQ5$Zk_kzU!R5J0HUA&Bc8VUj-0RzeJ6Aeh3w0$nzEl7?_+JDu6BCDnU7n21k% zW@9r(aA9r>x*k9>;z!W0;=`P}^(Rn4w*e%;4;07fuBzn)-wS=NUwo{RVtZ@%)W`%>BA&V`)vRx<4Op8kyjLZB;*lI-PcExr!Z4dt- z@R&z+>U<7w`*pN-Dd_$e!iUdREGHzKLOCgX-Zs4qW#h;tutSsVPHzQ;S4n0hTxoY8 zHx)F?MnrU!Z95UA*eDJaWXJiGWm1!*S#N^mkc`R3gULko~s!O0WO52&pdtAh%2`vN~4*3l)Y%3j1zhUl$~{M% zu8_sMYzD}R2v7K#R5X_MnuN#+kzG+u#qc;GI!jg}7O7*2+MqCynRPHfK{O&sApR@J z&j_9ffXd-|@#2#o0z{^z8w8n1{)Z7$=$>eZ)CML~ZB*G;;wqn{6Bp`zMjx?z30r7) zk@nF~+Qq*8nPvNP_NU$7C%WIIKYvYszD6T1*~i^KqhEhYe;!7~rF(+PdI3?rf19Qg z4csTXS0&L{N9*sgj#W%v-p}(EyzWes)%0ln^Lg%u5>Aq0+zEHIs^J!gbVfrbB8u4oe z&BF+IZJ~Al7!lO7u~rLfo~woRKtF{(Zc-aJZ6UZ^TPI&$xm{i@mpYeMt$%aKW%e0p zEgnNk{L1w>eM7|z)n}*HTQb?QaiO=B_d@T2`vN$o3$A?J&f^zlppNZ{uolksXt5G* z!S@XXn|a>vAO|mF=>*B-JDrn&fKK*IxJfxhE%UO|bw59Xor8mQD5|7ei8OrF9so&( z0V3jm6M#^$i}rkq^e+>> import boto.ec2, moto + >>> mock_ec2 = moto.mock_ec2() + >>> mock_ec2.start() + >>> conn = boto.ec2.connect_to_region("eu-west-1") + +Launching instances +------------------- + +After mock is started, the behavior is the same than previously:: + + >>> reservation = conn.run_instances('ami-f00ba4') + >>> reservation.instances[0] + Instance:i-91dd2f32 + +Moto set static or generate random object's attributes:: + + >>> vars(reservation.instances[0]) + {'_in_monitoring_element': False, + '_placement': None, + '_previous_state': None, + '_state': pending(0), + 'ami_launch_index': u'0', + 'architecture': u'x86_64', + 'block_device_mapping': None, + 'client_token': '', + 'connection': EC2Connection:ec2.eu-west-1.amazonaws.com, + 'dns_name': u'ec2-54.214.135.84.compute-1.amazonaws.com', + 'ebs_optimized': False, + 'eventsSet': None, + 'group_name': None, + 'groups': [], + 'hypervisor': u'xen', + 'id': u'i-91dd2f32', + 'image_id': u'f00ba4', + 'instance_profile': None, + 'instance_type': u'm1.small', + 'interfaces': [NetworkInterface:eni-ed65f870], + 'ip_address': u'54.214.135.84', + 'item': u'\n ', + 'kernel': u'None', + 'key_name': u'None', + 'launch_time': u'2015-07-27T05:59:57Z', + 'monitored': True, + 'monitoring': u'\n ', + 'monitoring_state': u'enabled', + 'persistent': False, + 'platform': None, + 'private_dns_name': u'ip-10.136.187.180.ec2.internal', + 'private_ip_address': u'10.136.187.180', + 'product_codes': [], + 'public_dns_name': u'ec2-54.214.135.84.compute-1.amazonaws.com', + 'ramdisk': None, + 'reason': '', + 'region': RegionInfo:eu-west-1, + 'requester_id': None, + 'root_device_name': None, + 'root_device_type': None, + 'sourceDestCheck': u'true', + 'spot_instance_request_id': None, + 'state_reason': None, + 'subnet_id': None, + 'tags': {}, + 'virtualization_type': u'paravirtual', + 'vpc_id': None} diff --git a/docs/_build/html/_sources/getting_started.rst.txt b/docs/_build/html/_sources/getting_started.rst.txt new file mode 100644 index 000000000..e0a4fb10e --- /dev/null +++ b/docs/_build/html/_sources/getting_started.rst.txt @@ -0,0 +1,112 @@ +========================= +Getting Started with Moto +========================= + +Installing Moto +--------------- + +You can use ``pip`` to install the latest released version of ``moto``:: + + pip install moto + +If you want to install ``moto`` from source:: + + git clone git://github.com/spulec/moto.git + cd moto + python setup.py install + +Moto usage +---------- + +For example we have the following code we want to test: + +.. sourcecode:: python + + import boto + from boto.s3.key import Key + + class MyModel(object): + def __init__(self, name, value): + self.name = name + self.value = value + + def save(self): + conn = boto.connect_s3() + bucket = conn.get_bucket('mybucket') + k = Key(bucket) + k.key = self.name + k.set_contents_from_string(self.value) + +There are several method to do this, just keep in mind Moto creates a full blank environment. + +Decorator +~~~~~~~~~ + +With a decorator wrapping all the calls to S3 are automatically mocked out. + +.. sourcecode:: python + + import boto + from moto import mock_s3 + from mymodule import MyModel + + @mock_s3 + def test_my_model_save(): + conn = boto.connect_s3() + # We need to create the bucket since this is all in Moto's 'virtual' AWS account + conn.create_bucket('mybucket') + + model_instance = MyModel('steve', 'is awesome') + model_instance.save() + + assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + +Context manager +~~~~~~~~~~~~~~~ + +Same as decorator, every call inside ``with`` statement are mocked out. + +.. sourcecode:: python + + def test_my_model_save(): + with mock_s3(): + conn = boto.connect_s3() + conn.create_bucket('mybucket') + + model_instance = MyModel('steve', 'is awesome') + model_instance.save() + + assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + +Raw +~~~ + +You can also start and stop manually the mocking. + +.. sourcecode:: python + + def test_my_model_save(): + mock = mock_s3() + mock.start() + + conn = boto.connect_s3() + conn.create_bucket('mybucket') + + model_instance = MyModel('steve', 'is awesome') + model_instance.save() + + assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + + mock.stop() + +Stand-alone server mode +~~~~~~~~~~~~~~~~~~~~~~~ + +Moto comes with a stand-alone server allowing you to mock out an AWS HTTP endpoint. It is very useful to test even if you don't use Python. + +.. sourcecode:: bash + + $ moto_server ec2 -p3000 + * Running on http://127.0.0.1:3000/ + +This method isn't encouraged if you're using ``boto``, best is to use decorator method. diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt new file mode 100644 index 000000000..189ce524a --- /dev/null +++ b/docs/_build/html/_sources/index.rst.txt @@ -0,0 +1,91 @@ +.. _index: + +============================= +Moto: A Mock library for boto +============================= + +A library that allows you to easily mock out tests based on +_`AWS infrastructure`. + +.. _AWS infrastructure: http://aws.amazon.com/ + +Getting Started +--------------- + +If you've never used ``moto`` before, you should read the +:doc:`Getting Started with Moto ` guide to get familiar +with ``moto`` & its usage. + +Currently implemented Services +------------------------------ + +* **Compute** + + * :doc:`Elastic Compute Cloud ` + * AMI + * EBS + * Instances + * Security groups + * Tags + * Auto Scaling + +* **Storage and content delivery** + + * S3 + * Glacier + +* **Database** + + * RDS + * DynamoDB + * Redshift + +* **Networking** + + * Route53 + +* **Administration and security** + + * Identity & access management + * CloudWatch + +* **Deployment and management** + + * CloudFormation + +* **Analytics** + + * Kinesis + * EMR + +* **Application service** + + * SQS + * SES + +* **Mobile services** + + * SNS + +Additional Resources +-------------------- + +* `Moto Source Repository`_ +* `Moto Issue Tracker`_ + +.. _Moto Issue Tracker: https://github.com/spulec/moto/issues +.. _Moto Source Repository: https://github.com/spulec/moto + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. toctree:: + :maxdepth: 2 + :hidden: + :glob: + + getting_started diff --git a/docs/_build/html/_static/alabaster.css b/docs/_build/html/_static/alabaster.css index 07a9e2a42..be65b1374 100644 --- a/docs/_build/html/_static/alabaster.css +++ b/docs/_build/html/_static/alabaster.css @@ -15,6 +15,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ @@ -22,12 +57,13 @@ body { font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; font-size: 17px; - background-color: white; + background-color: #fff; color: #000; margin: 0; padding: 0; } + div.document { width: 940px; margin: 30px auto 0 auto; @@ -44,6 +80,8 @@ div.bodywrapper { div.sphinxsidebar { width: 220px; + font-size: 14px; + line-height: 1.5; } hr { @@ -51,11 +89,15 @@ hr { } div.body { - background-color: #ffffff; + background-color: #fff; color: #3E4349; padding: 0 30px 0 30px; } +div.body > .section { + text-align: left; +} + div.footer { width: 940px; margin: 20px auto 30px auto; @@ -68,6 +110,11 @@ div.footer a { color: #888; } +p.caption { + font-family: inherit; + font-size: inherit; +} + div.relations { display: none; @@ -84,11 +131,6 @@ div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - div.sphinxsidebarwrapper { padding: 18px 10px; } @@ -168,8 +210,8 @@ div.sphinxsidebar input { div.sphinxsidebar hr { border: none; height: 1px; - color: #999; - background: #999; + color: #AAA; + background: #AAA; text-align: left; margin-left: 0; @@ -225,19 +267,15 @@ div.body p, div.body dd, div.body li { div.admonition { margin: 20px 0px; padding: 10px 30px; - background-color: #FCC; - border: 1px solid #FAA; + background-color: #EEE; + border: 1px solid #CCC; } -div.admonition tt.xref, div.admonition a tt { +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; border-bottom: 1px solid #fafafa; } -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; @@ -252,25 +290,71 @@ div.admonition p.last { } div.highlight { - background-color: white; + background-color: #fff; } dt:target, .highlight { background: #FAF3E8; } +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + div.note { background-color: #EEE; border: 1px solid #CCC; } +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + div.seealso { background-color: #EEE; border: 1px solid #CCC; } div.topic { - background-color: #eee; + background-color: #EEE; } p.admonition-title { @@ -305,16 +389,16 @@ tt.descname, code.descname { } img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; } table.docutils { border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; } table.docutils td, table.docutils th { @@ -350,8 +434,22 @@ table.field-list td { padding: 0; } +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + table.footnote td.label { - width: 0px; + width: .1px; padding: 0.3em 0 0.3em 0.5em; } @@ -374,6 +472,7 @@ blockquote { } ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ margin: 10px 0 10px 30px; padding: 0; } @@ -385,16 +484,15 @@ pre { line-height: 1.3em; } +div.viewcode-block:target { + background: #ffd; +} + dl pre, blockquote pre, li pre { margin-left: 0; padding-left: 30px; } -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - tt, code { background-color: #ecf0f3; color: #222; @@ -403,7 +501,7 @@ tt, code { tt.xref, code.xref, a tt { background-color: #FBFBFB; - border-bottom: 1px solid white; + border-bottom: 1px solid #fff; } a.reference { @@ -411,6 +509,11 @@ a.reference { border-bottom: 1px dotted #004B6B; } +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + a.reference:hover { border-bottom: 1px solid #6D4100; } @@ -460,6 +563,11 @@ a:hover tt, a:hover code { margin-left: 0; } + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + .document { width: auto; } @@ -495,7 +603,7 @@ a:hover tt, a:hover code { div.documentwrapper { float: none; - background: white; + background: #fff; } div.sphinxsidebar { @@ -510,7 +618,7 @@ a:hover tt, a:hover code { div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { - color: white; + color: #fff; } div.sphinxsidebar a { diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css index 9fa77d886..7ed0e58ed 100644 --- a/docs/_build/html/_static/basic.css +++ b/docs/_build/html/_static/basic.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- basic theme. * - * :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -52,6 +52,8 @@ div.sphinxsidebar { width: 230px; margin-left: -100%; font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; } div.sphinxsidebar ul { @@ -83,10 +85,6 @@ div.sphinxsidebar #searchbox input[type="text"] { width: 170px; } -div.sphinxsidebar #searchbox input[type="submit"] { - width: 30px; -} - img { border: 0; max-width: 100%; @@ -124,6 +122,8 @@ ul.keywordmatches li.goodmatch a { table.contentstable { width: 90%; + margin-left: auto; + margin-right: auto; } table.contentstable p.biglink { @@ -151,9 +151,14 @@ table.indextable td { vertical-align: top; } -table.indextable dl, table.indextable dd { +table.indextable ul { margin-top: 0; margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; } table.indextable tr.pcap { @@ -185,8 +190,22 @@ div.genindex-jumpbox { padding: 0.4em; } +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + /* -- general body styles --------------------------------------------------- */ +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + a.headerlink { visibility: hidden; } @@ -212,10 +231,6 @@ div.body td { text-align: left; } -.field-list ul { - padding-left: 1em; -} - .first { margin-top: 0 !important; } @@ -332,10 +347,6 @@ table.docutils td, table.docutils th { border-bottom: 1px solid #aaa; } -table.field-list td, table.field-list th { - border: 0 !important; -} - table.footnote td, table.footnote th { border: 0 !important; } @@ -372,6 +383,20 @@ div.figure p.caption span.caption-number { div.figure p.caption span.caption-text { } +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} /* -- other body styles ----------------------------------------------------- */ @@ -422,15 +447,6 @@ dl.glossary dt { font-size: 1.1em; } -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - .optional { font-size: 1.3em; } @@ -489,6 +505,13 @@ pre { overflow-y: hidden; /* fixes display issues on Chrome browsers */ } +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + td.linenos pre { padding: 5px 0px; border: 0; @@ -580,6 +603,16 @@ span.eqno { float: right; } +span.eqno a.headerlink { + position: relative; + left: 0px; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + /* -- printout stylesheet --------------------------------------------------- */ @media print { diff --git a/docs/_build/html/_static/comment-bright.png b/docs/_build/html/_static/comment-bright.png index 551517b8c83b76f734ff791f847829a760ad1903..15e27edb12ac25701ac0ac21b97b52bb4e45415e 100644 GIT binary patch delta 733 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 delta 3577 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 diff --git a/docs/_build/html/_static/comment.png b/docs/_build/html/_static/comment.png index 92feb52b8824c6b0f59b658b1196c61de9162a95..dfbc0cbd512bdeefcb1984c99d8e577efb77f006 100644 GIT binary patch delta 617 zcmV-v0+#*t8i56nBYy%&NklWd+(1-70zU(rtxtqR%j-lsH|CKQJXqD{+F7Jup|pRuhQFVdUw@0>ky z*1TY!!dA#IA*r}ObSESk-6OCkg5*#h0AQq*X$E0;P~qAd6`Z=k_k*lIM8l(T*@4V1 z6=21^AfaqpB{>8^307MuAi4LvISfny#Dc7H; z+j6gYtxsBW-+zM8hyV(EnlU`4l!hvR5JGs7we;ZyuMOZ}%HtBU`yGuatU@${nlgNv z)@wkpl^@`pz&)=}ra^zdnc2viWjmk>NX@OXRRewEW;1j{m zniEpp4XNQqxFVSO^pqvDtz7R)=J&;kS|OK?bN zFaUsNI{<(mvC0%^<5{^ZU3?Vp!AxUrWg*=czh>)+OBG{E;zntC()^4N5cd32keyG0 zXzSOWC1Q7T4aMl~c47azN_(im0N)7OqdPBCGw;353_o$DqGRDhuhU$Eaj!@m000000NkvXXu0mjf DXSNf59&ghTmgWD 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 Hu0mjfcLq?d literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~&H|6fVxZ#d zAk65bF}ngN$X?><>&kwMor^(NtW3yF87Slz;1l8sq&LUMQwyA_72h&sm+fe#sqFPEG6cGWQ5ul00000NkvXXu0mjfPn}Jr literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~&H|6fVxZ#d zAk65bF}ngN$X?><>&kwMol#tg zK_ydLmzem(vK1>2TzUEGl*lj!N<7$PCrdoWV0 z$w0*Ap!bZ4if7h;-yfL#MC0e;t{xY+$l~DX2EWYIPet1cohf^BdG+jXhtuq&W-0|c zKPmlKv-7OTjb}T)7@fTGd9y~u4{g8An;)c2U=w=nwQ7}zVDc>n+a diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png index 254c60bfbe2715ae2edca48ebccfd074deb8031d..a858a410e4faa62ce324d814e4b816fff83a6fb3 100644 GIT binary patch delta 270 zcmV+p0rCFk0-gep8Gi-<001BJ|6u?C0Od(UK~#7Ft&@XN1_2a>_bGdYY}X*$wg%Z8 zX4^eLuH_4yI={KTGvE4t=dOhQ{{Ff0@^V-tLGTw3SS|KM34VWn@%{Y`^7Hc(fX9g_ zicl0KzJQGf2M0JjJOnv9Itt_X_}EYomFiC>k|gO1*chE`et&{k_w3`1`{pLd8NJp; zSHKirG_`jDQGLSB!vO}iUi_FB)c4J!zJCD?1B<9>>H?|nnlLX&FX=!=Dd|LdaVwJZ zYLJv$1)ft?HNJok%;*_tnx-%Czi(NV2}dSOW^;Ujqko~hyL-**-}v7%CAd|8D^sxP UU_nOG00000Ne4wvM6N<$f*gB&bN~PV delta 342 zcmbQo^o(hOWIZzj1A~Sxe=v~ZEbxddW?= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // As of jQuery 3.0, isNumeric is limited to + // strings and numbers (primitives or objects) + // that can be coerced to finite numbers (gh-2662) + var type = jQuery.type( obj ); + return ( type === "number" || type === "string" ) && + + // parseFloat NaNs numeric-cast false positives ("") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + !isNaN( obj - parseFloat( obj ) ); + }, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + + /* eslint-disable no-unused-vars */ + // See https://github.com/eslint/eslint/issues/6125 + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + DOMEval( code ); + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE <=9 - 11, Edge 12 - 13 + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android <=4.0 only + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.0 + * https://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-01-04 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + disabledAncestor = addCombinator( + function( elem ) { + return elem.disabled === true; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[i] = "#" + nid + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement("fieldset"); + + try { + return !!fn( el ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + // Known :disabled false positives: + // IE: *[disabled]:not(button, input, select, textarea, optgroup, option, menuitem, fieldset) + // not IE: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Check form elements and option elements for explicit disabling + return "label" in elem && elem.disabled === disabled || + "form" in elem && elem.disabled === disabled || + + // Check non-disabled form elements for fieldset[disabled] ancestors + "form" in elem && elem.disabled === false && ( + // Support: IE6-11+ + // Ancestry is covered for us + elem.isDisabled === disabled || + + // Otherwise, assume any non-

@@ -41,22 +46,22 @@

This tutorial explains moto.ec2‘s features and how to use it. This tutorial assumes that you have already downloaded and installed boto and moto. Before all code examples the following snippet is launched:

-
>>> import boto.ec2, moto
+
>>> import boto.ec2, moto
 >>> mock_ec2 = moto.mock_ec2()
 >>> mock_ec2.start()
->>> conn = boto.ec2.connect_to_region("eu-west-1")
+>>> conn = boto.ec2.connect_to_region("eu-west-1")
 

Launching instances

After mock is started, the behavior is the same than previously:

-
>>> reservation = conn.run_instances('ami-f00ba4')
+
>>> reservation = conn.run_instances('ami-f00ba4')
 >>> reservation.instances[0]
 Instance:i-91dd2f32
 

Moto set static or generate random object’s attributes:

-
>>> vars(reservation.instances[0])
+
>>> vars(reservation.instances[0])
 {'_in_monitoring_element': False,
  '_placement': None,
  '_previous_state': None,
@@ -132,21 +137,18 @@ Before all code examples the following snippet is launched:

This Page

@@ -157,11 +159,11 @@ Before all code examples the following snippet is launched:

©2015, Steve Pulec. | - Powered by Sphinx 1.3.1 - & Alabaster 0.7.6 + Powered by Sphinx 1.5.3 + & Alabaster 0.7.10 | - Page source
diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html index c1609cd94..8f9a214bb 100644 --- a/docs/_build/html/genindex.html +++ b/docs/_build/html/genindex.html @@ -7,7 +7,7 @@ - Index — Moto 0.4.10 documentation + Index — Moto 0.4.10 documentation @@ -18,19 +18,24 @@ VERSION: '0.4.10', COLLAPSE_INDEX: false, FILE_SUFFIX: '.html', - HAS_SOURCE: true + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt' }; - + + + - + + - + +
@@ -62,14 +67,11 @@
@@ -80,8 +82,8 @@ ©2015, Steve Pulec. | - Powered by Sphinx 1.3.1 - & Alabaster 0.7.6 + Powered by Sphinx 1.5.3 + & Alabaster 0.7.10
diff --git a/docs/_build/html/getting_started.html b/docs/_build/html/getting_started.html index a8a5832ba..ee63ee56c 100644 --- a/docs/_build/html/getting_started.html +++ b/docs/_build/html/getting_started.html @@ -6,7 +6,7 @@ - Getting Started with Moto — Moto 0.4.10 documentation + Getting Started with Moto — Moto 0.4.10 documentation @@ -17,20 +17,25 @@ VERSION: '0.4.10', COLLAPSE_INDEX: false, FILE_SUFFIX: '.html', - HAS_SOURCE: true + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt' }; - + + + - + + - + +
@@ -42,30 +47,30 @@

Installing Moto

You can use pip to install the latest released version of moto:

-
pip install moto
+
pip install moto
 

If you want to install moto from source:

-
git clone git://github.com/spulec/moto.git
-cd moto
-python setup.py install
+
git clone git://github.com/spulec/moto.git
+cd moto
+python setup.py install
 

Moto usage

For example we have the following code we want to test:

-
import boto
+
import boto
 from boto.s3.key import Key
 
 class MyModel(object):
-    def __init__(self, name, value):
+    def __init__(self, name, value):
         self.name = name
         self.value = value
 
     def save(self):
         conn = boto.connect_s3()
-        bucket = conn.get_bucket('mybucket')
+        bucket = conn.get_bucket('mybucket')
         k = Key(bucket)
         k.key = self.name
         k.set_contents_from_string(self.value)
@@ -75,52 +80,52 @@ python setup.py install
 

Decorator

With a decorator wrapping all the calls to S3 are automatically mocked out.

-
import boto
+
import boto
 from moto import mock_s3
 from mymodule import MyModel
 
 @mock_s3
 def test_my_model_save():
     conn = boto.connect_s3()
-    # We need to create the bucket since this is all in Moto's 'virtual' AWS account
-    conn.create_bucket('mybucket')
+    # We need to create the bucket since this is all in Moto's 'virtual' AWS account
+    conn.create_bucket('mybucket')
 
-    model_instance = MyModel('steve', 'is awesome')
+    model_instance = MyModel('steve', 'is awesome')
     model_instance.save()
 
-    assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
+    assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
 

Context manager

Same as decorator, every call inside with statement are mocked out.

-
def test_my_model_save():
+
def test_my_model_save():
     with mock_s3():
         conn = boto.connect_s3()
-        conn.create_bucket('mybucket')
+        conn.create_bucket('mybucket')
 
-        model_instance = MyModel('steve', 'is awesome')
+        model_instance = MyModel('steve', 'is awesome')
         model_instance.save()
 
-        assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
+        assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
 

Raw

You can also start and stop manually the mocking.

-
def test_my_model_save():
+
def test_my_model_save():
     mock = mock_s3()
     mock.start()
 
     conn = boto.connect_s3()
-    conn.create_bucket('mybucket')
+    conn.create_bucket('mybucket')
 
-    model_instance = MyModel('steve', 'is awesome')
+    model_instance = MyModel('steve', 'is awesome')
     model_instance.save()
 
-    assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
+    assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
 
     mock.stop()
 
@@ -129,8 +134,8 @@ python setup.py install

Stand-alone server mode

Moto comes with a stand-alone server allowing you to mock out an AWS HTTP endpoint. It is very useful to test even if you don’t use Python.

-
$ moto_server ec2 -p3000
- * Running on http://0.0.0.0:3000/
+
$ moto_server ec2 -p3000
+ * Running on http://127.0.0.1:3000/
 

This method isn’t encouraged if you’re using boto, best is to use decorator method.

@@ -169,21 +174,18 @@ python setup.py install

This Page

@@ -194,11 +196,11 @@ python setup.py install ©2015, Steve Pulec. | - Powered by Sphinx 1.3.1 - & Alabaster 0.7.6 + Powered by Sphinx 1.5.3 + & Alabaster 0.7.10 | - Page source
diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html index b05d14c8d..df954b1c5 100644 --- a/docs/_build/html/index.html +++ b/docs/_build/html/index.html @@ -6,7 +6,7 @@ - Moto: A Mock library for boto — Moto 0.4.10 documentation + Moto: A Mock library for boto — Moto 0.4.10 documentation @@ -17,20 +17,25 @@ VERSION: '0.4.10', COLLAPSE_INDEX: false, FILE_SUFFIX: '.html', - HAS_SOURCE: true + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt' }; - + + + - + + - + +
@@ -44,14 +49,14 @@

Getting Started

If you’ve never used moto before, you should read the -Getting Started with Moto guide to get familiar +Getting Started with Moto guide to get familiar with moto & its usage.

Currently implemented Services

diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv index d67caac93..d08c6096b 100644 --- a/docs/_build/html/objects.inv +++ b/docs/_build/html/objects.inv @@ -2,7 +2,6 @@ # Project: Moto # Version: 0.4.10 # The remainder of this file is compressed using zlib. -xm -0yFDOls)TGf5 鳎XԔ뼳 KTL+p^AFVo'z4+= K2}OMľ,\ \ No newline at end of file +xڅK 0 z`x䱶i"5}xJ2;ERMH^T):!agD-pqTKr>+!C]hyĠ: +%Ӯ^? +qaOvuv)JP6p3T]%ބ` k2+95oN2 ^ם \ No newline at end of file diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html index 6fad36fa9..4eb37a5e1 100644 --- a/docs/_build/html/search.html +++ b/docs/_build/html/search.html @@ -6,7 +6,7 @@ - Search — Moto 0.4.10 documentation + Search — Moto 0.4.10 documentation @@ -17,14 +17,16 @@ VERSION: '0.4.10', COLLAPSE_INDEX: false, FILE_SUFFIX: '.html', - HAS_SOURCE: true + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt' }; - + + @@ -32,12 +34,15 @@ + - + + - + +
@@ -87,8 +92,8 @@ ©2015, Steve Pulec. | - Powered by Sphinx 1.3.1 - & Alabaster 0.7.6 + Powered by Sphinx 1.5.3 + & Alabaster 0.7.10
diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js index cadbef528..29c63e805 100644 --- a/docs/_build/html/searchindex.js +++ b/docs/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({envversion:47,filenames:["ec2_tut","getting_started","index"],objects:{},objnames:{},objtypes:{},terms:{"27t05":0,"57z":0,"91dd2f32":0,"__init__":1,"_in_monitoring_el":0,"_placement":0,"_previous_st":0,"_state":0,"class":1,"import":[0,1],"static":0,"true":0,"var":0,access:2,account:1,administr:2,after:0,all:[0,1],allow:[1,2],alreadi:0,also:1,amazonaw:0,ami:[0,2],ami_launch_index:0,analyt:2,applic:2,architectur:0,assert:1,assum:0,attribut:0,auto:2,automat:1,awesom:1,base:2,befor:[0,2],behavior:0,best:1,blank:1,block_device_map:0,bucket:1,call:1,can:1,client_token:0,clone:1,cloud:2,cloudform:2,cloudwatch:2,code:[0,1],com:[0,1],come:1,comput:[0,2],conn:[0,1],connect:0,connect_s3:1,connect_to_region:0,content:2,creat:1,create_bucket:1,databas:2,def:1,deliveri:2,deploy:2,dns_name:0,don:1,download:0,dynamodb:2,easili:2,ebs_optim:0,ec2:[],ec2connect:0,ed65f870:0,elast:2,emr:2,enabl:0,encourag:1,endpoint:1,eni:0,environ:1,even:1,eventsset:0,everi:1,exampl:[0,1],explain:0,f00ba4:0,fals:0,familiar:2,featur:0,follow:[0,1],from:1,full:1,gener:0,get_bucket:1,get_contents_as_str:1,get_kei:1,git:1,github:1,glacier:2,group:[0,2],group_nam:0,guid:2,have:[0,1],how:0,http:1,hypervisor:0,ident:2,image_id:0,index:2,infrastructur:2,insid:1,instanc:[],instance_profil:0,instance_typ:0,interfac:0,intern:0,ip_address:0,isn:1,issu:2,item:0,just:1,keep:1,kei:1,kernel:0,key_nam:0,kinesi:2,latest:1,launch_tim:0,manag:[],manual:1,method:1,mind:1,mobil:2,mock_ec2:0,mock_s3:1,model_inst:1,modul:2,monitor:0,monitoring_st:0,moto_serv:1,mybucket:1,mymodel:1,mymodul:1,name:1,need:1,network:2,networkinterfac:0,never:2,none:0,object:[0,1],out:[1,2],p3000:1,page:2,paravirtu:0,pend:0,persist:0,pip:1,platform:0,previous:0,private_dns_nam:0,private_ip_address:0,product_cod:0,public_dns_nam:0,python:1,ramdisk:0,random:0,read:2,reason:0,redshift:2,region:0,regioninfo:0,releas:1,repositori:2,requester_id:0,reserv:0,root_device_nam:0,root_device_typ:0,route53:2,run:1,run_inst:0,same:[0,1],save:1,scale:2,search:2,secur:2,self:1,set:0,set_contents_from_str:1,setup:1,sever:1,should:2,sinc:1,small:0,snippet:0,sourc:[1,2],sourcedestcheck:0,spot_instance_request_id:0,spulec:1,state_reason:0,statement:1,steve:1,stop:1,storag:2,subnet_id:0,tag:[0,2],test:[1,2],test_my_model_sav:1,than:0,thi:[0,1],tracker:2,tutori:0,usag:[],valu:1,veri:1,version:1,virtual:1,virtualization_typ:0,vpc_id:0,want:1,west:0,wrap:1,x86_64:0,xen:0,you:[0,1,2]},titles:["Use Moto as EC2 backend","Getting Started with Moto","Moto: A Mock library for boto"],titleterms:{addit:2,alon:1,backend:0,boto:2,context:1,current:2,decor:1,ec2:0,get:[1,2],implement:2,indic:2,instal:1,instanc:0,launch:0,librari:2,manag:1,mock:2,mode:1,moto:[0,1,2],raw:1,resourc:2,server:1,servic:2,stand:1,start:[1,2],tabl:2,usag:1}}) \ No newline at end of file +Search.setIndex({docnames:["ec2_tut","getting_started","index"],envversion:50,filenames:["ec2_tut.rst","getting_started.rst","index.rst"],objects:{},objnames:{},objtypes:{},terms:{"27t05":0,"57z":0,"91dd2f32":0,"class":1,"import":[0,1],"static":0,"true":0,"var":0,AWS:[1,2],EBS:2,For:1,RDS:2,SES:2,SNS:2,SQS:2,There:1,With:1,__init__:1,_in_monitoring_el:0,_placement:0,_previous_st:0,_state:0,access:2,account:1,administr:2,after:0,all:[0,1],allow:[1,2],alreadi:0,also:1,amazonaw:0,ami:[0,2],ami_launch_index:0,analyt:2,applic:2,architectur:0,assert:1,assum:0,attribut:0,auto:2,automat:1,awesom:1,base:2,befor:[0,2],behavior:0,best:1,blank:1,block_device_map:0,boto:[0,1],bucket:1,call:1,can:1,client_token:0,clone:1,cloud:2,cloudform:2,cloudwatch:2,code:[0,1],com:[0,1],come:1,comput:[0,2],conn:[0,1],connect:0,connect_s3:1,connect_to_region:0,content:2,creat:1,create_bucket:1,databas:2,def:1,deliveri:2,deploy:2,dns_name:0,don:1,download:0,dynamodb:2,easili:2,ebs_optim:0,ec2:1,ec2connect:0,ed65f870:0,elast:2,emr:2,enabl:0,encourag:1,endpoint:1,eni:0,environ:1,even:1,eventsset:0,everi:1,exampl:[0,1],explain:0,f00ba4:0,fals:0,familiar:2,featur:0,follow:[0,1],from:1,full:1,gener:0,get_bucket:1,get_contents_as_str:1,get_kei:1,git:1,github:1,glacier:2,group:[0,2],group_nam:0,guid:2,have:[0,1],how:0,http:1,hypervisor:0,ident:2,image_id:0,index:2,infrastructur:2,insid:1,instal:0,instanc:2,instance_profil:0,instance_typ:0,interfac:0,intern:0,ip_address:0,isn:1,issu:2,item:0,its:2,just:1,keep:1,kei:1,kernel:0,key_nam:0,kinesi:2,latest:1,launch_tim:0,manag:2,manual:1,method:1,mind:1,mobil:2,mock:[0,1],mock_ec2:0,mock_s3:1,model_inst:1,modul:2,monitor:0,monitoring_st:0,moto_serv:1,mybucket:1,mymodel:1,mymodul:1,name:1,need:1,network:2,networkinterfac:0,never:2,none:0,object:[0,1],out:[1,2],p3000:1,page:2,paravirtu:0,pend:0,persist:0,pip:1,platform:0,previous:0,private_dns_nam:0,private_ip_address:0,product_cod:0,public_dns_nam:0,python:1,ramdisk:0,random:0,read:2,reason:0,redshift:2,region:0,regioninfo:0,releas:1,repositori:2,requester_id:0,reserv:0,root_device_nam:0,root_device_typ:0,route53:2,run:1,run_inst:0,same:[0,1],save:1,scale:2,search:2,secur:2,self:1,set:0,set_contents_from_str:1,setup:1,sever:1,should:2,sinc:1,small:0,snippet:0,sourc:[1,2],sourcedestcheck:0,spot_instance_request_id:0,spulec:1,start:0,state_reason:0,statement:1,steve:1,stop:1,storag:2,subnet_id:0,tag:[0,2],test:[1,2],test_my_model_sav:1,than:0,thi:[0,1],tracker:2,tutori:0,usag:2,use:[0,1],used:2,useful:1,using:1,valu:1,veri:1,version:1,virtual:1,virtualization_typ:0,vpc_id:0,want:1,west:0,wrap:1,x86_64:0,xen:0,you:[0,1,2]},titles:["Use Moto as EC2 backend","Getting Started with Moto","Moto: A Mock library for boto"],titleterms:{Use:0,addit:2,alon:1,backend:0,boto:2,context:1,current:2,decor:1,ec2:0,get:[1,2],implement:2,indic:2,instal:1,instanc:0,launch:0,librari:2,manag:1,mock:2,mode:1,moto:[0,1,2],raw:1,resourc:2,server:1,servic:2,stand:1,start:[1,2],tabl:2,usag:1}}) \ No newline at end of file From 4cf34cc1139cffdf398fa740793dec2fe55e8f2b Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 14 Mar 2017 00:17:56 -0400 Subject: [PATCH 082/274] Add docs for moto apis. --- docs/_build/doctrees/environment.pickle | Bin 8914 -> 10880 bytes docs/_build/doctrees/index.doctree | Bin 18990 -> 30998 bytes docs/_build/html/.buildinfo | 2 +- docs/_build/html/_sources/index.rst.txt | 122 +++--- docs/_build/html/ec2_tut.html | 292 +++++++++---- docs/_build/html/genindex.html | 276 +++++++++---- docs/_build/html/getting_started.html | 319 +++++++++----- docs/_build/html/index.html | 525 +++++++++++++++++------- docs/_build/html/objects.inv | Bin 314 -> 372 bytes docs/_build/html/search.html | 301 ++++++++++---- docs/_build/html/searchindex.js | 2 +- docs/conf.py | 2 +- docs/index.rst | 122 +++--- 13 files changed, 1362 insertions(+), 601 deletions(-) diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle index a1145484936f5a699ba82d8b41e5a96f5f92bd35..9416cbe473a9d06a082f7007b12cca1b9021aeee 100644 GIT binary patch literal 10880 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 z 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 literal 18990 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 Zc`oSQhMQ2{j;p(QNZ!FRZ-Z~o{{X?28JqwB diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo index aa7361e7e..a91999809 100644 --- a/docs/_build/html/.buildinfo +++ b/docs/_build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: c0b40518469bd0810863cee40e68f904 +config: 0f8e40ac9295d621f3784a1c972bdb78 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt index 189ce524a..560ebc661 100644 --- a/docs/_build/html/_sources/index.rst.txt +++ b/docs/_build/html/_sources/index.rst.txt @@ -1,7 +1,7 @@ .. _index: ============================= -Moto: A Mock library for boto +Moto: Mock AWS Services ============================= A library that allows you to easily mock out tests based on @@ -14,58 +14,76 @@ Getting Started If you've never used ``moto`` before, you should read the :doc:`Getting Started with Moto ` guide to get familiar -with ``moto`` & its usage. +with ``moto`` and its usage. -Currently implemented Services ------------------------------- +Currently implemented Services: -* **Compute** ++-----------------------+---------------------+-----------------------------------+ +| Service Name | Decorator | Development Status | ++=======================+=====================+===================================+ +| API Gateway | @mock_apigateway | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Autoscaling | @mock_autoscaling | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Cloudformation | @mock_cloudformation| core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Cloudwatch | @mock_cloudwatch | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Data Pipeline | @mock_datapipeline | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| - DynamoDB | - @mock_dynamodb | - core endpoints done | +| - DynamoDB2 | - @mock_dynamodb2 | - core endpoints + partial indexes| ++-----------------------+---------------------+-----------------------------------+ +| EC2 | @mock_ec2 | core endpoints done | +| - AMI | | core endpoints done | +| - EBS | | core endpoints done | +| - Instances | | all endpoints done | +| - Security Groups | | core endpoints done | +| - Tags | | all endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| ECS | @mock_ecs | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| ELB | @mock_elb | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| EMR | @mock_emr | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Glacier | @mock_glacier | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| IAM | @mock_iam | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Lambda | @mock_lambda | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Kinesis | @mock_kinesis | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| KMS | @mock_kms | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| RDS | @mock_rds | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| RDS2 | @mock_rds2 | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Redshift | @mock_redshift | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Route53 | @mock_route53 | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| S3 | @mock_s3 | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SES | @mock_ses | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SNS | @mock_sns | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SQS | @mock_sqs | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| STS | @mock_sts | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SWF | @mock_sfw | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ - * :doc:`Elastic Compute Cloud ` - * AMI - * EBS - * Instances - * Security groups - * Tags - * Auto Scaling -* **Storage and content delivery** +Moto APIs +--------- +some stuff - * S3 - * Glacier -* **Database** - - * RDS - * DynamoDB - * Redshift - -* **Networking** - - * Route53 - -* **Administration and security** - - * Identity & access management - * CloudWatch - -* **Deployment and management** - - * CloudFormation - -* **Analytics** - - * Kinesis - * EMR - -* **Application service** - - * SQS - * SES - -* **Mobile services** - - * SNS Additional Resources -------------------- @@ -76,16 +94,12 @@ Additional Resources .. _Moto Issue Tracker: https://github.com/spulec/moto/issues .. _Moto Source Repository: https://github.com/spulec/moto -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - .. toctree:: :maxdepth: 2 :hidden: :glob: + index getting_started + other_langs + moto_apis diff --git a/docs/_build/html/ec2_tut.html b/docs/_build/html/ec2_tut.html index dd467cbbc..ff222733f 100644 --- a/docs/_build/html/ec2_tut.html +++ b/docs/_build/html/ec2_tut.html @@ -1,45 +1,157 @@ - - - - + + + + + + + + + Use Moto as EC2 backend — Moto 0.4.10 documentation + + + + + + + + + + + - Use Moto as EC2 backend — Moto 0.4.10 documentation - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - +
- - - + + + +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + +
+ + + + +
+
+
+

Use Moto as EC2 backend

@@ -115,60 +227,66 @@ Before all code examples the following snippet is launched:

+
+
+ +
+ +
- -
-
- - + - - +
+ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html index 8f9a214bb..1a7b34098 100644 --- a/docs/_build/html/genindex.html +++ b/docs/_build/html/genindex.html @@ -1,46 +1,159 @@ - - - - - + + + + + + + + + + Index — Moto 0.4.10 documentation + + + + + + + + + + + - Index — Moto 0.4.10 documentation - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - +
- - - + + + +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + +
+ +
    + +
  • Docs »
  • + +
  • Index
  • + + +
  • + + + +
  • + +
+ + +
+
+
+

Index

@@ -50,45 +163,66 @@
+
+
+ +
+ +
- -
-
- +
+ - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/getting_started.html b/docs/_build/html/getting_started.html index ee63ee56c..174ec6c63 100644 --- a/docs/_build/html/getting_started.html +++ b/docs/_build/html/getting_started.html @@ -1,46 +1,168 @@ - - - - + + + + + + + + + Getting Started with Moto — Moto 0.4.10 documentation + + + + + + + + + + + - Getting Started with Moto — Moto 0.4.10 documentation - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - +
- - - + + + +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + +
+ + + + +
+
+
+

Getting Started with Moto

@@ -144,68 +266,73 @@
+
+
+ +
+ +
- -
-
- - + - - +
+ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html index df954b1c5..cab7668b9 100644 --- a/docs/_build/html/index.html +++ b/docs/_build/html/index.html @@ -1,109 +1,329 @@ - - - - + + + + + + + + + Moto: Mock AWS Services — Moto 0.4.10 documentation + + + + + + + + + + + - Moto: A Mock library for boto — Moto 0.4.10 documentation - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - +
- - - + + + +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+

Moto: Mock AWS Services

A library that allows you to easily mock out tests based on AWS infrastructure.

Getting Started

If you’ve never used moto before, you should read the Getting Started with Moto guide to get familiar -with moto & its usage.

-
-
-

Currently implemented Services

-
    -
  • Compute
      -
    • Elastic Compute Cloud
    • +with moto and its usage.

      +

      Currently implemented Services:

      + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Service NameDecoratorDevelopment Status
      API Gateway@mock_apigatewaycore endpoints done
      Autoscaling@mock_autoscalingcore endpoints done
      Cloudformation@mock_cloudformationcore endpoints done
      Cloudwatch@mock_cloudwatchbasic endpoints done
      Data Pipeline@mock_datapipelinebasic endpoints done
        +
      • DynamoDB
      • +
      • DynamoDB2
      • +
      +
        +
      • @mock_dynamodb
      • +
      • @mock_dynamodb2
      • +
      +
        +
      • core endpoints done
      • +
      • core endpoints + partial indexes
      • +
      +
      +
      EC2
      +
      • AMI
      • EBS
      • Instances
      • -
      • Security groups
      • +
      • Security Groups
      • Tags
      • -
      • Auto Scaling
      • -
      - -
    • Storage and content delivery
        -
      • S3
      • -
      • Glacier
      • -
      -
    • -
    • Database
        -
      • RDS
      • -
      • DynamoDB
      • -
      • Redshift
      • -
      -
    • -
    • Networking
        -
      • Route53
      • -
      -
    • -
    • Administration and security
        -
      • Identity & access management
      • -
      • CloudWatch
      • -
      -
    • -
    • Deployment and management
        -
      • CloudFormation
      • -
      -
    • -
    • Analytics
        -
      • Kinesis
      • -
      • EMR
      • -
      -
    • -
    • Application service
        -
      • SQS
      • -
      • SES
      • -
      -
    • -
    • Mobile services
        -
      • SNS
      • -
      -
    • +
      +
      +
      @mock_ec2core endpoints done +core endpoints done +core endpoints done +all endpoints done +core endpoints done +all endpoints done
      ECS@mock_ecsbasic endpoints done
      ELB@mock_elbcore endpoints done
      EMR@mock_emrcore endpoints done
      Glacier@mock_glaciercore endpoints done
      IAM@mock_iamcore endpoints done
      Lambda@mock_lambdabasic endpoints done
      Kinesis@mock_kinesiscore endpoints done
      KMS@mock_kmsbasic endpoints done
      RDS@mock_rdscore endpoints done
      RDS2@mock_rds2core endpoints done
      Redshift@mock_redshiftcore endpoints done
      Route53@mock_route53core endpoints done
      S3@mock_s3core endpoints done
      SES@mock_sescore endpoints done
      SNS@mock_snscore endpoints done
      SQS@mock_sqscore endpoints done
      STS@mock_stscore endpoints done
      SWF@mock_sfwbasic endpoints done
      +
+
+

Moto APIs

+

some stuff

Additional Resources

@@ -111,80 +331,79 @@ with moto & i
  • Moto Source Repository
  • Moto Issue Tracker
  • -
    -

    Indices and tables

    -
    -
    +
    +
    + +
    + +
    - -
    -
    - - + - - +
    + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv index d08c6096b30217d29a974e1a365c719d4c967ea2..157697e1afd26af9b70476a677479fd9a0902829 100644 GIT binary patch delta 260 zcmV+f0sH>C0`vlqcz<<|OKZb05QOjk6$|cFh31%BD5d038Zf0tqezP_B1^$q_wny5 zS$@cBauL$(d^0N)yP5_9M{+N%0QLkJrN@>!4Id8S&5{M~;N^J-XD%*iWPu5%`5ARi zmj1zl<6c68Mj44e+lX0>?Zt7_@pZs z_Wl$XIDA%6VgIEBy5h!?D^)(+l}mSA#C17PO zpm<6$JKyXCMU+xa;7G>O2ne@;UfSeTDY!a>vmpb_!C@Z(XQD(c1DtS*aa2ok)jq<` z@hc%iU1*8VdBmXl?WgYAI*AIMrH+^-l+&(@k*AJcgrxUB3PZ}@6@qbL&rf!Bh8TA3 zDN0ZUx6C$hGZy%q{8Sy;)%p)z8TlpP-h{(os32>F{jVJ|EAlzvHE&M*{K7H}UZB^V Es81|pv;Y7A diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html index 4eb37a5e1..311903b7e 100644 --- a/docs/_build/html/search.html +++ b/docs/_build/html/search.html @@ -1,104 +1,239 @@ - - - - - - Search — Moto 0.4.10 documentation - - - - - - - - - - - - + + + + + - + + Search — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - +
    + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + +
      + +
    • Docs »
    • + +
    • Search
    • + + +
    • + +
    • + +
    + + +
    +
    +
    +
    + + +
    +
    +
    + +
    + +
    - -
    -
    - - + - - +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js index 29c63e805..f66b2d15b 100644 --- a/docs/_build/html/searchindex.js +++ b/docs/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["ec2_tut","getting_started","index"],envversion:50,filenames:["ec2_tut.rst","getting_started.rst","index.rst"],objects:{},objnames:{},objtypes:{},terms:{"27t05":0,"57z":0,"91dd2f32":0,"class":1,"import":[0,1],"static":0,"true":0,"var":0,AWS:[1,2],EBS:2,For:1,RDS:2,SES:2,SNS:2,SQS:2,There:1,With:1,__init__:1,_in_monitoring_el:0,_placement:0,_previous_st:0,_state:0,access:2,account:1,administr:2,after:0,all:[0,1],allow:[1,2],alreadi:0,also:1,amazonaw:0,ami:[0,2],ami_launch_index:0,analyt:2,applic:2,architectur:0,assert:1,assum:0,attribut:0,auto:2,automat:1,awesom:1,base:2,befor:[0,2],behavior:0,best:1,blank:1,block_device_map:0,boto:[0,1],bucket:1,call:1,can:1,client_token:0,clone:1,cloud:2,cloudform:2,cloudwatch:2,code:[0,1],com:[0,1],come:1,comput:[0,2],conn:[0,1],connect:0,connect_s3:1,connect_to_region:0,content:2,creat:1,create_bucket:1,databas:2,def:1,deliveri:2,deploy:2,dns_name:0,don:1,download:0,dynamodb:2,easili:2,ebs_optim:0,ec2:1,ec2connect:0,ed65f870:0,elast:2,emr:2,enabl:0,encourag:1,endpoint:1,eni:0,environ:1,even:1,eventsset:0,everi:1,exampl:[0,1],explain:0,f00ba4:0,fals:0,familiar:2,featur:0,follow:[0,1],from:1,full:1,gener:0,get_bucket:1,get_contents_as_str:1,get_kei:1,git:1,github:1,glacier:2,group:[0,2],group_nam:0,guid:2,have:[0,1],how:0,http:1,hypervisor:0,ident:2,image_id:0,index:2,infrastructur:2,insid:1,instal:0,instanc:2,instance_profil:0,instance_typ:0,interfac:0,intern:0,ip_address:0,isn:1,issu:2,item:0,its:2,just:1,keep:1,kei:1,kernel:0,key_nam:0,kinesi:2,latest:1,launch_tim:0,manag:2,manual:1,method:1,mind:1,mobil:2,mock:[0,1],mock_ec2:0,mock_s3:1,model_inst:1,modul:2,monitor:0,monitoring_st:0,moto_serv:1,mybucket:1,mymodel:1,mymodul:1,name:1,need:1,network:2,networkinterfac:0,never:2,none:0,object:[0,1],out:[1,2],p3000:1,page:2,paravirtu:0,pend:0,persist:0,pip:1,platform:0,previous:0,private_dns_nam:0,private_ip_address:0,product_cod:0,public_dns_nam:0,python:1,ramdisk:0,random:0,read:2,reason:0,redshift:2,region:0,regioninfo:0,releas:1,repositori:2,requester_id:0,reserv:0,root_device_nam:0,root_device_typ:0,route53:2,run:1,run_inst:0,same:[0,1],save:1,scale:2,search:2,secur:2,self:1,set:0,set_contents_from_str:1,setup:1,sever:1,should:2,sinc:1,small:0,snippet:0,sourc:[1,2],sourcedestcheck:0,spot_instance_request_id:0,spulec:1,start:0,state_reason:0,statement:1,steve:1,stop:1,storag:2,subnet_id:0,tag:[0,2],test:[1,2],test_my_model_sav:1,than:0,thi:[0,1],tracker:2,tutori:0,usag:2,use:[0,1],used:2,useful:1,using:1,valu:1,veri:1,version:1,virtual:1,virtualization_typ:0,vpc_id:0,want:1,west:0,wrap:1,x86_64:0,xen:0,you:[0,1,2]},titles:["Use Moto as EC2 backend","Getting Started with Moto","Moto: A Mock library for boto"],titleterms:{Use:0,addit:2,alon:1,backend:0,boto:2,context:1,current:2,decor:1,ec2:0,get:[1,2],implement:2,indic:2,instal:1,instanc:0,launch:0,librari:2,manag:1,mock:2,mode:1,moto:[0,1,2],raw:1,resourc:2,server:1,servic:2,stand:1,start:[1,2],tabl:2,usag:1}}) \ No newline at end of file +Search.setIndex({docnames:["ec2_tut","getting_started","index","moto_apis","other_langs"],envversion:50,filenames:["ec2_tut.rst","getting_started.rst","index.rst","moto_apis.rst","other_langs.rst"],objects:{},objnames:{},objtypes:{},terms:{"27t05":0,"57z":0,"91dd2f32":0,"class":1,"import":[0,1],"static":0,"true":0,"var":0,AWS:1,EBS:2,ECS:2,For:1,KMS:2,RDS:2,SES:2,SNS:2,SQS:2,STS:2,There:1,With:1,__init__:1,_in_monitoring_el:0,_placement:0,_previous_st:0,_state:0,access:[],account:1,administr:[],after:0,all:[0,1,2,3],allow:[1,2],alreadi:0,also:1,amazonaw:[0,3],ami:[0,2],ami_launch_index:0,analyt:[],ani:4,anoth:4,api:[],applic:[],architectur:0,assert:1,assum:0,attribut:0,auto:[],automat:1,autosc:2,awesom:1,backend:3,base:2,basic:2,befor:[0,2],behavior:0,best:1,blank:1,block:[],block_device_map:0,bodi:[],boto:[0,1],bucket:1,call:1,can:[1,4],cell:[],chang:3,client_token:0,clone:1,cloud:[],cloudform:2,cloudwatch:2,code:[0,1],column:[],com:[0,1,3],come:[1,3],comput:0,conn:[0,1],connect:0,connect_s3:1,connect_to_region:0,contain:[],content:[],core:2,creat:1,create_bucket:1,current:[2,3],data:2,databas:[],decor:2,def:1,deliveri:[],deploy:[],develop:2,dns_name:0,don:[1,4],done:2,download:0,dynamodb2:2,dynamodb:2,easili:2,ebs_optim:0,ec2:[1,2],ec2connect:0,ed65f870:0,elast:[],elb:2,emr:2,enabl:0,encourag:1,endpoint:[1,2],eni:0,environ:1,even:1,eventsset:0,everi:1,exampl:[0,1,4],explain:0,f00ba4:0,fals:0,familiar:2,featur:0,follow:[0,1],from:1,full:1,gatewai:2,gener:0,get_bucket:1,get_contents_as_str:1,get_kei:1,git:1,github:1,glacier:2,grid:[],group:[0,2],group_nam:0,guid:2,have:[0,1],header:[],here:4,how:0,http:[1,3],hypervisor:0,iam:2,ident:[],image_id:0,implement:2,index:2,infrastructur:2,insid:1,instal:0,instanc:2,instance_profil:0,instance_typ:0,interfac:0,intern:[0,3],ip_address:0,isn:1,issu:2,item:0,its:2,java:4,javascript:4,just:1,keep:1,kei:1,kernel:0,key_nam:0,kinesi:2,lambda:2,languag:[],latest:1,launch_tim:0,librari:2,localhost:3,mai:[],manag:[],manual:1,method:1,mind:1,mobil:[],mock:[0,1],mock_apigatewai:2,mock_autosc:2,mock_cloudform:2,mock_cloudwatch:2,mock_datapipelin:2,mock_dynamodb2:2,mock_dynamodb:2,mock_ec2:[0,2],mock_ec:2,mock_elb:2,mock_emr:2,mock_glaci:2,mock_iam:2,mock_kinesi:2,mock_km:2,mock_lambda:2,mock_rd:2,mock_rds2:2,mock_redshift:2,mock_route53:2,mock_s3:[1,2],mock_s:2,mock_sfw:2,mock_sn:2,mock_sq:2,mock_st:2,model_inst:1,modul:[],monitor:0,monitoring_st:0,moto:4,moto_serv:[1,4],motoapi:3,mybucket:1,mymodel:1,mymodul:1,name:[1,2],need:[1,4],network:[],networkinterfac:0,never:2,none:0,object:[0,1],other:[],out:[1,2],p3000:1,page:[],paravirtu:0,partial:2,pend:0,persist:0,pip:1,pipelin:2,platform:0,post:3,previous:0,private_dns_nam:0,private_ip_address:0,product_cod:0,provid:3,public_dns_nam:0,python:[1,4],ramdisk:0,random:0,rds2:2,read:2,reason:0,redshift:2,region:0,regioninfo:0,releas:1,repositori:2,request:3,requester_id:0,reserv:0,root_device_nam:0,root_device_typ:0,route53:2,row:[],rubi:4,run:[1,4],run_inst:0,same:[0,1],save:1,scale:[],search:[],secur:2,self:1,send:3,set:0,set_contents_from_str:1,setup:1,sever:1,should:2,sinc:1,small:0,snippet:0,some:[2,3,4],sourc:[1,2],sourcedestcheck:0,span:[],spot_instance_request_id:0,spulec:1,start:0,state:3,state_reason:0,statement:1,statu:2,steve:1,stop:1,storag:[],stuff:2,subnet_id:0,swf:2,system:3,tabl:[],tag:[0,2],test:[1,2],test_my_model_sav:1,than:0,thi:[0,1,3],tracker:2,tutori:0,usag:2,use:[0,1,4],used:[2,4],useful:1,using:1,valu:1,veri:1,version:1,view:3,virtual:1,virtualization_typ:0,vpc_id:0,want:1,west:0,wrap:1,x86_64:0,xen:0,you:[0,1,2,4]},titles:["Use Moto as EC2 backend","Getting Started with Moto","Moto: Mock AWS Services","Moto APIs","Other languages"],titleterms:{AWS:2,Use:0,addit:2,alon:1,ani:[],anoth:[],api:[2,3],backend:0,boto:[],can:[],context:1,current:[],dashboard:3,decor:1,don:[],ec2:0,exampl:[],get:[1,2],here:[],implement:[],indic:[],instal:1,instanc:0,languag:4,launch:0,librari:[],manag:1,mock:2,mode:1,moto:[0,1,2,3],moto_serv:[],need:[],other:4,python:[],raw:1,reset:3,resourc:2,run:[],server:1,servic:2,some:[],stand:1,start:[1,2],tabl:[],usag:1,use:[],used:[],you:[]}}) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 5b141a759..28a4b4e6b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -109,7 +109,7 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/index.rst b/docs/index.rst index 189ce524a..560ebc661 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ .. _index: ============================= -Moto: A Mock library for boto +Moto: Mock AWS Services ============================= A library that allows you to easily mock out tests based on @@ -14,58 +14,76 @@ Getting Started If you've never used ``moto`` before, you should read the :doc:`Getting Started with Moto ` guide to get familiar -with ``moto`` & its usage. +with ``moto`` and its usage. -Currently implemented Services ------------------------------- +Currently implemented Services: -* **Compute** ++-----------------------+---------------------+-----------------------------------+ +| Service Name | Decorator | Development Status | ++=======================+=====================+===================================+ +| API Gateway | @mock_apigateway | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Autoscaling | @mock_autoscaling | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Cloudformation | @mock_cloudformation| core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Cloudwatch | @mock_cloudwatch | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Data Pipeline | @mock_datapipeline | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| - DynamoDB | - @mock_dynamodb | - core endpoints done | +| - DynamoDB2 | - @mock_dynamodb2 | - core endpoints + partial indexes| ++-----------------------+---------------------+-----------------------------------+ +| EC2 | @mock_ec2 | core endpoints done | +| - AMI | | core endpoints done | +| - EBS | | core endpoints done | +| - Instances | | all endpoints done | +| - Security Groups | | core endpoints done | +| - Tags | | all endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| ECS | @mock_ecs | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| ELB | @mock_elb | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| EMR | @mock_emr | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Glacier | @mock_glacier | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| IAM | @mock_iam | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Lambda | @mock_lambda | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Kinesis | @mock_kinesis | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| KMS | @mock_kms | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| RDS | @mock_rds | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| RDS2 | @mock_rds2 | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Redshift | @mock_redshift | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| Route53 | @mock_route53 | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| S3 | @mock_s3 | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SES | @mock_ses | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SNS | @mock_sns | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SQS | @mock_sqs | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| STS | @mock_sts | core endpoints done | ++-----------------------+---------------------+-----------------------------------+ +| SWF | @mock_sfw | basic endpoints done | ++-----------------------+---------------------+-----------------------------------+ - * :doc:`Elastic Compute Cloud ` - * AMI - * EBS - * Instances - * Security groups - * Tags - * Auto Scaling -* **Storage and content delivery** +Moto APIs +--------- +some stuff - * S3 - * Glacier -* **Database** - - * RDS - * DynamoDB - * Redshift - -* **Networking** - - * Route53 - -* **Administration and security** - - * Identity & access management - * CloudWatch - -* **Deployment and management** - - * CloudFormation - -* **Analytics** - - * Kinesis - * EMR - -* **Application service** - - * SQS - * SES - -* **Mobile services** - - * SNS Additional Resources -------------------- @@ -76,16 +94,12 @@ Additional Resources .. _Moto Issue Tracker: https://github.com/spulec/moto/issues .. _Moto Source Repository: https://github.com/spulec/moto -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - .. toctree:: :maxdepth: 2 :hidden: :glob: + index getting_started + other_langs + moto_apis From b81e427b996a49790492b5e377ce3e6b3dfa406f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 14 Mar 2017 00:27:48 -0400 Subject: [PATCH 083/274] Add moto apis. --- docs/_build/doctrees/moto_apis.doctree | Bin 0 -> 4722 bytes docs/_build/doctrees/other_langs.doctree | Bin 0 -> 5298 bytes docs/_build/html/_sources/moto_apis.rst.txt | 21 + docs/_build/html/_sources/other_langs.rst.txt | 15 + docs/_build/html/_static/classic.css | 261 +++++++ docs/_build/html/_static/css/badge_only.css | 2 + docs/_build/html/_static/css/theme.css | 5 + docs/_build/html/_static/default.css | 1 + .../html/_static/fonts/Inconsolata-Bold.ttf | Bin 0 -> 109948 bytes .../_static/fonts/Inconsolata-Regular.ttf | Bin 0 -> 96964 bytes docs/_build/html/_static/fonts/Lato-Bold.ttf | Bin 0 -> 656544 bytes .../html/_static/fonts/Lato-Regular.ttf | Bin 0 -> 656568 bytes .../html/_static/fonts/RobotoSlab-Bold.ttf | Bin 0 -> 170616 bytes .../html/_static/fonts/RobotoSlab-Regular.ttf | Bin 0 -> 169064 bytes .../_static/fonts/fontawesome-webfont.eot | Bin 0 -> 76518 bytes .../_static/fonts/fontawesome-webfont.svg | 685 ++++++++++++++++++ .../_static/fonts/fontawesome-webfont.ttf | Bin 0 -> 152796 bytes .../_static/fonts/fontawesome-webfont.woff | Bin 0 -> 90412 bytes docs/_build/html/_static/js/modernizr.min.js | 4 + docs/_build/html/_static/js/theme.js | 169 +++++ docs/_build/html/_static/sidebar.js | 159 ++++ docs/_build/html/moto_apis.html | 252 +++++++ docs/_build/html/other_langs.html | 242 +++++++ docs/moto_apis.rst | 21 + docs/other_langs.rst | 15 + 25 files changed, 1852 insertions(+) create mode 100644 docs/_build/doctrees/moto_apis.doctree create mode 100644 docs/_build/doctrees/other_langs.doctree create mode 100644 docs/_build/html/_sources/moto_apis.rst.txt create mode 100644 docs/_build/html/_sources/other_langs.rst.txt create mode 100644 docs/_build/html/_static/classic.css create mode 100644 docs/_build/html/_static/css/badge_only.css create mode 100644 docs/_build/html/_static/css/theme.css create mode 100644 docs/_build/html/_static/default.css create mode 100644 docs/_build/html/_static/fonts/Inconsolata-Bold.ttf create mode 100644 docs/_build/html/_static/fonts/Inconsolata-Regular.ttf create mode 100644 docs/_build/html/_static/fonts/Lato-Bold.ttf create mode 100644 docs/_build/html/_static/fonts/Lato-Regular.ttf create mode 100644 docs/_build/html/_static/fonts/RobotoSlab-Bold.ttf create mode 100644 docs/_build/html/_static/fonts/RobotoSlab-Regular.ttf create mode 100644 docs/_build/html/_static/fonts/fontawesome-webfont.eot create mode 100644 docs/_build/html/_static/fonts/fontawesome-webfont.svg create mode 100644 docs/_build/html/_static/fonts/fontawesome-webfont.ttf create mode 100644 docs/_build/html/_static/fonts/fontawesome-webfont.woff create mode 100644 docs/_build/html/_static/js/modernizr.min.js create mode 100644 docs/_build/html/_static/js/theme.js create mode 100644 docs/_build/html/_static/sidebar.js create mode 100644 docs/_build/html/moto_apis.html create mode 100644 docs/_build/html/other_langs.html create mode 100644 docs/moto_apis.rst create mode 100644 docs/other_langs.rst diff --git a/docs/_build/doctrees/moto_apis.doctree b/docs/_build/doctrees/moto_apis.doctree new file mode 100644 index 0000000000000000000000000000000000000000..d53085fe2bf368362bc6af3328eeb0f9673c8d4a GIT binary patch literal 4722 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 E56G!o`v3p{ literal 0 HcmV?d00001 diff --git a/docs/_build/html/_sources/moto_apis.rst.txt b/docs/_build/html/_sources/moto_apis.rst.txt new file mode 100644 index 000000000..3414cba1a --- /dev/null +++ b/docs/_build/html/_sources/moto_apis.rst.txt @@ -0,0 +1,21 @@ +.. _moto_apis: + +========= +Moto APIs +========= + +Moto provides some internal APIs to view and change the state of the backends. + +Reset API +--------- + +This API resets the state of all of the backends. Send an HTTP POST to reset:: + + requests.post("http://motoapi.amazonaws.com/moto-api/reset") + +Dashboard +--------- + +Moto comes with a dashboard to view the current state of the system:: + + http://localhost:5000/moto-api/ diff --git a/docs/_build/html/_sources/other_langs.rst.txt b/docs/_build/html/_sources/other_langs.rst.txt new file mode 100644 index 000000000..6fb617c39 --- /dev/null +++ b/docs/_build/html/_sources/other_langs.rst.txt @@ -0,0 +1,15 @@ +.. _other_langs: + +=============== +Other languages +=============== + +You don't need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server and here are some examples in other languages. + +* `Java`_ +* `Ruby`_ +* `Javascript`_ + +.. _Java: https://github.com/spulec/moto/blob/master/other_langs/sqsSample.java +.. _Ruby: https://github.com/spulec/moto/blob/master/other_langs/test.rb +.. _Javascript: https://github.com/spulec/moto/blob/master/other_langs/test.js diff --git a/docs/_build/html/_static/classic.css b/docs/_build/html/_static/classic.css new file mode 100644 index 000000000..20db95e22 --- /dev/null +++ b/docs/_build/html/_static/classic.css @@ -0,0 +1,261 @@ +/* + * classic.css_t + * ~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- classic theme. + * + * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: sans-serif; + font-size: 100%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + background-color: #1c4e63; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: #ffffff; + color: #000000; + padding: 0 20px 30px 20px; +} + +div.footer { + color: #ffffff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #ffffff; + text-decoration: underline; +} + +div.related { + background-color: #133f52; + line-height: 30px; + color: #ffffff; +} + +div.related a { + color: #ffffff; +} + +div.sphinxsidebar { +} + +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: #ffffff; +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: #ffffff; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: #ffffff; +} + +div.sphinxsidebar a { + color: #98dbcc; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + + + +/* -- hyperlink styles ------------------------------------------------------ */ + +a { + color: #355f7c; + text-decoration: none; +} + +a:visited { + color: #355f7c; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + + + +/* -- body styles ----------------------------------------------------------- */ + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color: #f2f2f2; + font-weight: normal; + color: #20435c; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + text-align: justify; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: #eeffcc; + color: #333333; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +code { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +th { + background-color: #ede; +} + +.warning code { + background: #efc2c2; +} + +.note code { + background: #d6d6d6; +} + +.viewcode-back { + font-family: sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +div.code-block-caption { + color: #efefef; + background-color: #1c4e63; +} \ No newline at end of file diff --git a/docs/_build/html/_static/css/badge_only.css b/docs/_build/html/_static/css/badge_only.css new file mode 100644 index 000000000..6362912b1 --- /dev/null +++ b/docs/_build/html/_static/css/badge_only.css @@ -0,0 +1,2 @@ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} +/*# sourceMappingURL=badge_only.css.map */ diff --git a/docs/_build/html/_static/css/theme.css b/docs/_build/html/_static/css/theme.css new file mode 100644 index 000000000..c1631d84c --- /dev/null +++ b/docs/_build/html/_static/css/theme.css @@ -0,0 +1,5 @@ +*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:0.2em 0;background:#ccc;color:#000;padding:0.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.6.3");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.6.3") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.6.3") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.6.3") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:0.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:0.5;-webkit-transition:opacity 0.05s ease-in;-moz-transition:opacity 0.05s ease-in;transition:opacity 0.05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a{color:#2980B9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all 0.3s ease-in;-moz-transition:all 0.3s ease-in;transition:all 0.3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27AE60}.wy-tray-container li.wy-tray-item-info{background:#2980B9}.wy-tray-container li.wy-tray-item-warning{background:#E67E22}.wy-tray-container li.wy-tray-item-danger{background:#E74C3C}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width: 768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all 0.1s linear;-moz-transition:all 0.1s linear;transition:all 0.1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980B9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27AE60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#E74C3C !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#E67E22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980B9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9B59B6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980B9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980B9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#E74C3C}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.35765%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:0.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}input[type="datetime-local"]{padding:.34375em .625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#E74C3C}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{width:36px;height:12px;margin:12px 0;position:relative;border-radius:4px;background:#ccc;cursor:pointer;-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.wy-switch:before{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.wy-switch:after{content:"false";position:absolute;left:48px;display:block;font-size:12px;color:#ccc}.wy-switch.active{background:#1e8449}.wy-switch.active:before{left:24px;background:#27AE60}.wy-switch.active:after{content:"true"}.wy-switch.disabled,.wy-switch.active.disabled{cursor:not-allowed}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#E74C3C}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #E74C3C}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27AE60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#E74C3C}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#E67E22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980B9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:0.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0.3em;display:block}.wy-form label{margin-bottom:0.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:0.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980B9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9B59B6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#E67E22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980B9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27AE60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#E74C3C !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.codeblock-example{border:1px solid #e1e4e5;border-bottom:none;padding:24px;padding-top:48px;font-weight:500;background:#fff;position:relative}.codeblock-example:after{content:"Example";position:absolute;top:0px;left:0px;background:#9B59B6;color:#fff;padding:6px 12px}.codeblock-example.prettyprint-example-only{border:1px solid #e1e4e5;margin-bottom:24px}.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight']{border:1px solid #e1e4e5;padding:0px;overflow-x:auto;background:#fff;margin:1px 0 24px 0}.codeblock div[class^='highlight'],pre.literal-block div[class^='highlight'],.rst-content .literal-block div[class^='highlight'],div[class^='highlight'] div[class^='highlight']{border:none;background:none;margin:0}div[class^='highlight'] td.code{width:100%}.linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:12px;line-height:1.5;color:#d9d9d9}div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:12px;line-height:1.5;display:block;overflow:auto;color:#404040}@media print{.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight'],div[class^='highlight'] pre{white-space:pre-wrap}}.hll{background-color:#ffc;margin:0 -12px;padding:0 12px;display:block}.c{color:#998;font-style:italic}.err{color:#a61717;background-color:#e3d2d2}.k{font-weight:bold}.o{font-weight:bold}.cm{color:#998;font-style:italic}.cp{color:#999;font-weight:bold}.c1{color:#998;font-style:italic}.cs{color:#999;font-weight:bold;font-style:italic}.gd{color:#000;background-color:#fdd}.gd .x{color:#000;background-color:#faa}.ge{font-style:italic}.gr{color:#a00}.gh{color:#999}.gi{color:#000;background-color:#dfd}.gi .x{color:#000;background-color:#afa}.go{color:#888}.gp{color:#555}.gs{font-weight:bold}.gu{color:purple;font-weight:bold}.gt{color:#a00}.kc{font-weight:bold}.kd{font-weight:bold}.kn{font-weight:bold}.kp{font-weight:bold}.kr{font-weight:bold}.kt{color:#458;font-weight:bold}.m{color:#099}.s{color:#d14}.n{color:#333}.na{color:teal}.nb{color:#0086b3}.nc{color:#458;font-weight:bold}.no{color:teal}.ni{color:purple}.ne{color:#900;font-weight:bold}.nf{color:#900;font-weight:bold}.nn{color:#555}.nt{color:navy}.nv{color:teal}.ow{font-weight:bold}.w{color:#bbb}.mf{color:#099}.mh{color:#099}.mi{color:#099}.mo{color:#099}.sb{color:#d14}.sc{color:#d14}.sd{color:#d14}.s2{color:#d14}.se{color:#d14}.sh{color:#d14}.si{color:#d14}.sx{color:#d14}.sr{color:#009926}.s1{color:#d14}.ss{color:#990073}.bp{color:#999}.vc{color:teal}.vg{color:teal}.vi{color:teal}.il{color:#099}.gc{color:#999;background-color:#EAF2F5}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin-bottom:0;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;color:#555;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:0.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:0.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:0.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em;border-top:none;border-bottom:none}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:0.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical .local-toc li ul{display:block}.wy-menu-vertical li ul li a{margin-bottom:0;color:#b3b3b3;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#b3b3b3}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#b3b3b3}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980B9;text-align:center;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:0.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.3)}.wy-nav .wy-menu-vertical header{color:#2980B9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980B9;color:#fff}[data-menu-wrap]{-webkit-transition:all 0.2s ease-in;-moz-transition:all 0.2s ease-in;transition:all 0.2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:left repeat-y #fcfcfc;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);background-size:300px 1px}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980B9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:#999}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:#999}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1400px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content img{max-width:100%;height:auto !important}.rst-content .highlight>pre{line-height:normal}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .line-block{margin-left:24px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto;display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink{display:none;visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after{visibility:visible;content:"";font-family:FontAwesome;display:inline-block}.rst-content h1:hover .headerlink,.rst-content h2:hover .headerlink,.rst-content .toctree-wrapper p.caption:hover .headerlink,.rst-content h3:hover .headerlink,.rst-content h4:hover .headerlink,.rst-content h5:hover .headerlink,.rst-content h6:hover .headerlink,.rst-content dl dt:hover .headerlink,.rst-content p.caption:hover .headerlink{display:inline-block}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:super;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:#999}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none;padding-top:5px}.rst-content table.field-list td>strong{display:inline-block;margin-top:3px}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left;padding-left:0}.rst-content tt,.rst-content tt,.rst-content code{color:#000;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:inline-block;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040}.math{text-align:center}@font-face{font-family:"Inconsolata";font-style:normal;font-weight:400;src:local("Inconsolata"),local("Inconsolata-Regular"),url(../fonts/Inconsolata-Regular.ttf) format("truetype")}@font-face{font-family:"Inconsolata";font-style:normal;font-weight:700;src:local("Inconsolata Bold"),local("Inconsolata-Bold"),url(../fonts/Inconsolata-Bold.ttf) format("truetype")}@font-face{font-family:"Lato";font-style:normal;font-weight:400;src:local("Lato Regular"),local("Lato-Regular"),url(../fonts/Lato-Regular.ttf) format("truetype")}@font-face{font-family:"Lato";font-style:normal;font-weight:700;src:local("Lato Bold"),local("Lato-Bold"),url(../fonts/Lato-Bold.ttf) format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:local("Roboto Slab Regular"),local("RobotoSlab-Regular"),url(../fonts/RobotoSlab-Regular.ttf) format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:local("Roboto Slab Bold"),local("RobotoSlab-Bold"),url(../fonts/RobotoSlab-Bold.ttf) format("truetype")} +/*# sourceMappingURL=theme.css.map */ diff --git a/docs/_build/html/_static/default.css b/docs/_build/html/_static/default.css new file mode 100644 index 000000000..81b936363 --- /dev/null +++ b/docs/_build/html/_static/default.css @@ -0,0 +1 @@ +@import url("classic.css"); diff --git a/docs/_build/html/_static/fonts/Inconsolata-Bold.ttf b/docs/_build/html/_static/fonts/Inconsolata-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..809c1f5828f86235347019a50e78b4b486a6a045 GIT binary patch literal 109948 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 lSJOz#fX%lhbk8e1i0vq836dY`S4{>Iiq8{2xe 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

    bO)#R zarzLak8t`Vr_XWv_sz#`+U$Id(>I;Yv322Nge>_%4srx8xe zIj!Nek<)fgh0a>G=2uO;bqtxZPrJ?Y@O?Hed@@WKJ*a29xL00JQM$&Mq;JFxps0bkailezmU9|0 zQ|R1AOk}7um98>WrW&EL)JT=B+{Ξ29ypp!R(LOYK$fsCU(S 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 YaFbT>1t<8 literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/fonts/Lato-Bold.ttf b/docs/_build/html/_static/fonts/Lato-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1d23c7066e095b5bff2c373d4064dc4f33659783 GIT binary patch literal 656544 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

    f8495bJUR}lganziPo-_m8XTO;| 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%0AXJ$}2tfE_bG!=1%jk z1tfYj)y2Na%kV$m;EFHD(I;;w_Rv*aW<)#pvB6<)V(h9rx)17VTb zLJXluN-E7oF%ci0q73qXY_XZ;g;1f5QNtAjm38ggw237gJ^KFp^5uM3j_OOqxU3K7 z3tI^IV!RS{(D@!k?I{*?wD%CCQ@r>0@b_>YCEnYtZwjYFXEf-6&n@^q%u(@uPPN3S z=U(lY_M0GmsagOAd_S`sNBKhf?ko+7Ch6!{fn$fXTEodxNHS2oP%5ZStFv3In0^Sc zFsOl9;Kf`_mTmk=Ssz{a-36JZeDIdz|FY=qxxOM_uHL4o|N4z1Gu3Wm`|R0?i8*$* z)8h7@h1> zg1(RaIE+qsNznJ3bgU5qrwaP>aKtzIm+0`0^ZN2|%7}H4JIt2DrGSGRluDD*>}ej& z%7bi>3w>H>=qp(rw4$5y%2WIamVe!9&u}{8e*`aouk|55e*}H01u_XbbX1GV$HdLYWT$a198EXR ziv^v=N|ZOxiv^v=O3?Qk=f#3f*jCUx!|8n85cEUgbdGHW{cr?5#P=A{v2XGEHOgyS zD&_jJu+{P{3+IH$*E|nJ21(;f%1R1Un)Ce3BJ3DBN7c+EsN6_16lWSM=+t71e&YDb zmB(4)mh+!^^t!oK`oHLOW99ZWKHnPpd8to+<6-@a7cRKq1(x*i!z}5A3u@>8WXrLY zYmTgKYg>C{&6*=OwBghP{DBcKLmw*|>C$eb7$9A`CfZlBx9`B&xbYpp75qCmbfB}B z|Ml;HonyU4<#rA}HjvT#!eofn1VMk30sIDKi+m^r{jG?x+!3U|9Zu(VZ&Ciw5p=S9 z3;Mezo!Z0qV4{-`CCLQVn}~LWWQO}N0O_>rk6Tm=<)3vSzCg3DVYo-eW{KQtuuIFZ zOJlXr7@(4(7^kdci#`iyATH8 zn=jZ-d<&}=?G)c68&2*)-ee~i{c>}7zi>ObXwxn_4MH7!J%gQG4%x{A1F3Jh{wL~a z)A18}VKjY+e=DbFMU@{a=z&3;PI{iGr%BJ{@+eAIh zqMkr`6upHkOlBW&I|VN<#-7t}r#%GgTNcefoD~q5hixC26v39^C&zE+VoH$sx9a*-Ydpwk?XUX7ds_zoNT^e{dtS@KGcw(pIB0$N~$ z50W_C=gyKANuw@S=f(0p&d2K^uOJxO-Zg>P?LpNA5bXJ~)(U8R!R@|U)d9+wv)?&H{2?bLbIiOsKWYWCNGe|5iym-du z)%jzJAeu^ycg08{nYS)wAChdYdws~-rMoptclpeg8hnG`uYsn)nG`VAm^7LeK3#;z zc|vw#wwN%s(@YpoYA;iUC9psCIB8C4+rCDZh(6Y0xvY8&S^aewo%Rnw-)H(c0Gr2h z>@Vogvshk!8n)C}zE^X)_J~=Y(`mmE^vw};nhT)wx$t)6T%fkW&L74L`#vn{CY@tT zsuMaX(J{{snRP?w4to!nI)=Yj8Tee4__xB+#pmVE!|3#_g1$#O-XndVN!PE$i6or| zQF)8>{8jjY;pFuLs}7Nd`dWtI+&2&Ys_=`X>;94gTM;CYoU|B69e{p@gQQ0u-_lf8 zQs~Xf#En!EM#31CvNt~w9>O^mVT-Ahi@7C9oHh=swA2k63SZ(hm-1M=J0-Px4qWeb`{TzB}gQ8)g0RoSH2JV!?J*~3Sc3v~qUDH;6U81wJru4j}O=GI_OB&~{>zH)+#ch+vpzlQ= z(zxN<`M4eLrt4I~Um>i2*hkbq#=C{qeP%yv8_fQsaj}u!$m`TLO2<(e*441RoY%>9 zEb8+jsxyo`-AWSPsl*4Xt9mDwMY18l=SzBp^UuM!-N|KF(4YUm?7azKTh)~}{@#X-L@=SizRvA_f>J?EOu71I?h6NAR!4!*h1L}ggq@#wnBjxS|E#=GzpYaNDFkJ zowgKO+Uc~Nk8aEiw9`^16k_@NoqOMto@6;d|1u>R-$maWRcBlboBw zISo|(;-hxNu6-Nv7L_dCqDa`0uol9vX*KE*+AT;*%taKUQQB=Z;B3Sr*ftIGKUl;P z-2Y1YPAO~0!i|^bXfTqn>tLYO6_q=js@poD{Y-cL`ek~lfGY-EU_U%310d4IZ%V1(UBv*zr)gg)7M zX*sZvDPuVuy_fp#t^8dj-IjM5$PsPd!k1UKb`?6x&WMbPP%at&pDF&O;I*B_AYFcH)1T%p5766KEMUrIuG z$)`+~v~*7qygyQJ>WFJ z*1MXoxA1Q{;fFHdwBEe^5#d8kd+MLVj}u%FdH+~5w-87a=fn07@z1WQQ=Qv2VTX=c zd?D9KBT5G+JuHe~=o+e<(HRE(#YX1J!e5vANaa zuf5<6oY%iY^O0W$_hB5MuXo0;p5znfbIuwo;Q~AnV06dk09g;OZ3;)kLws&u%a}9K zGH?Hea5~+d$z0z4>0IrJ_Jt3#*D3ip8~=~^_~D1{9EZ0Fe&}nn;EB^8F*vvlV@|gp z7%wI`#f!-S!#k;drtyA~$5?xcJ7Hr(Jl7u9{_9!r?(shnoUh-Mb0Im$=lFGHy%CRw zc+4E@4RwkI7|?nf$ikm%y)DW2#{ZmSy_?3rFE#1*OkQu2YWjVQerp*09uj^;<5u8A zryTy2@RkNnYX&%5gSRr*0KCom#2jpj)ml|4@d8Q|F}>2`aB-`(QG`B47Dci*>h~3o z7fXNq=xa8M^!9k2zD}NA7yIb#H_vlN3-p1XWKYH14d8+--(*5XeH2gjV|G5gM@i1B?TEck_<1q-JD3$&U z$5QY7LQ4mRuchF>(!z0oQ0P+pA6mHP+rL)fIlm2RQ@@=Wmq8d)-yvQxJfeO>zdQr} zkOGejCxlJn4ACH*#B**npTQ!b(|uEX5a%CigTtoKG|{YIq5mE-k?g{taCgF!=$2$- zy(k#$W*kRjg>ZUN>LC$tqhKue+0Ni+ zTpTR+Eoqn*FIaz0+uSB!Y|i^j z0Td-0o{4S6s3jbMv06U_F;)Q@$arQ{pXC6{`k@hp=wispK}I||1F@}aC*7T|?Y3bV zW=obGzY}^E|N4}zcVxGw@l<8^O_@OK15oC&-oT!zhr*|a!k0Cm%$KkWs$(K~m+y#A z4@GODu`t{HI6*PB&0uU*DaK$_10|Vlo|2|}MMId>JiaA5n#BvMHnew+Cc-<sA-P5`c zPn$RM+OO|<o2Hj9uZZLWw#D##H6Yi))j1nitgD397XnY1uBB6}Kj3(=%Tq*%v35=0lYyAo z*L7S^I@hmTdk8PeCQT<(tbALFKw9*3m1Re0@BfNrCmu?ELE)h$&;j;jg(n8V zTkkWO+YUH%`!)E1_JcS_yw7AV!4a3l>#qa;J1rbC_ps9bek~k!eZV1a2O*0efh;Z% zdW8SM<*lnz6pRHhO$wEw3=fi|gD6f+f!l<>YqHuy_#u^&UNVd_5y}crsP8121sy`Fbb3XEram+2HPxqyFOkj&hfl2pyPQLSM#4BNLz7JNxC2!Pl2sczJ60ybfwuV#^R_jJ zU(Obx=QmnCeGhM4a?7UnzCDj@TDG_0lythes%q)ABP%ao5J_}j`uAD^T3)+h){2?? zUc6%0iw739)k%R5{{_oI>%rEW)*~+5nXt#;pJGelC2t&??w*95OyuYv{IDq-BN!Xe`NW@$zXyHB1V-n=E&5v+d;oZQm+d*-Xo%QY;nAG% zgBftzi+KCPQ^3bE;G~1|_Qx{dq$6|qi41tU|7SAbw5Rd*Ckc*qX5+`6X2N;=vSVIs zXq59!8(JLXE+JMB856h*-JqkAEHFxrg50zAv`^HMZEJ;QGfsZB<{G3E7o&Kml}dM( zn+=|7F{QMkwD9B|jxnL{?z%-+z`vwT{kuMAt<<{i7+wm!Xo{){V6 z>|Xcfi>4QbrHdPuCZc`qU>=u|HBREK6B&3zKaj-=!oZBr_A-sfp^He zclAyckC1myX!;==62q_fy6_bZoa}-e??=l*1^0fuQ!N2@VzIE=}FAZ$TVB*j1x{S23*JdstbYHY~8);)N^v@~`i*MD=4N8K= zx_4)=xA@|_CYn=Roj3_?Y}U;0-$(P2~@Hf13ZmH);3{DjYdG z8vcfUqVRX}xilR9j-$d?nco>vH%grc9p@?dJ3NS{Sta~j;i{oIO*MM6p;xK(U`_v> zuJ+I;81oJJMucS>%=rfMb}*=5LDdJFDBHz+qny9digFO*1`3z5j0zSDlp8AfunHOs zEBOEmS7rz1ynJ)sC+UaEJ}BLcBw}S{UaugOttuN`x_HJwqMyfPc}u;eCB>MRCwJio z7#&=>k_AFhtdsgWoOQ0zpZwgpOatiuS%AxXOUFXcNt(|{PS7}4Y=?pcL zo_;NRKlr3Pi@l%k@u@vkY*cB5?fqm2W3m?&RCDhA;)DXPgy4P2xD@SsaTcJOhkL~9 zm6!~E0RmLr@YM!gOPvkb#Gn>lx-rXxcZP!+-BNUN?rdF?5hbKeyw zudP}hG)Z;gb=NM6NT0dwwp071UNQgl31zaOH4k4j-JExo&m5kry0ztG;x5#D*yCfvbO4;S5)0Q{d`5&_!k^ z22nBTQ=ts~XrV%!PtR`csYQdgie~l;P>uyr8GP@qr_p4)z;fWppr?(}(_$N*~70sBoII{>fZ(HqHcn^o#D{u{^Mw zy;_t_H9pAhIe#6vm2Ki6ErWrn z*c(`N5&cM>QPS%-4>R#k)w=mt+A`mjQHfWn+O7UdA)J%n{@Xr^(-*=+g?^#GueYbW zi`4{bsDmY#2p#{?ftHba_8Riyn~d5y@8_g{`T69vA0&4&79h5t*(*zpAtoN~VJxvi z!x9G$oVs3Ni8-ki?_x6GvVff=Mbi{af*Ei=G2Sf`?IOO8_(Y`v{2|FdSK;F8`Xefz z7+zugz}kzy)gMgJ%w;N^(aaN5(9CyWT{aJo*|6j`pJ-s@qR__hn3F>}5vdeoBZ~2f zDrFJD(AzbdMMr^5&LYd6{XQGp;g?oldsy)HNE(nBA zKqHwlLJ^}1eaIKHIG7PeABUu7w%(1&yLCx+o}!=uc z{N)Td=@`8ISO%Qp>^b~k2At0S9DYK94?=%^U#9-aK!@AOl=C2mQ~w-(I0H`O~sjwCYMQqpvEjH2)P)9Tq)>es4_65 zkv9Y}+Xe$O`ZB=>7lx7&K!&{DOh-Gf+kMXLEe)rrw&(O+Yvz@9T1tJs z%Jz9(*nPvn-oXV8Ywtd{lMHPQ3#NA#Tdj@Kcyr~KAN$HZ{XNo&(?e^%xObyJevxSX0tTaS;Cxum6R3m5(xdT_UaK`C)F{lbOdT zvm5kU<3$Ze=HnyM%y^ccenzHDdS+4I(>$XHg~`uMX}_W7Ju`cco@Knk!DPv%|M)Ob zU|ZJ7M)<2y7I&1wDz*<>^O;54jNN|YFtS9E)5ekvxlNlU(2?UU=8EF)SjGH&8KrS^ zzE6|vNxg5eFm}ru#R*Yiw@=UqA!FvJ>w{a_4N#|d4&ywloe`aDaI{tMFlj{Ww6bxS zO^QlUs#g9SeI*j9ZcyxiQjk7DZ*;#wYB+3Z5`~a2ORP&4FB+aRtv?bk$3X~k#-`fz zRXLN>qR*ww$4H)TT#rpwens|3$CTrd%M`TRD+6s+?^+jLeR%696i0Wb<RX>a226C6H)C=T5REY_a_K#-F(aUR=S_Y$1> zmN@*Rn9t#`VSd1TOAHR%lLo9N08Z!A4&%e4g80TnGvi9B{r@ zyAd2#hVwJqnX;jVlm{uyJwg)C3fewwB&$pIxw#U-BREOJSi@ZD*^M*K%K0pp$QAK$ z2fE@ir&=fQWEzZ)$kZ7|&En3U1M8Mtv%L1yvK5J)O`SPYjhasB?(CS^x#jj{ z*KFx(8ZXX~cf|bTh=b7L6F#{w65b~M75FE-1%%CsJQX?Q*Cr=V5CxOK2Z&u+hnd0K zqs-T@-fQL0Xp(x0#^MMrMWEC#R_c|`dxQrAP79OqKm~eQQR>wb5-*@>Urp0qj%lBQ zHYXM(f3LexndaxD?QEKz`d#;a``h<|KF?0xqdUUteynG7wON8a0DJ1$nA{ftry2+| zhsWwM8!v{>4)H-2=Hn-Y0x1TN#%M);`wevIkqTzFX1bkuCOp)^=>W4cmM&HGl;s|l zO5)xR#Ny)Wu28VEs;H={GZ^ZsF4jvQCE-h{4t5Z%12Fp8#pwEM)O5qRK)-Cf;%_Im zF}nUHYO(dIa5_^=Y-4ANw2cuLKPPY1c`zpzq7bea9wTuJdPKEdS+|pFowLdyI7ZT; z5w?nr!!TX&$X=#SHK=U`BkDlF;F$9rtX(SVE#ZT4q#_;-v;5~mFH&(`A`14pt6eGI zB##?X+?9x>fu~Xv-}assFTj%|DnV{9e~^k`(|=Bbq!1zPLu693KsLQ}`WO#YBi?@r zQ@juPr7>aS1j&#mCN5++y%r08Hit7BI5=?;!zoT{(i+%g)%J&Z`-$%WK2v@;1wX35 zUjw{TzDI@2Cl&ZV0^T66QsKf01^y=B&6@t7(exAN?UDR|TP2k>o}z!OrXaq`&Yx^v z;_DL^=N$i-(*LWVl{tEe#$zE{;?apqGS8pD)l(STe!~aKnB&61i3i02v^_?BD0asI z7MaWPaHInCXAwe)QdAp*Hji|BvU!1yn2C!W^$QFve~{U&u*i;Pso>^QH{<`s+n6Ov zd?`t`P*H$P7cW46Istc{LWq(HQc#L>5i{w?heOPC!70uv;NvU77k8<1B45?SB@71~ z$H!FMOF9JyQOplHwklklVDpkDzLva)@x4ok3l|@ULmv}>G$Uog51wF_%pfAFBnga; zxYtz}S%6)zOCkbw?PvmLXSJlmh!;K>9ZZ8LJ;0iEK9em?!8k!Ew=KDWh=TFLt8WQih8 zyq>&As$?>uo-A@@rBGR6*G!;HX9y21%Bt6(umcduB@~;9N z?_@dFFh8Si501T4u9xId)~C#@D~d&%%B0#8K!6}!W;Hc}jA3s<>`?dW*pTh$N|D6^Nz2qjEJB!cU485dP zxQ}yhUI5u9)gsEv;{2rkfePw4G4XgF1EdzE1fWi|0aX)0Aq6VeV$<|$u;byj_gc|W32AC+LNEFkYks9Itdx7SgP47T%;nIT<-(KXp4 z(yF_Cn%U$Fn8v_~hL*DVmnf zcGHn$wfN&BHyx22b&XQ4iqXMl`0Dg}5l=;Nd-@?SQcxq*L{MlZ77LMm3lbO+ z-l*k>R08@J${6OrQ>)b9aMIX&cZ0pa-#vX^-MY)m3TH1{*SPe$mG!5VEsjlVEcsyZ z!nV~FdZ~79X;Zo9+~v)4>bxx*53Hs$#rh4!jYAFe+mj|T;g~#n=mWEPBZyVxRg-ZGr>SF@rdm0Ip2Yl zP@Ct0A@VFpA3*mxUV}kF{Srm}_4oF4;Xs2$!l@_1GDSi;mW4`kX0Kiji6Q^>$J&)T zm2)w(Oa<+glNWFD5+|!N#0OYNxO}47P>L9pCgcwOm?OC`g!C#KHl4QGs!AgXPTi0f zXvfmfBiuka`H`|dQ6SJn=~dzg)`pJLSa@laRju5Nk`@GFjx8!QTsf&R>bz3PCLB@8 z_lhIA`a+Jj;B2KS*_6_4u^##59#6zei@_quW&{uo67l|uavl1?5Tj0J)XnIGT;ZO{5AzUA#ts&KNwr2E01{UGm$Yzb*N_Uwlg`2AWquX(HN<%hxN zb;1uh8`>hG-cTZ%^r+T_h*D&oX#f(7YG8?~*t2oEHNgz3X2@)Y+D9i_l8tKh7KOK~ z(ya{!kb+*XgrwzoBhi>{jPnk1V-bCm<0TN4`W4U-iN89bjtGfjAo6fEVKisRgKFSO z5g5s3{o(A5{U^+}LsaD`8=SYIh3q{iGB~naHS2(o7L~O|J!I@jR?%7}?=iTbBdcq+ zSKti%0v@w03on&w`qQ@-?{vvmor;Hz346HgB zH$|vn)2^mAo~;7+kCfQ;?7rEv_dUC7_c!*X#b0sQo-(_ zn_k>|>5Kb^PCsz)vBwS{dGygk8oW`AB_HQIImWqw2hI9YgAJz#iz`3;a^#vGl&0}!zb<|`eHGv9R8-J z{UVnC%;7)Nz`IzCDu@4Eh0}PLOd~k-KZaK$B#(AnX}TF>&vlr`SMVNAu*ene5;_xY z9YtrXZ3^#&5?G4brge4Y>C)U<9)omSrLRvQd zlgqE+~CPq}JTUY_pwH0}1?f?1( zL~TTU+mGL{#Oj?^k*ZzFc!1?vGo0y{XW)e7G=n3COoJ=KLkgU7u*Z;tO)+M~Q<^Mn zq64;mr_?^=eHnfHGzGso15UA$?3?obDd0C~;mn@T+t18|Ltb%sZzep|e_sZiYI5-Q zom0RQnebFU0~zov{#Eh7?sVY&cW1PpO1@FNA8U_zKMn5F@)q%a9Nsu_RL^aaJDD6l zh+7v(zU@surLTfdl5#7qNm!6qA&4O=ZmC54`fnr$^=EyD4l7We3Zb~)v>Z4 zi6`?Kk$7V5e`wp1n~`I(`%5gx1p61I>s2kIG?V$|)4Cs`w32UMu^VY6t@X)O@}gSI z1UP4NXEwWC02qXW?OON`u+{`8x`n=iJhb;yxT+T(&4H^Zd=v0cAY6JK!x|;>g5MRa zZ6nSja zW6}EA&z{=1@64#Z>B!ctNA&H$u9DBn=&DCtIy_c_85fk`9zP_riUuKy11JkqqeD;z z910fnuS^gkP00~WSc_yLmYZzN{n3Bm6n_7d03T6uO-?9y-_w&=4;m2a=Zz?VaE)7kKDZpTX&}`1U;_? zta7Hc&6xrlLHOdw@lRb%cJ!;awU?5%rdsul2M1ruNZ_)o?3I=)l%a~dyX|xhAPN%4 zU=J39Wvmt~*HZk%TC5Z-WP_5OIZ41wZla10zbas;3D_dtiQ0k-nCgh9iZWQE@pd0C zEz5kK;18WDV_11v4W6XI4G*7J8dRhVWX>CkpxHRO@!>s@n1Y%^_ip}j^_9!Hs7bao zS4ppMd2`1*Gw<7c)S%#mWVe zgLVAqLcTYM6vQpa#6Z*sysc^*kTTgVl`~GNT_Oe#Ef)5itd{CAMQw;B$fsJC4o`^U zpcJPokI01Y8Cp%++64)Zf?-!ST(@#ob%E?Euy>bKCYs}pJMTPo%{8BmmCCw>tIjQZ z#%a$htW8vQ^Ppq#C4WT08F2G!w%>CcpHy&s0qlDkT*AHwTmttDF4H-I+Q)juAxVEXdF5w~Y$_Y}%cM3`vq_zz4fFd{^=Sp=YIDp01u+0EQLEzB1 zGh;C(g~{`8A!Lm=p0p}CUZ7z#q+U>i>$8`WUL?I#6OYdqR?txx|8_z9qGolo-Xk%U zPcjO(+)T01Qv~I+MPEN@(T`6$qE4cKWPj!fRWJD?SXZ1rrMG0#j~Gu$I?hw%vjv>- zAn}?w!Qre%RXu}Wgmq8r0#v)Io}CLg{IH@UWW&c)IBXsD>|Dd!zXSs|@hoePb7U`7 z#im-srSOMMS&JA_Ok0aM60e7}qC``kg^NFO*CB>)Pg96kdMD58a8|Yr*3Vg7J!|D^ za8&C#_ik+6w0eOOJ=iv zeM6UzHp9cNgbsSbwT0z%cvL-b_Kf~rOPgnQVv4{y#|fPW_}ovXaI#;E6Y;l%6Q%un z!%U@pOgMN6c}z}Te=)D)e=&0htNpV3*I2*u?b?1BuM-?KtI#jr$5Y@t`H=1@;Cr!% zXbak`Z9Ssd7=T1e*?2SnZsVZ<=q5N{WV4Bikr~WL4a7#Rm~ANg!k|_bO36a1>dVcN z9qBjo%yQm#n0pQ8JZ%r)uqocB0rq_S`>Y<2!lv8O@|aZ?o<7yKw_Y=eYxn{$S;lUS>H^mw{wR&%Yrv2MV+o!XErjau0Tf z&#=AuC8BlES0wob>6d`_2(R(@E(dg4xu?VrPl?g4m=UvJoXRMCJ}MymYZt5MuPGOB2w5VueD3KdtG+?uv&6eQEUM60fRhJTEgO*#uRxSLA+0b{D;R$BMU1lzv%revdU*-s+l-=ekQCZIMg7 zSBcK#AL=W8Wj?3bVlxd5m)964C#+uYxoxM^TU@-}FBZDJ$>+w#-)LyFWELyI7hm=~vPS zuuc|ZSdUjh7_j8nT1`W3UajT@>;-CjER{1FjlgOYXNpvN=4v#vVk7BWZ?Gc7gE5{9 zi(AdVn_7gwu<9b?|Djokwp)f5T^PQb`3-#6U3bc(#Xl;Goc@Q;s7p{=*U|cJNk}*A z2YfB`V{9$12JVdVw

    j;E%EMHG>OJDDV*KLZ8Rs;3>)vyGm^jIK@Zt_J`B`$b&2n ziuZp~1MgvRO}zaHP5+Bzil@4o^?y{;f4`>xXEgn2zdNS(gZj?%`Ml6i9P`3BAt#PX zmuJSpVXh(8mgEG!scD1TV0lh1wZXjaWHC@QF2q1dPw*Hh>Ng_>N+SA}i}7ujkPuEK z@^EO*uL9$fS%%1~Rj@$DScIKcx@b}|%F;Txz0`=iJjB_ti`gkB&}h`HGLw^nos7wk z(WAaBn2>Y{nZi3UP1y-t)#*MGiBJ7H+i;9V^D1mY+ssr!BO(e3%HZm1Zmg?8KwmJR zUg@rc{n&Gd{ZNh~U3sYyIgG$F*#KF3dV5wN(7)bw>gubL<(cGu{p~4!kY2w_e;13G z?5r*-s_tYFl&Am7Bgyj9!l$E>&P@qDsucCqc1X{P8GNL-EdxKrSofewpIx z7%$^giJuBQKP^MVi8S0;$9Que@DG`z2TSqx#&Xu4!G$LjIC#(aFC5Nf!a+^@x0y`f z?GJ0<-)1t0_kU6Y|2mU7y!{DH|373hXCLeTsHXjMn*N{Bz_s5UQ~OCiXZ!)57n2c3 z^~n#5P#>PnkIRU(JYj7~=HQzobEs{08{ALmWHN`w1(_pPaG68jn=EsT_cNKZO7tcy z!=j;}zY51cm5jR`&?AjiasaVq|PMzKKW7_@Bja>bW@7Uk6$Q7_C3dB zD3eWyyakUd@SD;yQJhfVJE24Wf!RE+Cm9Og{Fhn#Z1}+pIQc+%`@}(3R2;e7UK9FkkX5vwlz) zNefSpXH46_;Ssex{GJ;87(P(okg3A=xlE7t+L^pL zJ$A_SW5VGNW$IIU`a_uvBGf19zoh0za$k6ClHA{k+}l(eoIpInS5;i<{$L<~E5iZ& zt=c(}zm>ypVRONy&ZyCR2jVwtgnJTsr3m)5BJ7nm1ma=}&ev(f1Ca;_ep5qKU>KpC zkT7q>n35t1nVo)HW=jg7;<{+=#!y<+UYU(CLis!8QEp7;W(F#CoT|EpSa6rz!vPbP%i3OeK+`f!rN7roWY?6MbCV!~w z%Iu2_U+}+jJTA%PDfnMG9JR*uKAGZIC;-_Y9K8ZR(2p5$2R=rt3@-kb;Ls09R!jqOjw)E0==M);>r z+Y*~23uQsW5tI{UCR`Sl5S<+V*DHqDr5-MRb2r<=r0*Qfw(KN7N}l_@lBnV3fIxn9yd>rUFMy=mE`lNhb03&x)UC>tn!r+E^?+-CN_f$*Fs5 z{KIgQP$E}6Tp85ku9y%Vz!8;b#Q*5x8nSh}5SQK74OX)K!5^~xOtzX{umlT#A)cK) z{0Dz=-kTQpA#dc@$s;9Bag(FentT&C);PpJ*&WFz!XeQXO%?-JYz)l)c@xG2%cqie z&*3z84nGFR6Va3&G(~wj4Z$s|f-rTLrRX-0O*5LlWgy>il`aK@~F zw*+Dg)x=mOd`;UHR*w|(CJHc=2B87SK?%wz1r6u4EKZFwmwrw*tt~kt4&TlqyQy)S zyq~SP0e=2cMuRtFu(U7Un-hL815Px^+aJz=(>ieYF@j?qSpQfDuMkd@*>N#AQi60y zoZz{B$o@equiy#e?qF}V85W#s(Urw=&HB>u*DR6pr(Z~}`&l4l`bUf1Xm(n@Z;C>g zeE6GUzxV~+u@dL_JN~wkl6Jr30(WWVkQyuMN2zn$uPaEW|Fot($t^yIqp9|U zBic)O``3m0H0^0m=Iu{pv?spf@RJ#E;x7(=_7nA!g;U~%tTlUH>A!`dFW6e%0tCRX zvzX(7!wC-@K9&KeadP;{40yWzVHFNtW0c{IwHJTesq~MSR1I8yi{M!M7WgHOVC}<{ zF-h!|=Ydax!n&DUSN=$IUl7JW#WYOrgAsO&$?!fT;;*Ad!!qm z+g#jKFpgaebJX1`ekeaBSWvtAJbv#BUF)D8ClMJ0jNs~pR9fg(RvDM=M4Ku5V9J|e zG8>{Wvn%3_MsW!OE6l8vpXYajX>NOgf`EuR+?b)c>_t4Qx~-Jd>A8sh+9|lDxhmo{aMD76|m&-KNQ=(pEh`< zG8Xs|Q4WeyyPd_vQKotrl#RSNM2h2ODpR|GGN3bCPFa5)ZPOh>-K7HTd?U8 zkhQ>SPn1U8#^}t=(>gcIK>{Ywg0-x{TGp_&j0=fGZ!EL!G87=B5|Q&t)=OASN)7^Z z^ZN2zCAwpk*GR5(8uLC`jb!mkTG0?z)Kls%jyeu)E3=q_^ExUAn*yQk#ZB>6uk6Hv zhB~WaSWuTsT0CwD#6#85nY5}yRbD3y3T*6HQz?WsH3`+iCC5-0j@1&Tr753WLpays zoS~3#=K3UfA>nFs6=mHz3#=AiNH|>>*688Xtloa1t`$X9qZaA)CRea9UTZRU)fGic z{dTXpxwyNx+uyN%s5&}hU2hx0-t2|?U)f3v3#zKNwAJU^^ZA06&pK~b%i7shq5hR! z6|})iLghq(6cTYi4yy1CCI(Q#6%$vhC#wP%?cfw%OIBypJc6?TSCE@bxFxd61g)&d z=dkDJp$B8R8I#~wcx2z;k!ucf?c$l|uz{wq}EgWc@{Bv_J5juI>(LMUZT-3})`965n1ah{M& zcG{AVV4(eCb)@3L9ftz0!r7%Ai(1@4m!UFTR`1xG+#=p@l%1|wzS`lo z0Cax*rxQZbucf#=23cP$ED-*cm&_?F5k+GV`}$xdvSa%lk|gJ2U^3KfdSuG=2=aw4 zjgLbg$HWCa<|G(&hPB`=<7gfZ$VOI2lL?gVtgLxCnnOj*Y>pzxCeSFogmZJ|D~ZZ< zYgvF4meh}Ay@it?ZkzXG3H%HE3+B)5Xd|0hk*~mRl;Hrf@Z09oDGeyPDu&|}mqKOa zkw<__U%WgUg+cZFccPm%b}tqGm&$AVL|de^G=l%#QP$ia^cg*|(yqG4?e361+_kXQ z=dw1;Su&>~GGjxcf3!Og3p+gFXjMbFXHoq?tNz8ZVt-kgzbKe|(Vj2soX$L_%~U2u z25b)5RN$@XuPxLWEp`-$_II?D54MGTv8Lj1jV(VKC~9?hXE#(#t0^q{f9r@qJgOM< zzx_Ij!dbezq)zNx9-#t}7En1z3qhp(!|s%5|{`pBg@7a z&VG!zMFc-A#SeCrmxKkl%R8gaM4q-rK}#Fc-W-#LuXEK7x4XkGL%!4L%a^}d5Qus_ zIGamxe6Y1tFR!s!Wm}P3{N1F95lW$F-X*QW8V7`?M7)*T)gqFv-}l*F@)Bc-`@zec zL7UsO{{dH-ycmi{*&)O2$yKKOo9)(PA&1H5_|WQj%4YlxQzJ2^VclKADU3-clqE`V zMxzyGM>W2}Y_H(eMsP!f%=40{{FkX)fjcgx$XJ^PCwBL2nq5&bds9!(ra2WAb2jxf*3~sO)z!&|qq8>lCpOKB z#b#|v^lzLMO>USmXU_ELbLPwd9!bw}NOQ$zHlD^r9i`;4RVP9frR7AZwD^*^U{Xfg z-#R9-IW}`sfB$A0)8_vEO*3Q3^%-NDE&WNFgYzz(ea_3u52*&zm4hU!maCXo(gS1MwVwQjn>{2F5^2m8s%F3AmL6gDpeO0q|`R&09XF=J5 zPUWyfTJ@oA1zx@OtmJWT4A(?4T{s#h%;Pu^x3W z5b&($e?XU^$A#QhLFGe(BMW+gh174sq8aCRin*`$z)9B zx5a&xVV}j|F@-E;<<>)6ik-5&q`Jv@omXGxPrfWUoIW;II@f&ktcr%Ov5IrdWAWCbdwWl|m6VS}1SJ>f>s9U;$ugCaPTmK$#l+TR!a}kqUjWG;pT0 z(MKnrA(fNUX4Zs8aE2aeC?C@UA%DtKsODSRQz(XrVua}vYq}ZD3kT8KNW7-p!49HW z6grzj-%DwB={QiRg>`zqXF;F-QocJMJEseoM!l_(Kw+`6!>YePV;Kt(Te(BXEt!)?_S(;NMHPM>YM#cIK^FyHAaa7TL^BYpKHm8GD^ zGOY1KhAw31PD}LR0uWu13+43!wO z#2+#~5V2QMT`;GBdRi1Wh7%!%wwu|UH;A-5It3ip5wwKLAQ7tGQO2)qr#-S+YX0s6 zuO4s~>k}rc!Du%P=!zWlImuCA`(8K^3??GqwH2U42llXK_)e`5Pn5?BO{40!Gd?&6IYI+=1e153bPy241B2EqXxcX^jcg`G$ZrNiuddxFquTjZInU807ZnV;iscStz6?N8YQa+)S^mfWr3a+g((X--Y~;_oDEskCZx5G zIlabZ2GfAS=eYOCS08gaj4KRzowCdN;8%2ax$^IExpw8dMCXLra+lY)$(nbHf4^WN zD4hjsykCeVBB}4=%b=6xGyvQ(N|;=rjzZ2ciawDMu2DKmJBq_!!6*zX^d`B(;(zcf z|Ln}a*X7!6;e+_R#h1UyM}q(XWB(cg4zzv=dN9ffMdOzMp>V%Mduqk#+e9ow$v;>M zeb;RM;e9Tze#np~%lU@cSR7Ey#YL{4iobB?pLCR?+`jWUD-|%ncdMX3zmD(Pgn?tQ zNkEn3dxr;mpTfGJS<;h*&E(0XB||Gx)UB{+(sR0H_|E76+uq*>&379 zDmj{8@b_<`4%^yz*@ST2kR|LAb=OfZ6YoJJT!e2s1f0uZh9_%@%99&~xsK|f4tLFj zfb2NYDeU63>SD8e(c6(4OY)#2X!C`WhfMY1SESkRMT&L1tG9ISUUB+XUkts9hz}R^ zxa%Dncw3^GEP_GI!~?xVH^g)nGU#(1ih_et3kW%KQGaof7dhy0nG2;jJgu%$-0X`) zDSX}tG%FX>n=vN%V?9z)`-Wm~$6-5Ju`L>c!MN#rw(Mp>^{=vpTw->fquf4*(`v(~JGB``BG47~Pov47< z9_l}=eH(abAXMIDwKRdjsDR58e02YY*?=r|s#0b|L{puvmDCQ81Lgo9{_exyI20JF z=im3cyOV#u`#1N_|5|l@WZ^YyMcdieE{fFSTjPJd>Ia`+VN5f7RUN6SiqJ6uj!8p=jh&gH zZ7|od*--d9Cg;N>Tl&QE<|ch9Las4!ER4a`SULXg;H9H2vo3vj<;sUInborL(!udR zR5kpzVSU5MUFUUge_qso{_~>l`SZKC-Z=u`FK&$1iw*fpulefQ9gl8nYuondjC&&ztw@S=JCJUvh0CoVJmq@Pc0WbdnZipva7>F3EcS$yd$cJsEKFNi(=`A_Go7T@HUn zg(r9EkFdO3f`g94OF2FXp2BCG!x5sL!fVF!m`r$8-p=uUOeXwJdY-~VOeQ=}dY-~} zOeTDv={e#3OwS4blb&n7PwO!0Ijd1Yc!|kfAM>2nW%B##`ovTCQ`c$c#Cy6OY`r0` zh>q8CI_7ZVZw8lVP66+o0^XMaCqCuzF--L|3kz`Ro(YP9Gz2px_9sdcg2NC>0T~I-5$49C5m!VldYdZg z)Do)})svP|a!)^-V%jcfJk{0FXf17sEV!~XJb(GwZ4~X6d`kCEyfU}+#`MJcg3?0U z?BQ@ng_kNhL1yr=60T{i*i(eZ8SQ4ndxg7m!uy0nX*i=L-cP^q`81r-Fo$<$^iONU z;R%7q;_&aL;RA?vB-zH`n*OI{^iOL;{eXrp!4I`Pqg~#AxA1`mPBh8kJ;K)%_%3}D z%j4znIl@(GIFkVc$6iC@Cn}%fIm6Q#o-=%9KTpjIdkxJi`*{j~*lTirU&U8kW_Ucq z`wXAi&o$p?c+GyE!Xsn=((IJkYw$xn!12TIthQk50KCd7zT=202fj7WcTc=8^)O!f zIl+Z@CZNq=kG~J_`{h5PUk-nl;NT(F9`Q5|;c;$vL7jaZlPKYpDp!JiwlRYW-G?>l z(GK>AE|{J4h`&YNxJi%rTi#rc=&hVDF#E}|XrzEn6D|r)g?&M_?e9L-*1tN`HW(S$ zFg>i7#_I-V3>U}!w%SG8r_mg{F!!@D$0FfVRAzm!gV-DV7dZI zT~b0aP4`T#cCc17^E0*LeGwAQ)Qa~-_&8H57T0p72Iz)cnaI#sBotCkk|%0H(1Q{n zScl7T)KaJsxp6xz12>di;i49oz{ahf8T#%M>LsnFCrjMmcd zUIqTsEO?&+e+}@3j5l~c{T#0JPdvxrol5(EVg01x2~9sejE8yq0S#Q+|1<@jllH+A zGtvK@j2C%7GnDoZ06wT0=OBkG<0RhX?Yk6s6&q(7-mUa=l);HtdHWs({xXA;KE>du zC(n8G+YCo2Awy`S&)Zf9}=cG6|yXg*1Q`F*7o08=;yXh3Y(nzzLPSGn7e<(Fp=5^w? z9We8@=JOPQGIc|ij89WKLmRa-#w*MT2irmno}g$flUZO>i-ci12;c|onwo%ku#WyW zo{FyWs`=BJtZg^0xM)tE@v@eVQ_Ghv+7UT5t^G;p1wLD0)7<8zqlxNO@qce^(*NuD zBdd3=m@n-cubq*=vVyP9$GSGKz3?pnjB?4UU)~i zll8;l6w}G!$1>m~^Ev$83^?&EgUd6gfcH)T@6*B=pYVSAwQ$Ct9NswvJTV1)KnvIO zKTQi~e9QZvJ_US+7Ov@MPz%@e(=`RWI|EMq%)i@{0VjTDaMbJ5t_$%qhd)DbY_5sq zQ}Qd&TP>)`yW$wa<5+AJZ6Zo0BFg0^%*FzrCbPRp?Y>enhEY94SY>p39zRw&a8^b8z}tv|z(VhIep^FyR%o-WJdU1T-4JXdK>U#$k zh{+ekgf#!=Z(TV0(7r9<4Ypv3Phb1k&iPlYY=RXQa%?N+{e9TP>V>LAJQ%R%A^eS* z3yX$PK{z9PyI!a(_W5N4uBlcGcYNG1iXrL?+rJMC38NScOL3Z=d~d$nYFc^UYZsmR z+#5FzJZUfSI5% z62CWKHo=)}qxR7AsXghbQ#@yQI>U2@uk7ckc|p(5*+0op{(XkO?Dtdng`WR0?=yU6 zzpwc|!)x~Q6n^6}!|xfMGdxdvuHqYdzIJ?&ArHeY{v>!QAAuRDno1!JWPIO%P+9#5 z1OoTgF|Qc1hiz6-USHNUoTVS8zEiF8HZ*3T~9{Mf|_`LQ1+_Yz;wxB~3X z%|9`IIV8No_yrQcwAARtdi;gpaCETyzc3Sm)BRrzE*w|jzKK8UI~n|a*3WUJRyF#U zH?n^IgZCp|$@=GTx~GD{g(t-wX*k_WpunHbXiwke?T>5V+t~QYr-S)2)6MnrhP2aJj+k#KnoF&@sga&d!Y&YlO(KKl!M=2&#Lq22ec zUVZPLA)ENE?caLj>Xr59zL5O$qhC(``Q&-$ofK`--VI;a*4er33mg7^&7E7?+P2)e zM%vT7df%5{IPZ(eKfSzT$IGJqi(eG&F9Ri5qlrIH{7EihI2@-rip`=U4QIGwaN%j; zuW2}23l2Y?0S^L)9R31_3-dt(U!nHoeFF}U3-A(g+A!Q=%tNIqBj9ntq5lPcF5z!H zk3zqBhrNebTK3)%_I*&2d=0ysBuQ{kdL@49SUteUp%>@DJ6tEsNK6Z}trRrHSOF`9 z`+;a-D8*7)8I5Emld8oc#8oQs^X&>} z82L^FkA@VOKWdLnYkKC9@}8Q)f>?jOaow$}Y8=%=oy|*nLetK_eSUJ)<8uc}8q3|u zccrJ3?|I^_L0@ge^Kfinv^6wqpv`WHcQ0t1ziX%*6TsZ}VnU5%n-|GDhdi4YegIy8 zKNeH{G?5Ce8O25sl%9e^=i_kU7{M{`a@=Fz3*DkqxIU4G8!BX6wg8U}uN7IFPK)3v zj0k8(>^b6O4F;xZ!(OIjGHxSB;iqW7gEAkTNd9lAsj7@eLZu~MHv+3XsmMU`?{GH{ z1wO=aEi2H9Ff!zlzyQa8p;CtADeUVyc*)v3H?=zAiTWj526x>tzh=|s^%b-1rM{~6 zxt*)e-mqiOo^|Jpjo8L>Ub1oU$z)w7-+3FHlm_r+9xy=KG2MNuP%I8cTSFA+RHjUa38Z zFq6We*@M&R&Wg5{#`<_PSQ_wSF)ULoMl8UTM#5SO|r@Z3s?cVKX#aiYN)k5y?V< zkmRsUrz|(iWxq0-l0Wh`6gPO2-#40n^}XU|$Da#I^7H)#|L$xm*6TkyE%iiJ#KwP8 zT`kqdR+f*ysMmW#$%UoU!{HeLaV!jh#_pEG9TRj{T^x~AI1nH!W*ze?YsY30EM(4N z!KbtoAsP_=)qQXZm(8CN4-j(2my=z3DS4T=4|FmJdHQ9Ua1{kE48pPV@dI$kS$PqI z(;kBQ0OA0HS3z#x!r;}sJ^es?18`f%--^e;2?uj{#0kqA+nT;Q~Q#qNyxDmsE?9hHTJl^tcljtU=Q23{OJvVQ%MQK{#{x5_#y3JWXnq`kuD zt7yk;F#ijazY)KzdmH@Lj*@If5s_ljD?Cm>dPRv7*sOb4yunZi8?xc3EQk%&t~!LW zwEMi^6v<0Jo^A-2j113n>n)y`-C6D|DAGN=;NWBT_SRctmZGyZ@97Fuo9TNK*x~h; z8;r1ZB_^KJ{|uVK`NBR7#r%{YKCS;5`y!2NjqZSW1)@oeVnPV$f0paH^eXR%o>M#V zT(onh`gsba#ncYzezLEMT?PxZ3RePqE*27J({peOTY-dN9ax+rdtZ3LVENzyqM_th zb)Q4qm^kalv?tG!YUvH(V%%W(qI3j(6eftcMPaGZlOWDhIzr&d#qzulk4i_N+u)rm zbXUvY)i>fD)Y+sMOME=@MOdTBl4c=rDuicJtC5kfrN3|meL>$ihW>@*KkD2Qd(nRz zzXO(To#q|EC5Bia{E07W7^ehy~KQPrq~hLl4n-|I_;uMs;rCG(Pun zG2f_(%La1#;fMb##_``APs_x6#UJSGf-6N^%ZYvvqAiFED#|5Xap=n8`elLU8F2(D zmzFjUR>WsC2c*SuJ^r_mpC<2$0U~fyz4feaaqK1vrbvG>W>>J51o6 zer*Q^31yzMXOK09tR6^+2P}CD=MT*uNOZK-)x^p}WhBz`8ZC{Hu$96|+2w;QWDZx0 zv6x}iSq&{DT^*J*>GrnlD=iTK#*>opRR5+m5#}S&TBoZJy1|+9e-`XO)fv}9w!$QdtZi$3M@PN5 z)#pC_wyx0alV0{&t-fb1VfUE$;70L*F?T2*EP%d#i@wH{z7Pd~zL07S{UzGg9*ajQ z!cRAT`fayQ+)6zr@AA2&H#R2Mk3lDXmiqj9e#kurxlU*N`;3E->)c*@NJRaQ9PlSI z;H_$V@eL7rSE{{9g$rL3@8xjRNV(6@p~6uk;tLFZJHGqRWOERIkNJ?zVL?v#lNs<< zwLQLjwx+#Fh2y(xI2_;oXG4by$9LB;IO3aRBhzi(X5ZD^A(a8w+&Kj}YF3D>J?cL_ zrRfK@Dh@}4!5D`p_v5mLFRF0yw{&L;@PnF8^0531!2boyNBZgkSOZMhkjLU|gU`|C ziK*HcV0+~KU_76lqMyH4+hA-tzJ+lPvvFcPRP&bUc;fhj#>=JkrWWi?Q|{-89Sw~? zsDJUJ9#hK+OlJH+`Lwj&aP*VCV>rs+`SjlrI}AnQ+s2>ZJGDGxE~OuwODFfq`hlVz zh|pxjj=ekNCD>=^4kXykb~Nuh2XHDc_cauNaD?OW7QZ0&WK&scxm% z9btq{C2Srp{00Ej6MUGipob=fSY=e(q*!HA%V70RWCjVNl3cwoc+#nFRH*RNlCFuHhu zi?5=kG|(LP`Qpui(v}LJ{>Iw*=XCW94)%1NGrv}RJkS#NdgCnt{6w1;e7yxq%sVE^ zrHfLOI(&A(feuwVK(x?bHo(Rvq4*sosToAvT7ZcVl=+jaW`vmW1nH1Ze@%P*BMh=<|pk7C%qc&xjhpO|C@oz|uXl zN)K5AP4ReBz;dW`)^1~a-TWlJ5J^C8E{|^N& z{-!U<$7nT58^TU!7+=DQ{2xCz(Ix*GM7G`7#?mZ!{N{mb?F3eEZyV^Xh}lADwLuQpj=gcRg&<{yA3-1&+^`TL zxrXfeM)9c?s~zFiNNH2VJ=9|=XhuafoBYd}_bra}))vtP6Fu`PdmSd5(P8C0dm8j* z1AX~vhel;&Eq#&vSWxJ7OJvxBa7=y2uKB)Xb4MCWjrOJjd@7Ruy!>=&Q`A${L)7H3 zmC+D-Xb6g|`2TBucZ?^c>nC1=Mn7wKtP0wV3kjlRS)t4;)xdCsV913KcMk_?m;GDFKK-;RqeTczvAlFy0L>yx$jKfrCh>2LM%Z#k(u2}xeO zGYWu4h89vfMTxXM+Mw1c!;rK(6*OAdRy`r<`s6o`91&+E@4a7qUHOLmfpop`yV!fr z;zgW87FobVnUSuDGrIdX$t0VtZAIRCi{$jE10p9=rfpfYUbaxx0yLlXE z#SHD6DaZ!cTgrlMgq4Ke`Tj%^9wN;*$Fpqe6JdeF4fPJ`w(;@D$CCFy@POEse3kQZ zn*OHoZ~D-?AZ{U218EZx;^QD`GHGM)*gfe%TCJCPC20`DBV!MX?|m=%DyEOV|8L?S?&8~|r4t>7OS?r6{xHmViom1m-I7GpWi#xjJl;Ir^} z(pWrj3h8#y%23iX>4MTu3Zz8El{B1GMdc|p#gXHlSaX=pVKQmgREdbvyb;qtk;cv9 z?wW|N*lEkxJMu4bHTTVm&Db~)e0py~qr>BrL3hL-sX2k9|Ix{*=qvNP!!7>pY6_F^!cJKLp);TvJAgJ?Z-kW*< z_cwky=bp3A-fOSD_S$Q&-3BEl4w{gkHDEwiZc!1*!HU7&0`)_9qnEcFE7x1zE5El- zZS{4KBaHsBlEayIhqWir+p!2cTdT+{5vh7er^ueZ%oatCqD?U!-sipe4%2VFyRW!H zP1Nc57kvMFJ$;wb@*ulaG3aM(Hyvf?&>cqoOw05Nvj{z^Hw(r_nvup^Q}c;0E93h- zr&QP)gvuu0nw4odFoU7lrMObVQrS;hW}91$dHI?8{8?QRA|r}R5QxZ#=mEo3)8V9& z33-K6;^U_j=1nL`y00MbzWjobQA1{y#wGJ#TbQmLOl^UQ9K$7KHXovhI& zFZteQO&@EIF=J(ZWgusqC?UZz6+_?DGavdxEmmv+;!~K*WlXxJ4)VT5UJ!n+TxCr; z_h^R7P|mZYlKDB=RxDbLmR`Pt+n>@n(gvi{x!b+ z_oPc-=X_JB&4Vp!Xdjvl3V? z<8+i-Z!jpmwj4@l`zRfL1(f#tU&z-OZ=hwBb}Q}fkV?OBMo-2$nG8dJMUH^(^(n8E z&Vy`Gg7qj$h~9~8x%P|LXz39$YQuPql?IK$78!RzgIdOfg@=cEJG^tf9pPc&VQQgz z=hx2oggEClt5;v|r{A1)7DY>RYdmk8186V(m61mGAsB_r|7VEWR`s?wPS@b*LA+}G zqZ>y;g{h~H#0-&eqmRTEWP{S!&P(R2m~MD@^$pJW#5m`*-c^@gs`d}#KkrKr|Ecw; zX_Wmn`W4JsP-V!GUrVnmr*SP%=p)rO5As#}pf+F>82aDCW zO6FIkrB%%@Nh>T&vxO&@%_uIOUY6WmP*hl;^I7@Ga+Ug)Z4zmVjyp{XKQ4aDRYv?n zlFuzDD#F_*0`c$58}qoV&zZiqi~X3P6jtVAr0AxXMpmbFyw;x@y(Zo3iFyVw;gLATuf|b3j5uv4kmB zM-MeE&7Rfa`YP-5>y{}we({)7Z z#*A?arOG-oG4Kiors;SUi6-57)!7?eK&6y;W0>E>NXCi&hut8o|`O z{c=+9zN{+-Z}rtz*Bm*5?iy)}GL_h0H0rbLCM(l-nL8t`kXQ}6Q$+kOQr~Dpr7BAw zN@j;CX6kcHXoOLp^{KFFgQQTUIaNnWJ?^Q~wjBk9MFs82Wz&m`XOv0zi1_L)JJlt& zX}Y0_QrYlE<}|R)RkXT0C@v^IzA~>ODB9_VIq!@Ls?4oOU|~kk@P5OC zVx0aw42!SGd(7c)vW8?#NOy$!$G9er4@n604|SwZ$Oy5T{2dt+G92Olv8fX$gd_$= z24{{JsZwt8ZZbz{uhW)BE2T%))~Rut*kg< z6X(`frk?zgP4(yY38h%+aU{pa1mZdA`; z1CYk#m7cjQ<@BH85)$I#6B0UoS2ZhVM1D+6{)n8SF_Dp@i*iQvkB#lmuhEf_W4vD` zM#m;3#6~CfT;Gc?C{Ir>FNn`bPR{6iPd$0e`g_wgjsPW4ndMvAP2);0SmaPyj=L3u zk*#Ez`Mv-xtl|^ncmcM)lRCx-*~zbsPGg`F$T2_q++aK&nH6eUIR1}d)pX7I=f(v) zD!x5WF6H(T?~a7%J!(DwNPLf3#+k0MT>>o^8@nfpOSYF52r{$Vt=Wv_iYwKh@`~<# zuRB!xzLpifr~e$+LvJSf#w)Xqq}Tf+GhqFNWZ*W*z-|9wyqxUq=V;I_L;qFm4-Bxe z<=nW5_sPsM<~YJ@ajNOYBXb<3r04cL)%(pX0r7Ll7_mNfH6UFktlBIouJo+ZB+lPE zV~47K^mcRAi=J=1zw!Ro^L0mM%Ylzn#rtk_US-anxhq!8-IG(97cKKyCfiJo274_B zJ!avof7O_aYJatNuIgmV`Hlwbd#e7Pez886)i(S1d*gfK{VBd*W}C(uDI4+A7dq8@ z%kz%e*3Ep+`%}d4``&ne^7oYKGVeOaY^?z8fuGZ``(d9}Q zJ#t&S9W(6bDhUe9&t=YY9A+49SH17ZP`C_%_w*p+E3?^Rk|V!W40;PAboJ&ZcCeEhV>h0z1rs^{~lV6+~93Sf-D%5(!mrrSe;%=Wp-|I zZze24vz$g!H&G8qgD=4<6MIASX0tb>RiDrO&*akj#wl_s`sROAnM}6DQI9rIpEE1d zj0)X%NKk6A3_16ywyA8EYtJf4PjOkI?fp&ZmNRy0%0~xUDm$1LQl<_ySzoMbe)}V@ zX8Ub)zsg*r+U8dFi`M0JnK#fel)f+PupjbQ)p1NE`LP|}&=Whp49Pj7j1KQt=iAQj z>FDm44z5X=ZGGLO*ndYlMaoHM*%*tyt&|k!_{azaRf2ORY&OGov>`ah&$2lw81rL$ zLb3Kc?UX0@n2k{a7I4T0-O}iO_XHm|vpD>sVq!8Qy!rLR^Trm%kBy3QIMXZokDC%( zzG_BtVoY)j2XjQ*Y&V&lL1BIoAxsO3%FIkG9+TsJH84CfF?n=V?4Z14Q*egM&)+XZ z{N|Ex%RIKH7^~;|niJ3PG4lPLGkkoD^kffpLz5rR{K*IT^a;x0hjyD44+N^jcic}e zt7z1vQ>AYotYUBf4{^$xU~&e-sbHN`8O*tcQvu;oNy%d>V+Z96r;>e~`oAk5)2SCR z_Gj^^=PEZmXdJa-7)9*#&D3xfz4bY3#yE2Y!O~aQGV=uy5eq2M2*V1#8ayWGXv>nm zfml`aBjQ3XVorO4i>l9l`hgQ%w%fxJqs9cQj*{G%l=zsSa7X{>L8T=z1Llv+N-3XL zS~zWpD=^aTAD9poG`|tgWnq=lk$9w z?g`en@bJILFrKVZG+C&I|5FUNZVQZ$))~(B*Fi&chF8MyIlT;r-~YcHo>C4MTHjT} z9oJ%)MJv-E#7m`*B+Al>2%gEJRQwqjin2wx)JGXCHXfh$cn+t7AZF&WFsp|(eBU|W z!gBqc!Le!KoM9Cl9Fr0jo)#Nyztvl*K6OJ_N^DS2ERT$<)RTplGu3drjIc&4`WXg7 z;OR+F7%2NMq;E_1R;joETrapejARYC&n~!V&4|BNmqC=jP`3@7K@%Y-;7gAww2c zrKVQNwKCOvarwv*!-tO;S>B`5D$r>{%fI-%iPMo}8>5r7s4_LRa#0USBSw^$1KmFP z&*6`%R8v-Q_NSO5!d{lqR4t5A*=ZU7jCn4d*Ph+(86JT47$qhxGP7r`ZjWh4aUI{L z$`n@}6k>P8SM<*smK$|LVnRelsQoE#N^n3_Y;r=;$Sk$M`(jc`Xpk%_A$@GMI9oX} zv(NtV^ks*A>a;#%%&dqOYfy{SQgk;jQhObHPp{{1{a9@H4U%!s(i zqP(JhG4bIcLE+B)=;DC`WAdgAPfs4cVCay!Y4*`hKfeIK(Zk9|1_b$RjFgH+LyH%T z%*@D;vc|qpzoaf^%NeSYksrusN^d?HDO1eSQ^7;hw>^)q#Pe}tZdnMdSaEot6!O0( z&(>!gewfpvhE?c!)|>gBJZGlsdHz39wuUX^a$KeO&JWGPVG$xDrqO7AvmEhcoHdF4 zsG7u1*0+tI@n+&mc4&5XW<)kV;8UJ9>FYoGMjaVl9i(mvP7Mo9iVE}#iwR212)*G< z=5Gfij4q<<7?;F=e|lkry)4K-Fu6E0tso&FBFq%ynHn6BoS2?cv2aMqqLG>5p@$%X zsv$P7%O0ZB%~_eJp&RFlDn}K{tQ+apndEESlDw?i!y?e4wL#Ik#3mE}xR8f6?QZ5(BY_mzU=TsaC=R>~u$Sd=nX z6PG)aHUGg>ZLtxt(y)17R0|SQf+bbfAxgUc!}+YYzH82=9Z6S)Rt^rZ;oeN+U?3B< z&+vaTxxqcrI0-`Bq4Lu#t$MnWo+by9WroVJ?sCW)153j2)1OOLFICPSU6Pq!792aL zVEWt}hQy`bfJK)QgmLPQO8^=q_Y>fGGc$&ftw}0%=w5aH>bm~H4XP-db^+LEURH(~KKXa~> zS-C4oE-8sYP^TUUVvqv*{ul9b87#jMKVPK2GLELwILXT-OB(e>Pj1;>$#-k!__^;r zrmH(@a9VPtpC!VP=$DuhaKobLFpGVCdcTl05tf8Fw&i#`l-a_^&|^U%r}R48ieMwc(vSMVR@Ok*X3K9U0Mk zm{yXSBwO>Ta!LyB+Tq5TM8;uSjCB~3tW`B=?1T*eu*r$G_~|VzNx^}E_FzkV;t1>O zQ%8Il8e9>wDrR%TYc79>-_!`1ia*pcPrcgqYw}RXyl+f3$d{d}Tn2?^=w>*k8cMNY z=W?+si?~y!$tm*sIATvGBq&=6Rg0I$I+uYCUqH4E!5Rq73dexpBpcn=e7YP&t5+wE z$UH?3ESn>vr;Ye9B)B4UW$c#Ke{}`0;g-nT@iLg#OTVz6)e8puh1iq0@5@^c;X?0; zLH;Eq+pH1MC-dB>mu0j&gX~W+o^gtE!;lX`g(@N6JvU5046E2Q=%LFV+87#bE@c^2 zKzNxcCP?~kDD}WcvL|MYoA!u*s4pGb)j{;#LKXHbj0-lYBXT~{V5kV)PBZjG(9hU2xLT_|@jf2EWdk+%h=sCM-{0_F~nqszD z%B>N>+it#gXGpMZn$2&ZU-V_yTHg)z-{uT)xcx)FI_U7fFfx2m)Vs)71$-P2AB{7_ zdeRnG>q%#b{nHd!&J6Lt#4p6Y(m&*@_Z@yhLF79W6g2$X@^7Xpx0>Ubn<)OYe#*p3 z#>8Z9f<7&{XHgj~iA<;SZExk9tVlN)QvtJ_mwl$N&EKERfBJHbQq`Vu(h}#Bmpos! zLA|2g`xR@L6XJ_xep*p{0&AL0CT6N7^qsk8&pgN5yrG886*>C0h$(l*WVZZ-BF@6i z=zSn{gFKnL%<;^5H1fpC@jiLt;VJ3_kSINXVbe6a$jbTmM{!Av$QA%bU&Db?Ean85pQ6JgLQlr1K;n_*cgS2ziD>hsh z#Xx6|Xt8)b%Hw6q=J9ErOyRU%yFuN4j`wAsOeOdVN3A>lKrg=2$X1^`osRBa+4>h! zZHa>F4c@<;M5J1K{DEFoPAOY`X#U?>4yv-$ayI)6{6>jVhEBXs)@_L>(N~d)8qC6A zHkF8ytZ>6e#XOVaFY&?ck4TTumz1e&gRbxSDw{;enzBKqYH(Syq`sTe^a&xjS~4hj6)kdl?gSw_iT| z$dQ_>ucj!NKb68>&og9yUlS%Lqn`K68A#BnKWC4{LglRoIig(OC0|BC*_zR6VdvT= zysr4O@T{6yq|!k9Wt-2Wh#Dz!efF1ZVrzIffAlBpW6LM9CF76&MBgg+M~9Y;avA*e z3ysXq%*bY|er#pm#=@!0Ju|xd#s<;GPQ)^yRAp(jz9-g>HFwq*SKr#vbh%es;tq;- z1jWXL6pmV0HmP}1X6nSV=RO@0rB3$TYW{OjoO!EbRQ;vX)<1c{=$18Ym`o8N*=aHP zlWI$gW(-YfiO7pkN4`=MRTL-nVYK%$>-CmM{BM7UtTCPSxSiRk^GrHTL((qI~pYc61;3_Xd|$sr^n0PQl8# zAb5vlx7)@OQ>YxEI4ur~lU;jHOyuOR*uSlZEGdeh@?mMAtSimP$WHb1OOy=`WFulZ z7D3i7={*_!?409A3HzFt=u&J=77Xf(PsL4R^5!lr8@!-0HL+w$;gtFU(~Z8(aF-PY zMwaicpLJ>N;PTd=)D=!0IV3JHu;|*oIVFj?!9gyE$y1z@JNtt93(lRIRyowO#n=Tm zJ-=q`yfIsT-QN7`E2f9K@)CyRfBWF!QR4%{6R97W>{)sd`xxgbt1A6ClG~C;KeK1A zX9n%5hq}7MfK{m^`rx}9iRK%DsFxYPKOC%)*=hPfMK+V}Sl`*Z@YuIKw!UX`f$BRu z&9@TlEdR&|wi_8;T(z{yl|Eu|+0gL=(xa1u$L5^5yrE>t9qr?*&U=Y|h5r%=+=e)}K>8FBqL|KejMbl2o=moIrh$?ki7BJx_XnutTDp zf-c?j6i!=b2XmX*Qf;`AsA{Y9lX>(CB$Kv{h_dmgq9M&q$3LLN^_@FT3SU{!6I8KT z>=v8-l%FVedrt~dMe_QEr(|S=hNWbrOBFG;gk+Ve-pl6{b1FfaoL!5n|NBS?BCDt z93_V#pExSF;VJJ!R$onI*S%)4$HU!FmwVOba-F)MaZuh=yxK?k#TR#9rp{H(eF$7| z0`*((z2lC1A&l}i|3ar3zdkelnabd!*{FK@kq`#S$GNGK46&gxX_+|=zXX~WIrf;Q zg;RIzGMO1(+KiKtsYJ4PaZs^(>B39TESkFQ@rL5Y)pa?tllp)W3x;pGGI!w>%QkQB zzKVr>Nj00UpS9+}_2ul6&qnv~b?3FC&R#Mi*!%6h`}W;Sn=U*)pPAJT`tx}Atr)6i zi$Y&Y2F_S`W}+pwSZXbkaB3=1xFDU;ezJ_iL9G%Gq`q{c&(R?IlpVG6j2W{RY}inL zV{Kv4vKv-z_|z0QXUeGA-e=UK0~ek%cFOsSioJJ}s!0jtJbRn%9MX?c3M%ttIk7rH zsE~o4r6fI-z3)|Zns~w_`QRWXp*R$W%}L_2o<`Qu&=xcDn1ZH}W5d<=2Rp)@j&R3d z^|h;Fk{lV{BNV>YXNSh-%E2&{>Y;IC$;h?DvqmxUk#*D}q=N{#K(UKJsmlPD5 z5bn2IeR{VwBsspwbyeb^iT#VF$AnKVFPK@LX5@jo2`^PW50T>Kqq^!A?o?w$fox7w zCZ%Miq+6VFggf<<%?FS$c7UbS(8Xp|c60T@pyVs1l>@_2xY<;6nm3Di0s-*&S*wX% zuk&+7LiL=^QzwOI56R{LS7%&5RpzbeeYE3x)OVUnkJ&Z|xf!HBqy`VUc;WO*mX(z* zziPqaOY=(ubE2a%(lerDExhbRkyIF285(X)_CC=sUEj*~lDZ+oa_2<&g#?8-Ovyv0 z6_w5&n&js=Bb@!#4c>gB^XBCJ>Zj^^=~E2k;RK$bHhcD~H+}M?_oFACP@`<;c;eI| z?+d1bGUP?P3(Py&yx|j66nbhbfo}gfKnO2oV^JVHz!bd8s z<@J*@QAC%Ug$QZc_;{W&DLf_fxIm<4vBX91Ug%A8vAQ$4qIyKxa3*b3{ zU3&Qx52#?|a;R#S)p60`dL8ah4TcwFBsMeb>X3~1+x{337nGIIai=TE|7u54bk5_C zTj$2U9~tU>Jv7DaVOuWD2zX!@;rpTWtgJ4^z#s;LdkGpyl&da`lJjni{E2KuDruE2X%l@%QcRtwqBM;xMkG zTLy)iKB7akOcSTG=1eHqqeNxwVk>+XJ z2n5k$nRd!HLf3=F0WOi@zO{X@J}p#rR}_~Fn=xZr%lNdk2`y7+JZ_B|T#`5B&uU%P z$OXkimW<5&R4-E_{ojzjpOPwjXS#IhV(yzEUP!fWh4zY=jFm}K5$JE6*<T(Q0V4D>x)+yY#VCsv6YMcG0wl=vudD*Q(XO zd*XMdsn&Nq51J;jkreZ-(08xuOVN~Z>My*LMz_rB-+xxiXznMEpCW0+ns-{}kyeh>)zk=^ zxVR@+E#k)SD@Hj=_OO(gP@4$1InAuMwRUc#6MOuWkD>Y|3NrgZ7jnzIzukQvJ#7Bp zbl3a0#H3gkSe;gDfPIx!)`2EERtEZ89o7YwR5Tg-?T&zi(9nbc?>y7N=y30y5e0E^ z$lctKxR@~So#_K(^TO4+p|P+Sx*s+_VJcvsH-Ck_mSyaUjj@xPdAmbS3Uc*w^_-_AtKt_vXT=&CZcThlk>|Y z4BBG9%6v6V^IWfg%UOiRb8Dps2Q|;Po%DQ!Cr(c*UY^U@g1+!oo}cQ^-_*l%&am%! znFsX(ZI7Sye5mJhd2Y}n=ehaldDx)GM`wKR^GA*6-ltBYXNaVWSl;Tr!&ZzRHcaLQ z)B2k1RulH195N{Od+kJrYPah)2OFI6MHmWA|1V8?XBiB{>MbZnq3_RDT#$L|2cG%f zE)K8Trv|;?%}mQx=b6s-T*Q$ySD6~V$r1X;vQPXfou4oH_{j!oJlD_S)1SZWdoKJo zDHYxaEF16>Ig~2pwMze7=46zH;sWb&i4`fV5w_aIZ|p~lYEhZPV6}^P0H>I*+SMQT zDH>-Zix=j{eAvessh>bRxHmDU*=%U74}I$uq@pO!~!@uf3-+ zAtTax^A73J8&11R{ft2ZO5K^A(0<*ZF+uB2?9 zZ3@pGHEP15)`^vMb0<%mS31ExGWp(!)I8g+C|9N{YDZK`R!UUX#D;PG2B!p4=QOWn z3%t089&2`G29u~Ir6qa~o{dAyv9j-Tc$hC$+4jvQGlqIojXGtbJ2py^*65xjGgG3j zy|UjB68yL36|3hJ_P3r1pV<>h=A%dH8lG}LsihQwFiF9m_R(=2iq24wPvOzL|I=|Gb(2ljGH{7I3go4RCT2|Y+=&( zinPo&WmsQ=9`?Bnfga}B(BdrTAH6H;(uXr-WgJsugq}XnPe-Cbqnu~_qiM`YPl&S2 zQ8Ox_aSAkMCO~5r zK$Pc+&*wiFlNqD$2~4L8+ta7vWVOG(e@K>^7nP8iY4#6HFG>om8kSy?8J;|RafQtq z9ANFYGX4+dDSCXi_L(ExuAtv5A=hrb(sY}8 zn_&~FxA|agY-MQ+?zg?AzaOm~ zv43EFp7*8t`%5jC>hJHf9O(JpIgPfW=le^oM~(N)gM}C+M!VAXwe>?~w8|dHVqZs< z=TL`-MuwT4rsyb>>U7b3SQs|YU)aC%f)Ru;J}Xdc9NTMS)_@c1+=-){uUcd*p>CqX zY9h*oO#D@ttbrYel{s)a8gF%^sJTR zRyMdJ5^f)8O^7;Q4fB3lkYf+9svwEvCyBFCUN>;c=;# zdJh*yxcrN=U9}-st}zXdi(4A6#zaJUpSaKSa$$){{ku8Oo2}j#78_zGw~9>}s9kR@ zwZ4PTArXJ@aJFMVPx;aqQ5xbW!w_cE3I^T$ouPD`*(|9hMEoxlb4?hQzb(*it6}$? z;K1M|Nd{=PH4L!3%{R z+1{XkQj#1z-P&Bcbk>YfBQPTe%Q3J?xk(p}~P<-Y-4ex4FJ~&&5Dc5N-Hi>L!vX{gSx``P<%$T8d6l8mKhhHxmtZ- z-1+yp-S?kAj(t@t8?Kr@{i@ZK+epFmk<+i`>TDIrBMMD#!;-l2p zqH~hM!jf{LZ;Q*!j2o8cvPWhnWqW@bAD)vI6_u6~9!VLGhV@Eel&C8qGGZ2I%g>C<=qdDA1q=FeJOHSX4n=I4xAw9H*t zwRqP2VIoTkW48^`eChu^!@tHooeSj|-#znVlxKbSX2y{I;JfEg9_4M{y;T{geCWHk zaad1+@7}Hysu{j}hY};3Z|W&Il~Q%J@7~YSg4Q*j<2|u4x~7K-P-59{(6|qTnjYGM zlsMZs>}S=2=2f@amiq2h#bN)@ch8uS{WIUaS;@A4>ASZmVGft?-m0u%-&!Lc8_%!z z-P@Hj9e4Zg9ZI1yz<2Ld8l1y?_kNCp&bxf~{z_rk6~6lbWkA?dzWcyFv;`>x!@5Q` zHmz%RFJIB>%C5k*wx+b-)s%}~D8nxbCSG(R_ZYZ_M<}>xw$p%<+?4Q=94gL+UniVeL_lU+;ccW{HdsSn7Yt4$fHF_rWuy;=S&VOmMee#57i_PoZmwI_Ru6SctC1s!ZDHKi)zkTX!>YHD^jHoIH-xXk5lb+xQ$Y^$$zwba$EcAeGcZbgP*enU$Wu{N}( zy4J2hWZHyY(qGzGi@cIa*V49D*INC{T6arRef2scQDR>1Zm6!80(7^wocMKpb@OtV z)#6@X*V5lL3z@8`uWo5^*ATv`xv{CSneu38$#ao*tGlL+@X)@--Qr$aU)RI&wx*`K z<{B87=UVPwQ>T|geO+s7UGuWW=G84mLAk4&T`Q`aS2s4SGm53YZng(!O@z7W5^^J|IT&t^B!QZ+y?ppYue}o{&O?9hY6D<620fNZOi(!7vJeF}TrEx7wZG+$dF6q@%Uk zUEiY1KLi_DY%%hz3u?7%SzTSdlqgjMv0mM}A`gDMVU?c4*5*duwX`<3)wH%XBM;4u zk{2@DTD{a=?`}0%BXZu@*3wYdLYa(j5K%4`)if~AmuFq>C9lhy>#AERkX{+j*6Wod zwxX`SNyzx&I+mkLY~vc_pt!KGu%Byr{ko@E|-@3@i+PeCBxsGj?QWs4y zep;2QX+>j09b)GbO`XTOj&e~GT-9qRjcSp@Wqc)htwF$+*Zq)!C3xfN)2qHdI`ISz z-{*P#3A)nv;uIQM8L2d4>91q#!A*C6h0?0HSS4S>Xo^c2z&z9<{_=fygLt--ur5Na zV|1=jspmwU29A!W|ERPntN7Kz91NE-ihtco9dG1X!;@@1?pD$&QwsQ7tN-=aV`}OX zQ-4A?asqaNPQhBfZskjtGK1J!K(n$&kJqJ)AzXt_?-bHq%~Ka6YLmEc zdL7~9TcNW}r&r>UT&&Q)nyHK@q>D}7oA}+p-SN0jj3JMAa|qK+d~Q8`7yRiDzMI1q zaw1%9BAph($QSio8+p2%{7og5N#xR?)1Z0;Z|k9R08fSYgOs6sExew@T~FTfm7Z`t zPh7oEPNuGJOg$w!h!QPS2J8Qp5U=YrWh134yj-T|)W}6o>GY(0Dq7s+)y22M;byM2 zaBMZ#W+cR=$M=s*kRd7mh|K+vL>`x|Dku~Qaw)TrN};nBDrydBac&lFKE@cYW zRs62!iG->{CVTkd;$2VfTZqe$JduNCIzQIwGPaC2HGJpdsiZ8DA@xc)C33Qy?+kf$ z>+*P9QrieEvM%y#)XzfhdZ?^Z&W0wDVTnoP{fECVCX`XD=Ms-db~Rkc=b6-IBbF2C zlC*z_yV)nyMVC&q!40XUGpUC?`AF@h(kNZw-wI#77*B2tDH3fWd=+_g!8@U}4(_$V zIfHjXuSjfzkFza=G_-=~7lSKOf)dL3LS(stIO=&?LkQv3a$;{HZmHe!uRN8M%Xluo zIAI3r8ufIAzrrDrZkKWv&m@GQhYZP=(r@z7D!NMYx>lDM(K2oN*T+j~De>0&^s7;l zQZj9Ns%!hCSxY(+ujE#w=6FsGnwRswLCI{kIJR{L&UuP41m{hjE(ReUSdiLP+#wbN+3dU9iEgr2lT z?@Apr^skhg)Wsjx4?P;eC;>^!XwM`Sky&Zsd*oPjoIz6qcaolPQR+q`=@~jjq)SS# znLmlipwGCkJwZ04oJ5c16H-z-u7N~%v_ik=ILU*wC&xM5w-n{=@p3+%dud6Q5lbC0 z^wd!!7DHRFCjJ$A$%uSQI(_A&g||YJQMZj>ZRDy|&zFRh`rg=EE<%NbszyE~E-7u{ zLyy!*P7VDa^;h(?)CiHC@%-x1%EfTPD2;(SWqr%}xW4Zx*X6`mM+&W^Cux0Gk7rXR zLWz`LPh3(D>Y=`er~is}OzgfM9V3#lJc zW=2VfUg)tBPaXcpr|{!!Xr>v-f8Ju!J=K4V%rSPu7$PtI*t=o3Vi`4N(kvyeW?gf= zl1&$Nl~O{FseyirK5$QH8K-BHZi!C+C0kt_|HY=eeSXOaX^vHO%?-+c@BS z|0QCfM^a%L6dph(0W)8Fn{vH!m-47SMiju_6`63eL@B4QFomASVoI+8iIFc9@}Xbv zT&7>uW#f$Nn^)*pQ&hWgo!4Pp&)IKWFR3-I*ZH3Rw9xqdM5b|l;T=77yrcZKD5e8R zC;mQq;(K%WlPA14M?T6(Xo&EKVTEbd-`d87Z?zq;C-67NJ|}#u{W(XRV@Ft*NPOa8(m_}d!(aztv(9kHpg7xDK%e0ExY{#w(UGiGHxon^^dnf3UP z{biQyuoHJ1WcNuke+^FNsQquZK(%3d$^%^U4;N?I_zJ-<3T-{Gy6o!@uVKF1{)6xmM1r+*tCk=whnaOKLCPt|)@k6XQ|v7@o0^;pxIrpMd9J*%Yc z+jUQ`d%A56_vbg4H7{yC*7mLZwI1Us?|3s!-mWP*J8#|7>&LDCdPDzncAWG34F@+k zHuOK&yW#wEz31iu`S)&q@7?g`xp!|ks6X*?m3Q)Hy}WIAwLf{DWpmh8$JT?Fe6ihe zsdu|$`^}dpTz>uKe-vCX;^ud53A%mK-jD9M2x;{LwaDx;^G`OoQ;=ezgZ2m-=b#NcWcM&w<^K*U-14x-tXuALrSClVZuB@m`4fon9^W> zoN!Ml$@X6o?kU3kif~U8?is@UnsC1%+;0i@hEiaEQ%$hHr54x^DCgSWR*LQKsLA$s z)l&O=O1u4UY!%e0WZFN{p0$6XJ?n_le(UI`ec{Mc-gM+EpE>$#U5*0f9Y>+k?I_Zo za}3bFbxhNqaZCp@z$`Ea%mwqne9)vl=r{{BYo9w>Kr3hiYrtBtLA%#+E@%hmf%Cye zWr|}H*9*XAumx-d7lMnFI>*K8C5~tUaNiOyh-bFzM^$FUj={FUUj|>-T-fcx4;4LHt5uzcfJn}f)Bt);A8L!I0QZg zhqdSZ5|u~&l9XThvA?aKOMBZdh2N>lOMYqm&gMRcce&j6<6R!V^Lf{w-!ru@{a(|~ z@gJqV;r}xw!~b6GY5)7wR{#6e3;Z8Y?(qM)Qt1B+WupIsJl_u|n5Z#Pnul7} zu9RtiSL(oJ+6~Hf?Q-RE?dNKV_NqEw+osOc4yd!WZgq}!xmv9qRBJ%3cD`Dt-K-wg zK2*CjuW1~$t4{m9X{$1Xns%4zA?-8EChaQA)!J9owsWX$=TO`3@;jj2>3^Bl?Y~{? zR+tBN3JR7f=YEHZN+nUP0z%Dnwga1~q^q+P*ME&7#ow}12|g`V!9JdR0Z)#=lf&fy zsN|pgb(6PGO!sNek-x*_?RCpe?Mw3Yi8DzFauzWf8pKzJu(9aPX&!YB6R~TR5Wec< zt1d!z5;EDDtPFIzlyYZ^lIBcR!uT?S)hK?X7XqTRPbrhX5z}GP?@~*(SJW}u+v;@f z4QP2DT3&&cFHFm|BgFc#&g(0+ze3NSpyw59l6J)UHh70QCxo%D2j_sJ+7ZVP?Fjtu zg#VpTTgJ&swc1Cd;z4((DC@ODl;pS2`wlUGNz8wT=64*oD+!JR+CgUm^d@RY$wMA_ zh$9bi&>!T?BtJ#;h>|GL4r09)4p@o#0b=|;^v_cVYkyaVfHB%`^)uw@2KHky?LJdzI8mNG*fZGT@jWDaMjw1t|`5F4f+1Zqgp51uE0NARpht zp)Y-w0HDxdu0?M=l%21vS1H-k?lyXo3DnS((0Y-vRU^Ey5m=COs1LMI2z&v1O z5||98fT>^_m=0!unP3)Ro{};L%mwqne6Rp41dG5K+RMsfTE->NekP#KDoeEwlp0VA zkZENZ*X3XZ@7!P|SOx0AYR~{0$x9PB3pDe)1+>SeGfJB`O z+QE6?e6SI00vCYIU<=r)y{B9VF2ZuZ7z=Y7DY?$?10fHX~{Y1+tlTeX*wg3nAB5oQ~|uRuRt&HWA~%yb=L zuLn2qd^cfk1wR3Mz-{1muovtDcYr&=UBvrSaG&-mQuV$C{#shKgVqS}OXhVy1)kA9 zKz={7{s}w}UH~27Mer82|6S_l2ij}aPqg=JY1%^*sw|%9(Vqc|o+3Ue-&;Xjj zZQypW7wiLffIGomw3dMY`E>+?5D*H&Ksbm1F(4Mi0c6~Pj60BVM-oT|$hw35NF1pk z4Wxq%kO{ItHpl_FU?6DL4$;aUqLn>FD|?7m_7JV?A;&K5Eys1>dT;}{5!?iB2DgCS z;3r@YxDDJ6_JV!j4sa*93;Yz^4ekLy1NVaa!2RF>upc}G9tMwqN5SLTLB|u|N$`~R zisM({Y48kq7CfiD=J*}>J@^B79=yQw4)7v)3H%wn4E_RM0k4BMz?5;Q*ao329AI(@HzMb90kWfH~12K1-=H~fNy~Zc!8$9 z>{NgXOu!5*zzS^WL_2T*CkO;VAQ*(8r9(j&2nWcgGZI8=A2?${97q6(AW8eynatG% zQa~!m0GS{gmqZd&_w#xD0Fu=oROc;3{x6*a5E9 zUU%*S*MaN74d6y_6Sx`N0(OI2!B4;*a2vQC>;?P49pFxY-f^OLoOgqJz|X+F;689a zcmVty`~o}(_JfDO!{8C{D0mD!4xRu{(q8@&JOzFQo(9i=UxVL(XTfveci{Kn58#jB zPvCj*0_XrQf|tO{;4k16CD{2Y_$wPtybj&~Z-TeL0l?ZK=evr(^F7ds#q&Nm2tEKG zfser_;1Ku}90s2g?-$@GI0m}Gm)d(+x(EF}(z^UU2A_aK;8XC0_KM$8a13;VFTq!Y z{~CM)z6BobJ%4loEsnI*+qlvH3NyKZ7S2=@U@wrVaFWBe))jVM`;J% zqaAqLXI&gcuYG9LDaDFyVy3?Mvr9mk_84g#l~xqX`L9^cf5jdQ$Li_Cg8K|>xt!Rp zC8jq?w*$*(vg3dfisds4TO|x@Cj@J!f*9+ZOBI_)D;D&t*or3Y*TnuZvA>G7JP4b8 zAU69zZ1%rkC7-2E^?rnPTuiEmN%b)H`T*?pV(j&AN&RzD7h64$)K`+1Z*@y)lU8im zrmeDEs~y8SF2QCW2vsXdu~@0lQ%u)3VwrBFPktYk(mwj+_n(%E;o1zf0*u$z`>2_# zo`EH}7#yc+%L!C1(C*hMTfz1FshbaVbA>wki5uxBZlsU6kv`%^`iL9pBW|RRxRE~M zee@CUqxITH>$Q*8YagxGK3cDRv|jt@Bi>IR@qYS<_tQtbpMK%}^b7B&UwA+L!u#nL z-mmB0K`wP^Ijy9K_1=Z`-sQ92JNe=>O6PN==2fKT9jx~^u}wR$-d`n;V!e04`5x>2 z4J7G#toK*3-n+2gyRhE7u-?0{-n+2gyRhE7u-?0{-Vb8EALQhbzSjFcYq^J0zR&4B zpECR`)!5Uu+BeWX0owP9O|34$u3mxk-$yU#A*~a7--O<;u+9f#TgM{t<K>C9MPzcTdOTd|+ z8Y~4hfL;~0s|VZFgYD|UcJ*MpdazwR*sdOIR}Z$U2Yc0nz3RbM^D2R?TxS!eR{IJ&)Qcx+pJ^fXUoBX{eLdGE z&4VrK!3OnUgL<$*J=mUJY)=n1rx!cZgPrNY&h%m{dMsCJN8#EV!Zqwh4|VJ-r1&fB zMh`Zj7n{(7P3XZJ)qyvv18-CZ-lz_|Q5|@rI`Bqy;En3Q8`Xgqssk@n2VSTSyigr@ zp*rwFb>M~SzzfxZ7pemc9)tffuR+FH{E}qYk_>`|--` zcZ|`zj;rdzJHcJxr{HdI5BM3l7u*N# z2M>V#;34oZcmzBO9tTf=CjrY{vCll%XCCY`5B8Y{`^Kpd9cAe*kB%PFb_7E$5{l5 z!9Xwwlz>uD28M!RU^plT6`&GSfe~O77!AgN@n8a&2quBaU<#NDrh(~TCfESZ1slO8 zZ~@p1wg6-tyU2rG zqaH)&y@jrRl-A&NEP)qje?KOb4`~68(aIImro4hy{RFN04k;X?ReOaN;215yF5s(Jg{BeD)VtUbL97Td;Y}~o z=Hi3cj1S^6{0*D&BW%WZunlb=i(Z{bEZykJJJE~l@z>mlzveo)b3U5?cj}qi{j}Og zuqBRQ!C3I&?81k04L+P3p?@-|=96li73s+o-RzsjD^A)ydS=TI%Wm z>gq`9=2EPkDC*`?>gH1FW({?79d&aZb#onca~*YaBz3c%eq9;;y5aQQGU&Gz({C%G z-!_OoS|WY40rb)SN*`?ieY64e(TW|PYMbeQZKnUVnZDFE>gsIj>HzBKJo-(=^p{Go zutre-_S1SzqxBjBmrAg1X87 z&&Z=*JVrferB84%eS+(d(RSqVeeBS9JWVFXFU)vXEx?Y)%>jZyJV*eEzyOXA?LJH1oSvyNH&VIX_wrUZfhl zNHutoYDD_*BJIJ8vcy zjuyVoC&iB;#gC{Xv@PmLZHGFF>u9cHw9C{9+CFs>I4<2^A>Ci;(tS|t!~->xmcQ); zNgsvXJ`qd@OF)Y@6ED;(EC4sHzZ(zKE<8}Xumb9^0_N*A<09T~4IZdjj6to# zBB;ajv=`6QUOZ2?<9XVP=V>pVr#*O{_TYKigXd`vo~J!{o_67R+SOab;I-uf?Ewp8 z7nZHsuPhg8FIX<(dNEh2eZSQ9;*Hv)*TO$>|Gah!HSrcaQG4)2?ZFe(X?<6li6?3n zR!19FhZ`@{F1%1Pu{_$bJle55+Oa&^u{_$bJle55+Oa&^u{_$bJle55+Oa&^u{_$b zJle55+Oa&^u{_$bJle55+Oa&^u{_$bJle55+Oa&^u{_$bJle55+Oa&^@p^UQ_3Fgy z)rr@u6R%e%UawBPUY&ToI`MjS;`Qpp>(zj8v#Ou|G z*Q*n+S0`StPP|^7c)dFDdUfLU>cs2SiPx(WuU98tuTH#Pop`-E@p^UQ_3Fgy)rr@u z6R%e%UawBPUY&ToI(6-Joi^c=8tfLH?*=~sd%!=U(e5Suec*oZ0N4*60uO^nz@y+P zZDz0LdxraG!E@RyJX*8xXwAZ-H4Bf{EL|&h@UE|Be3{>W0k449!5iRo+VVZ#b%OW7 zLGS@U_VH-V#G^G6kJg?OH0lv;FCMMEc(nH7(b|hgYcC$HJ$SVC;L+NHM{5rrtvz_O z_TbUlgGXx*TKJY;Ev#zuv6$xT+StO?3T$X-(aa96P9PdOh-)zTF`9am=Ek~lW8Jv1 zZroTmZmb(O){PtM#*KC3#=3E1-MF!C+*mhmUAxcLX6hP#E?3d=^SLen3&9d_CODnu zU(UM~zztS{RiGZM1`XhBunw#T=msn)HO!NXL8hp7e+(=I$ryPVg7>%k4+MsO3j8QcPPgImE*z#ecLxE<^T`@kLG zPJn*F!?X(z(=I$ryYMjW!o#!+57RC@OuO(f?ZU&f3lGySJWRXrFzv#_w9EN0cmzBO z9s`eqC%}_h8lZT8lZT8lZT8lZT8lZT8pI3;*D>C@j`7xYjJK|1ymcMpt?QhJ!RK0?wAfgcby$^kSe12H zm33H^Zojv*eYE5s`+cN!`h5&O0f)e+;0tZ0wD5SEX5wj@iKl5M7HAt5Xd4!28y09A z7HFG4b(9^G3@hywY^-;%u|B1hdxzHP4O*cCv=HJ;I)Lr;Iu_ekXl?N&{T4-!)|*4nGIb23VJwmP0Y9j9~lDf>@g&qmO`J&pDD zJFK@(+P6;Hw>;XkUt+yIf%WzqtkL(O;(4gp3Kedlf_CI@Q1UhUZx>eEMl7~XSZfop z&>p}-djJdV0W7o!u+TPPq1}mfb^&^56_(is{8!0XTW3?hZ>An^p#H8VH@_!0Iapdb zSXvcWTEnok<~lQ!aB{eb92Vl=io~L7rhXiyKHN_oXvd=ZGv)Ifs~Jqp0P$mNE(An@ zGVMIL5DXXY@Y$`8z=`YO#0PNVeK>JFIe(R$OO7M(dmSU!FOusHa{U^)mhs#p*x-lA z{afT-#viw1e;*>}hplgOeMdWl{e1}g`w;f`A?)u%*x!fXL^+(e9ZuX1CvJxmx5I^- zbkChxyOJEw@#Q#!9Lp$a0Xfbf$5rHZIl0Xxw{~(WzUAA=<*$`^aPS*` zlVSk6=vidCo0*L!X3|7y-;(MaEV`>nc`AOCFG%$o_*e}e?}d*)gO9bRkf^)h>&;O2 z6x2QClc=rmb_-Nq3YD)x-Q`d>2I_|C5{1tLsd`%H?ep5zSR_{?T^G?WC`Q8S=od^x z%9?3O`;(Ij_7wCg7ABg$nr5;%&o z`IYlYZ8_TNY$Wk9xc5hD{&adwMy>uUQWyyLWK`7)_dbFv-Ec*u>nL34fD2x@Amixb z-z}uK@)eW~qKuAEC&HjEk+SLsbzxBFqO8UuAu;qeK4padG9+TV_BVBq_JUfX{Q+<4 zKKxT};h%aI|J3vNcwfOs^*mnGkCE?>DT9wG1DWmgE&ioH;$QkBDSzuXQ~T6^6mvTw z8Rwt-J+Xd9tcU*LSY_73N6`2_GD#^CLdP zM|uQ6we=Y#86L&Mmw5OP>&#Z7o%zZ(_>t<|ro=FF(!?r@Lgq#e$NyOlDnKQu0wcgk zFba%jGvhH}EEosIg9$vJ$aNCe$zTeY3Z?-xCaciGS-TR>iimJ!HkbqEf_Y#*SO6A+ zMPLauoC&JIQsp{kyXaVcNIed4KQp|XF9^>74jCYqP zmoX!ByRuassBBUPDSOlsMt;}WS@G5 z@}RmH)GD8-b;|m_oHR8lyG>_-bzp~5V!Dp&^Y`PWvgi(k+TyF!nbH5kt19yNs zm2gX|vfCN~1}eX>4g$YalC4iE;Zj1@KY{1De*xg(wY~_xP&V1pmEBT$j>ow^0iFb( zDB+Gz!C~+jI6?_`fzQDg;3zo8^KS4Z_zHXtz5(9?5Af1Tgebe6p&$%|188$bf@tMB zXDo;V2_O-O^dxh2ffSGmGC+?+<#8`kRlrpwtC)BOf<6e1;d*w0imB+YO9^+nljCi`ncnP}k5_IDw=*CMx>j9>N z8DJ)u1(+X&m!KOjK{sB4ZrYS?yae5N3A*tTbknQvrkDN&J@ohJp?A?kKZ?Hi3Vrc5 zZOq%~jF-_Fuc9+#gyb*ii*M*fAEoDflpb>zJ?5kIn7ipQchh6;rpMe(kGY#3b2mNa zZhFk!^q9NpF?Z7g?xqLa%{+{5dcWPYHQn@nyXpOQ)0^q0H`9%Os2l%KH{83Z+O>d@~-b^>WnQnSB-SlR<>CJT0o9U)E(@k5`jWzlO*60^lobO?AzK6xx zg~i#0#o2|$*@eZ~g~i#0#o2|$*@eZ~g~i#0#d(x=k z{(;u_WjOK-wcVfE9ztyopxpf^_a5!uMSJ3<%$uoga~%&+;|?Ij33#%SXycQaSLRYC zBl#sr{%EAW9z9y;%u|Yx`j3(LFj~48q<5L$YpmRL^pPk=K9xYeR{0`_&`Sv&j`dZ@ zioGS^Oi-;{i+0OFyKxdN*A}oI8jtJo&(J2m|4Ou+;DzYIF=#{gr(E6v{S|uPx+jXdepSrUvIwgLA0C zIn>}BYH$uUIENaXLk-TM2Io+NbEv^N)ZiRya1J#%$9btz1`lS^S`;!Oyab#Hs?qRE zl@N4#2s)j!LAlCiUn_WLEQkXM%GGc{g##nuz#2HP1rBV316QNdLMYu3ofCOn zrMwEb7AfQ5h!c)D;fNECIN^vBjyU0n6OK6Hh!c)D;fNECIN^vBjyRp07|pv(iG?e5 zj51d%XTgbhI1%sTM7(gKZ?4H%JQT-6aXb{qL-8aZ#pgot6;OOR6kiC%txzoOdc2R~ zcpt^_P<#O?j>c16t<3EkJF$%+@8=QICB$?AF|8-2wmzl5g)c`?LeO z^tETC9%LaASx7_{5|M>OWFZk*NJJLWkVQQRMZeiO51C zvfyVFd3lw*+yzzVla~v~OIe@1yhVv;QQ}#Ycorp|MTuuo;#rh<7A2lViDyybS(JDd zC7wlzXHnu=)PW)7f4W`=0?1texeFk7XHfEClzf=4}L4&p$9vePF~ zqv60>IItBCTmlDnAW>mRRG3eq!h8}XGioQokpMUn07nAgNB|rOfFl8LBmj;Cz>xqr z5&%a6;79-*34kL3NJSY^5f4|E|I>A#9f~i5VyOe7x0=4E4s0exqYlhFNs`A=`kRPp zyD#tO64TjzO8+9h9I4lVX(xSoAz#Y)d>vmlL&Mp8c|o5q%gJpXxeXwO`DpQ}+GBlU z_+g9OPMh@st+!bBpJ2xy!j9M5C#-ws0rlw<{Fb);1KRb!^@;y8Lj8sHRm+qCeV;;W zXb+9ZSef=)Qh0^$U&A&yMjRg!+DmGTwJUkFh95g`(sfoO@%=yc{scOz`tAe&Zzf=p zL8~o3wYJt)1(&M0*QJUpxLbEytt~2At9@Fl)}`9IwC-yw)gmq^n;=j@am5t|2)kiI zW=Lid%r+AsOg0D-=Kfw2(w6pl`Yh-F{Cm#toZs=hy^~?)-aFsV`?G()-#dd9iI|^q zojmM+`)fMo7lqZ3GvpV8NNLX zj&W@8l04*N(dSF@kdKQ_4Sp{Vsp}op^BgY-8bv3oMW%1dMdryx=804`7hN4Zx{Wl~ zvw}0&gc)qYjEMV+xGy=$sqDiH_F;yc0eQu$*MDoMf<^WU!oM zaAb<_y&CMwp3GoRW{7@cqTiU@3XyZ^7H}4!jHR!EAURR*F=sAQ>Dd zk2+2sb(}ovIC<1@@~GqFQOC)nhRUOc%A-z|N1ZBSfiF$5eVAm!BLFe(a1NIpbJojIm9#o$(WulSR%r-x+J2v5AdpVi5~9#!i)>jM~SC z&ibLVcB;u_ob_Saxb>``|MjfLnz1(&{cEsd7MT5%Sf2r z>#P%P^{mOd}@Ij^;w`{}eM=m-_3Z>x-*&V!vNhq}P^w^qpXbi-AB@-2L` z!*%QYqB`H46-<{KXXM5ixp78roRJ%6WrK^Bd5;DsWWowjGQ_nr_RW!Gji&ToH`??&d8}Va_WqnIwPmf$f+|r zgq>=*o#s?^>JWCS<#v|!gZ}V{ocmF*cC5_WFWXD`#j?6$SzWQLu2@!AEUPP))fLO?ie+`hvbthfU9qgLSXNgo zt6s2LyrpuPxwP$5Fc^KI*?gLt1oyw4!sXAti*wJ(w$$@@IW z`#i(@+|B#E!}|>4eFllU2B{Sc;(Z3mCI|68*NG8_(X?Bf|6p;$F?umSavT9yz?EW% zt6(Ht13!jqy?-5C4>!TD;1RLhVIWM~X8p6=&Qm&NxGyakV((3~|QQ;*4*L zGk&82@>?;+Lt>2k#26QdF@7S(_=y60)DGnGW{}{&q599xb@&Bcz74p2D_?|Dqmq7KH?;gf?4>Nj0 zjowhBH`M42?Hj$JMz2t1DXicdYV?L0y`e^LsL>m0^oAO}p+;}0(HqKl597Os@!iAt z?qPiQFur>j-#v`)9>#YM@9CJzuvy zU$;G9w>@9CJzuvyOIB%3KFQGj#=>ayzEfC__kU4U`byvL{jaJ@yZ>EPX}fKBnEShU z{vZG4uQ|5d*~$_&ywE|I$A)*aVoQ3h{?@FaljZMZzYD%NflJo}=lmz%Xz~p`zur9K zAHT76b>gRLZ13_ce|C*Z*C=#Smbl+~=l{(43p4SuuF>Hd5%cShalRw@p5eWB^_N-P z3EwDWJZpWU(1q*j{YGIOVxddZ=)7CAmA( zyIJnsjn3WZ-0PgXrT5&0S?GmaYn4yw0$5A%x0t1~aL%G4E8))y%5_W^nn|`a{5V*y zxA3d5UdDq4dUUzpwdjKmHCRG%AEE@ctVg29|~55M+ep6{y~lP4ykazARV3* z)bc}hqMp^^zQJet7lV4|X@Ew5H-(1=&0c5nvx01Re9#ge9IVN2@LSH%JG@ix@NRRf zhnrhH+}!H3%&k7lEUY1BVVzxcisLZH;o%6gu&y!->#E{$;ez7v;jP6J^5cpphC38b z3Xd+Hl7F%IRha6%=`b@t&djHunECV*GoNlZ^J!wg2OS?W%PAcU>Ax{Qu77t}+<#NJ zpycrI46F6eu*&|7k~6~j!KP$V@ z>zneGWjE*7l>O4{Tk_+gUk&F*cZ08+b+9{p1HS3+JskIhy#}_PtYlZ=FHCa8&dbuW$94+k(;2+k+v|U-`}* z;i~AJ!7fqroTI-9PL1B3e=&Lw-0OGzHasc%yKs2)KCkbG2jD??2p)z<;8A!id@}kt zJmK?C`uq3r2Y3ob!87>{(LcdxcoxRMbMQR8058H=cnMz4ua1s$91jyR+=+h zWqnFQHk0HbR-b?vhI(&oX=V*RgP;aGh12l%u(fCb4v)36>W|p^U(aBk3-tpEh z&Z?s0%oaW#P7j}CpEj~jg&hN)vF7Iw@|8c(@LC#PUpygPPrDZsPj#FQGp(e*KkTN- z-Mw>`(^f-l)C=Ae*3z_EnpR8GYH3<6P1{V~>y*2>3Z523d)lHW!b#vMLA0kW>PZmM;czOP24>?& z&BmuaTWHUgsM+|mXAAAw5;Yq?YBoOYX{J5Rw5OT&G}E4D+S5#XnrTln?P;bx&9tYP z_B7L;X4=zCdzxuaGwo@nJ#z2PQWvx(MhqBWan%_ds2iPmhQHJfP7CR($J z)@-6Rn`q4@TC<7PY@#)rXw4>Cvx(Mhiaul3!k=I?JPTvsId~pkfEQsbyaX@9I2aET zU?NO{S70(sfmdNFOoQq0jy*Ke);b-{XQueec(8~+ToNW}%X-?YC3@QcsW zj6BU)Pcw3SVvZ(s(1QPTg%_sx@M9Tke*U_$i%*L4NpU_Y&L?f=lQ#27aXu-|CvE1F zHuFhwJ}J&8#rdQ-pA_el;(St^Puk2UZRV5Wd{UfG+RP_y=9A(yU|H#H;UXH)f&CrW z-+}!drK7^t>|Ot0F`H1yCRDHqe`FJ~eKuhQoA9__nq?CTzqW}@Xz+^{vI+073GID0 zVVYmw_R2GP-#?dVi&IKvkTY#gI&0eUAT^2Xza5KjqE}ryU@rkG_nhg>_Q{E(8w+{ zvI~vuLL_V1Z$g&Grb|K3yWZ8u*yO3oU zvg|^ZUC6QvS#}}IE@at-EW40p7qaX^j$O#H3psWn$1dd9g&ez(V;6GlLXKT%U>6$L zg$8z^fn8`|7aG`w26mx=U1(qz8rX#fcA6$Lg$8z^fn8`| z7aG`w26mx=U1(<)+S!G6cA=eJXlEDN*@bp?p`Bf5XBXPpg?4tKon2^W7uwl{c6Oni zU1*Q~2JU5Jeha^Y``~_f03L*g;9+_V1Z$g&Grb|K3yWZ8u*yO3oUvg|^ZUC6Qv zS#}}IE@at-N_Js#uorvKO=Ie4N-KM?l%{+{QOY6Nb+gqaKZ^o3L~f@3l$1a-31^5v!DKwX(sN#3#qm*9XKWM~h4L zi2gb#5tn?;bJHqVq7`&4&W~5JR8{IW z9V}rDR=U%?JDpNIC0twlDopj>bePE!+#haW5jOZ0$Fl}2#ryex5-D``MG9TUy6dlz z!aVE!X{IP(C2e0x+gH-|m9%{&ZC^>-SJL*Cw0$LQUrF1mX?rzoucqzQw7r_PSJU=t z+FniDt7&^RZLg;7)wI2uwpY{kYT8~++pB4NHEpk^?bWosnzmQd_G;Q*P1~z!do^uu zq3tcSy@j^7(DoME-a^}3XnPB7Z}}HQ0ri-1GHtIHLl@!Yjuc7rxTA!r#Nm`$z^+{Tvr1eQ!pQQCkTA!r# zNm`$z^+{Tvr1eQ!pQQCkTA!r#Nm`$z^+{Tvr1eQ!pX`kWQsE}?b6%Zm0oE_X*hSd8 z#A@XVnq7(AE6voYf@+_t37@3_9oXN2{T;p9|C#8Z$-lMeP8R0z9HXlK9XLVGej=Fh zt>#e3-wU&x5}3XQ%hzN1dXYk52Kcgme+-lTMH-mhRdRS(QE~*F0e_1aDntww+e8c% zB8CbPLxqT;Lc~xZVyF-?RBRJ5REQWVwuu-j_{PN|hL8EjS9+s{dmXop93J=g6JYdd zOb3Q`VQ3eIc4252hIV0S7lw9WXcvZd@fUgiB9B3NmMza;clG$cWakaT`gw znUpI@xt@eSBjFtT_BQ*rn0;GJ(k*P;VzzAoiLYVT5~O|wsb5Cwmy!AiQXfI;li0aV zcCNG65>E&hlYBcHw~_SUCjGbBxW#PTVm5BE=%-us)6K>m#l|hhf>Bs73JXSIL5sMt zMcg}5H7S}5H7 zSP$T*%z^~nLRs}J$r;bI|>_{v9Y<&_YPpscE-lHvGHweY{AAB zY;3{C7Hn+6#ujXB!NwMBY{AABY;3{C7Hn+6#ujXB!NwMBY{AABY;0lA66{%mJxj1> z3HB_(o+a3`1bdcX&l2oef;~&HX9@N!!JZ}9vjlsVV9yflS%N)FuxAPOEWw^7*s}zC zmQWAq6vb=`Uc&f1JJuua@51;T#`j?SI^I3Urme;L9;{!7^=q-dVB6ZUem&Or@a{SG zZ5{TvVtxnaw_<)P=I1cK74r*G&N|F*6X!P+@_r0o$BVaO_+||6!tf3Z@4)a@3~$Bo zR`#tB%eNN1H#@Y59ooVUO=5>St(e-1sjZmW zim9!b+KQ>InA(b|t(e-1i8)NnVPXywbC{UJ#2hB(FfoUTIZVu9Vh$5?n3%)F946*4 zF^7pcOw3_o4ij^jn8U;zCgw0Phlx2%%wb{=CiY-r4<`0tVh<+vU}6s@_F!Vq|1yr= z{7>WPyV+qG9wz26F^7pcOw3_o4ij^jn8U;zCgw0Phlx2%%wb{<6LXlD!^9jW<}fjb zi8)Mc!^AdBY{SGhOl-r%HcV{8#5PQ9!^AdBY{SGhOl-r%HcV{8#5PQ9!^AdBY{SGh zOl-r%HcV{8#I|0K`UPxTi(y^83`>h->#(ws4K8@fBiPMvTbpxwI39xvo%zKoN2vmP z%xQ}A!>e$uMx?F6#R?bdQ8l8;LO;KTKP~KvSn#KDoXh!Efmc}?eS&Let-p$kIal?1 z?6@;DIKx_JsCI@WeO-p6Z*TDJR^QIEy91md;|$+t69za-#%#z><-divd!_4F$qEYo ze`}p=J>6)e8_jg1kPkQ03%lQnZjTJ#;0@m34c_1lX7UCzd4rj*TA1m)$W=e3E6sGI zk&ZOdk!Cv5Oh=mOM>Ebh<9jo{H{*LVzBl80Grl+Ddo#W_<9jo{H{*K~zBl1}6TUa$ zdlSAl;d>LlH{p8|zBl1}6TUa$dlSAl;d>LlH{p8|zBl1}6TUa$dlSAl;d>LlH{p8| zzBl3fdVF7x@9Xh>J-)BU_x1R`9^cpF`+63$5lA;?_po+K5{l zacd)PZN#mOxU~_tHsaPs+}em+8*ytRZf(S^jkvWDw>IL|M%>znTN`yF<8rVB%e0EM zs?ilnW6NrOxsZot&Br)`6uu3|>6{$j+dXMA`WuaYLFQd}wnBySa}|lt$$u^R_w>sC zd|GxPTm%=xCGb7?K3ocy!4Kea_#ylVM!*$tC0qp~!45z&>kgT9hs?S|X5AsP?!c|% zdwI1pTfD)q*n(SY@T#CC8+)~+#;;1#5IcF&mNuMf_G=0&Y(B&L5B!S1tgu<)SA5Ry zZs^sRNtjyetjnGCEoW`S*luIgwueVwC4E8Q}rFryn z2?nR+Y2);4I^}BP^lUndhMQ?24iU?Y(v#iERhMJ%a#wy6dzWMHa_n7BhaSV=#o$^D zuEpS546eoCS`4nm;93l>#o$^DuEpS546eoCS`4nm;93l>#o$^D?#AG54DQC@ZVc|m z;BE}=#^7!Y?v`uykbe*P_mF=N`S*~25Bc|ye-HWhkbe*P_mF=N`S*~25Bc|ye-HWh zkbe*P_mF=N`S*~25Bc|yf59Vc7MnaIHrXuKI#q0Pnb>5r*krSQ#d%_rYsDs;#U>Bo zN&`FenAl{q*krTV~nxd^K+M1%RDcYK%ttr}?qOB>~nxd^K+M1%R zDcYK%ttr}?qOB>~nxd^K+M1%RDcYK%ttm0dD7((bF*40AH;Nmw=3H%^o3#X|C*yII zYD7(MKQ!r`)mWaStDn-#@3?J$}uV8eFL`!?a+ND=@l&O|D~;>#({3s~fPo0izo* zx&fmbFuDPw8!);7qZ=@~0izo*x&fmbFuDPw>oK|>qw6uc9;53qx*ntJF}fb3>oK|> zqw6uc9;53qx*ntJF}fb3>oK|>qw6uc9;53qx*ntJF}fb3>oL0ie`u5I*yK7=?jq$b zQtl$i9yi9yi9y^snkF(>Clio7YTSjs}V~=9&QDF{VAqw~uiymUrA7RrUVV`PAvxR+{ zLy|2d*+P;nB-uieEhO1Ok}V|JLXs^c*+P;nB-uieEhO1Ok}V|JN|LQ4*-DbFB-u)m ztt8n>lC31!N|LQ4*-DbFB-u)mtt8n>lC31!N|LQ4*-DbFB-u)mtt8n>lC31!N|LSr zqiFJcvHgW`5nK$H!1v($a4B2{KLGQ}*rralsgrH$WSctKrcSo0lWpo`n>yL1PPVC& zZR%v3I@zX9Hs$}ai<85)9Jb}KEr)G6Y|CL=4%>3rmczCjw&k!bhiy4*%VAp%+j7{J z!?ql@<*+S>Z8>bqVOtK{a%@V>D%ll0T3l>a+1uCLjAfg#tPRUP!?JFkZWDIZW7TGi z+Kf$=*i^7Nf5e)?jEh3Vxg1*xIZK-v+@Ip0eJydfomXyTWyU&Fq0-aF%4AuYH8{Hl zSJyaehJ|^bh53Mm`GAFamxU?JFnOI-Da-`tp|6jR2g4z7C>#cd!x8XpI1-M6qrtjdTx`L` z7F=w>#THy_!NnF_Y{A7ATx`L`7F=w>#THy_!NnF_Z09H2`N?*EvYnr7=O^3w$##CS zou6zM>1A1!taCxy0SN)EaZ-zMyuOM<&9J!+TUWd@?@yciM%9ndq&Q=@PZbS;z`j*}ptO}QsGUru} zY46t>jXE~4o+oU8Mz*rqF=ISij9$U+(QmA5H&TV_eY??GXSCKCt#wB0CZn~^Xst6^ z>x|YqqqWXxtutEdjMh4%wa#d*Gg|A6*1CV89qayYV=Pa@GNMNs(P2jPNF#cV5xw1r zer!ZIp0+p9_Cj5^z~6S#$&gMVGR}}p1F5v*Zw>y| zupcSj=X2iYb6UQPmRFKghLkd-lp&=I31vtqLqZu6%8*cogfb+QA)yQjWk@JPLKzau z{L8fa8QS|N7!A+D7Q7z;1K%PS?15pajUV%XQSOn#O~~4nte>OkG1S$Vcl;z`&g)k7oKSlmrs1LH@>T;F@#R-A+C(-LWK-en)nwB`HZ87Fn2*p ztZ*kQ+{p@evcjFLa3?F=xlO*^N~W!3+DfLaWZFumtz_CtrmbY!N~W!3+DfLaWZFum ztz_CtrmbY!N~W!3+DfLaWZFumtz_CtrmbXZ{wbSQ=zhfH*@fCo6;E2&;kJ;KeqyAT z8tJ*@xrps5R0!(tRen`_#Ap=_-msHO6|2xV~!*6#A@N^ZKN3 zF7(ZJUH47#U`#w1bN#}+_vc*y6W4#o^*?a^_r-x9ivvG)wl#&SP;W)(3A$b(W+?co z1$ed!&la%EJu>4huJ&0?_!ctsYifc>zZ=K_`6yKKi)ijsl+)|udigQbGZYj*x(dFog0HLK>nixV3cjv_udCqeDx$~1P&gh= zfD^$z^K}(`T?JoP!PiyrbrpPF1<$_<=T_m|Dx6z|bE|M}70#`~xm7r~N(TFXHD_px z40ekQc8d&lOVrMyGT1FL*ex>HEi%|GGT1FL*ex>HEi%|GGT1FL*ex>HEi%|GGT1FL z*e$$79sbwhe;xkU;eQ?e*WrI1{@3Au9sbwhe;xkU;eQ?e*WrI1{@3Au9sbwhe;xkU z;eQ?e*WrI1{@3CEChK7f)tVY}0n!+>RR;>WNnuv;CVbqCkDKu^jgN(WP!{6j$N0Da zA2(q_2Obu>QCsz5oii1xlI_maMK3mCcO7;w?Tb$f5#*w6&hS~`48c=DhkMAIdo(}> z{RJ8BjwU?II<1XW(! zpS8F?Y%6YnMrZ<-yEx;Rg%(&7&MRK)xDHxjy=XA!y|yq}+zuVG@J@es>6C7OjXq;< zhGKg&6o2mZW);&O$1RZe?_t>1FR{YC%QFkfP(Psxv91dKHQ%8a;D zj4XW#-hf%~9@zi3)E>8`Rgi?Ns5J^-(aA8zWrG}tz{y_!(EB>0WuDbjb~ijB-k)dB zh&p?LdB#Lp7IM%A?dl;`Jlgy$qSfIXD?(c=L7?!|N_*7Na43en4JR0|u zsM$F9K9sYv`(e{hJP2_%HobvOS!}uqo5mIGoWGyck1iULpH*~Fus11>#PV~@@cxY% z-uGbn6-N0`qj*-ov-3YSa>p38sYYv8qq1+wn(*>ca~qAwWk%#8Be1&>_?ln-dD$S( z47xSkx$KF2wO={HuQ|fqzu-=P$A-@=9W!;S9d0ayRP*^7){hcnM{=07`IjkB$^&qfFB>~G%c0MYf1v}Y$( z&7H+=`-&lk(7S`J1-yVAxBIuVZgAEOSe$d_9QJhht!@2Iv6Hu*lrd(Z-<XYOFVJMc^c_rH_-pX#bpT=ir3A9dA-T=9i%?*AcYnq9V+{lNy= zk8100_}Cd<^Bd~?hAPjD>4e(-X<^GGZ~(y`qvTO z;?6&I=O4QBKf3F*yMDx7AMcFk1rvQ@QqUPr3%XRIyH&6^!4}Ai>H7N}rDQcg#Ie2K zwFB%ZpR_9+%a>#Max7ntJfC9uYFWMv%a>#MaxC8yEZ-9>UykL=v3xm}uY={wv3MOU zUXI1fv35DuF2~a4Sh^fbx0a>rVCi!1@-w!%(YP)W%`OqmE)jPw6L&5%zKhuELbl(# zQjtV+#qWmG@F!RNK{&s74$KYTU?E>Gp6B(F@U3D`pJ63m$FG&eDSy}dYy&hx6IlJl zQs!9797~yFC3CD~j+M-@k{v8#J!_a_1#_(66Rcp470j`KITo;!_3L2$a{c;2|F9l^ zTUoyj*6#_{FGu#DkbOBWFJcArtY8kO|G*07a3R48=I~)1E7-vbcHl)PE7)PKN+oVA z>i5TR1&f$r5j*;m7pu5{Rb0R-F2I?WaAq{lbd*|S!$Q8!LQZ5M8*r!rhZVFoyf6}Z7gIP3z=gfJMgT7h0L*#xjyaaU?J;qt}{9#oEDu4 zuj?kf8@?5N4`##r@KLyowX9$*bF5`8Ygvn<8P+n#TIN{G9BY|lEpseo4tLk$Z71G# zu#$NzUPJjxe-i%!U%w+w+ezejazLR zFYzQecf6T~R7Q6VKXS+K)8}P&k_p)z-pE;3I_nx|U4&~bbn9z$>rmgwcsg5$Yxj5U z0jkwIvS6MShw0;dvKAx%!&4^QOS^k%cQ3OstqIGvGT@|e1*un)`YO_XsaMkHnQdE; z?zZ!lM~3f^;2ihd<_!ubl+kSoUnz>n$oi|{7Jl*e;RCez0cS15E_oW9r@?t=&(q*~8r({Q^E5b5 zbMrJePjd_LN?yD&n%3rNZC;$Rh34jIZr)vur@eXFo2R|4G&k=q+h}f=sA^TwvGz4Q zE_|zKsN?aDr>iUXS$s15vz%eNoMAfc?WDO6(A)=TZI0GHP&{A#cmXVjD(~?Ov^Gy` z^RzZkYxA_Wp4R4RZJyTV#W$mAZl31mX>K958BKFniD^cQX|{-Iw$S1{EzZ;6JPpo^ zW46%ZLhO>Ky?NTZiuSIewFNEB)6hKa%+tb+_%#K;rqIMz8rVexyJ%n+t$Tphx-r?-K0Unkw!r29JQ&L-Wrw~_8l(tVwDUnkkSNcJvL zy^B;cq?#es%zu#P-%Ikr1&Z+c9CM&e3lg zR9f@Ap9$Y4t+z?*ZPI$1wB9DAIixg)l;)7q98${kO6fnBOjoaD(w<4Q2WebI|2o;S z-}dU>zV`F9W{f7!G?H;6S#IPCdju3#VHQ?07FP2-%$7WZ^PRN4w%41SWai@VLi9l6 zzZ(FT2h!8_5$CEJ2irmQL9TT2h?jjop&+e%|wX>2QvZKbiT#Pljmufp^-1z%8zAVt6htM_jr zkwRp#tGoUxd{Zp5ugGM7?+tOs7Z}&Aj*1OvU?skE@T~qL?#;lv3E^v1-cNcSaF?SbpcQJQY;qEGWJ=hA~VFxyOk-K}y z9jxRH9y4BlBEd!ytZ+w#y$}`~gI}}DYyI}Dvu_D@{9k`-FS`W|3Qp+#b{XcZcZR~A z3x)j_3TskVIsaL(g;lZzel7FOk9@O`A6FZP zEykeRovtUXcG4>B)=+^*70%k=tn0+Je}COd->&`-*DZ|5r>_01>!yv!D@J63YZm5z z6!yI+%rjc%I*w|ra;TBr5hL&um9SPT8MLP;>1?Twvb~tqp$NBb9$KxGOXMO(G>8~^%AF|VP+3C47 zXblZY(4Yj(X(;X#m-hGksqOUR4-en;E8nCon`ue|O{t(M6*Q%Src}_B3YxNprmUeU zo9S^6xo+jfLi)Rs{;o7K|CAqeb0vLT`Hws)IyvwJ#=sL815aKIz6QI2r`39%N0ldC z*^3GGHp^@u*f+e<6R(~P_7AVLTgN+g>sS#y;_0~MKD!jxmW4kG;?`3Y)^emhDW=h9 zvbsQ<{reU(bwW8sBs`wgzOTp=ANkQc_|ZFz?PQV9RO9=uG2P3U4l<^D8Ph?=??B^s zVA0L^_AC6m6O$j1*FI=A^5b4NIZty~?Rn{UL{9NcvSE&=c_Pgjc2O{^He!xiqCu8jP|cM4BI9u>|kem?y7;upgQt=_28ovP(i?p6~d!`u1R zJNVY)`PSoAg>F$5x`mHDzPLh89q+Z@lgV{5%lIbC_$Jv-F767a@}*N*$H}bYWRcKR zylm`uswb|V269kx-;3P8+~0G3`+LrI{|&Gytnxhd{XI|pXwOmK-*ePY@~rgzJuCfa z&q&|jGty5gx!z}f3jYp2gP+4M;6@;wZJw3B|6iY#{!*|@$$0-h0VcvEcm*cI6nMk; z{`L&@TT0&c_urnOeoM*wo_p>&XWKkK{T9zp-+!Cur{CiF>BnsI{Pg=uYW-dJ*XO6- z=lSV7d4BqkQcwCU#kW#V`YhcEz6f7}fv_|1rKMknUEnLQD|{93vlKr|@w0Sy_y*u< z=^n5r>;;2hFzgNcz`n2_d<#U;rGI-?d#h))mzN$KthTDAs`OB+nGSP29L^2zC_N9( z2iz&W5H5yG;Ct|WxHPOTy$pT;m%|U?M=%1efGgoD7ztOyHSlA&7I3fhI=CKw3jYp2 zgP+3wNJ zdGH@@^E~(~%}2gcE_=0IUEZ;)%R6OB`_!+3YN&w}q@flX!kJ}_&;-qpfh@GZ8dwYK zpcU3b4%(nSoZ0t0_yxVsga41uf?v@4Eck7o1OJYA<=t?hm~f$(aG{uRp_uS^QTbHQ zF{p~Jfwiy>T4B8&miQ^!2JO(HGI&aO$bjj=HwOGU{P%zI4EcW_@TPzB4EX`?gc}CD z%SQd0#T&!oy~EN~vUH6s-87b~hQ%7kT8&|?#;_#QSdeL~#vfUVF|5QGR^l~QVhqjQ zh2{>Xxx3KZgJ|w9G&wS#&IsTsO@0icb^EVq!`ywSO0>Z`45p94^f8z|2Ghr2`WQ?fgOQaO zS&5O27}_DUavDZX!^mkEISnI+V&pW8oQ9FpFmf73PQ%D)7 z-dOr@(RHPd1QRiGcjS(#(ECHYepg-eJ(vyehc%>MOZq#I{vS#I(g8Dyt{X5jcznR?!IuZT5gayPmiOO; zw*ou(koxF1|n zJ8JLZ{(CSR-tX0cs$LJDKFPx;xAE{P9zKQl_BW;j`-dZV_}M&s zWuJ#n^6;PX@R#xMf8ycmc=*{o{JT7SGY_9MV)J?UB|QAGM(%e!d~zEPKZS>%_TPB; znLKKFPx;dH5s`pXA|_JbaRePxA0d z9zMy#CwcfJ51-`W+qUuWZS34)cCMa>PxA0RJbaRePyP=*e2Rxp@$e}gKE=bQc=!|# zpW@+DJba3WPx0_69zMmxr+D}j51-=UQ#^c%hfneFDIPw>!>4%o6l-uZdwMf_I)*(R z!=8>|PsgyQW7yL%?CA*BU<7M0f;AYy8jR@k@FQ4*5v;)o)?fr{FoHE0@n3lO5iG+9 zmfKFPx;dH5s`pW@+DJba3WPx0_6HvYgq5C7AD;^BYF#{aa>!*Al@llHznhLsv& z&)Wr_-M28j*W8ITYgS=C#0-_`dt_Cg%loFurV4x5@5NeF6weKc?7ViCnQFV293I|V zazyy7RgCvq#n@~WW3yF^_gckxuT_lACFjCjV?b0>kvVp!o}UtE~=HAvOv51z!7 z^u(YIcI+!WtKuZ`I+>gXkk7Bk<#^A&Yb2L1l1Ukv97rZ-*m3S~`^Ft*hrQj&XLr4j zK`Kv2k<+o{bgo_Y_93fR$?9BkDj}ys$*CgvM(|}b&$jMSN$NNv?&?*R?9f734d| z$i-dZk-m=7dROw4nD7m9{mj+gaN|h zQE)UIV@=a{ymu@dXXfTmI37-b6X7H{8BT#=FdR;W)4+-ld%QN-)b!5bN`(9 zwkgAL#eR^!sA!bJrJr6~5;6 zZt!*3-G0m8aKsB;`?0!1G5x=o7&)ao6f4>X_Ei_%&+%KZze?f(K6jwkL*${u^FxYm z(h<7ZT&G`}L2-);~DEmO9&h3PO8 zKJmG^LBvdh=fvVo#Yq*rRgm&`eHbflfJSJ7X6Msq(6fvcx4;^oU+cIIT4BBO+0R-W z-z<)AR*7va?(}yTd$|EN`b>BDu6X`keax7;>GgV*;q6oD1js%mr`}(4~H(!`J%nt$Vf) z>thZ{|NX*O`+qB(+JAq&&;uL~3}^Nq65iMUpm0_H4^@jlf;qtj_Azd-k8y*2j2rA@ z+|d71NQF(Jlp+0BhhzIUhcESC8!l8;{=m$Jv2u?lvxCQ)xA&18q-i^x+ivf$XS+?| z*pmIjhf5BC17Qdp1P8+*a3~xGhrD#6tzSY^`fXmszfATBR=2S~51czvQK0iOOk{ysAlF)g-TKl2$mc%xxlepNuaN<77)#M*?-6&s0&zs^w%#TEeGzsi#WTdIsSR zb_nEmqurKTf--7*vX{84gO)WjJ=%dmh!ONuw6-_QZxai~3 zLsT6P4Q9)?n&ewer8js^isz}6-U+{k-@sjPH{4THT6(YJZ{c@vAKVWQz=K6YN*{_` zSo(0}tWvY9N*^uSq4cq$14|$GZ%@FJ!A_pPIJI;%JPTvsId~pkfEQsbyaX@9I2aFw zr%X?B)8WtX8q9#1@CM9+H{mV$gP#0&DD0;pwlXp8RT$^9FmIH=YOV!{KzWMzZWoU{A}=1~#?q zyTGoNd455e4sqFquE(~PT@07N_u%_*DO?6WfXm^B@FN%jSHP8?(s-5QNVpoVfgi)Q z@DsQWu7{t(zr)W!+*2m*G1Gl)*^R(|n(02)O!u*7x{o!>Ypf?Ik1f01^EZFxc!&Gr zS3N~JX8ua7>>ju`jFX~ z*UCo0GxlZw6O4vuVGKM6&%+DwB8-KX;AI#G<6#0!gh}uUOa^}1zVWNerouFs4u6K% zUe0c$Q}zO?jqc;K`}*vDKKm`7-QT|-;Ijw% z>=3UH3dcqdhC|>`I1I$_(IY?%A3YL|f}`OW5W`2s@X_O7C>#$bz=?1YoD8SHFc=P} z!f9~2n#dV&CY%Ll!#VI>I2X=?^L0TkfD7RwumatB+A8a5tD@hBOW`v30bCA01l`c+ z2)F{SgsWgATn*R2kKtPQ30w!)n^*PI;2>-L7g+BWvEFSbYyB5QZ-Sfc1@OyYptb%B ztbIGk+PC|xeJkuuyue!j8oOeJ*1&zm8n~mPcl%BEz`eRUzlGnyeIVYCiua=r!b9*d zJOYoxWAHdU0Z+p3;ScZ>jDlyv*R7a%Jvthmg)#6PJP$9xi!c^mf|p?&jE4y@5hlSa zFd3%6t1uO&!E|`Xjv?>b*YG`<4e!I8-uWx>Xv_@n1>pzgk;Tj-i$xd15?BhKs#R4u zt_VMjChW)1;yhwfwXG(#ttPdtrf8?J=zl54-Y@G=(QcjwdliJA;PJk2PBsdvPfni`BiJUYoCr!&q({j?ZoHQ*bP0LBsa?-S% zG%Y7h%SqF6(zKj3EhkOONz-!Dw45|8Cr!&q({j?ZoHQ*bP0J~*x0V0>8h!(J!QF5V zu&r{+xSTRBr;O`q{8>+9Uhs$~rj+|#OU-In=GogToFi_J_=ICpov@HM7IxKbluKsh znJqrM8Mde{gerp(YqScrmooX<-(~}*B*YFg!0;o1wq4 zQE#JOZ)2jq#zy&Bs>oa$eT|ppWGQ`(mr3jrxmikXmeSXFS?6h_{45o@I$WWzF*5QK zpZ7cEWpTUl?-Y3;?1+pCXUW0Ra!P0WDv>Yrh2aC(U zQu42KZ{AgyMfi!o=Qz#{f2SW(m`(Vce#mqBA+z*DX6c6%@~>3!ns9;~YJwbUrW|Uf z{Aq&x={@<=dvd4uY@QOEr^Mzdv3W{t zo)Vj<#O5h+aY|gA5*Me$#VK)dN?e>07pKI z;*_{JB`!{hi&Ns_l(;x0E>4MyQ{v*3xHu&)PKk?C;^LIJIHeDfvTtWf-yx;%kP-){ z#K9?jhr4tO?^0R1OSkYY-NL(c3-8h`yi2$6F5SYr%q&RjJEZj;()tc*eTTHZLt5V< zt?!W5cS!3yr1c%r`VMJ*hqS&!THhh9?~vAaNb5VK^&Qgs4rzUdw7x@H-{CfWhui*( znFS;D9Y*RqjMR4+sqZjSN||ocNnSfFj7UVK}D=VMXW*ZcBEdz zNWF$1i!sw;%(NIYEyhfXG1Fqqv=}oj#!QPb(_+lD7&9%#Op7toV$8G{GcCqUi!sw; z%(QqWE}n^tXX4_SxOgTmo{6(o2Z(3l;+eR3CN7?di)Z5EnYef+E}n^tXX4_SxOgTm zo{5WR;^LXOcqT5MiHm3A;+eR3CN7?di)Z5EnYef+E}n^tXX4_SxOgTmo{5WR;^LXO zcqT5MiR-8QSwH2^`YH4DQ|9TX%+pVqr=K#fEDg2L5Y94VVwM>bv&@*7WyZuTGbU!4 zF)_=GiCJb$%rawQmKhVX%GN^;+Mqq0#h<77^E7{+=Fijod73{@^XF;)Jk6h{`SUb? zp61Wf{CS!`PxI$#{yfc}r}^_Vf1c*g)BJgwKTq@LY5qLTpQrh>lpajE9!$9&Ot~IR zdGrITUOx4Fc=nE;O7T-Eek#RJrK04i2b0!=N$bI+^&?Hy1esPvCmna7YZ}cS&q+D zQ#P;`b6ATxtiHkN#^H@}OXq4rH5bxQy1{ub%wr)_QVq z^)~ZzJ~9j960;!AQCt4}e>-1>6fW%TguPaDlbH_pn(6S0$lzcz9nLItzy3ezsGIzC z_SyeHIw$C|w4cXRk& z|DD74%8r5Wz_D-~429$21UL~+^8KQs7EewJf?V+dU;Npga}WQ*G2i^o&|`lvbkCu` zJ7LNRADyuBgoYE^PYg~Rdg8e!UU%Y?Cq8@PM<;%EV&=s5lfHS<-Y1=V(v2rQdeW+s zgOk5?=2yB{l&qzHr+Pjwx-*+zx}$~AH6;QtFwM} z>9217{qQ^9yz{hsfAjm{_dfaLy${WN^4KTueKPa=;ZN`R^q#+;`qY=6I^n5{pBnkc zrl-H+ojafY!_(uRp7(UkOUFLl^31-^p7iX6V_tan(q~6KyK+qFm@kjHcFYT7=Du+8 znDleApKE!3-xn@^{%6m>_I&ziK`1lK7d3yXc<8L1SoAHlMTrmFG@o$V@ zIzBUg!-QbM7bZSFVYdlGCLBBAj0qzr+&JOx3C~U#KVkZWwCk~o; z%)|>PUODmFi8oGsd*a;_r%Zf&;&cB0_QX$Id)1`VCa#_M`J|mE4e{S;lP;Zf?W9{K zJvr&+NpJXX>7>l0&tEBh<;$-e{L00z-0;fN{u}?w!dDvn*ZxY+!Hu=)YH&4EI z@}rYyPM$kCJ*D52-KQKdW%!g4Q|_NKcFOcAvtM00ef*4LavjL>v;Mid>zd4)~6e&Pd8AXu3De2TA!|3 zpDw0P7t^PU>C?sZ>0UQs7_8)Cnu_t6V=Iy z>f}UqdUV~gzXLp5>=4%f5dFGt{km@bx^DftZvDELeqBtzE~a1CtzTEJU)QZ)SEFB7 zqhD8}Usofys*_tKo}RYGo+kXt3>RtdRPLT;6iTP5UHiQs-cx(DDvurInCYl9ps zA;+rHr<*UwS}7`6rbk!U^=_G5D=ybc$h8*g(N*cub<4Rb_36_3b20t7n8=|bX!5yc zuQTB%^030b`-|ja3H`WT$k=9 z+sgIZx+8albM)N0<#q|VT|&QYx%@5~c|aEbpwHM(KJt{~C^HbA3qO(X)yenj4U zy*l|`oqVrOzE>yTtCR25$@l8ydkOhoA~Mx|P4oTfuJM{YaE8}2!}*cd9p8{!&T_ps z!%sb@ZCT`P&)0m%{k-e5@441&$M>D%1Lv^%G&0vQ2J^f>Ke#Bez_A?EKO&35xe+yx zNQInrg<1A?(~49=70pfr6C%mr7m=E9K_umRj-9^SWgN|xk8Ff)*XRj;8QBv2vN*64ZIRuJB6^9%R=)Rx{;*wG zQ(WR$22r{1fbf&z?d85ZIDP?kG|OivJEwiocfS-aEFNeM#m+waWvexI@!hZZ_g&2e z{Hk?NyICLmb?fxmisC(e26u{ar}zTjz0ki~162Hd`}AF^XK24`Ird6D&X_!Vp}uCU zcsk64PyE~5piEAk(9=xmX{L(novQ&Fp$RgOg%&W|r+BU7I%tLU&XMz8n-vJ{(CKxT z``7>*{kuM&T>K+_%x*o*ZavIydHE7O%x*o*<^76+ujpaM`WdHw#;IS4*8?0+wKC;2 zI0Nh}+V32&pQyfNte^V2zGY0`vRmJ>qCcJL{|(1IJXd#5k@{ZYGP!-7+`dl5>4*L2 z27Afx>*V)!^7}gZeVzQiPJW-zryQzJ8SCF1miNyFhxBi8T&qT~PX6EO{S7{&AFBqC z*e(bwwkrx3ZWjq|+paiVv|T^%^$(YBXZ4IeX7_f3!?-#@gI;E=~i^ zJ4mP~i^J4mP#cd!wtbe{j`Dl zX#@4s2I{8`)K43zpEgi-(WRd@P(N*;e%e6&w1N6*1NGAe>YEMJHyfyLwu`=5wZ2)k zzFD=tS+%}dwZ2)kzFD=tS+%}dwZ2)kzFD=tS+%}dwF=@H`exPoX4U#;)%s@D`exOo zufkNA2Gils@EXj3nZeGbubXrEhS#&;O?b=mO5b*T2h3X5JFC_^i|L)k^v+^>XED9A znBG}T?<}TwR<3teu6I_hcUE4y*h+jgR{gVb{j-?dTQNzYTae$!v%1md!+mNYB7Dan7&#}UoEDu7SmUY>8r)` z)nfW;F@3d|zFJIQEvCZ!1%0)czFJIQEvBy)(^re>tHt!yV)|+^eYKdrT1;Oprmq&$ zSBvSZ#q`x;`f4$KwV1wIOkXXguNKo+i|MPy^wnbeYB7DaZhf_GeYI|VwQha2Zhf_G zeYI|VwQha2Zhf_GeYI|VwQha2)5|=wMPIE(U#&)8twvw1MqjN)U#&)8twvw1MqjO_ z?2q6%2Wn9XwWx$zR6;E(p%#@;i@HuNDxnsYP>V{aMJ3dt5^7NiwWx$zR6;E(p%#@; zi%O_PCDfu4YEcQbsDxTnLM>^qtx|8TQg5wNZ>_Sgw^pgQR;jmEskc_Cw^pgQ zR;jmEskc_Cw^pgQR;jmEskc^HwgTc%2`ixr%&AiEs!{K%QSYiz@2XMns!{K%QSYiz z@7kc=wL!gWgL>Bn^{x%-T^rQ9HmG-PQ19BH-nBu!YlC{%2KBBD>RlVuyEf?04b@MZ zub(zwKW)B#+I;=A`TA+|_0#6-r_I+-o3EcXUq5ZWe%gHfwE6mJ^Yzo_>!;1vPn)lw zHeWw&zJA(#{j~Y|Y4i2d=If`;*H4?TpEh4VZJB=BGX1n=`f1Db)0XL{Ez?h1rk}P< zKW&-%TU`AuuKpHRe~YWX#ns>9>ThxNx48OST>UMs{uWn%i>trI)!*XkZ*ldvxcXaM z{VlHk7FU0ZtG~t7-{R_TarL*j`deH-tx7-bl&DpF(VuxHiB)`_?@{GxGgbO(Rr+dG z`f634HdCdqR^@3kRh~9e<*5o)`f64BYE}AbRh~9et9uxPDq(KP|4G7S~UU>!-!_)8hJRas9Noep)>G$M6$9wMBYri}chM z>8UN!Q(L5`wn$HHk)GNjJ+(!8YK!#L7U`)i(oZIPbZB0aT5dTNXG)E4Qf zEz(n4q^Gt>Pi>K&+9LJGgr3^2dTP7ssqLz#wrlhgJ+e74PknI#lzY7p7Qtdz0!v{T zEQe3SCHiaK`fD}OM6iRpWkTIDp>CN_w@j#8CiK^0`fD-$wV3`|On)t=zgDTgR;j;M zslQgKzg9WmiE#daKd6{J72Gx8kHPQ(&xGA-n~4GMh0FETmg}p<2Jk%tmYC;tk(r-0 zK|ipQifnS3Y;u`wa+z7X8)THrWR%Nfl*?q4%Yr@4LEQ`Xm9gv(2kJl#frH=>t7r~| z!{Bf@0=^ALh9Af*Kag2|AhVnoe8+pof<5qLmK8F~3Ylev%(6meSs}BmkXcs9EGuM| z6*9{TnPr8{vO;EAA+xNISysp_D`b`xGRq2?WrfVLLS|VZv#gL=R>&+XWR?{&%L5yNt;$W3tPb>@p_1tdLzUmR&BET`rbgE|y&`mR&BET`rbgE|y&`mR&BE zU6#u(%Vn4IWtYo?hh>0|$QmD2V=g@Bs@ym)B#)*3ZMnZc4d0h>E|qc4mvJtWaef?B zsY@q}WztF}(SWRTp{#S6taE|AISWsTE?6a`CX};{UT=qFz_W6$NGba1| z*q%s3ve0F+&?U0aN?GU0@9X7Exwo2fbEe!0Q|^S7 zrBf!|t)|>3m~wAl^GV-#jjs)vAg}ZJ|1jZw!T0~YulMfXhCrrH)rruFg?@vs@9L*k#m;(s&D2ypwq}>-bFbU& zcDp@}kIeb`_pVIJ$C{Mix&~d>geGNsJQMSniTO?w^Q|W4%h$x-_15$l_j@jhpK-y* zR&R@`IcI9lnwqnw=3vdw{k=M=D@@K;tXcNB&J1OO-fDv0W`a(epaTnh_W>A)nt z-6WkgN#Ah@D`T1-Gfht&acbTx$ChTn{?6e6xZNg5Qu$|2@^#_h%&m5AKf~no_-?Tk#+MYIT=SfxM;J7?m~9a8h0BOmM1kl1}X%aDnC+{8U@_3#c;_o%6R#MC`v>du+E zZ#Q+{ZtBiiJv?FZ&Y8S(ChrlGch2O!)#QD&$vbWG&Y8S(hpshgKS<=L{l+^=S<-_Q+Uo4o;HQ2P2p)%_@XI%^w8@aD|>`9C_U20V|rUn<7w0QZKm;I z(|Ex&UNDVcXc|A(G+r=`7fj;?(|Ex&UNDUZrtuR^<7w0Q?I!V`n8e3S;@eH)>9zKE zrt!cuo;HmKrtz$4eAqO8t!exY)A+P$e8e<9VH(ew#wShVKR1o1tt_5h_au3;JVl-= z$H~*=Pvz-yygWmm*;}--c-G3|Su2ZYtt_6kvUt|Y;#n(;XV;w~r^<8XdE)%lRu<1% zSv+fH@vN1_v+GWmGvvkc68HB~d6|E|TwdYduavX=`_JTTd6k?aua?)yYyJOo<#qo3 zdiitzK2Of~?>G49{Xgq2@b5QSX?&qvt_7BZT-&g zyE?Lt!AJy*M8HS{j6}dl1dK$$NCb?;s~L%akq8)xfRP9oiGYy^7>R(92pEZgkq8)x zfRP9oiGYy^7>R(92pEZgkq8)xfRP9oiGY!4SC@(Q`s)P0$JP(CDA$%o~ywlNnY%*6N3xwxIVxShF3GZ$&*;&$dD&0M6J zi!^hQW-ijqMVh%tGZ$&*BF$W+nTs@Yk!CK^%te~Hcms2hW-ijqMVh%tGZ$&*BF$W+ znTs@Yk!CK^%te~HxQDqYtT%nJ7X|jBz+M#CivoL5U@r>nMS;C2uongPqQG7h*oy*t zQD83$>_vgSD6kg=_F|E}SY$62*^5Q?BF$bbvKNc&#UgvL$X+b67k|cHoWx$7&0K6? zE;cb2k7F)QU@jiUTx?=4&RaDX0drAeE>31HHZd0`GZ%sPFP_3)Ja+K8-b?p_UXH=Y zF&H@pBgbIm7>pc)kz+7&3`UN@$T1i>1|!E{8H_Z8k!CQ`3`Uy4NHZ8|1|!X21Pn%+!N@TfIR+!gVB{E#9E0&Q z1|!E{8tjKvsZF~(So zF&1Nt#Ta8T##oFo7Gpb$F~(xdvKZT0jO{GOD2tJ2G4d=%p2f(s7W&tl}QCf;E+ z@s7bgGRJuAm3eWVAy%WrYLr-w601>SHA<{TiPb2v8YNbv#A=jSjS{O-8eH(4S$V6G zr>sVv8f?pw?C(u69w%*hg!42wlCa@uIYyi_Zo{K}G%s#2FK&38&mS+&zqG;mmo_}X z*N&AZ%AffBNj^SVoRN$@*}|S|VNbTOCtKK)E$qn__GAltvV}d_!k%nlPqwfpTiBB= z?8z4PWD9$;g+1BAo@`-Hwy-B#*pn^n$rkox3wyGKJ=wyZY++A^*^^=RWSBh}W>1FM zlVSE`m^~S0Plnl(5%y$+JsDw7M%a@P_GE-T8DURG*pm_VWQ08#VNXWblM(i0ggqHy zPe$025%y$+JsDw7M%a@P_GE-T8DURG*pm_VWSl*DE_*W0o(!`medos?RjI$@> z?8!KLGR~fivnS*1iEEt5mGS}kpnOQKlE1e4>2Ktt@-Z=QvL|Eg$ryVw#-5C^Cu8i% z7<)3to{X_4W9-QodosqJjIk$U?8z8=atnJh%$^LhC&TQ?Fnco0o(!`m!|cg0dos+P z46`T0?8z{DGR&R~vnRvs$uN5|%$^LhC&TQ?Fnco0o(!`m!|ciMhX3j6Mz=6kx2BF} zP>yC$j%HAf-f#ySqW7{WxeZ&HoNY3~dX37MuZ>GqCL||!N#OhQGU@kP@VV`VP5C`G zC16tmHYH$F0yZUJQ_^fonoUWwDQPw(&8Cd8DPwHP7@IQ2ri`&ETiBF~*_7+qlqVea zdFR;qLN7S%mR_4x30Re_tjbnaCC#eb&Z-m+{WDf&kyTk_Rr0J#kyRtdy9Q60=fbR!YoDiCHNzD$RXH~{ol@hBm#i~rPDpRb=6st1D zs!XveQ>nj^kIKj7<1EG}l6Km3vl+*j!So4$U(I5X9%O)0M0 zlqEK$&8B>uO)0Lf6Zj~bQe;!6R&C1RY>M;Qth_fj!=!9uQuZ?`MJ8pKNhvZZQ%uSf zlhR)^Fu|lemPy&hr0i!>cC7J!2_|J5lQPSs6quA9Ov+X!rNpF^n3NKeQesj{OiGDK zDKRM}CZ)url$ew$CS{6A37C`-CZ))v^w$vF#iWcfDH$eZ8i?WAB30RbI7G;=4c^8Y)W>JbPN`^(*#-iNGqGVW`9S5DY7R;_9Vle1nfzkJ;}2ti|k2o z(3*uJdy-*KGVDo_J;|{rMfRk)x^AJ!o=mVON3kcFL;Y?K^}A(H7TJ?U_9Vle=BBE$4{+EqfBMCrc~q7_1FqPsZ4j4103t+Q;@r*^?4`Qd(Wh z@HqCQ#GaJclM;JUVoyr!Ns&Ey4ttVePqwiq+t`zUJ;|{rnN@pIWKS~eNpW>8!x($= zarR{VptTH>>`9S5>91v2Vox&c$s&8Q$et{+CyVUK;yUN`9S5DY7R;_N2(36xov^ds1XiitI^|J^2iKat3>H8hdgqdvYXu z@^$v)SoY*t_T&WiQ-WKxPuN|8w^GATtS zQ-WKxPuN|8w^GATtSrO2cdnUo@vQe;w!OiGbSDKaTVCZ))v6q%GF zlTu_-imPioicHFZ^RPV4*Zx$VF2~F7Zc+|sQVwTQ4rfvhXHpJlQVwTQ4rfvZn3Thr zl*5^n!~sCgoYyfE1aO0+VtAlkylQ zQ%uSflQPAmOfe}_Ov)6KGR34!F)33_$`q3_#iUFzDN{_!6q7Q=q)ag>Q%uSflQPAm z1WZc6qy$V#z@!9BO2DK9OiI9{l$ex&NeP&gfJq6Mlz>SIn3RA?37C|CNeP&gfJq6M zl)IRefJwQBNy#xOIVL4wQg$#YJD8LmOv(->We1b8gGt%Jq%1Nii%iNQld{O9%rYsn zOv)^iGRvgQGAXl6$}E#I%cRUQDYHzRCS@y=vXx2M%A{;%QnoTFTbY!tOv+X!Wh;}il}Xvkq-M2sV@%2zlQPDnj4>%= zOv)IOl4DYGOiGSP$uTK8CMCzDM5tqfE*ulQPPrj4~;s zOv)&eGRmZkGAW}>$|#dE%A|}kDWgovD3dbEq>Qeblp>RI6q9lklX4W3a@49xDKaVj zt0>GcDKkt;j!DTeDMcow$fOjRlp>Q-WKxPuN|8w^uC7ZdGAVa4DT7Q(aYIcasY^r5 z5==^lNy#uN873veqy$V#z@!9BO2DK9OiG4Hc_WkZIVR;;CS{39DKaVj^(jRrCCj8t zFe#bCaFI#bG4N3)Wr#@`Vp3kso?OX{%rYah%*ZS=l4VA+%t)3Q$uc8ZW+eM7W@L;R z$uc8ZXZ^Z|oya>k-cc+>mW9Z&5ZM(2;Tods!%p^LC;PCIHTXPZFvJv$GX%RCf}IS( zzc2&$re>LfJxsxz+wPV7q+*>$%~`k>oWE_U_ep2l`lM;Ue-6ffG~xe?$^OeG`!Ad3 zv!?j0DSf{wy?l#faoU94w z-a|B%Lo}5`tVA;DWKBBRLvp=Onrgmns`<9FN`2C#k~N9!F^TLkg?!o6vDd_rHDP2; z7+*F;WK9uSQ$*GjkvAO-ngGr>0c2MufO_v=H2&+@YM5CKGpk`{HO#Drnbk0}8fI3*%xaif4Ku4@ zX4n5rcFDcs^`T*AHO#Drnbk0}8fI3*%xaif4Ku4@X4m&nHYEeHMh=lfWvx8OF^YAv zUIt}DFRLl$HO0K9m>>M3)R9&S|Dl%nBrWj-$J-vEB|g;gw#RCTCuoQ-ch1S9HN@9x zhDT{~Sxqjh$z?UUtR|P$9-tI1_GxvVCa)#S2+Z;}h;BDq-JEN_vw z%G=~G9-tI1_GxvVCa)#S38Tvn6IYI0ePY^O%HQzP4{ zk?qvTc4}lhHL{%=*-njYr$)9@12_-0$5DG6wZ~C=9JR+$dmOdLQF|P<$5DG6wZ~C= z9JR+$dmOdLQF|P<$5DG6wZ~C=9JR+$dmOdLQF|P<$5DG6wRfWSPSoCs+B;EuCu;9R z?VYH-6Sa4u=>;1c!`t8(9(wITuRZ9s2fg;7*B_?L}lWYDp&7w z4#EC=ov!7mzS#>ole_KlUTBZ^5__%wIpO|?Jec=@ulC2So>S^!UhNO|YJZ%g%cpq7pX?R?Ft7NpdBwlT zE4QxipXk+epI6Sw{#vj2PyOxm&vN(w?cdI2sY6mP@b2Olq}Ha+@Xp9Tc0QQvz2pBT z=YjcF>R8theq!oG=Zyc9cN{+4v4nQ&8dvwcmJ@%D>(-qr=keg@^Q+J6-OqjB&u!oD zye6OW{?do9xkxVV%{jNpr`BBJ-yct%vgQ-LeSCHQ3;fO!&uy5U;q+YP@N%CZQ ziab@0lc&j_%G2cpd6t|gC&|h3Yz)pAs5J-xTOoF zn)>5)k!vTs($zQ4l2^&ADN06U|<2vsZg?cj_sFd*oNw z+*i$6HTjjb_J_UDIa?p@ua@^$%loU%`D@+cFK~U`7NILEuQ%;p7|}F`7NILEuQ%;p7|}Fx$6OGotw4J&06QXw9XG`nl(+c zrfJqR&6=iJ(==}s0bE{$%NHn+>Z z=U7dvrfKccu4>xVX6DJ<8r5cvYO_YQS))EXJyi4nOmzMJZ4d(;e zL`|EhX%jVVqNYXEw1{0?)h@1T7gx1QgV@DU)ij8j22s->Y8pgMgQ#f`H4UPsIn*?V zn&wc`9BTG&HO-->In*?Vn&wc`9BP_FO>?Me4mHi8rZv>GhMIlh@7WhV-o9|qzHrdK z@3HoMkG9{t+J5gzNMA$x8q(L0zJ~NQq^}`;4e4v>UPJd9y4UO@Yv^7>_Zqs_(7lH4 zHFU3`dkx)d=w3tj8oJley@uR1Db{RzWP&F=K86xEUtWZyU+U9rQ5giS;lAkd}ei= z@BH?FRc8ZMoDICfSI_eCY}dtdH8=a8lKoF<;LW|nF}-~&tD_HGO=@YwPPabPt@rru zw(nl@9gC?4`AYxxD_2l<`?lM+-M+nYJ>nk>oaA~`C(CKBGzlM*YS{{l>Pt|L^--{Mc_~bmjSc-gkb%cYeWle#5h!^(g&SyQ%bja8?;bc&PII;^XQ4reHLh*_OxJlk!Iie3rB9sbDk`V-E_UVSqg=W9;jY|# zv@16s<(vh-=N3O4xWGA#FO-W?&veD+KRon0`JD61{mgVY=-x(BpLVaGarS}}%zf6Q zcs47)g|)}0esAqFQoonl;Jdc^u7~-qV|>?E-*vO^I`9{Lb?PwR`4Hdvkb$4uOC|PF zi9J-yK5@xDamhZhhGg~B6MI$r#=LpCZeFgNm+PtHdQE$>x;|*I37V|*I37S?CG_53PT1n8flAviNLDNctX6iNaS~*u-{#|A`1p3Y#J}Gm@054B{iSl5T<-HL{Qr0R_h0(= zdo;)Q%KPN~a;5+0TE=QTdpB-1mM$u6FIlYy94?_3=}# zIC+BC>w#CB%{Bf#)C&=#u1R#)e9`w_FMsdrU-9v)@-^8kUzZ!@8}d#0mV8_O(S81tvzYl^ zIu}dx(9ibvA9|f@w|x$U4(~M`9c(%}*j$_G#f}U%9T{w{4J0p*oEsS-)=9ubcJjX8pQZzi!sAoAv8v{kmDdzW$%&pXEDplYCdcC*PMJ z$PeX5@?*J0Zk4p_cDi2q`rG9WpAXBOl9BB)Dq}J(S-DFB$;+e^WJ;!`D0j<@{8VOT zr~FKI$-Uz5jLvm*uA_4uo$KgaN9Q^^*U`D|s8Q1qqoyN9O-GEHju4;I&5u>IfMomYInvNJX9WiP;V$^iRsOgAN(-EU4H!#l)%yR?t+`v3HFwYIla|83- zK%ENAyPxCb8S+dyK~9pB<=J9?IB0)3Xn*LOMNQ`{YC308 z(-ElH5vbS^sMyh`*wLrh(Wltar`XY_*wLrh(WiLum2#GxEwA#koFlK6*T`$-TzQ?m zUfv*YlnbOk>0Kxn$;I+!d5gSN-X?z`Zi-N04Gi zkYY!WVn>i-N04GikK(~=6Me@~qXkEe z792HN82pmlAYYcR$XDfSvRS?^H_A8UoANFBw%9j0YP8^}(SoB!3yvBsIBK-usL_I> zMhlJ_EjVhl;Hc4pqecsk8Z9_#wBV@Gf}=(Yjv6gEYP8^}(ZZl>!w=e495reV+Mf*$ z%bnsH^MhN(Rl^6j%c!`{{GjX14`#)+<{dF=4%+WIV$^iRsOgAN(-EVlqeV?ei<%~u zu8E~PsKE|uuqKwSqd;9pfx3hDNmXhRNnIKe%R(<2Y{@=Zl(sCne(?@hUT(P-`#xEe zwk(P5g*k6B=S}9k$(%Q}&HBLI2)~H%iwIvq_zJp@qI(nFo9Ny|_YS&;=pLebi0&b} zhv*)ndllWQ=w3zl5Zyy`579kD_YmDfbPv%zME4NgLv#<(Jw*2q-9vN_(LF@>5Zyy` z579kD_YmDfbPv%zME4NgLv#<(Jw*2q-9vN_(LF@>5Zyy`FQa>i?j3aRpnC`1JLuj) z_YS&u(7l819dz%Ydk5V+=-xqhtCi$kZhxsh zi0%=(N9Z1*dxY*0x<}|9p?iex5xPg{o}hbz?)O_$*hTjyx+jPpB6@=8qli9==p97w zAbN=CRYdO~dI!-vh#nz&gy;#Pml3^+=n107h+akXCZcx`y@TipqKAkcB6^7EA)<$f z9wK^(=pmwqh#n$(gy<2Xhlt)m@eYcIC?2AC2gO4a4^ccs@esvB6c16nisA{1cTl{8 z;t`66Xu61|i)gxtri*C0h^C8Zx`?I~G_9a%1x+hxT0zqanpV)Xf}*1+I*OvBC_0Lw zqbNFxqN6A}ilU<^+CTSqG%IEn<&~u z(I$#EQM8GoO%!dSXcI-7DB48PCWo+@h@v5ihA0}MXo#XAiiRi}qG*Vs zA&Q158lq?iMLQ_kLD3G1c2Kl~q8$|NplAn0J1E*g(HKQz6pc|dM$s5WV-$^1G)B=F zMI#iAP&7i(2t^|ljZic~(FjE&6pc_cLeU6CBNUBLG(yn`MI#iAP&7i(2t^|ljZic~ z(FjE&6pc_cLeU6CBNUBLG(yn`MI#iAP&7i(2t^|ljZic~(F8>k6irYxLD2+76BJEQ zG(ph>MH3WFP&7f&1Vs}RO;9vJ(F8>k6irYxLD2+76BNB4Men!XqHDcH*LsVt^%h<0 zExLn0lU=eGZRe#d_eoW1qIn`~6Iq+c+CRTwu`o1wC$p87j3&}+eOli?&^~jnOtn+vU|+dKqo& zX6Y7tbHFA&*Kzdc$pKHblf~)J*D)H$Xq;Q~dh_u`VpYhROMJW(T`!X>{Qq~$zn-R`Jh?5j?4)%C&-*2bArqXGAGEKAajDu2{I?h+(qUtGB;Oe z?IpAJ4zu>s>a3lhbh0{YCn%kubb`_eN+&2CqjZeY@j*ep|ALL7NgM3-OB43lu@^!gUz9HX~Z^^giALXCqpXEDplYCdc zC*PMJ$PeX5@?*J0Zk4nKu|>=k+C!{8#M(owJ;d5WtUbiqL##c-+C!{8#M(owJ;d5W ztUbiqL##c-+C!{8#M(owJ;d5WtUbiqL##c-+C!{8#M(owJ;d5WtUbiqL##c-+C!{8 z#M(omJtW#gqCF(qL!vz-+C!o}B-%rwJtW#gqCF(qL!vz-+C!o}B-%rwJtV(n25)K) zP3@tnJv6n4ruNX(9-99@n!#i3A=VyZ?IG44V(lT;9%Ahw)*fQ*A=VyZ?IG44V(lT; z9%Ahw)*fQ*A=VyZ?IG44V(lT;9%Ahw)*fQ*A=Vz^|AZO5t37nJhpzU})gHRqLsxs~ zY7brQp{qS~wTG_u(A6Hg+Cx`+=xPsL?V+nZbhU@B_R!THy4pimd+2HpUG1T(J#@8) zuJ+K?9=h5?S9|Dc4_)n{t37nJhpzU})gHRqLsxs~Y7epY5Ni*y_7H0ivGx#a53%+T zYY(yZ5Ni*y_7H0ivGx#a53%+TYY(yZ5Ni*y_7H0ivGx#a56jxa@WCu&gQ6HHEsSP}dadnnGPusA~%S89de$Vof2|6k<&w))Zn* zA=VUPO(E74Vof0)*s1|EG=PQ%(9i%P4WOX`G&F#Q2GGy|8X7?3R~kS=188Uffd&w0 z0D%S&XaIo*5NH5_1`ucffd&w00D%S&XaIo*5NH5_1`ucffd&w00D%S&XaIo*5NH5_ z1`ucffd&w00D%S&XaIo*5NH5_1`ucfd(GRi1`wIIBlC7--maOqYv%2mdAnxbu9>$Z z^LAw3j?CMUc{?(1N9OIwyxq_MoXu1&_xTlqtQtUM-maRrtLE*hdAn-fj?CME2JpL{ zcQ?`iA`PJb9=@Ggz^IvfL<`tC@C?VGp6STUnXVssmg8V&dykv*nGL*J8*s*aEuf(V zM2H_DeuVgqRpLix@yIM5nZ*yBkM<*0(|z3Px~nyUYy5ji2h_NQZWfqD(pi%>5@y$JOh zsMkQf2I@6XukjnG*Fe1n>NQZWfqD(pYoJ~O^%|(xK)nX)HBhg=?xlfx4b*F(UIX^IQIWjXx zX6DGu9GRJGX6Bli*|BSRq#P~B$fM-Z@)&u%Y!XN4%*-`2bIr_LGc(uB%#oQnGBZbJ z=E%$(nVBOqb7W?Y%*>IQIWjXxX6DGu9GRITGjn8Sj?B!FnK?2uM`q^8%p94SBQtYk zW{%9vk(oI%Ge>6T2I@6XuYr0E)N7z#1N9oH*Fe1n>NQZWfqD(pYoJ~O^%|(xK)nX) zHBhgCdJWWTpk4#@8mQMmy$0$vP_Kb{k(oI%Ge>6T$jls>nIkiEWM+=c%#oQnGBZbm z*Y>Jr=Bk;wYG$sQnX6{zs+qZJX0DoljTBGijeFG9Tt^&-@Z zP%lEg2=yY=i%>5@y$JOp)QeCrLcIv}BGijeFG9Tt^&-?e@ID3SRyH&5G&AosGw(Dr z?=&;-G&Ao+#8Fmy)a*Qhh$DzNf`}uCID&{Hh&Y0XBZxSHh$D!&6A^bJ;!Z@|iHJK9 zaVH{1h}b~H1|l{Pv4MyUM2rwILc|CWBSef=h`0?ABSef4F+#)!B32Nwf`}DFtRP|q z5i5uonu$X*acCy4AYug(^N5&7#5^MA5iyU5c|^=3VjdCmh?qyjJR;^1F^`COM9d>% z9uf11m`B7sBIXe>kBE6h%p+nR5%Y+cN5niL<`FTEhZ99%F5-?vJ|5Ebu3#X2g6 zsF+5@1}ZjC@jg_{qhcNv^Qf3d#XKtJQ8AB-c~s1!VjdL>=HSArp>CjJ0~M!GaS9dl zsA#>pujPDnu0vGJqhcNv^Qc%s#n22~Tb+R$s8~S73MxjZ7@=Z>iV-SCs2HJQgo+U= zMyME}VuXqjDn_Unp<;xJ5h_Ng7@=Z>iV-SCs2HJQgo+U=MyME}VuXqjDn_Unp<;xJ z4ODEPVgnT`s8~V83My7mv4V=BSuivUhGxOgEEt*vE2vmO#R@7`P_crF6;!OCVg(f| zs8~V83My7mv4V;fRIH$41r;l(SV6@KDppXjf{GPXte|2A74xW=N5woU=20<^ig{Gb zqhcNv^Qf3d#XKtJQ8AB-c~s1!VjdOqsF?pPRQ&f$asw3`sMtWo1}ZjCv4M&WRBWJP z0~H&n*g(YwDmGBDfr<@OY@lKT6&tA7K*a_sHc+vFiVak3pkf0R8>rYo#Re)iP_coE z4ODEPVgnT$sMtWo1}ZlGA0Cgbpkf6TE2vmO#R@7`P_crF6;!OCVg(f|s8~V83My7m zv4V;fRIH$41r;l(SV6@KDppXjf{GPXte|2A6)XRTvtS(+>!?^q#X2h1QL&DSbyTdQ zVjUF=gMTN^WIuSl{Js2xd`WJQFN^a{ngt7H!Gc+^U=}Qx1q)`uf?2R&7A%+r3ueKB zS+HOhESLogX2F74uwWJ}m<0=F!Gc+^U=}Qx1q)`uf?2RI_+z<6Zk5|4EnDPv?dB(P zhYZV|q79>B4HavsSVP4cD%Mc3hKe;*tf68J6>F$iL&X{@)=;sAiZxWMq2jPvaF|Ih zqGAyhi>O#c#Ud&eQL%`M6;!OCVj30GsF+5@G%BW1F^!68R7|5{8Wq#1cpoa>hl=;1 z;(e%iA1dC5iv2mUf{GPXte|2A6)UJ%L&X{@)=;sAiv5*=;mVwN@T#l+ocNneaT67r zsMtKn6#w_08TY`P*f+%o&xsK#MyME}VuXs(ZtN13p54;y`0LJW$qFK36A=%d6K4=HM#TTl6-o!siNDT{HW9Ijh)qOnB4QH} z|Fi9A0TKVhX9xT(MEv*c=x>@6BSef4F+#)$5hFy55HUi;2oWPhj1VzG#0U{1M2rwI zLc|CWqyOSL@xRiJ{-4Z=zsm}x8AO~x#2G}KLBttEoI%7HM4Umy8AO~x#2G}KLBttE zoI%7HM4Umy8AO~x#2G}KLBttEoI%7HM4Umy8AO~x#2G}KLBttEoI%7HM4Umy8AO~x z#2G}KLBttEoI%7HMEuX16AOq~K*TvjoI}JpM4Us!IYgX8#5qJ9LB#$wJ#&bdL&O{+ z<`6N5h&e>eAz}^@bBLHj#2G}KLBttEoI%7HM4Umy{+#&h%;~S26Pu|&aMrV~- zdX&Vv>>pW|U9c`Yu$KBm)>40H?N&#qE_IgF%jARdA-PIEEPo>(m5<5Cln{=;0Y z_OQ?N_6~f<1iownUp9d+o4~ueM%e^jHi4H-;AInd*#utxl{H{x6L{GK{@;79xC1M^ z%eqF_1l~1)cTM13U8CFo8|xZfU8AdObWPw*PP%IX@0!4yoOF|uZgSFP6L{GKUN(U@ zIqB}|8n7lO-86wWP2f!vc+&*l)iwI(mHXYUv;E5nJgaNmqi1Z_AzXiOWvcF)s=KD@ zvJTN-_tjkUexHBP$6s96dSw-NRd;BvuJ_umJ9JIcUA^IVJ#X2{b*}Y>GQZq4Nq6;y z)$=*|_pdvb)9zkbU+Ay(>QB&3Ub$<6p4J!2`ogjadf5cMY=T}kK`)!2mrc;iCg^1o z^s)(h*#y08f?hU3FPos3P0-6G=w%c1vI%cq3A$^7?wX*>Cg`#Wx@>|jo1n|TVJ%kK1YI^kmrc-R6Li@G zT{c0NP0(c%blC)5HbIw7&}9>J*#uoSL6=R?WfOGS1YI^kmrc-R6Li@GT{c0NP0(c% zbV*VuD;OK7rOdFS6}Gr3tfGot1oo*g|5EP)fc+@Le~V{H9>bx&|MRB z*96@)L3d5iT@!TI1l=`3cTLb;6Li-E-8DfsdF3Xr+~k#;ymFIQZt}`aUb)FDH+khI zuiWI7o4j(9S1y~N%O>cu3A${8E}NjsCg`#Wx@>|jo1n`k=&}jAY=SPEpvxxcvI)9u zf-ak&%O>cu3A${8E}NjsCg`#Wx@>|jo1n`k=&}jA$tyQ`!ae%JJ^I2udct-+VY{BNT~FAqCv4Xfw(AMo^@Qzu!gf8uSyru7ZuQD0 zSLq%| zoYnA*-p^MFej`@?!wRu)bPjXxU+|8>r`S45sQgS+empAouPn9!m5=o9!Bdg>*^M9paHX#B+3rSLzUdu0#BT4)GPMimFxaN(!li13OLhH+qd5@lV|QQSSX=?)^yb0bA4iuIGB2=eiH$?jE?nc{wkXi(Gs241MNe zTsiPQ<1ct_vz|xZdCgwtd|(gpTX~hodZ^#TgZ(B>^_#fZd8-~|`?-6=&oFSkd;VF! zi}~rAI!MDk%)5uCy^NKduFubo*pXIlEr00E#d+WHjj(1j`;0iJ) zc|YFC&JXY)pMBA1*Zb^x_tk$7)KA@ZDfLQ^dW1*qc+^)rN^qS=eTA;k@~Zs2TV3E) z`CYHdn>>q)y!UUwYh=B@*zfs^t@n)nao~-;8~knV@VB|c-{u&v+>d+Ze#|TPQ~pjn z{D%K;&-gUY_}LzHfBzXD`UU4^v?7B2zK;FA4wtXSU%R2s12px58hG_u}-zhCMw zZK=1iMP2RVb#kEpwlthx|F`3}5@|0zZJ@^<^w@(Qd(dMKdXCWckhF)SJwI8`Pu4@y z9+LJR*t$5l-oU4=>b$|K&KpvH*t^R4m6u()YT4?}j#l2Wx^uVHoh_?7gOztlKFOJT zp6qH$Pm!m}arO~U9GadctMXX=&^IDEBya=dyK#I@Ap`l z`d)dTykES7jR&hbI#lJqI^0*o>+};|-Pd|`Upw&J-e(7%C#UtkV;k+i8}V-2=&bHq z&|Rx*`tL@3y}c?YW^2k@ExE^0qN+Yy)n^O-LX-OJoIYFCXXm)E1{YRZbG5Hu(~PLh-5+43AYMNXCH%Jby;@&b9Gyhu)y z)8!1gK&ri~tZ2My{b{=B>2ijgDKC+i%FE>C@(Que+^WZpRgWF39y?Y&cGkaE&Xr4h zyRCZMZPjDTs>ha9kFE92ue|;}@?LR1vGvxz{7&yM{A_QJRgZhDdfa2x;~uLX_gM9~ z$NpeaPoLD&C+!y|?H4BP7bfi&CiV15J$+J7pR|vd)YB*R^hrH^Qcs`M(rx$FQleWxBTjr!KbJCVMX+KoZ(+hff!TzXVe^k)Z3-(C`J-wi(7xeUko?g(? z3&>s2(+hffK~FE}=>ep5Mx~gA?`gN#Zhx&DB-xwla zs9%Trb*Nv5`gN#Zhx&D>Ux)g2s9%Trb*Nv5`gK*muIkrS{kp1OSM}?veqGhCtNL|S zzpm=nRsFiEUsv_(s(wAEU(e~+bNcn1eeaxpJ*QvK>DP1m^_+e^r(e(M*K_*yoPBda zzb@$41^v2UKV8tT3-;9o{kouE7xe3beqGRZ3i@?Hzb@$41^e-WeqGS73;K0Izb@$4 z1^v39Ul;W2f_`1luM7G?LBF2UujlmZIs5-P{d!Knp3|@A^y@i1nA5N4^y@kOdKWhA z!iHV?^)CH-mwvrVzuu)^@6xY#VMcZECvt}j%bk*ut+GwFi}&H+O4U)Is-r+vM}ex2 z0#ygSAIDLks-r+vM}ex20#zLasyYf(<+D0`R)?o*;8BC8YVcGIo~pr9HT2Rsy|jQ) z1--POmlpKWf?is6DqvB~Nl5`RDk5r^u=D zTzQ^6UtS(HA+MCP@6d6ihzYUM+0wxS`nq9L}TA-19+wxS`n zq9L}TA-19+wxXfq_)SM!`I#O!gLU)$R|v+<^D*&St6i~fPJ7wM$@F_&tk=a>HN+n*TTON{!QcGJRj4+zj6E<$G>s>8^^zK{2Rx=ar_&{zj6E<$G>s>8^^zK{2Rx= zar_&{zj6E<$G>s>8^^zK{2Rx=ar_&{zj6E<$G>s>8(;T!@1DOz-XZT4{95;Qxlz6$ z-;{63x8)zD+Pf1k?_B@;nC5lA{tx8A@(_8bIP=8%!{y;}q#Px-iS@RL^|p!iN6Rr{ z+gSf-v3;z!eXM_+u)*s$$^Ru!5Pi(KrCaNdm(%2QIYZ7Ae{+0p;d2Y0Tln0<-utol ze(b#;d+*2I`?2?a?2WNE_HKd%R})-Ka5cfz1XmMW9mUmATph*LQCuCx)lpm>#nn+< z9mUmATph*LQCtmibre@eadi||M{#u&S4VMm6jw)abre@eadi||M{#u&S4VMm6jw)a zbre@eadjS7=W%tOcZzwZ*vgIA%8l5{jo8YK*vgIA%8l5{jo8YK*vgIA%8l5{jo8YK z*vgIA%8l5{jabj`uS=ZAw|RV<$G3TWo5#0#e4E3!IeeSLw>f;f*Oasl-{$ab4&UbR zZ4Te&^#8T^HivI>_%?@cb0(?Z$G3TWo5#0#e4EF&d3>A4w|RV<$G3TWo5#0#e4EF& zd3>A4w`qKv#T94f19AihNbRCY$B!a-)1hzA4|5 zZ;S2P@tuz2I~~V&I*#vj9N+0UzSD7hr{nle$MKzx<2xP4cRG&mbR6I54E|Vdkz3_9 zNy`?wT_53Rv95%5C9Er9T?y+-Sl2&iaS7{6SXaWj64sTlu7q_ZtSez%3F}H&SHijy z)|IfXWW__s*Mxjc%-6(xP0ZKCd`-;P#DlwJcW*Zy?&fVm-X`R2Lf$6iZ9?8A=|t1hi7V6g$-ee%d5-g` zoFb>nbLDyRe0hPqP+lab$?0;2oGCAsm&i-yW%6=)g}hSEl0TEPYFpGx2&O8h-4+G4@z(MBWF6QAb z=HU<%-X^5H`QI3bD|(wubtj_ZO~`l?GCuJBx2(zToE1ZHwf}#O^Un_T&Nr8zxaNPP zejg#T2)Tfe3kbP@kP8U8fRGCaxqy%h2)Tfe3lF^a?H8uQ@3T+HU)YYH^cGNZ0U`V6 znOQ){74w0R3kbPjO59{hJP{>tM#-BHvj4ufP4?JV+hd<=kA3dHF(hC1wXew6q;E^U z?qlDWe8b0Y%D3b<*^}@3f8Ueu%Mav-@+0}N+#E}e#&xtD*Chz}E$^$lL+Q0io=58NnKsuQRl3fp#dM)NZ$}0>f>5@Q12vD)JdkOlYU{1Hv0D;$ZxYpFYMi9kAIUr{!RAy zH?7#Dm-zTnd6~RiULmiPv*c{)+op4Rn@lvDOf;KT%+t9(_U+T_eSCwwQ7(|ajk?gs zi{xVIo2j?>_*QwF{Dr(-E|GW0JLP@yez{V-=F#OQbh!y#s_1ePy4-{=H=)Z-=<*eG zxd~lvGSQrGqB&r@KI7lE2NTWtCYtk2H0PUW&PSmCH^2YtEeLco0^N*2HzUx^2y`<7 z-HbptBhbwVbn}YE8}~6Q6Oxm=B#^vJNRiL*Fj8dYk%&(e$=yLtB<)fA3}# zx*3ITMxmQg=w{Q~iK&OI%w+xf>Va9tY+^6^Q!~WRR{H+~-Ti=N+}H1RzvxB%4y5z& z?i-wC<-zhtD`#BUiBq?ljArcr12(tDbXOd=Uu52&?q2V#gEby zPxmuCoMC9B&P^Sc`iP&QntFqeZ$E|iPp&GHs`tFQk>>Z;V+a%;kK0ns2`o^?rzY-?y&gs{`ky zzB_QfT$CCcxH$D!X!moz{pZf0{*g78$@}Gta((I}hrT;?)!M`5_|%UatND@hZoYSI zD|Oj}9+&#a`m}73+fyGIe6T!39x8t*hszQ2u+)18kCdb25%NemT8@!N$)n{l@_5-K zPmm|cljW)MG zXMyiZeT9=qG_^!iOEj;R=GD@?TAEi&^J-~cEzPT?d9^gJmgd#cyjq%9OY>@JUMn`mYJ_s#*Wrlr-iw3?Py)6!~MT1`u< zX=ybrt)`{bw7hGfWQ}IYjNj5;TAE8sb7`6FTbfHtb7^TPiH4GBD2axWm?;x8Wn!jG z%#?|Sl4vN2hLUI~iTzHZp(GkgqM;-jN@Au=%#?|lGBHyoX3E4&nP@19hLUI~iH4GB zD2axWXefz>l4u4k&7h?jv^0a3X3)|MTAD#iGiYfBEzO{%8MHKmmS)h>3|g8&OEYL` z1})8?Wxj~a7m@j*ZN6xmFWTmdw)vuMzG#~-oUhk)S@y{9K41LWvtK#S8cY)*b2VCa=y_1t9LB*N2yP zLhkU_FT1aA`0HsWiP$FFRK_qa`eO>)7SxHXPFx+i8dwIOK-|xfwo~=0BlRDlb%%nz4Sifma`92fYJ`>iygVvO{ZR2g*c-uDKwvD%K z<89k`+cw^|jkj&%ZQFR;Hr}?4w{7EX+j!eH-nNamZR2g*c-uDKwvD%K<89k`+cw^| zjkj&%ZQFR;Hr}?4w{7EX+j!eH-Zoh!Ca7Po33u*V+kM-1-?H5=+vXSiCCdZP_t$!% zoN4=g;2gLYtsJ+C9hd5_)!yeA)IQTm|Ge4@w&AvIxNRG5+lJe=;kIqKZ5wXehTFE` zwr#lW*i>TsZQFj^w%@kxw{81v+kV@&-?qu4zrK2(X(QJ7f89FfeH#BhjenoUzfa@e zr}6)1uVZf8uG_Zjw(YuYyKdXA+qUbr?YeEdZriTgw(GX-x^25|+pgQT>$dH>ZM$yU zuG?CDqSYr_eWKMTj*TRajU-xqqJjU{t?|9vKBgr1cqPxsPi0ni%Fkq%>~#jec`3_% zQk9yRet3%o-eQ5bSlD2nq#6F3^`~0kzP`gx9cddn({}e_+tC>&<^Jn*qy5FtyvlZZ zb=_uP(LYb}f-SLWU$Ni5VrF$KuG-!f>>uv6f4JASc5~{%ZdrEAz3%xr1J82K-{+<- z9(bOdo_d&j8}Vqb@M!0GwAZ<(Irr4E<+j{Y(>=Hbv2Fc&+j@XzKeWI4uE+1cJHbj- zt0$VhdEY(nyX&^|s#`3q>?`_v$1&d#SOdGJ_ci;xdCw&8nM`=pZ@Z7sciiVY_WBu0 z9yxD+S6}@(`g^1P{;uO^xy#RTmwjEwzOMg_zNQtf>3!HUdSEZya4)}oFWf(ear?LK z{rW2{zH1M7;LOteo@d$LcjsIU__)+YZE>TvxKUd?Rx{kF84hTM8&g-vmHt}4>^n`3 zZlgxGQKQ?a(QVY|HfnSmHM)%&+(r#702*gmg29jiONNq2gfhPF{d+o+*!)X+9+Xd5-OjT+iU4Q-=_woyad zsG)7t&^Bsl8#T0zgMXcR&fuqAuX;!7;?!Y&qGulb>^e}e9$dE?0KGj(5?R0;HOd#^;l0d>%1{lbPw)s!nJT0zkYidImxf}#}^ zt)OTHMJp&;LD33|R#3Epq7@XaplAg}D=1n)(F%%IP_%-g6%?(YXaz+pC|W_$3W`=x zw1T1)6s@3W1u-kUV%<@;x}$7$N7?F*veg}Bt2@e8ca*K}C|li8wmP!zUOl?jZ`A!O zVK?kAw)xq%_-QiyUww_+udM9oujuGsRoN;IM6Dod1yL)AT0ztbqE-;Kf~XZltsrWJ z>#I8&R!7wes#Z|7f~pl%t)OaUt@pd~cm4APS5T~iVigptpjZXPDkxS#u?mV+P^^Mt z6%?zWSOvu@C{{tS3W`-wtird{8UOm=7v*|!E@sBR&iL0E|2pGeXZ-7of1UBKGyZkP zzs~sA8UH%tUuXR5jDMZ+uQUF2#=p+^*BSph<6md|>x_S$@vk%fb;iHW_}3Z#I^$m- z{IT33x5{mjmMwBO<6e?~-^@SIod5OJ1Ao(11AoI2?UmPLzeA4tD+Vi{ZS`5&XLqKK z(2VaxiQ+Gs@tIap9{8)Tdd4)QkFzbBJU+tHzxP=rIq_}2f-P~RF z<~e;f=id6~tiP@Q8Zyg!PZG-?vHTIsAF=!q%OA1)2fwc|V)!G5eMt|j>wyuI5~VH^ z@4Z{;fY*A5#98RP$_W;`e-4*O-?P5aiodH@*3b9fdza{Pb$zYBLaeE;_0LZdF|-jw z8!@yILmM%)zwUi!k@YDNLmM%)k@YE&^(hfU8!@yILmM%)5kng>v=I{$F|-jw8!@yI zLmM%)5kng>v=KuaF|-jw8!@yIv%18rE?J+l#H=nct4o82%3AsN&S?EAGuJutYI%*k zR?d~z$?L`YFj>%u1&vsjhy{&U(1-<%SkQ;y?%6tNYDMS?-gn)Wn&^^sGeBO7yHm$BNjuh<%ILw}^e~pZB4;@|x%$sh{+k znDm$HPu=}jSZ3uFIq9=IeKy|zEOm)ybcts4mS4Pf4_JtPOBiz>qwZtWef0Oj2W&c z;R`&I^E{I)Jd<~=+)saBcwoEfU*n>GeTp5w`1-9ryYm;1)@Q@i%A?)r(Z1o)`m<*L zdLI2-mHy5D`Q`KI|0G-7=61Kaef2i|dz)Ul@15?Uza8JTGTmv?pdF8vOesSCWV{h}=TRrwx_xj6c)qjk@ z_ignU+gEOt_gVk(b1Oef+pShs!1%uYZ)s(sUU`oFE1Ha~{Hy;L$gljXKLH>39rRb% ze$k_S(e3)SY2~UQE6@5>p65F~&yRYZA6|LH{?FKd#O#W(`m9@i)-6BltNqtNd*vtl zTeoZfcXQzNyV?Ky>c9Gb2VTGZ-(vr7)Bh*(06f)^B&~H~Y$gpLoB@Kld~suaD3BfB)+_|8qP&-`+EmnR~ge?{B}B&^O-JPTM(O z*efqP-y23zHu+w??=ALZ|Im%g{3RT@{JOt{y>&84+m`Wq*RWFPKgH7G??cI0>i6#q|Nbq%f4BJi=l%X! zzkk;6!<~4-9$4&K^?v`7Z@p4%yRfz1_Lq3^H-Gn=dcO(RBMap?90MQso8^8Jo;lPP zC({qxbkN)W687h|dk&v8?1_+LZxxvs?gZ3dJnxgv`=s+e={p}ve%R-4`SqfYo_MKm zE%mLXz7@*#ww~VBF2(tSAN7AY27g<>h4&Noame2AewzJdu`?DWpr|dw)?He>pYS_N ziuZG`EB(fme&g4n#tUaIhR^w~U%%nk;h4E?ynfv$%|6K#pZufN^4*ZHF@ITEJmVr~ ze8m~V8TyTWTfC}og>vzVZ@vD}?~8hO`1j^((lTMoh5fjVBcX;{>ulR{!zXLcZGHEO zGaNw&{6iOf_?PbFI%6H3pEuCwj;%YAFPZ!Pz&@b{taDcZN- z&t!TYnVwU;pB8`H=8YW_yiu4OEGb+bEGyIp@7PbS$q0d1;qE{s*j|M*f);ZeRvIC& zs_?boJ!8(-7QP>>Gy7$|>eYtA2|;K6gCJ3u5Ns@r_XJm%cM$G3bamxnh0Ea{CirZhyiJ_9xt6f5OY{Pk6cg2`{fa1t!9&@I5%q-%p1#y#78+ z^4i?X%CicWSAD@4-vhyz_vmi+Dc%G2^m;GY8}{*jjN`trA5?q4KO6v`fw4aOEZA`< z`g!l2FZx9or!MrY7I{{S%ng?6{YFypt~n>x!OnGu9`JA;@;ma1*Y0bG>bFS8$t2Mi|X1CUyt2O6p&2FvPtu^Or&AD20uGXBZHRo#0 zxmt6s*6h}r-CA?5)|{&~yR~Mw)|^}Q9ht$Yh5D+S3a?e&Y<=8Y9PLaFsYv8AFEnElJ!wqng-Jx#=@oQB4 z8Wq1r#jnxZ;pcD%+zHlZj{X9yg%hsCd74>RE*co_Zwv)~b!4S$43;W3y4kHcJe0_MS=;Lq@+2&+DLlka0UU2L9- z%^lc0pN!AM=mv~l7_6YzS6ZoSmEE9M^H}UBPu5?;=w^&gVRQ>d&%o#z%<+m38R(@OIZy5d> zhR>+5%2VYO72Qw6Xs{C)InPv$b3D-TAUN3TL*S-hbk!|zH{1gag&A160xM@=&cw=@SUD3bXJX|{telCJGqG|eR?fuA znOHd!D`#TmOst%Vl{2w&CRWbG%9&U>6Dwz8fe7FEEgp1$@a4}p0 zm%){5t$=JD@;g-UDte=nd^Ra$D z*3ZZK`B*<6>*r(re5{|3_4BcQKGx62`uSKtAM591{d}ySkM;Ahem2(6#`@Vt|#AY^F_;mGhto{I_lUTeObHn^VX!%#NmHoyCyi0q>c!! zt-VSN`M%nZ)eXqOCUUR||2N@(7v5*^d=q|e!tYJ^y$Po`;pHZr+=P>xaB>q)Zo^7ie#z*UjDE@JmyCYN=$DLs$>^7ie#z*UjDE@JmyCYN=$DLs$>^7ie#z*U zjDE@JmyDdM{fV?{Mt?l51+(no683PLe`sX53i%RV#B=&4jEUdXH!Eb?m+L)qarEGN zJ-A5^w(7xmOFpcs{)^mE*<-#pL9d3kyS?bmwCQ_U5xtjm?GN-`p2m8Q#(GaLCiP-c zFDCV3QZFX;Vp1l}Xyc7jpxX&6nS zc6Qtac2x!14R-f>57-m-5`XONxDOwFU$6J`x*GO(&I24j17lUVKI`~7_`KI&@I5;T z*e`oU$w8{)VLbNX4hzFkc-f1EY}q0*_cOrfi+&vA1hR@M($f7=3E3meLY9CMKOd7-eryrj@oZkBkt-8stb z&QV_B{dQ34v#7ZjBYgIs<{f?mPJnNDKcTR${6xoZ7q*n2uy;&vBG?zN^2ujT7#t08YGo;OO@3Ns}6-()`fTmo`shn93^GN%BmJv z35nv)_3uQqcjW7EmiIq_YrVb+?y*)ugADOHqZv92t0M`}4v`+{Ev$~-XMWOCYMD>N zi$)??MLlYbjHop-qSnZWzFJrxeGL}F8-nE6j2) zDZH<>HxhAp~E3x;vtpltD_>WUXsUSQ?qdsJ^5aD7)*+4~&V7Vedw z$`mfa^9AnY8F!L!C)eZnY#gt_@fsYjDH)Z20>^7{d{-Q=!SR}sUGuX_c7xr$-UIf8 zz4CoHUW4N&mF%0}jN>&pUW4Pu;dl*>*Lcd4aJ&Y`YjAw$k}vqp7rh>r&z2k*9N_6M z!1ELNRlhc$`(EP>PA~a-;c{aRPA@su@i-%{Pq0_~w+c6utt^~bwhGz`HMm}b>nGxR z4X)SXdJV4E;CcAmGtQRs~_rd1EaX5b*&e!04P1)A` zES#^w`5K%*4(DsigF;lxA5vbL@5B2VysyFglk~(%<&{2*=4au3jb8Z{zSopb$X|x< zHG1Yt_+F!T{#5=Q-}^3{QrNY8qT{WFpYsiGE5E()>+(B{xK0&zt=N?B(`#exM}KD3 z3D#nnl`r7=_wjsU)k|K#mhY=-gcev?xCqa8k3{o*_M_k5e)NaodQIdk?|)J_2iI$G zy(V%`el-UC5Z6z__0w>DGOnMA>sR3VX;^VMKG)!LEk4)c^F#Rj1AIObpO3`n8hoz7 z=d4cOE=VnN}XxIHSkp7z{?RsFPQ51y|t*3 z!>$}w<*_P-NL%6>4i6G zyF6`|r|t4skjH|2BnsLJ19G%o9s}|iki&qS-XGHQ&+GZ;_5AaC{&_wBJk7R=X4^!w zZKByW(QG}Kki&pH2IMdxhXFYZ$YDSZ13EDvr`NaW^}Jrs2Zse2t3#M&8EjS$9#9n? zgo63)BdngWqkWo3%7l&zK2h>%IiAs00NB-D+PlG6dBJh^Xg;X$aM|CxqRPjsPK^F+#OM*DKmGR+lea&?zo|Q%`su%K!Bx$AMePTqCV?w9#@!fwHCH~9BP_<9@mAGiC{`;Xh>6#u5}@wYvX4*%`D%bt_> zywAV+{w>=3oiQ`^-DTfHs=rbFo$83K2>PM>k_8+(ZP5bvB@U8O>I^f5jDL-WG zVV51grRJ~@HJgsO;K-kjKkeufj{d{Zi;jNl*b|OD?bu7d_1?GoCsa+?ZNe8O95UfQ zCXAnO%!CsroHXIo3Fl0>Xu{N!AG@M%;&T5sojP#Fr_MR;+?w+aI`6#meso^{`4^wR z?EGa@-n-za3!c7U-36O27`iNf`4=v~;qt%L9a1;7Zbsc(SJX{DW%7BGCr_R>`S+8b zoBZD7_Q`!yN~cs$Id97RDNCljcjfQ@`QKH0UA5Phhy3%W|LT4C&Hw(NRZ{jXd#P+y z5R|PcTZ>t3<(0v{<}DbOUh@I&j@~4esB4`!DZ$5m){>;UjEzi2ZOqbQAWE=uGphu z?_f&Bn2P;^t1AwuI53!6ahUPUw^aPS;^p9H6|Yt-4t`$ISn+N!tzu2Z`rw`lD}V*} zRwODm1@~2KuGkX%zM`<*cEKat?XcYr!4uo!BdqdRGt>R z9gHJq5puR0Ig66BUCG(5sTf_znwuRF=tPUP#MVfor^SiW{A zU%Qg8-O1PP4)1vj$=^VotmwxEqIxQ;DoV+-0m_1x6%3Y==_`M{9n-dzo7GPrt`0(^RJ`x+vxf>y1tFBZ=>t4qwCw~`d8@rdGvf6 zJ&%E2k8u1?`TcLe31DRbI=_w1Z=>_)(fRY}{CT{Cw|NI|^A4UXzoYOR4cE;E%;Oa= z<_Rz62`}aeFXjnvJBBci`)0ke=oJ zPvBawZ-NGMNIDDC*oSHC!!-6`8v8JfEoh?|yV-&^wxEqIXk!c7*n&2;;5xRTjW>|w z2{h^HcSpQnjNFTb#cabXY{R_ZQ#d=a$kBiBGg{>3hn%GEi=4x=f96z?PaksVFuuj} z{ED~u6&Ld>Ch;pK;Zo=)+=>qyoplEPxu5?W=L=lG7r2@)Z~WKdyu}x2;tMqK1>Vy4x9aO#_3^Fxc%wev$fkF(>0SDEqdQ(! zaaiG$io*-vVUs6mzr6OFqy6S+zd71(j`o|wzCO>sKF_{B&%Qp-zP_d1HfpO|t!*@5 z^x+_E6}9K&C+!*P372`o>qtt=eQ#@r0iAj-$gs1&0Nl19&7d-KE*q-n-Q?NVb ziKcj>DV}JVCtBu-+C5P_S>N7MEyMmWMof|S?LFaNF*@NX|LQ6K>M39FlrMP722a`G zDPO?ej3;ft-~{F-ur`IYDXdLlY64Rem`aa<-J7s7g_S9+Olg%Vn3%xC1STdhF@cE* zOiW;30{aqLEuqz()M`&^wI{XOlUnUbt=6E`8njx2R%_5|%e2}H*pD`Ave zMpm|<#xsIm*%rmzuBDz5oqtN}oGz}~U39T84fq4m#ZN^SzZYGEm7AM9**AIXQ^Zdr zc(=+Dr0hgM!tIFa{#Q_=h0=6PS>?hkkOW697KTCc|4e~#wc&lCI*OOGNWbKK#D zWZ~DeUQGXPaF?gJ%g>ojlPUZ>7*JapgspPV1u<8o7<>I$-`?SlpQH`zXu~?%u+H7@ zP8-(IhIMiZqvaGv(~5PpVjZnmhX)gB#X3BgNF&zKh;{gI1C3ZmBi4EHU(twlG-4fo zOr#C#w8T+b;2JIPv=*2z3pG^*ZK}M&wX#sx(w4svZ#_(7{zAON-)24N_X&r4^N1N8sq;)iD z9Zgz?cN6jL27J4L=B%SR>*N_m)1Gy-XC3a{Kzr8V-|4tFk@l>kJ?n6AVtKSMng*@I z#Ra&y02dd~pmj889SvHCmlN@FB3@3UK_90t`l&2B2G`l>4{kJ8?2a!(-Y~g&(W-P zxIGa&Ceo~RG;1A({E8N>qd^~_LF;JHIvTW&2Cc)G8)(ov+VdfdS%%vSu;z5EnP}av zjIphm%IZ@z5_mI-Cfq~j)KfYg-r4ea*;b4X*N<0 zYb>rD#P>K}e(cmjGe2V!A7hh9>!tFegHMvEI8C{nrd(cmfh*lp*s4w9vJo%4%0g#; zJE)=ko}>9Xo&9oJ?;Kk199PKGb_*~&jQ17|>-JW<{ZYF8FLe8(bo*ai`ANCc;`k(Y z@Mk*y2|E4>TJanj@f>$@AFX%}t=LH`cG85MwBG`nZvk8WI$Qoa4Y&X!a~K)MX*moT zaMyRb>pN+(bHrR{yX#QLzd~ERLR-B;TfIVCy+T_(M_WBdTRlfxJx5!0>e(s9s?(%8Nvb4Pe0#BiaTUT5;7zS z`aVUXJ4kc~iS8iL9VEI#zb_=w9qd48(errp1|B8wD8b6S%F4WoPYHZV;8OyhHse$1 zam>S~Fsqlqr_l3A;Zp*i68Q8NmL`Eu34B_DPn+>6flqH2Gt>(Uo68o_7Ka4dmi&*NAE#}YV}z_EEamcX$DjwNtxGmg!}v8Qls9*(7OEQMnU z9!n38r3cp%tjMeQmcX|JzNPRjfp5KR{hK(q8Rrr>m%zEFaPBFb+svZ8igyWIOW;}p z*Alq48P_)B+GbpP1J^dQ5^bzR8!ORPb&Z_MEPK#C1JA-sV09r}+l*_QacwiMZN|0D zxR$`R(CgWZYnyQ`foq}n(}inK;o1URTYzf|aBTsuE%+$&{3@=!#&28UvP7nXVqGV z6!Vy24l~SOhPg{Q%gT@EB|pqbt}N{3NyURj&*(5uIBGcGcn!RznHNNc$Ars#r6W=2f-y;*>BNN{v6W=Sc z=~db3*ZA9ui#~UlrM{Frj3*DZ4)E3#+O4p|;Jt8pXRBJ)Aok&AS>soI4ruk%v_gjF3AS@i9+pUpjkVI(uK9)@Xj> zt|Ew!JS};X5yRfrDBjknkG!oDi+4HTP6uJDd9?+j4kPj!JMzN*;s2G$^pw!mOY!|h zdU`3IKZD;d)5S}}yT;X4JZ;6#G=7G5zReSqu=Q>3z61|nq+gff-ivtmBHlfNch7jD z5B*%Zwai&w{{*&WZ5RDZpDWDbzFT;v*jj7kC)Qe5 z_{BmS-M3U$Rhb9#3eEIiBT=)B~_kZ$Nt{GP0zArO3)@sD# z*vx|pe{{W&@=IkNozz`*23cIqu<{N)=x1x?W{* zM`zl{o$gKvt-ta0c zcn=FYu%Lr=ZDw7YF`>gMA^4g_o=Fa1|b|!oyW~xC#$f;o&NFdKLby zVxO}jv%z8iZI$@taF@ojzW8I9m_77%Ac#C5JXn`yYWl$BDg;=U;9c^IIHai<@_Ajg&s#7&06a zLKch?ACAMxBY2$0$vqs84JV6iwvAMOr>s})Xw?7KH+zDe!f#{8_A=Dl)3V#sp4&${ z?fQ~{Zs>ttv#oY4_QFWl38Un3)&nb}ikm@4~#{DO!7$)}E!cXKC$OT6>n(Zr5+4#EfIH;viMi!!f&tXT5Di zz=tEOL#@!H&xUA)oR;sU6~dX(uRHVWo~7Nh=TS`89 z-B;dy<=t1Fj_)NWz2u~qob-~DUUJe)PI}2nFFENYC%xpPmz?yHlU{Pt>p60sBj-7C zo+IZu!a8AC8|<|P`?mSn8(i`A;SmkRk^1hj$33R4r?4X}G*Watmd-nfjXPYve%ly` z-+0QmJ>}N&3B_ItpKz%sTH9o=A310v2VvH}Pb;LgLRu@NwL)4eq_sj?E2OnTS}UZrLRu@NwL)4eq_sj?D`d1n zMk{2rLPjfOv_c7o<6*vqI775e;rQ`$I+g)-_s`D_t?TcMayO-4}I{QE8pcw z!i7Uu@kD#e`)HWmO`LWu{D0M!;?!2W#^mAvh9gpa$z&HL19GsXPa5}!WnlRJW2!@P?qI2_N_<@-aosx4J~cNs&cT zWRcV>O(Kg8B8#;mi=@aRDI#tX5jTk}k|K+w$Rb&kB74Ho4qZiQ+Ry81*k8-q7p~+p zU=MMTMN(vu6j{7qls5asd74HM@g`ZRAz7&*QE^gMYDh$!6cHyy3rW#JQnZj1EhNi2 zjJq<2QM8a0EhK4`F400#w2%}nB#Y8(Z4=SLhO$0KYm(w}TC|W9EhI(4O`?UQXd#K$ zY0*NmDB)#}ft$u^~}#QWV_7 z^Y0M__lSaf{#$LKtwh1ekF>_MUf%@ktV!BwXNv@MLl0P+K?J1`)spvE^D3KvD#d6agef07(%*lH8?5#7Pmr`$a7@Ip|hl=>h9FV8$Jo zaeuJcY!W$yAPXO6=eKyhmF*gG%(0$qF#@XOWB@X z_ymkJ-epvAOxGH6{$JLVD0@y`!kpzYbC%1@S;mU{%gk9WGiO=mG=(8&meo7H46nc< z`uSC#EiOEQF=v*&>9e;W%)l>md>iBd`1AR)xMMT4`0fe_v+`@b?+tb@H`cP;ILmTl zEcx(xK75`JpQm$X(jx5@+Jmp2*UL}n?Ks+PLSYuWxnejLuz!rthxbJh-M&!SWz5JuDy9u|`F-K+ zDR;5fU2Jq0Nq3QP2T!?!b-``^(uGk&nA9zs(TzndbX6mk3}H!&U!?rv9eSsU-f5zD zn*2J<#0}9q&Gb$az0*YRET(rB(>qP{P7}S;MDNU!(P*M~Hqkq?WHg%SohEu`HNDeB z?=;ano9LY;dZ&rriPJl?WHg%G-OCu%f-PZow*@1@3~v)%(?r)a(KStU%@AGFMAtOY zHBEHQCc37Hu4$rcn&_HYJeo~(O^&YFMAxjQYgW@WO>|8YU9*_3X`*YI=$h4ZO%q)+ zOID(Zu4$rca@sx|7Xnfl*QB`3>>0!n&_M%I%kN^X{K|U=$s}x zr-{yKqH~()oYizr6P**s*cN(cmg?CkF~B&Z>9^PSIh+mczyJ?t1Dp5(n;6bf{BSn( zGBzhoOcO#g>^E7T#wx+0YOP}W1)F5XKx z3gZL!&=bblo^RXgU;kp9I&ZEA7gEl&j_j==du^V%xZ;j!$-i@O0tm>K+o2 zBoU$2O}R=MyL(+}v!@M5Gp9Ul%F~8XYYG!X%NEAUt1vCB6ZClURi1pkCtvT$*L(8y zp8VsZcYE}jsKHgYlC@4U)=7RkT_unEow(eIyPde%iJP6c*@>5(c-Dz$op{!XXPtP~ ziD#Yo)rnu7IM8XNPAMt=ABlBb5FgFy5A9w z+ruYd2cL^If{{M|q~oW6XBCWc{4|*96zuG{3+xKJ!R~&u2kZ%I4Z+^9kJn=yKisXM zx-dzLTooK(?&)X1h$b!bkR2Slwaz7hSSI+Q*%IU6KsX4K4>pth5I7VLgTtW)%()N# z1C9hUgoAMBsap7o`7q<*t8f&24UUGd!!d9y90&gi-+<$R4{hz3erw0{2NU2#_%@sb zC&PC@P14#i{nn1@4^D;e!D(W;%jz2c9^CvJBu7RKW%$ldxs#zUe4>!P#a1-1N zx4^CNGq??IcMsMg4el@-tT`i`H|EWBw(jmlR$vz0~JFgv*M%O7xXZu{z3Ce(QNlr+R%2TnC=3)N@%wCsS$+ zRq4;|%65nEO!IzvAy#UBb?LoUp!==wJ_u(1l>WiJ&Vol^HvADDg~wnHJPvc=3Fn;$ z&x%cdWR=NLu$L-#y|rauhF60z6|V)mR4gv;HqmeGoqlWY^jmwUzv5k3Q&?gh+IYn} zSZ}AXcHire)9G?d7T(!Tt!+CyF>F^22NYIsH)yAa&#QBO0lo-wla&X;L2xh}0&1U? zR(7!)MR%ng>nrUBSNUbAg|EPP_$nL)UxTCJ>tHuYyHRvk+D)=j-L+EPwenoBhHT~e zpzSL!R{OdHw0-3d?No6YTn=?`C0ylmtN&GA4L=4ouF9$KQ@_0yuJihOxB+g2o8V@+ z1#X2q0k0~50e8VK;cmDGro*pb2ACaEX-$L5``~_f0DcRz;g9a)QFsjIz~e9%o`8A& z{uh`JPr=jhS9l%XfHz?YEQPl~4X<(;EQdIl8CTf?D?nYZaup;%R8}b>tHk@tG;{;b zSN1{%`e3u??|00?01Uzw$U{MmFn|(Jo2)8>a-eIg)FrE`AOcYs0iWR6(6d!L!btce zd{Zvl+TDVS~>Uy{VZiHLmXK)+b4nK$ge&>?qR+(LHmD%N1 znO$y`+2vI;!49QXnO$D>2Y3W#!yn;Mcns#i<1iPVfO+sI_%l2Se}Vb%6g(Z2TV-~+ zRc4o4Wp=q$W|v!Kc6rqTtHdsZzr)LRgn0!P!K?5(ykU&!o3I3yg4xkkZ~Ocmco&)= z2FoE1&9Dkq!+Wp>*24R+4(P0^R`>umKs$5*J5Xg;nyM~HT2p%yq=7A{>H)UE+G73I z7VEdRSiiN!`mHV2Z*8%DYm4<)ZGo*Y1UblClUrWU4k~?iQ0a@<(Z&uceRfdkvx7=s zWLMZN*u@SieGxm_MD~KcVIQ!gO@t2-kp+w#0At|`@I^Q<7-3z(F_A;PJ`4_r8aM*J z1Yd?)u==tcRQjwdxQiWB`s|?6X9tx&JE-(oPruK4`hC{Z@3WqMpB+^Ctf${+J^eoG z>Gws{Xd)-U$?#n`rLZVsC5ydzm7x{%1gYSZ0!rj0&T2DV_ zJ^h&V^kb2G;eIpX9)RD%gYY}}JdHIO3PagWL^&D{LOY##te3 zu@&I=HyhbrZdU#n2pX*u^R{C~)wrVYYx$DLkimz2yDEV5iD@pS|p}SA1uY z&tCQZHSZVuPJ{QWgU?jHS5jKJret*G+ThB{_q|_NvR$Paxs|Qne-P|m*;Tl|>I*?b z)fa=7sskMl3f5MAhn1LWZ^)a2gRAZczEyQ+$&{*TC6`qFB6zIou3&uCFN4FPdj@l& zdj-!%_coVkjMw`He~<3x^;oYz>+{cf{ROYT=<{*G&U#_Zh$n*)Bj%T!G2*F`OGi8% z+%w{@!RQgs1P70JJ~*t{8&Cc(dgHNw_eQZdj{T?JI8tvM{Qr1w>_9e_k%cC*@FH1g z#MRcyKjZB?!SOirLe=PC6rQ|@BQvY+V5RPYUy7*4`Q^*O8zgCo7?>|fvKBhiTxXh5 zVqBxMJth-5s6RVNSDbXkNmsn`n}q@E+zgPeIO&R8=VqYtB(NqF>57xCIO&R$t~lw6 zldgE>Y5smXoZpfo9d%UO^FDk~1it(aiyr>v2D#nY7@uFh9sFk z*JstRf8mX?)BR?)^DcrVGHpv?xz{TT14h3O$Y#c6Gvnp@vqGj;j2D-&j9cv`zFm1? zwYVuRZi}9-i3S0?)gh$~qm;;Z)TzCTh9Nba)W>8zXB=~ye(xAHXt>6=w z_)z7$UdLcXaB$_SpjvDY7aPRI263@LTx<{*8^py1aj`+%YBO=nJx0bfE>4Jx6XN28 zxHus$PKb*W;^Kt3I3X@hh>H{A;)J+3AzoDt`@;e785j$nh0lRZv(;-hTfHW3^_tCA zui0$%nz+?#;xc)0aYP*dwnldg4zx;1Jo(q}SjTmAqJB%h#yF2ayd%`}jukY;#2Vl=9>97q~LNaYs9if zvN2G4n$N7Ik8zEq=X-yl_ZNGAiDTaH3s_iUba|)7}jHa1}6Ti6)G#--T!8a6J%#>TR}Fm7+3?dxy*VQ#e}jZ=1jnTD)PLsq6COM7K$udIB0R@Naa>yVXo$jUloWgW7z4p~`;tgJ&;)*&nFkd<}F z$~t6a9kP|jfPD>Q9kQ|xSy_iHO_-$#vov9rCd|@=S(-3Q6J}|`EKQiD39~d|mL|;7 zgjt#}OA}^k!Yoagr3te%VU{M$(u7&Hf~;CWRz@Q$qtP#;(N8<}lg38U*hm^1Nn;~v zY$T11q_L4SHj>6h(%48E8%bj$X>25ojl>HPlFCL>*+?oINo6CcY$TP9q_UAzHj>K5VX15+m5rpbkyJL4%0^PzNGcmiWh1F< zB$bV%vXN9alFCL>*+?oINo6CcY$TP9q_UAzHj>K5VX18Vm{c~B%0^PzNGcme>skAF zX6@sdwU1}kY^SWfJhS%l%vNdtD(zpT{YB-2qVhpe`JkwLP*grBDjyV;4~ohMMdgE{ z@w~=YLEicx%@!N+0*m`1-7`eH z4bW}_v|E;Ti_vZ|+AT)A_0w)K+AT)A#b~$KN7`*HjrDm5eTz4JzQl2vT=!t&j2CC#;x=2}T}_0e1_X|6_^tDfe1i{@HNbG=1#y-Rbgq`8`CuD6EmXfw?< zKy$rAbFHMgGBnppn(K9%tC{9nM03Sxt{BZVNOKL)Tmv-M0L>MnxmM9!t7xtm%@w1$ zVl>w>nkz&|CvF*8t5mKywYyTmv-M0L?W(a}Cg3 z12oqF%{4%C4bWTzG}i#lH9&I>&|CvF*8t5mKywYyTmv-M0L?W(a}Cg312oqF%{4%C zedxmt&|Cu_^Wg?)t^t~BfaV&Yxdv#i0h(*zV?Nvf%{A~bA8vr=8lbrbXs!X8D@$`_ zX|62I6{ERgG*^t~iqTv#nkz-Sha~(u;eX(+-nck~{qiL=f%@w1$Vl-Eb=8Dl=F`6qzbH!+`*s$h` z(OfZ_D@Jq0Xs#H|6{ERgG*^t~iqTv#nkzWXW(r&f*TW5PBisZx2fZ}RAPqA}!wk|e zgEY(_4Kql?e0)vFK^kU|h8d(`zDO$!&&1{OIs**SOm??skp4UE>bdxWhH> zaE&`$;|}fVS3+Z#Y@72oSaPOX%UN)ddW#xINf?cUYbbA9vHM~5$2Zit@@iapHLkoG zS6+=Puf~;EOxyer@`;Q`eEumd3B$>x=&u+C$H|4SNF-Q`{dPq^6EZ$ zb)UStPhPb;u0kKyd-Cc%dHXBm)p+u1Jb5*qe1)-(>Na^f(7YUIUJf)b2bz}y&C7x2 z_mw(I4zvboM^73za`M13N74q_L zdHJ_|#52gh<>lY<5zi3u3=z)|@eC2q5b+H1Z+ZE*y!=~U{w**6mY09aNB#oy;VF0; z{tC~)v+x`|4}XIfz*rplx4isYUjEI#S4K{>LdNW$KG+QXkc9z#7_JKZzTODOE3UF0 z@rUb8Z)0b+dmqQ&~;T%?P4XoZ8TCf%`FwODa!g`g|c9qn264gqgT1Zq2iE1HHEhMUi zM75Bp782D$qFP8)3yB&cQA75(*kpI54dvQRMYX+R6xgSmbhT8Nby@M6l`rhUP0CtG zSqmv^A!RKUYm87{Z{^@*p;g7SUB$GW}4T)S;$@%ub0K^W$}7hyj~Wsm&NO4@p@UjURJJ`mFs2YdRe(%R<4(o>t*G7 zS-Df<$77UURJJ`mFs2YdRehv)u@zeR7y1}r5cq|jY_FT zrK%pF<9`be!tdbsFcThvhv5(K2+W2*!lUpQ%z?*YE<6GA;7{;pcoO~s^WiCY8lHpa z;cxH)ya)@78(#>2hnHoFUV%mMD!dMF!#nUUG(ilOLmZl66|9E$U=6H=_rV%_szzO^ zMqR2#U8+W1szzO^MqR2#U8+W1szzNjW($qkLSwejm@PDB3ys-AW46$kEi`5ejoCtD zw$PX@G-eBp*+OHs(3mYWW($qk60sIxq!Owi0-t~#!00IzrFIpib`_;|6{YqFuBj-s zt0;A-D0M`312d{slsZ(DhE$X~RFpbYlsZ(DI#iT8RFpbYlsZ(DIwEFIM-GI8gVM;M z&T|+T^RA-QuAT}7!~MX6mysa-{>T}7!~MX6my zsa-{>Ju(gM*JcmEZ{b1s9sC|;JFb5uoxj;X#Rzs}S5NkEW zS`D#QL#)*hYc<4L4Y5{3tkn=}HN;vCu~tK@)evhn#99rpRzs}S5NkE0TGXmq)T&z4 zs#?^lTGXmq)T&z4s#?^lTGXmq)T&z4s#?^lTGXmq)T&z48d(N!!#nUUG{JI+Lo>91 zF<`1iovKBhszsfuMV+ceovKBhEM+%K*{xc%UbSewYSDVtqV=jp>s5=^s}`+SEn2Ty zv|hDnJ&W4PqPDWAtt@IQi`vSfwyG4h^E!KZoxQxyUS4M}uQS{arb|_*OI4^#Rj5l< zs7qC-RaK}}Rj5@}s8v;{^`pHH!g(uimj6mjwQWC%w^`>Sf?Df*ggb6LQruM|{A7|( ze(aNJKDn*m!^}Dy2nSo4?GQK=4uiv?296L} z{|6iiUy|+nvd?N^+pb2dS<+^fw3#JsHZH2sn5ZqmPhcus1DV3@M(s{BYImA3kXuSd z8M8f_@42gWBzA+n-1WZjd7pj3ah&l@M-=ws%k0M=-H-qIS^no|OOBRRJO+*{JW_Ik zv5QtcEjiolyH1(FtH8C5MH(pGwrf&j>2$v5uYLBcShvyoo-552S_NyoPK%AV7M>^% z0;sOV&Umx;pt-G z>0;sOV&Umx;pt-G>0;nti-D($fv1asr;CB7i-D($fv1asr;CB7i-D($fv1asr;CB7 zi-D($fv1asr;CB7i-D($fv1aCe=T19wRm;9cy*$9b)tCnG-G70sqBJe;To2$pJnT3 z+4@(Mz?*Z!dc(&Sar>Vl|ps zjb>J(nbl}!HJVwCW>%w_)o6~K1!u!Ka4wu@)bRO^7r=#Z5&QryhD+d5_#s>dmqQ)= z1g63@@Kcx;92)tBctA`S`6b*9_W=G!eg!k&*KluPOZ1JxR%3CuM%Ngh+h?TpX6T13 zn2BmG=73SP%_H>9|F5*gXxd^Y+F~c#;sDy>fPZU?e`||>YYW=p|08X|@A>cc89h2U z$SCWx;T$*@E{03thj11A6n+_uE;-DM{lo1!ToW8!QXA}2@)dj6jd%R2+b` zF8P(nq*Y|nDk{kZW33hPS@V>vJ}$4{DoROauaf2@~(te1bRmw&96f2@~( zte1bRmw&96f2@~(te1bRmw&96f2@~(te1bRmw&96f2@~(te1bRmw&96f2@~(te1bR zmw&96f2@~(te1bRmw&96f2@~(te1bRmw&7m`FtS%*d_niCI8qZ|CpA4Ov^upxyQ8J zV_NR9E4aiA9kIXMW0%}xQtmM+_n4G#Ov*QQ$u}lNOC6%64$)GFXsJW~u}l83UjDH` z{;@&+u|fW^LH@Bp{;@&+u|fW^LH@Bp{;@&+F(v;P?t1)+{9|1H@mcxD7vvw8$vdV> z&yX*?800$T8^c^*X8k*X8k*W~J@{9HI zi}mu0_414L@{9HIiYa-;q`YF6ykdi>s#R3gDynJ~Rkez$T18c@qN-L=Rja6~RYcV) zqG}aUwTh@(MO3XKs#XzItB9&qMAa&yY86qnil|yeRIMVaRuNUJh^kdY)heQD6-l*< zo^m3moLYICN>-bkVMfj{BWIYAGt9^tX5r2b^ zrRDn4a(!vJzO-ClTCOiG*O!*-OGgfdL!IX^I2_a;r2Y@CFS~(a(zj;zNB1VQm!v4|CW@0 zOUl0`<=>L>Z%O&Lr2Jb_{w*p0mXv=>%D*K=S{)*-4v|)eNUKAX)gj925M_0UvN}Xr z9g+JB4Uq@nx9}kR4t@_a;URb!{s6P!5tt2sgh$~qm;;Z)Tp&|&bPaNJ4RUl1a&!%H zbPaNJ4RUl1a&!%HbPaNJ4RUl1a&!%HbPaNJ4RUl1a&!%HbPaNJ4H0Yo%F(6d=u&cY zDLJ~799>F|E+t2olA}w>(WT_*QgU=DIl7b_T}qBFB}bQ%qf5!rrR3;Ra&##w}r;&OCxIl8zUU0jYXE=L!aql?SYJu64|tQ_47a&#}q(Y+u?_ktYV z3vzTX$kDwZNB4pp-3xMbFUZj?lcQTEN4HFlZkZh2GC8_sa&$?#x0KvlO71Ns_m+}- zO9e;pG^=@<)jZ8=p60K4n$cLG)x5)M-eEQGusX7**nh7e9@#sX!&AIIGA6iL zj^K!oJjR2A)x5@PUSl<{v6|Od&1Pd_n%7v(YpmuqR!8m% zYN9KG6NB-=dBM}hGd~?%;E67B{DI8hx2+TV56?R-W3_ihGT5`~f#5!Oe{9w7f(cc> z53Z}48Jt!1P;hP4!`4ZAG}yiB6<8GfwCe5P;;MIzxorxrs)_|CR+-~j6%Qsvb`GA7 z>|1hdWPg7@z?y4ggPkK^@cN6vG&4Y+ zjvQRFedL7T(#SW%tVnQH@apdJ-ROFT5 z#K@xHpvbGP-)?1+j^LE&ijq(A^8dwChEJi*cmL?>;gt`zBZV_svE)l;P+H5S^!DJ> zBf{Cg`-Gop!SPyf{BR47FSg+Dx3t`Nt+t<5snQxHT41~u7_SA!%lkBHffCodQy>3M zAJ5gt&* zkK0Gwt&X{CAlDNtSGkSzq*}aQ39GC)Wxc-OedC+f898BX{-D$68^iGc!Oo(u3ibbF z$yeccv(8VpV$yemN)cD3h-+67S46}WDf^>Y@N=AN4^V_iD*z`Qi6VPL@O7 zwqtMea7*@ltR+{<$rtwu){;G1vPVnyXvrQe*`p<)KRF77fOT&)GCYQd?cf!2zt$`7f^52?xzsmc$j$`7f^52?xz8PRescAWdM zQMXUlEo^y_;oZ~4cc*+@`a)zgz z;VEZ$${C(=hNqn2DQ9@f8J=>6r<~>~r+LaNc*{8e~8CDwe)PqZKIW^7Dp>j zF64OMIdzJZIz=}xJjV;q@xpVw@C+|JQ+kKbe+~B3tN^m!Z>%54D^K&vbG-5#uRNpPky7m+Qtcm7?H^L@A5!ffs#I01R8>^% zAF5PURP7&9?H^L@A5!ffQtcm7?H^L@A5!ffQtcm7?H^L@A5!ffQtcm7?H^L@A5!ff zs=|NO{vp-=A=UmN)&3#X{vp-=A=UmN)&3#X{vp-=A=UmN)&3#X{vp-=AtS!dHR9`B zBficx;_F-^zRoq`>s%wg&i#LJgzXceh9^V~i$o2JL=B5X4U0q#i$o2JL<>_z3sXf4 zQ$-6?MGI3!3sXf4Q$-6?MGI4T>^UBLj>n!;GfAnLq|{7OY9=Z5l9YN$w|YspdP%o> zNw-=_w^~U`tt6#Zl2R*4sgXYiDYcT6tY=D%B&9}@QX@&Jk)+f}QfeeAHIkGX zNlJ|*rACrcBT1=|q|``K>LV%jk(9_GQMC!|ULkTwh#V3khlI!>A#zBF91g|6^Xha2`ShF z|6&xnT3SjiEhVp&lGjSfYo$aeX%R|VgpwAaq(vxUuB)3bv2D%9aDAc%o@a23Op`}p`eNo$?yb@vgjlN7F4-N$;g`emzM zZuQGe!Cq?lTT3d~z7fU!6T@0OWb3pdtBja89v#2<@VN8*X~r)9#X?>jWlSt`&{{xM&ZBhs@akqt>;90 zxZ-kG?6aEq=UlagXF9@F!u5`ZoGtAvX%*T7tZq2oS$dtN!}?+|<6q-4Y*q#2|Mu{I zd-=Wzs~i4b-4l4MxdJN#<5GhrzFW+iF3ZhJ+_o?A7XJ2o{OuR{+iP7f?|QjlJ!~*f zxU;Y!NEF`ZcW*EzdyDbdTa3rv65Y+}#e2Y>uovtN`@k637xsf{*dL68HPa$*rbRyb zS@;}$9_-m@rbXUNi+pfo@P@ISOZewY-9@9jB{l9e+?P7sJ-NqS4h8So!+ou9zwdAB zd~Q6AJIuJde;J?f{)fsQ(|aF_ODgR{QW@?;asqs_u+?0nt>zjnH)4Oe5&K)sHQH*f z(Q+g9mm9Ia+=%_r|OqrY-Dt5P*EHS8?ODd5QAU!&T2$z!mp_bzos^I1Yhn;VgqAW$jqz5 zGE*Opx8cQ|TpZE-cfVX*{O%jR+u*yQbiV7mOG2p-^~gQ?OV(eq!@p_pmvEHN66|fj z-lfHn;o%CbvEm(tYl()k{Z8?YQhuHC>r_xBVp$uGRTcAee>grR(EZvf|g*;5ch#lNV=wE4&76v5~geNL%!Znzo2F_n3CMurD1D?(cDP$qr`x?nwUSAFbB5J6-S~9WckUuJWwEba&ytFX67fGt7Fq zgI;*F{5bmI~yzY&$(uzVsVrBy5XrkMw*`6eIat z3Nwc9;97TZt>=8gmFJ2zvtrF`@RnIf;mq8o;O)Y10^WJhL@r|(wY+dQnN0-EMY(Mc zR)|bjTG4BjeE({iY>n@&E&Q1bx0B(HppAz405({S(@ryiHBZU(dRk`{xsC;0b`lAn z#%Ltb&h-ECRA*XE_bfXtgfnx^ezB|hhO#;4TK>uLNqE|r;#;lrejD6w#MB)Sw(VNE ziELp*h1t2`EL^i$%#MX^vst%RZJW)ymA2bz_Q|%{tlueIRdrM0Qu5tib&KO2j(34v zM%CTk-vbTSGCk6$_(_gu!WA$XrofeO75oUUh96tM^gz3I90Ui$A#f-h28Tlp90C6U zM}nS?ei>>()EpHxM@7w1QFBz(92GT3Ma_0mPeet{QBkv9)DuxrbM$!YsoG5^`c1F{ zKy(6}2;YX2;AHqt;e)94@S_vqRQMj82B*Ur@O_vBYD-aT#z)VFbKqP!56%a>O-C<; zi{J-vFH5pGn38x`S3MYvHBZd8OD72!rjxKR;q^a+>;e}X^5lMv2${vYh! z3v`s#**5+OM3YI#K}11BFvLg_&?w>H5j>!HMDbLu)vC28Dt)ck)>>=TT5Gl1gFR?% zt=3vZTd}r6MbV&W6ak^aOqe7PMi>GKAsI4wBF_K!Oacbfw}i5>+8U}1M5mY^4DEI zv*Wkmv%O!z_e!%%EL@uP8&Jx;1??w2JhXkQBD8) z>yU#FIrz|nK73W}Ir>YZ9~=Ex^`P$8m`?|<9luo%>i+%1f5#nm->I^dvxf?++%ak3#sIg`%62a{J&oP|MlwsuUG$fdiDQ5 zX}*XtTDvjYMq{*hXuc%Px0&YKMDx|td`X&bi?Ldg=G#W|HPd{{XudY%wuN-xX1XsJ z(RR{(oyKlWA!~u?za;&ar2m@ezb*7%FxqXW|JKrft@K}#icJM7mcH9W-zAOd8fd&_ zeIHZv{&Dz|zdZp@i6-1*rT8zbC0HHGqVJM>n2R;$+i1+U(U@ z#(Z7Id|k$T8;$uk8uM*5=G$n@x6znyqcPt`W4B1ykn4}AnbYYS% zOwxr(x-dx>Ch5W?U6`Z`lXPK{E=B1ykxP>mBA&_n4}Mr^kI@dOwxx*`Y=f!Ch5Z@eVC*Vlleb2YM&`)^nQ64-^?F zF&Z82_fo$f1Z93d*!u{O(UxVkZB`|Wbj(pccZi=4HKIMt9P5s!aGu>Hp0{(*OR!MA zhwfc0I{J3+V$s(3Ej75s?)!?}_Z7SEyCL1wSM0v8Os~G;_kCN^@xHRE`r5y;S(>8Oj-FL+3 z$X()ZB3agmbmVSSlid7pcu=}cMf%O*gS;OKC;ItPfBz^qR1h zlHrXYA`)h=Fnfs#1p6wq<~;@TRC9k8o&%M~^A=bs_LBc!m~P5@8D6zRm#mq*MX(rT z%;d?K$y@F?dUNY8qIoOQE!J2ByK8sk+iOykr#sTnDbmmp#DjKtf~djUA_lD@BCm=X zY}sk0g>sOFh6-gD4EP_4ivHgp!7OB*&GLB;44F3$BE7P`i&mQKIcy z)1)=;xYrxCs7Zdo5HoiPnJ1-Y!BSc{N(%>T;b5&RlyNdf3rDMJA1H>U82%sBz?*u! z73$=z6y=RN!z%0XVs=DdEdsnojz^O|a4m(fKK&Xc@FpeD8bpGy|5D8VF6Ms@yB{eY z{jPZQyCO92=3Ny!7IVCp_q9+MW4xdD^-%x3Z~EJHp&ww2BlB(umE?UVl#_R(yqiKX zx|sOf`+C(Sdevaxf~X#Dl^!JO>3`Q#AM;eE#pU<+^d&ugYdmHDFt^Z$qB%;W>T=QC zE3Id|J`?-A&>o>1?Gd_B?DIi)Z*^7&BxSUol77F(2OeRy-voZZYGGE}%xZ_ST9nns zv053cmGO%_mfMfzCbHZy=Hue#(dLEnoHeKhznzb~qc#9XT<^^$H5a$JJXvG*$XyFMP zdBR$r(99Dy@`PX(;w@(y<4lL~hV{H*bKjYI`JaX=o#_l`I)!KC@P>SMKi}Oy-`&r# zV&fBbgWPd6cm5;0LiRo?FZghW$es4_?PAx`uXn3=X=!i!oZyqk_Wq>fk9XDMU3K*? z-zwT^)sw}Ir#i>ET5x`-Xt!?<@a_J-z3biUea?)fcKgF z&>w5`atXSlo$g4`9c$>0HFQUU?nv52qh5vMES zbVZD=NYE7tx*|bWBlyDPE7*F#_M^A|y!I`n1V5vP{nwN8dty$r8<8D7Ea@n#ub&7rS> zeN{p;{J%X_^#1EYH-xf6-+>$9yKocS47b4d;8yrP+=lIb0Jl3vklA&I@7`$!ru$iI z%u1Liwl|MVtyMC$I{T<5E>o+s&&6POkUm#}9Y6Y9O;LOeW~7^CXw}QmS|>xRzK`?w zasJFR`g8wxpTGYa9`m=yVaIcOZF*hCa|Wc>$+SuY&#cU4$+*;!OkYKxH@y}Y?5PV6IooHIe&mh;Eym99)(%( z7|4#WK4PV8uFhP3B%7;KHdm)?u1?upowB()Wpj1P=IYE#AMWRlBUKc3D^L)@mfJ)kx<4)Ny_W_rNdUm+&jN*9hW1 z_%+-Qzk%Pv1MoX|5PlC2!yiDOYkkIY>ob;HpRqi57CZ)z!=K>K@C3|;C*d#f6wHCS z@OO9y=D~A%kNNOC)Hv<}coANLT6hDN!E*Qqya_8{B}72&3z=QbxoThJs(nG}G|TL2 zmf6)Tv#VKVSF_BnW|>{hxe3?+{9a~Pb8ZXnDAwcb1^r=f;HR>#*2%g`$hu0%x}rB> z2ghInSyu^JR|#2H30YT(4DXbC9|d}SSyzdGhh$x?lXbOD*3~*$SL#APXRlTgMdRbTXvaaf7UDeCFs+VX5tt7chO&9bhV!xzFu@M*XhJ`0z@XZ%tNQS-WQgAj_klgd!uP{(;J5GqsOAxV5FUcx!^7|gcm&k=2+xE^!HiZ|-xAih zgjM7Wnb0Hyy}#B)hY9;Q|48t%&SgWR?V`knq^rv z%d%>gWz{UpYLzUjRkEyB$+B7{%W73WmRl9~W_mU?d^I&ZUlg^0B3>hk z8l{S7Q^j|SqSjNz_ff?`KF~r@(>bE1b138QiJC4FHJw8t|Jhinj!OQksOj^n3%rsw z2~IQ0IYX_^htqSY=Gh{sbNbFq-z{o7r|%Q+RCZJ3c3OG`y0YBf2?9baRgA<{Z(@Iij0$ zL^tP%Zq5|;H3q&^;h;A+r-CQ8Lxj=Mtf#~J}(ai;-n+rrY z7l>{y5ZzoLy177fbAjmQ0@2L{qMHjuHy4O*E)d;ZAiB9gbaR2|<^s{p1)`e`qMHq( zn+>9y4WgS3qMHq(n+>9y4WgS3qMK3C&8X;RRCF^cx)~MSjEZhXMK`0On^DotsOV-? zbTcZt85P}(if%?lH>0APQPIt)=w?)OGb*|n72S-AZbn5nqoSMjMkGgyZr0~r9Xik$ zwMuleUUaiQ@7mBHW7R6r&3Yr2BaK*YHe#t3-K-bg>=fOsH*Otl+)u|C3Q~@6qnZ zj&T2#T&<58;q5Y_3&sC9!1OGqC-21;{@7e$T)meLsc)gm5{~m>jE*FQo z(g=Qh`ZOcvKL)X=ELDoF-QHk)@wVqT7+YjAWR3ri5R=-E?wXK)S9*K?-SRibn?amu z2JwE+LK5e_hwm2Rx|Mh?$nFfX!UBG4!DWl^*!JFM@(jEu;`8>7Gk6Y$=Zs0;pEDLt zgVT+D&ww-iJO$43^Vu-f&*#9oa2}iw7r>`L#XK{6EoS!aH?w!YnY|V>do5=6?l-Hq z(TrY~*}N{Zd0l4ny7c2i^xuX0>LL2-tM%2x#TLu;(R*6Swl#fCX4a-iM4?y);(+uy zA`Z3ke-GTHCGuKj9B(rl@ssRH^3l#rpJeu-OWZM7Ker>p_);VJ%Z$+9*N<#865lQc zx!qav%&rzgP%pep%=9Sn!Ox4DU2TuUQ$z(RNUQR;=~b&e-w}7F)tzbDW2jYXYFt@M zw#{0yZ8<(;E!j3}$+qPLdvP6UudTyHZLUwhVGZh5QS5xPPDN&bitTW{>&#P=NX<%- znpM4Kp579vc}t|G_smmQ&&<TlHTeyle8%_lhnt3_;3bk znP|*1(U>mLn72e@HfLsxg4v>Arsy1hx57d+rptYMM>J-Y*`Y4cn1pCdLNq4f9)4g} zs7pj9DI${;kx7ckBt>MpL}a=|WV%FTx1iHOV+5t$_-GD}2cmh{Z@tQ3)1DI&A#6J~lg_006F5{>EdMDHsj)7q0>(dD&# zW-*@$X4Bu{Gi$MMVOGe~`*o^ynJ0J#<@$PP87xm1dz%03X}+6+J;D?H00lcZ6Vt6r zPxoX$NWq>(!Jgs?Un>t_A|-pir+l{9?y+(J@~PP(YPN`)y^xxnOwAVQ0s2w1pVbRY zq-IMzKi7-0|3;MkK3Tpuh~C{GdUuoP-A$D4WmzYtzmhe{`$^tU_P5j0w^6;fQN7Fb z9CuT_MS71TDc`f?4dlxk$ftmdDBz;LPo;|};2%=JKcs-~%Kkxm9u<67_7BtZ^fHH1 z!bS23s;S{+dYl=0oEdtY8G4+!9w)BHnW4v-p~s2qapHQM8G4)?H zrClVKU?R0$L~R#Q+eOrN5w%@JZ5L77Mbvf?wOvGQ7s=LtUAF$~vh_E~*54#sf0Jze z*JbN(lC8ffuM`gQdl?)IBcR;hDxeZZ`rWn2*54#se-rgyOuZLV??b5fO6t9sdM~El zhfwd;)O#`YUQE3gQ}2_g_j2le9Q9sIy%$sOL#X#k>b;nHFQ(pyQ18{$dolH1OuZLV z?~|za!PNU;>b;73KbCqQOubi8?~|$b!PNU;>U}!(KAn1>PQ6d3-ltRV)2a9A)cbVm zeLD3%oqC^6y-%m!r&I6KsrTvB`*iAkI`uxCdY?|cPp96eQ}5HM_vzI8bn5+7z5W>L z{T}N50KNa=)cZZu`#scq3H5#i^?naV7=sb+#R!K|?`Kl)G0ZS9&uS>@{gk|$()n@| z@~QaARQzNrelit5nTnrG#ZRW$cy!m zSLzYR>JiKJhVPk$8>bhn#Db-Izpr4yNhi2_+0utJz8bx5}M^D zJ=&+NAigy;DSemK!naxle5;kechE5V(J-g$?cNr@owfT_(lf7RU8Y5sn_c)ETxtD9 zd-~q2j`T0HlIh=I)HC&thv*##XIsIhcYMZ5)jOR-CO&jICOX zZJZd}Sn;$2avL3Qb$XR}+Gz2#(c)>C>SC<_DeHu)KK3{-M2kcqGEQO|09-x=pNGuQy)Z zGtOAeYB8L$W1O+kRW-S)99OZ2t0)LfbnoW$=*@4qSA!kxXO8r=BVD8akLv$})$`Y> zv)0et)ZXT%_A@s%z;xOPlvJdb?p2A z8{fdj)3s)i)>N=@1shLf<73$PQ>riQt%bK};bF$F`>^FTY?)%qzp&+vZ22Zzz8X40 z>$lQ(o9VmFtgx9Ef0q@CSmDtgk9~$Ee$5gaSYka({94PSS{}4`K$g8$wR*W$FVpHe zt$v#x?4SpOwJVKUAIwAFO$%<;@-4LBW?FDFEx4H$+)N8@rUf_Cf}2@l0!uu?5|6ON zBP{U4Sw?li)N~IfEW}*p6@Ak+9i)XIeY* znAy6=;ZLGtPry^@+4R6tdSEF%5NLrw2OL2M%%%Ze!_%+f>DTb|Yk2xKJe|bTui@z= zo=)QF*YNafczQOTZo$*D@$_swJsVHY#?!O$^lUsm8&A*1)3fpPY&_kCr@QcU7oP6I z(_MJF3r~09=`K9og{QmlbQhlP!qZ)Nx(iQt;pr|s-G!&S@N^fR?!wda@bo-9Jr7UM z!_)Kd^gKL04^Pj-)AR83JUl%QPtU{C^YHXMJUtIj&%@L6@bo-9Jr7UM!_)Kd^gKL0 z4^Pj-)AR83JUku2(-Ax!!P5~u9l_HPJRQN)5j-8i(~I%+Vm!SVPcO#Pi}CbgJiQoC zFUHf0@$_Omy%>8W_S15bD0>G^ni zKAxVBr|0A8`FMIho}Q1V=i}-5czQmbo{y*JkEiG3>G^niKAxVBr|077 zxp;amo}P=R=i=$PczQ0Lo{OjF;_10~dM=)xi>K$}>A84%E}ou?r|077xp;amo}P=R z=i=$PczQ0Lo{Ohn!_&+3#c#|1*c#d!NB;#k{~0&GWc67;-26+N{6`%83!M9`Rj2#m z)xkJ*D^7h8r_RQsZ)c9Mzg}mK9;a20lQgQV)&I8YS6Ay-SLRH~_v$SP;mfJ!j z^(?>Dv!wJa5xq)G&oNamv9BKDLOsN0J;PulOMLW!7fScD6q634Psm zXT1PJ-{Z_@nT73i)?e!DF4x!1*4I6uue6qXevulTH$wYnNL;Apn z^nnlQ10TW$*XRfD!v-_3!Nu6%Dhz#u8L*SJx(Ey3gBhk{hI_){^hwy^svdUub0#8k zAl5Cx1j9q0>0yRj%_w~p15d@ihngXh_k$0=kp7&$bCbUFK`b#z|G6OieEN&w*RW1! z`dQXnjCrSqzHVH0wK3T={pw8p>P&s=efrdI8;@P9PyM0(bfdoX=lasKjltsj(Vyu< zf1?lmw0`pj{pA4tKiXKE}CXsG|jkZnsHIWxF}&< zG|jkZnsHIWxF}&RY$!Tes>XXX+zo>LWY#k)8U;PJQG|ePpLTvQr<~sgIngkDRHGoT-nTsgLZ` zM|SEXJN1z>^^u+W$WDD^r@rw7edAPp;?erVbM%9U=m#h12d~oqouL1ls^2?Wzjuzl z?ht+4XY_H?^l#_s->%Z{d`-V|k$&e1{mwZ3&S}O6Pw00Z(eE6t-#Jyk^N4=uBK^)2 z`kf2)J73NFcBqej=OF#giTa(d=|k?r|hJ9n;4n>b@|t6xHs5IbuI2)i+k7N-nF=QE#G(t_paq1@9>XzaPL~& zyB7DZ#JwAE?@HXe68Emey(@9=O5D2=_pZdfD{=2i+`AH2#&BhfKg9S$3|Gc*WsG0M z_(cp?#&Bf}SH}2AjE}_lNDNoTaAl0I#P~`KSH^HjezXx+ZsbcF`O-#Qxe-@x#FdS>vJqD{;>t!`*@!C}ab+W}Y{Zp~xUvyfHsZ=g zT-k^#8*ybLu584WjkvNAS2p6xMqJs5D;sfTBd%=Zv#a^+*?e|2pDp0CL-_1!KD(OF z7V_CLKD(OFp3P@}$7fIDv#W9CYCc=WXNU6HDqOh-J>u_ZquB^kAb-1z)SJvUmI$XIHSH5Qy*d6y; zMPC=-oM2a`4{**VT=F(933PJ`cPzslAK;E4<29I>I?M>{UH$o3%+^8e_t&Q%fZ;~! z)BEew55#bjY2QIcP;H^CzRj6cpIfpo-E-4NUQY&p$zHhUrak`gk^k%d^f7xru;-Ir zncwoh>UGf2eS=rdF*&E@JfCwbypuaR_l(@zx?f{*$4$t5F8J(@*Fkv)b^o0Ex4f^0 zpWpfS5C1>->AC!IqsvE^SD%+Zso%lfKXv~*YGJ>#yMG!yyx*c-UTys{uQ9p(n!A76 z|CSG51q1e9w(ke~FDsZZrqaLH7T#9$lj21GWBNa^cWCb`_FuO5-}Wio_v(Fb+}EcD z<_1LtB2m=r}(gAh9!DlzaF+M`02nQB|{F3!v!V5 zPmcya4<9sqP{{?szr!nsPpbT&?1HlE4!(56aphCWZ?E`$#k$H5MwgEqQhi?ad80;; zx_WfpsNauTc*tXi-d^>~s%3{I4lf^_$HFHB|6hGg^`FKZ)B9B&8SM%8ZUf<|>Jv#Q$vB&KE?Hhi3bZmUwkZ}`szAigr(D>vLgZ`D*Q4jnF zuezgldma0yqbra7)4%hYn0@Th3CB%1ZDRJWuL&1Ucw(2o1o!nP|I+Kli7Sp9_iw(g zK7L=X&m14=ew|Qkh3($Czsg-03gy0%`+Dfm+$Fiu(3sp+xpDe&T{u5o{5ayx}`o7xu^u*iy+~@t*p#iF&4p9AcfV|^lHt+x2dIKN zKo!&h;bPbe`orGf^WlA<8pgm_7zg9wC^#C9fn#9;OoZd$csL<6K$VZS{7Ggm2k+L> zV)gjqTG~%NzPOh5)5;NAIYKKB)yk1Qt=vZ|_tDC-kF+vV(Im8|mh2g{WapU%b)U%* z$BLwl6G=Np<(3I>YWf)!P8!XR?Wl#6$Wntw9gO!?_h|iuGwk6Ed+c(CU~i|JUF-d> z^{=k=39~%v92+SnyYFMBg44|1&J{6=OsP$>SY?pKIUCGN1QnZh^MH^4rmI`}@mJS< zv|X+)s8G42r;UrP+Bga~e^?Ew+5dL<-wrEg+SvF#$KJ|&-Z%Dp-_>QRLb+peKJv{5 z-wf)^Bz!a8^UcJ+^vxG~zPZEd!8sc~{+ks0e^$ThzpMYWs|6CSCRmHrt{tsC?dbT} ztNHgTgZ^uKVcf0dW6l4Lvqs?<)pw2!mGaF!JprYj^|L$$7xJvLc-GH()=ypEKRoNh zJq5!(1><|XV6(fm*+_l+E_-2gKkK{ihj9}BJ*nqe|JU}z_z(BM@B}7#&M)<^-K|`$ zl?Qmv+qAO3=e$iT`)lDCEj(NckJ7^NTDVRNhiKstEj;`qE$n{IcdWMwp7ZXr^f!C5 z-E;Sz=T3E7K0nT?qVCFRZ@RxPxxcl6{XBQu%|{1S$&zMJw({~?&tR=*FyR^O68Gu8 z-cLLNe_j0X*VTQbp69Rox`NeCF;Cs=Mw%;(FkjNze67u_dC|f=zP6*U4k|l+pgQq2 z6!NEX=c?N*JA+C-n?fE$Ay1%?&!mvYQOGA@!mIG$=T#y;TqWWb3^-K8W00!IUr-&Y zJX442U9-HIH5#JghlNJspON@yB>owRe@6CH%pMt<;pgksnEtT#Z988Yi8n^#jgdHG zB+eL#Ge+W!k$7Tc?!8uEJ?DKsJWsh7!(PxI_6DEB6(e2$f}U%?)U{va+V6GU6|VUT z*IbB8M&go@xMU<3cV{f4W_V~4%M4?g(JV8JWk$2g3H)UWe>s=GOktfvS*MzH zs#)i3)+yf2Iz_Bg#5zT+Q^Yz&tW(4~MXYla>l~#&-G_CGSf_|}idd(Jb&6Q0h;@os zr-*fmSf?mF(7fV)Fc^lw0WcJXL14KO@57-C4u+sd8Rb3b%c{H|4%Ln`2F3yn&#FbN zTEwbFtXjmXMXXxHszt0?#HvNCT9mPB0jn0UY5}Vju<9^Y9mcA|Saley4rA24`y7^@brY5}VjuxbIT z7O-jos}`_o0jn0UY5}VjuxbIT7O-l8mCAR~u6MJ_SgjqW$2-PTcC3DDg7=BuPlc(* zP3QXm^C^xCj5aP20lpNj3?0_nGFm@M>qqr|iZW*y=nO^9Q0@%loME6d9OMiGo#7y7 z2sF-d&Tx`59On!r&QRtIWzKNoKR-iOm40+==x9f;aP(1*zMrFya`d1l3GQyWqn9~) zxucJE^r4PE)b);by`yQH4%%i*c;C?F;Xzoj)HwMdI5gBR?^Ubq2mB|#v%?o0x!qBR z^&IscM;#WrKp!!JH;>@WBY5)&-rSEj_v6j|cymAA+>bYp;LRg=^N7$4xE}7(YmU*b z?i~tF(cYoj8}Q0d?G1Qkpf-P6n?JA3U(x0-XmfvUK0}+&V9~Giwx}L-1aBU}n@8~G z5xjW>Zyv#$N9e z_v6w1^b`GfbiW?O5%d$m&Y}HybUz;5ug9bN^>}nY9^H>ekKoZGc=QM!J%UG%;L#&^ z^avh3f=7?w(Ia^D2p&CxM~~pqBSIIlY#Gazv1}R3ma*)3mL1Qs<5_k*%Z_K+GL|i4 z*)oBJ_Yl@Sn{^Li-2tpy&ALH^;bhjG%(|Db?y0PM zD(haMzc^wy3zxBQ84H)Oa2X4iv2Ym+m$C4rJn&K$9?!x9S-6aa%UHOKh09pDjD^cs zxQvC%Sh$RZ%UF0k3y){v@hm)^g~zk-corVt(`Srl;qfdyo`t*jUm4}SyC)vc!sA)E zjD^csxQvC%Sh$RZ%UHOKh09pDjD^csxQvC%Sh$RZ%QE#`gIdi&b=Q@l0c>_OdmYSP z`RrBAR)KvA*e1+2qk28dcDv9I^y*8sf2{V8-KEFKcf@@hsnC%Mcl&hMr^CB^x=-f$ zI!JC%joeJ^$2OWpTU_q|lR z&(V@|-1C5aOWpHQ_q^0SFLlpL-SblSywp7}bI^PE`|G7=l*S@o}1b118VyN+1#Uyv0B2v zmCI0<*)QfVo}A^fT;4Pbx6+JZ#4KJ^Esa%bZN$v(byrY$%k10|vvW(#$}KS~_lnuL z2D5RmWle%pt;asiDE}lAH2w@+0{;b9 z!`I*%m;v^;Hlw!0jM@@2YRk>2EjOdK+>F|CGiuAts4X|6w%m-`ax-en&8RImqqf|P z+Hx~$%gv}QH>0-PjM{QDYRk>2EjOdK+>F|CGiuAts4X|6w%m-`ax-en&8RImqt;?Z zt;LL5iy5^RGiohn)LP7_wU|+BF{9RER;j_PQiEBg2D3^HW|bPuDm9o@YA~zRU{Sr!K_k)S)~TEN)2X}8q6v+m{n>ptJGjdX%)s+ z9VF}D-3931-#4f*bB=RE6FkL7_Ijo=`zjfKfAWq)du1JG{B%47SJ@q-EO(UBf9oj4 zyBy_QF`n~7r~O+;2rTvRzip|4e>QIH+o2LeQe}iJyXEW{L;g=!neNp?_I~O^A6HX) zfsyUYJ-zV~5uDdO(f?je=^ib4gokW->iQT32hdb~kcZ>YN&RD1c1d-3Va znJ!UJ;8M6!Z~uu^tV4x*x)_DVCWFkanC`S6hiqrb@tZ|-8_ecAXevAVjQM?YCK z`c%0dy{f#s?#1l;N{@Y;u|QC-JE(x(WlnnY4of(4SI?ZmH+QKOzoW{0=H7KzP{fek z_7AGMb+Z2!SMahp%tB{>)Y*fmP3C*fzLNdB*uTBU{%hHPqqDxQJ+HI>TkPKz+Sjw( zT?5|OMx<~yJ}$z=MIwfO=9wdT=5W3K zcfqW{hTV^_^ILnkjz*EmM(a<4`IknYemB$e?|!U*UjM$3?M0Ry<5cWB)oxDbdN$7Y zeu1azO4+^L@ztPmeh}+zV)rHyw5Kz7vCAFYy7LIfh#Vg)f;_?dMDM2t{ky2`x$bUv zHT|HPeC7yN4z*f2)N18WYq$fFX6|-e%iHc)F1rRbhx)T?ard=QRl8nG(zOIp;-`0h zGib$@daHVG50S z4vly@jTnrYXV8e3(TG>_$+Kw0D|ydZdd!hpIaMozQP?!C{4%|GH4nO!2VKg8uA>*v zpt$zolh^Q^Ym5P=(1W9B`zf^j6xx0YZGSs$AMCd`g|?r94W<}9O`+|l(DqYk`zc0G zQ$$&&(DqYAS*FnTQ!+8okM8}qXV4~=Snrxfxuy}p`7@sl_O06*IyN-ao`6GT$PW!2 z2gkz+a3V~Cli*~S45t822%QF}!x?ZUOo6lDY?un?z`1Z9oDUbkr{F@k2tExL!)M?M zfB76-315IO!nghX22q6XKu}rjyKocS47b4d;8yrP-0tdsXmoc6-0AA?64$@m+|U>! zv$3Jixl@E`i0L$~Hi+IyTyeXJ-xQI7h#G5YSO&9T|i+IyTyy+s|bdh=>;?T{Z}3@T-b&sY%p}!o>7TXqds=#% zmLA)yrE9gc(pb3CSh&(yxYAg-(pb3CSh&(yxYAg-(pb3CSh&(yxYAg-(pb3CezM1C z;dy$u!^A*83zxy=p>OJkO7ue|`k`Qd*%JLwmR1+`w7O8M3$=Rpdt0d0g<4&x)rDGJ zsMUp9UFca4=4A@Cx=^bNwYpG?3$?gViwm{5P>Tz-xX|-j;(0Ccyq0)g&*|y?Z)Jgd zSl|~da3>3V)AL&5c`eCUVwCsp$WDpp^$r#(WRXG^DP)mC7Aa(rLKZ1xkwO+JWRXG^ zDP)mC7Aa(rLe@AvV}VO{@%{!f>#UEqGVs{!f>#q3zj zj>YU)%#OwESj>*a>^P7e2eRWpcKmgZ9Z%JY@37;KwBn~)@oRP*$c_Vh>^QK;jsw~8 zY<4VW$6|IYX2)W7EM~`Ib}VMcVs^OI+aSI>RcpSgHCJiPlWhA*@!kE{ww!It*|wZ*%h|S^ZOhrVoNde5ww!It z*|wZ*%h|S^ZOd8ZQ$79DaF)5zUc9~g>0vxMSP8OYF8d@txg#2UF}wYk-HzasNA&pQ z5j}PbqQOCrwj&z6`?LMY5!{zTN3dI&y`}rH-MCmtXF&-TiF$S6BWw zwtJlIe#Lf2@XI55{PKt%zdV9pKE!taQRJ5GirMT6G0w|F*Y0AsA(@EIELZU>S8=1O zxY<=)xWkUl`lSA{pKp)kYnPj6=xz0(q3`Vj7 ze7TA*SMlX4zFftZtN3yiU#@ZwUiKsgzI&ppy2ABe>iU1%>+1Gq?<&4p#aFBNY879t z;;U7BwTiD+@zpB6TE$nZ_-Ykjt>UXy>^+sQj@AzhWV!1-wI6z8FgNjYt=!%7!o6+P z=W6o>pD=fj@w#7!_WGB+?f@BvLqid*e6;8O27P$9cXjKThX2^BeiRz7b!TUIH0&OR z-NUff&ERQ6JZnRGp0y!8&swshu!zE`yO`R!|r?7eGj|uVfQ`kzK7lSu=^f%--AB? zxZkj736Cq`aV0#igvXWexDp;$ z!sAMKTnUdW;c+EAu7t;x@VF8d4SeNNbBWWe(CN)uy%;O=Y+t?0{|=GqHdIFaFj)f! z(sJF;_IbVT^5c$saOQbFewS|!^&AbuvUWPvd#_766ttkmoo&rd6eR8 z&J!V>?Hen7Bgj==qE&B+AIC)!gIZ7(jxWlY3&2$=U9i;oKZH!|9kE?qnY;6^}BPJw820t)GQz{VeQhS)FeO zJDx6cHS1l?dRMdA)ojXKO>q4mezV&a|NS=#U3-(*_yeMOZzxH11xkSFbCsvU*%)^|*GOuU^;N>UFiM*A-V;J+89)8Cip2e?Jd_17N5bhhg4_ zdoT5V5F896paLpkq`_f5bCdp0$D z-w2za6*fZ~Y)LOxhbyi!dR%4nGt}XVtBf93gDb8g`eHS>;wqxYvvd5Mmp()7t$4Ot zOW9LEjiqe$1hVZgo2{OJx?8R4ZY|Ckm;RNyTfb6w>sRV+y{Veao2tpYsovI`>TNB~ zT>?>91sl_g)!SMe)=ssy;$bxhRGC?<)>d5g^SB*Vw%Ji-o0?j2RnFg5Q|oP2XIjg{59gkvLd#9*XVi{M*$wI$_2W|N$EDPdOXc3} z_aEB{^&xAvKM1{U)%zRjVlGz?ynAQn&DP+yo6T=Eo4?&`{${iJt!DF6*54+r-QHpi z?sjW%+s*E8wg$J&`rEB$_}k3zw^@7JZkE5zEPt?L#%4499iFscRwLLuYqMGXHfy)v zH?yCz-gc|?wp-2cx0&H@>-$u?ZIA9fQQOqI`ammQ&%MqwdsBL*mUL>#OfBiul1?q@ z)RLK6GE+uCn{~F^Wd*d!3TTrR&?YOOO;$jgtbjII0d2AZ+GGW^$qHzb70@m#pj}o# zyR3kASpn^`0@`H-w95);mle=1E1+FgK)bAfc3A=KvI5#=1+>cwXqOexE-Rp2RzSO~ zfOc5{?Xm*eWd*d$3TT%V&@L;WT~@$$SpnN+1#FiUuw7Qbc3A=2Wd&@P6|h}az;^3k z|7Z3iy~(>@?L&idfjD-#v3mq~RI%F(#$XMu*vCtu7p+m+(hm3^|84DfiIenmqUBOdU z@vgwbR_k+{{B^zB3!7w}jMYEO)8IjW;6Xul_F{87je+;bEP7L)v%>t}j-AR^sXY-K zrGY=Lc7*Oqjvah)gChldxW^soc}%in5BFe3{5^iTEpx;NL&x%5Yb!z%;W#)RPJk0( z5}X7l!(>o>B6KR82B*Ura3)NFv*2u)3g>{8TcPv7I-$@7@F}Ou@qrx%S>uKM_kF@Wf9X04H zdRfiSJFJ$mn5SrWi~X}B2L09wJr?%2jb#zo6 zU6cxDW#M1G<9}!X_G(EM!(e2C|lL-Y%}|wGW(t~`<^oUo-+HMqAxbl7n|sd zt#m{x(=Ts!l`FI_seLK!dpD@2;fj~`+`+%Prq^B50@w7atBq^lRx2E~roXIe$a8ey z-+C(a&7e>pRypK0c{fL99s0r3af^5dQH8^(u^9rkc=LZFxY2 zzRzmU?J5fe`_{+vpG%)kBL#cHb=N2g>Y6<6>RvQz2(r8Ga8K18pvo_Z`>sHlMcGTf**7ohLAg*m&TtOSn{|Wm^ zIr2yLk@C$C_m1kX<;CvLpB&*a*SkQ=gY{59bgd8cYWWXc!*jCIbF_Ysp4KmL?kCx@ zN$aB`BTojECCuNqXGOYSeSXsCCnW>=Eb7zR=e@p%d0o)=%D&fR_xH*U{_VT4@22jb zN*>J~lRY7OQufdGJS_K$p4Uyezux(>Cr9@yT)6XfMA0opw+uXQ;1>oyIPi&q3kUvV z;5#3E?Uz?F!0Xo~k9xf~e8BLb!*3n_$KmU&kk89|CU0dZlvii8eL!Ag-fC4uHsy7N z4$PmFKPfaO|E~NWhsNgrH2-IzBl3Td|EthZ`A_7}4;`ETYW@=03rAASjj9~2pqf{Q z?D#-2Z_%@C*B7@_${l)@poa8sJu@FrA6xV)?Vg=>{qR=(@K)X)Jn2oIn;m1>sGe`T z=cYOPr#t7No7C8HH88NIKP(fcYHtx(Bmg-S*% zR5DtjlF>*v_d7L_f;}_UnQdzDjBU%$>@ERjNVttXa!aM237q!MZJ}p-bzhx zrKY#${#;d(>+C6aD4)KXf4s=2=kbyI_{fu<-8Me*ET5jhPyUWc!di2QXSdd~`?6Mj z38Ms=&--JP12M|}Jn+}@GdcFaknM37Dk5AvF{rBp>JKfn< zpW5P6t;Xu#(biUD^^LB*i^YokPjM#OD#(ip#?vvMSnCt*M)Rw+?CqUr3VPMoGQTbJ z*IS`W5Y2eZxp!Wn-ngFqgWS1yT*nr+PqKZG!}Sk&Q~z+r zbEvlc%oAT;IXEl&AB}&>O}8E3j9CpVz9lv)&WB(U@VYdW?Idf8t8!yT@CzzKJ)lXW_fj zuk`tW@xVg;V0Rz=9{X)$vzOTHC0UN|<(wWGnR7o^m-Cyfp*g?x^8;B0Ils#)%X!GY_kZ*I)BbOc|C<~7M$X^;{*3>h=l`Ge z`~1+sIWPEIjn6FbnHT--CBHB9nU{U$6~EVqj>>7s8j{nPRh6?k^!=PQes9Vema{f% zd`{f&Z-t6;)`brBOx>3|D%6mBNT@xxDiq5-EYy)ZIujkZO%Jd<_vX-XxwnVT%l%>2 zow;{peJ}UUP)+WSLZ{^3rDDj>?PPgCXkOmX(5rdFLX~+XejXnBM_#F)%l%y8@0ETY z<>y2E{m@XQxYv34PlN{NKbbWn|1Vj$>eGs*4|6e<+y&W1!VZN%0_agrv?A5u%-`?OMTUE{N#I^6FZxda*&HlviW%Us$ z+yk@rGpZ|4RaNyyI{+;1J1-sS`@CHlUxJ19>J%~Q`--1y?b-P%EK!YnnRjYXMfbPe zaz>_S<&1(u;83W7!{Bfj4b?CP#=^Mt<2mEK9|1?gQE)UI1INMy zmI2k6xDR3H`o^IFAx9jI;>E~zZ=iBx3?fUsy`uSP<`C0n;S^D`| z`uSP<`C0n;cKv+2etwpIewKc|T|eKhpPwZP^|&b1<2jf6`HJ+NIiG_o(?7CX*$kdB z!;WP$a?eh$&z%bAz`1Z9TmYYf3*jQTF8y}yx8QpCHrya`^&Pkoz6&?O&FL+oGmCS- z2e-oa;WqdI*zp?2Y{4;GL~Is|*etd`@lw&Pi0D?te!=hAFZezC1;1y%;P>*LPrsXg zSNi?@yQ%bG&Ca{}*9Y;;T0FB>Kl^*tW`i1#Vf!6?!vE@3`cQS_H}F^@?9)_3po z-OuBw!8mFVjtX-51{?MCbG$h!ZoR1D){Bli4ma(ceGk~zlFl)c=xDFV7SvZ^x9&;= z^YvaCRVQ<=pUk~}qRM?#m%U3q%iZ#U$7kZiPy195&wj^w@_jOsU7YnZpM1kEf;WZ^ zg(^5)mBi6d4P#&|jDzv%i$h1ik#Mx%kAY+D!!!XV!f|jsoB$_+UBbjD>cuGP#VG2< zDC)&1>cuGP#VG2cuGP#VG2cuGP#VG2cuGP#VG2cuGP#VG2BYYQbf}7zM_#WH}-v|2(h3qdR*0H5$$HF@CjxC`di35#KAMVaBa%Z16 zgK&r$ghNE=M~l#pwzg=3YW$zgn&kZ??1vzo2tH#tLpo>%7QOt-;PyYNY{&@)Qh3ii=otuq120^ z)bFtz)~2uBV?+94yCN>yvo!sJm`Z~k5wBG%^hUe$-Kf&{jcSJ8sAlMmxxa-6;CJvK z{2m^LKfojKN0VJW=f@5^90`~%(u)j-uKy*4)jF{pBL5QA$FgKH3jYY>BL5QA$FgKH3jYY>BL5QA$FgKH3jYY>BL2wT4<2G<}4 z*B}PhAO_bU2G<}4*B}PhAO_bU2G<}4*B}Ph5Vn7;7+ixGT!R=~gBVTRy^SSkqfDNz-TA&p+LmO;?t!8*SAqCsuZFol=-MaJ( zd5!54^E%Vls;PUe-TSV!d*8Ko@4GhdUHhfI2k*mn=t?ij_nc*QQdCV8RTDM!eyH5O zt`+ocrP1d|(ZoY2t3#m*4ijZRoGu;>)i4Ie!Z@)1E0#%NnG}{uVVM+`slhTeSf&Qc z)L@w!EK`GJYOqWVmZ`xqHCUzw%hX_*8Z1+TWoocY4VJ0FGBsGH2Fui7nHnrpgJo*4 zObwQ)!7?>irUuK@V3`^$Q-ft{uuKh>iDQ|ReREqwm%-(51^sS6gwU1n1^6O-$=~fy zX+FNyp1G|UsTm_RW29z`6zunw!bmBM)Et`O|F8A)*Zuqr`|W-cu7huZ`qG%G1~b)S zrdrHYi``l`36F zm9Fb^wV%HRGt5>yKbC3k^DV#M;N5vJO)aKbglXcKCWUDhVVVS{Nnx62``qWVzlQt$ z{lRn%#;L(LH5jJ`psNp7RxQQBWqK2EO;U;Rhi5hOAhMTD2CTh5e8g8P7 zn<&;MD)oH|_5ECX9_QNgIQJU6L(PC|;p^}X_$FL$p77gngYoQl;70f^d=GAg@562I z1GqQcf;C&PW((GA!I~{tvjuCmV9ge+*@87&ux1O^Y{8l>ShEFdwqVT`tl5G!Td-ye z)@;F=Em*S!Yqns`7OdHVHCwP|3)XDGnk`tf1#7nCK9_FIoe$4Ljak?Q@FKhfweTvu z28+P>6@#WQXbOX-FlY*crZ8vhyU4u*0V1&2Tt91hivGX}=OI2aE{ z!O?IG919a*A{+a|$C7OU4{^;)c6i`8qfdM#G3 z#p<e^=6Eo!sscCp2FxUjGn^itr)!( zqqk!8R*c??(NmcmtZy2j{%mIs)_*?Jqbhv!;zc)qN?VBP4;MoO<5DZRR5^cuwP z%=vZK39ZV+bm#ia(?0Wb)>JXE?uh5hVty}(`8_M3f$aL|L_2MDx#Y3iNb~5YP#Y_eN8WsF&@^;;gtY){}$ZGP+eCA*n0lV%- zR-=Od&fUoR^f99Ub0|lwF&7G9e;5J>z=2R9)_RE91G!!FSR*}_pvNNgScD#n&|?vL zEJBY(=&?F_td1V5lb^MkCX3Ky5t__OHkb(ZvZu+SG+C4;i_&CKnk-6_MQO4qO%|oe zqBL2QCX3Q!QJO4DlSOH=C`}fn$)Yq_lqQSPWKo(dN|QxtvM5a!rOBc+S(GM=(qvJZ zYz0jgrO6UBS%M}@&}18EvJEs@P`!K$UABQP+d!8k=&}S|mY~ZLbXkHfOVDKrx-3GM zMd-2!U6!EB5_DM|U6!EB8tJk|x~!2dYoyB(bXk-xi_v8gcpMoz_UF)zN8d>9ho$mY~z(bXt^7i_&RP zIxR}4Md`FCoff6jqI6o6PK(lMF*+?~HGP}h!S(c7lwM2FYYBQSO0PxfwGDeLOt;f) zQJO7EvqfpPD9sk#qfs`-T6>;sFuyCejc!ZOZ3((9Mz=+BO)grW7gjS2t zY7ts3LaRk-wJ5C?rPZReT9j6c(rQs!ElR6JX|*V=7Nymqv|5x_i_&UQS}jVeMQODt zt+s+zTS2R>pw(8;YAa~96|~w4S}jVeMQODttrn%#qO@9+R*TYV30f^dt0ic)1g(~! z)e^MY23liq17U^T7*`M&}tD{EkdhBXtfBf7NON5 zv|5B#i_mHjS}j7WMQF7MtrnrxBD7kBR*TST30f^dt0ic)1g(~!)e^K?f>sN5Bumh0 z30f^dt2NSUjkH=Lt=34ZHPUL0v|1yr)<~;0(rPhUEk>)wXtfxv7NgZ-v|5Z-i_vN^ zS}jJa#b~t{trnxzVzgR}R*TVUF)wXtfxv7NgZ-v|5Z-i_>axS}jhi#c8!Ttrn-%;~mR4I!tF5Kg*3xQgX|*`57N^zXv|5~2i_>ax zS}l>cNj_!^w8Ca+1G7)`TZDd#&~Fj?EkeJ=>9;uj7N_6h^jn;MtCKIaRkqe1@__PX zy%xy~DV6~{K=#X_>Rf%?{J-2XSs_XDRnM81N|=*+%dBIYne#WzoY!aDxh4C|^h4Qa z$qJd8-jsb_x^tIYfh*I`n9q6140DIs@vV8klxq;Ex}f687|KuOeCPk4t$4GiR<72{ zOfQ!krz-6qU;E|0_rCYO_rBj>4>}!(BrnHF5<-YZ<|N5Tl1w#5J(7$h zBdH`AV;ZT{^cb0@DH$V4Mv|mANivcoBq2$XB+mYS*LUB?IY(6U`_1$GKL5Qw_jOwSf}1H#+yZA03h z$rC*ISEC0h(8CfQ)+_hEK%bHu(*F)I58+-m;o7icJfM>30iASXIv;r-$p}byKRgp^ zY)E-4k8Hsc_}AXWeO&#-8_qa_uOtfZfcG|@_Z!Ck8tLwZY#)B8xJl4GhIQl9054?@gpBfv|Vt%3bC%g@X`~3h}y59P@_Myj8I?+4kVD96XV zekjjdyhUCguQz1xU=K4}c+L^zz(&XpAXh&|-&Ljq4MzW$JA&l*v};fK0a#ANk08fCQ~qAxBWc=sn}wcm_UI?UZpn zKV9S>G{?^|c&PFml%3q5bz8_tZ9Iy+mwEdyXvX^pdy)4ad53G}Z6WU4^50v8-(2qj zP*}uR`HvfXTKxZ&_%4F*7J3gM4!DApx>&7|7n$7^<@NM_1CN5j26VjE=E1`kamRSifbK&M&B6ZxlxDU5e#N>b z^%w6Z#Hp#j2T+%RHb8s6#1m}uFxvrnvBA&xstX~%Fdl{T*TSFo*oJG#G@%Ppg#UMr z9g^BK5GAqMdl>X`9F-;0&Fcc{$A;#E-;Weuz}+r~)}q!>_7Scwp9pt}dbAZa3`hu% ze1H;tf!skH!?mj@In=GH4R?kT#?hnD-gSPSb@f`|e=oibDEIez3CtZ6Xh|0*2hd>k!k zeeXg@>Ld0zmhM3BI$RN7MCCcqM*MHa@3UjIbsL`n?Ie$V;nzdN_zLHJD03Tkz6m-E zGzm}nA)bOY)sJ5|>sFpS7;#^Ohc7m71@!dy_&-``V~#@KWLmG`x!?A#g7=v4uEE{^ zmv_1MsyEpCZ?q93Ae#;i-Q>N*I(5*ypJ`2o{8hYBWV&xc`)A|-%R*^=i8N4?aaSMU zJd8bqzX$J;dp%gp-d)~&r2H^s^KtJO^^|E~?^V)y;GXhkk_XN&TMFDS-X#2gQPw=P za(_k~v~=Ln`V?}FT0z+gQj`S$8mN}24`@>foQ6BS1%OJ z)C#pil&L*cd^bbAM7;!S<@ZxB7cJE*)hk6S^=kDRakly^^?Gqmpk4rLT?IZ2d?-2w zJ_>9R=LL2Ic8Jcwl3}HZBkq##P2I#X#d%#!&G~W4JL&TxX0n?hwO` zdyGGdF~@hwUPZ?hrhsD!I zrKyYO&7f(+AImY5Vx^fi8;jK@KEWlnnrEBmh;8P%=Fh|@=7r|PV!zqj>@B`E`bvhk3VBV*bJWqjH*gzxjaD*j{8WQkvRt*l#G!kW)Yd5p;Oa1mVSNVBUcW zFBc2>XTy_4$7*s8j=Km!tOgNBj0A#-IX9R;4WbJ`VH{{pEr#OljfF$4iC>my(-9|U~s0wIYK!%FZhGX-VV88gzms9l7r zo{#d=SbjFjue%7S7orq{EX5E?v8OQ9i&2&~%QB3z?1wKjU5*k>u|$I?QLL-4UW0Ot zqFjH471;-?_nLf= z3pcJqd8;Vz;h;AeHw(=eg|bhf>~94>1{$DK19WPD4h?ur1dQ3nlOkw51#K{>4FW2aSu!LdS&=QkcVpB_OYKcuP2}4WH z$LjK3p(`$Qh7Wd9v=!~ZpNlo&Y;grbUChWHfa_lgy-r|m zx<4!3U-4!zB)>+G>;B7cDO@jxg1(fA%XSf zZpBr`VEs5lOb~ZNzb7JOv5wq5uy7_JV8tZ_6W)!Jv5IXQ1?yug?UZ&{9r|1aD=8`M zmGDb|D(Yl6j^ps^-2WKBr1CNyMC&{+pG z)&ZS*-%uTf+4%|eCiNzfSK$qX8Zt^9g}QNzdJE)Z)G^@4VvRdPy>s8;S-i7)( z0duD7suR_Th(Af41UY5^h*Q)*s(*xJ8rH%y)rYXUL_&R7eHce)V%0laeG+Tl8S1m@ zvqIGe=qNjV1XiDj=y&LMU_Gb1^t(VO=o9b-*}L_-@#UO}`b6;e==aDk=wOYCNZ`f5 zGPzPk;ANo%-U_?}UbfSzz}CQ4kq>+t_!Qq}+Y{J>Rg3Be>kB7XiuDB&!G>5n&j@}S z{8r@Ie%E9BT@UT|RiYm2b}?&pinTh$x|%X}qlGSFoh)KaOR=VT~{vgZUdD!UK7?wHIrueqntjbnAe1NaU?=F{2}FziA_vgc_KQ_DL8+Ecum{ z67(4Mq8Ec1;OJqUhJNdA^tHc4j~PB8=udrtUIvfmC*EZE|(IQ;GSv#R8Uqt*T4 z{kU6{Q9+J5t3JMg6n`L3W$Z%#{tx8eCS58ij_mR-^*QdX@?M7?y$_Gz!d9^G_oBQ% zr*<`wRzLJ~A1s2AIC~$$d$5syTu8(cDZt?y>+s8#&??wOm zI6eKoMbA5(hISfiyqCRloM0gijPSv2c7JW?;Zl z$I%^lmP(ZUUAVgsamHqkb_R|D$tq+5|Elu3rVO!IM+!E=j}zs? zc)P6c>*Zbf))fBtRNV*Tt%aQhbOPhIg-v3B{#JOFhj7m4xL=tcsFB_*=*$uCib9-W z-V-uzVh6e6LJNF#!i1;96U&?s2f%kYzc^^*~&fJR8{$m@S9<7M?Z53)&IG|!( zNx5FCK+mo(`d^o$2X_WmE&R1O3;nfQaAXYnR^8Y`>5Cr92+@zdlK$+KT)|$+0QO1- zqE|9OT*aQqFWD2h8a%X^KZ&>Zn_@2B+v~+Md}A-+8~YW!vG<5K@UA{8-om>&BtFDjIwf}U{R~eiyr1{; z{d|D$=kNG_KFarVrF=gtLcX6BzMmB})G5?SG35JMG3EPNvGIPsTuI6|vr<>SnU$vU z&8(EkH?vZXH}e$b9QjUGI^doBiqb{Cjg_15HhxdJO}>YfJLG#`}Ps`?ZfEX-wX{K1+8+SRbxSKLkNg*STWkfiqYdCxdS19Rim*o zA^ZFh_W7gi^T*idkF(F8V4pupy;Rt9R@rmbsGT8dr$X&?shvsonQiu&gVfO^b<|;R zIl|s@0-E}eu+$lHWjpqnlhj(1T5D5l4Qj1Ptu?8&Cbib3*4orsjanOF?-wg~LU%3d zu1npusk=$)u1VcB&F9hE)Tyru^;KnWQ)O>ctI^xksk=ILS7&ciXKynA4gMCk=wYlg z9WcLxHk<5u2HEp8*z+{l^R%ehCN5r|BSHz8Dal3!v1HJ z{m&TtpK;z%qf!aA8@oy@Tw##s*&tcS%lY}XuXV}iBuRMy5E zYooy$n4s0F&}xmd9u~7677;5lSsRtWD}h&ptc~x$B9*qNM)b&Joy-tJGFdZofzJY; z36pq|$vPQloy^c8P17Px(;`h1XEJG#YP3i-;7p@{07-PwB(`J{TQZ3)nY33mVoN5m zC6m~aNo>gkwzLapNozJmYc@rM$t3FNqe>=GM;}kpXyt0Oay8;fCh;Vb7OzE%*E)&C z>*GmbTD&3RNiHqkAT8b?E#3$%-U#2Bb@%74_Z?MiM zXl>?cZECbOqqH_N#KTPDVJ2NF#yT-Z@jtnt7c9qfHz4nh$nZaNt-|O*wS8BxRsPdg^UA$n)l3uCSa;Y? zpLn-=7gavx{k8H-^y#~L7sH0B>s40XQTY*2h;uNG(iNj!x8UryUPFun+FsCWTQi-S zJZ#8+!4^5z!$;0ZD|rKK4>|7h7Oct5=zsq=&au#Q=NLLIxdj;4_U(uVyx%dur+PDC zAHD^P0^WugM|!!|byoT#VpTGF6Rbo$IokUSey`(Mruj#42R5v%Z!kvuIIP3fcms;U zINpBmW@-D#%^&0S`IFIVdj;`fBf{RH2ZXN~c=G*nx~`5Wy0@!k}Sxj#_TRn1_D^(Nj&|bw12Tnz zz7aX7St?cMBgQ+&zg^^LwZHkEGm`i2E=cZ?`Fx`7hn@!cx&^ww@kHk#?Lw`>Z0+MV zwB&F4B8&hpmmDuX>TgtPLPqjBc2W!i!_n;pr>l1Ij z7te`~eIJqfVV^G4KYC$QrzJh5{H?mwy~hfY87IsCla*IhS||UPqwKXe)WIFcAux>a z!*5w{h|GC!aP72?->Z6(YB_4+U*#Uqt6R|;0}n@AG>4KCq;<0A_tUF-3sm7>zVzRw zs%RtLK@eg+f+98`sNw?zO?-%;i_Z`O;!ANDHr)|XiEof$xEHG%g_IChfUp%CUwd;E zSJYKvN*XIkWR)z&_3}y{V}Es(x>(z+SUDBrdi9n1_~!I!N+XQuA>gZ82>4ELnNlX2 zX$!SQqPezM`==<^UeK0_mfA|~HPKpoOM6GO)&8Ze7ag_twf99QZM*h~=&XIJ?G)#0 zyS2|nH*KG`Ph6-S(7qNuwC}XHJk1k5bxqgB#rk>rdEyeihu%Z<4r~r= z7MBJ-4}32A1ilV@Eq-ClHRg)G##-ZDahdU{u~S@b95s%LE3D>LFEPOCZCx+MTQ^u^ z#lzME)?D$FHP2ckUbYrnFN#&xOV&$R^W|mh74fe18oq(O-db&~7VlZ>upX1l%U_-( zXEo-mj-1_({QU|cC=MVP;%fv`97G6-ZxAeT2q7fCMHx6KgCpRNBG@R6O03y|V#Dgu ziULbp(G_?yC;=sab(w-n5E28W<*>9|mR5+R<)F0i-FhXi;7c$HykbR6Nhv9OWiE|U zjIb0FEX6cSF^y7`U-v#0Wtn1G#!!~0;jBg|(KJgm%@WP9M3X4dg(9Rqr#&YeZ4pX0 zfYMzo4DFvNV+&=x6!b;yMG@4Np}Yeq?^nUUhEflq)ZYTXT3ZeNZS8IFYqT}s-+?v+ zsSN>YLkQZiU8J>7pc7H(#HS*z?Sy6|pc$Wm?$P#$nD#leBuOnvQcGggk{GolMlFd^ zOQO^gms*mcmT1(H2(`qdmV~G!A!_9NZrwGA9eU+M2MolV*?tBYeM8Z0H>W74fR~z4Ld`f$DN#xw zude`OQyM4@;04eST4E`uqsCjx8Oj-G6=c1SDovH9kTh4CUkTw5-}+RNI@;8$oXL?i7L?G>~@E47u7yb3LBroExPDbAoS7E}M4X=}B$BCD;_ z*5Tauv<-;=zP3@+(LU5Z6vf)d+79Tn)WTDtg}cFj1}!Xs7VZWAh4uw>RJK(m+Lu^` zyq@-z_LV4wMjn9VYwe(@uYIGTwrhv9Z$(4xuyz={Y|nDq5wvHPR;g7Yj%?K|9SyUH z>#DB8J3zK@mLAZ9qEt6@0}@NOL{7JL8}g2NN8#w*^b4@^`_J^Bf$y$&2Y;b{p{Pq; zxAY3V0x^5)Jwbcvmmq~pb$o4-J_x7K2jLX@Ah`5Fh|veZr4K@kHKZu8H?S99osz9- zdEj8+Al3o;Ch(021`Y+#av80RRw8X&X$%Ct!MFk6|H+v6GLm_wc_!#3<|X2E^A;1| zr!l9RQ}IQmKbwEXxcvh2SqQM319Uu}fF@iIoRm%@9pE3DqTAbT0sV10~)y$lcGSt4BBHx~a38~U7gVT~wk z824U_{?Jm`%?lyx2+3H;mg4?Pd3?0@2xRYJ6#NjZvaPuLt={SK$p4G2s@LZ|QGN9j z^z&!JQhf})yJs*qc)oWn!aLY&>YWbxR_|%>w;?=@G0t16)A{M#$wqand?u<=g7y3j z*0(=y!+7^~)gpMKPORFil2`SKtL)#Z<3H`cdi=g-d{|7sthtXeeGXP?!4AXTCjTh> zd9TDOg>t0$6W9y`y@67WQQJ4YEAfv$yf+V;ApfUBwi=rLEii;vFmhb5t^9OOM!h8` zI)b`!S8X}0n=*a?UQdPmn}$r17h!lb@{0`N#aX*#wR37Aibfv-xRj|T3|+xSEW?t+wV24lKjv7 zdr#pmfYQhfK041zl*RX-rsP4=S)@-(ilaNw0Ipiqb2`ertap9FBrsOZt0X3KZme@0tRCJw}$W z5%KsnW#`FJd~WH&KmHiMY>)OqCD&_aoQD zGBLX4Z)N-o)r#Gw)?Uqc0^Z5hxf(w~yc#07(!Td?{O=0fx{C+$zLH>H)3 z&^E#jNWl)+3~N;O?z6|n$c~C4tepKEyg^i0Ptp%ssbSZsv}-ilH9GAYjdqRBs|I-0 z0IwS4$g#nZW0NDtA$mYtybqiA5$1h3ybp)>;qpEL9IG=pR%ddoE=12zi+5&YW@mTc zJvwj&>AUW7+`a)v>>F^bKEqM^2CyQFF@HhMVZfU8n8Q#;ul2JzCSSra`F0$W2fBmM zmSgfUj>%gblMi!D-r|^i7mmrR^kdQKm)?zDE8RFgZ_y*Y8%OEG^jGP|v3iqZ^<6kt z-2>QJ}pQn%_*w~DA+MbxdP)U5{8tupGCMcwK|@A@<8q0oTd^=HyUp#i-T z8qm95&Kc^5c|~%bNn83RG@w^PTY4pw(I25P{SnIOiO`mw2o^mN!t|@}M(?0*^h)SV zuY~qByn|GF)Q9Pn(4JliCcP5E^h!93UJ32#mC%J=37viKAl8cWw8yl^L=z$cY3)hv zDWMP-ND~)G6BkGm7idFVpgwVda^eE@i3>!C3!F<_pgwVdMyzQqi3@1N1sV|%C?_It zE)fA)1L5^RM4*C*z&S(&Du@V_5)r5%A`mAcP|CX6kcdEQA_ApE1X>Xhs30PcARUJ)|5wod zkJJ8-)3ZY-3UC2YfD4EMoKF;>D^Y;+i2`(`-$xgEd~~75M+4$#J?Qaa(bpqPFRV_) z3+fRsh!ZI&CsL3mQqVE*ZQxtcgq~X6=nvAF{va0pL3|t`O0SR*(S#^{LqeRb;L^@#c6? zIghBZImP@vc!_sJ=mXN0J|J!B0}>+wV$c&LMjXVTKS+#dh(WIqivMnAM}^h4`LPZJ+!NfT#@)9b_{+L9*P5~l}>Ma-pwUTWQl zyj0Lvts8ySy3s$yq<@M@|CG-3QtL!7wNCU>>qL*0)9A5s22q>V^jqmbzm>Cy;IyFk z%2~v5TF{53J<*&1vD+ZA+n*D={W-DQ9>i{YSmUhQf$U1er#lg!6cOAu_;SYcq6rb7 z91)*JM0qNR@|;7IryfzB3Zgs}M0r{f<*6XbQ%tY9ZhSWv@!i~j@8&YTn=QVZI}!cq zX#d^*yExB&)?NrIF`#l{KzU+7aeCR{`--8R68*7O+j}sVuq%2*Bk$Tae;s1RQMc>L%zV)ueC*S7qr1JO1Y`lk^lIPKVmIsx8R!a zov+@itar%a5%hmK+E|_JPvY?058;|09gCbe#*y(JtiAs~zbDaw|G#YX3&JWIK#Sx& zuP>qPa*x*IfBqSK41n`C_5hPxNMERX{ObUPT#s_Z8kC^-a21T*g*o%@!2{+FU^QR< zC-12CIDcUlQ~{O49qg#O7FGZtHd9lzf^VfKl2YX=tEo+SC?K+Sm9thxXD#% zUsX(UxhkW#sByI(Z>CeDNhB0SP3j{N#Xn={`=@UFfRC4 zZSbc4aN2AWu>uOv$UV@a%{b#Jq*)0Za2n2^_rqsb9|f}i5%lo0Y6&^n)}h@&das?_ z7v;Efh}GeoFYuFXL@`&f`Lr6PI6nUWi+$wP7kFaneKdp>_-yh-XP2Ug)`-mkxA1UB_ z1=VR*b0-_ssE+YJ!vUK|nE+LiGxp@HzE8RS#^L|;)qG!{A4`=!omg=PK9yg>%V@X6 z3&CwbA3(uOp36i(&fkAsyeU?}y0c)_b<{iQo$>v+^D)AEq5gB&a~Hvy8v)DhE?8`L z!&18^ur#nb@HQ;2Zv%%h9*eP9<7(JSHyU>ucNr6miN*}$VdD|wPsXFhe;YH6KO3`* zImXk*0^?a@q4Aut$avmZZ2Z$$V!U81HC{B987~>jjhBrT#w*53<85P&@s9CGHG3zm6$)>HqI4o44vQE^ux;9$Hu4Q~ zF8pz9+L?~bL*XJ9CqO>Ym^fiu zK&34jLk^XLR$`{QfxI~nqs`LNh;gJ>;YhCztgb$>x>JeOHGs!Q5U891|Bw2>?;P-Q zrcHg$v?(TfcM50P6cfX%&zUw7$16r0xiSfSH^9{;s#lMwUY@94j;LN8qIyL{^-759 zm9jj1ud%j7)XpWM)|!Y~dm?J>h^TcWqSk?k+BrnjIuKDikBC~7-YIe7YBAz!8RBXd zy;II2x|XGPicO3yLyRp+jLjs*rVwLG5o0rnvAM+9Ok!*yVr(WcwjjMz&L+|pBGP6O zX-gAnGl{gBMA{NW+CoIyOd@S(5^3u~q%A{ortKZMAYgN zQOgrWJDn(6eWGZmbGA=0v9$V}?NiLzKE=e<>JwKxow!;FakWz7YV|qirsA3{v^@)VF=M1P~&V4H8tfyikVJRYEA$rdEcvyybSO?-^or#CF;rys#VqmAT zC5jLOJC*HGgc#VV#K0PGmXyD4M}4+X8MaVuh=kSWjHzPIm@4LssbbEUD&~x-V&Y`= zIb*7rGp33;W2%@lriwXZs+cI+DMZmu;f$$bL$1?Q%yoE*Ib*7r=vsZYXl;nDWr(iT zC%V>#=vo`1Yi)?GWr(iT=d7w?&Z;Wrtg5=4RaKX$TRoy~NuqA`h`QAy>Xs+!mLuv` zhp3xF)UAl9TM1FOhD6;O5_Kyf>ejH}Wh7VfDJAMwO1v#hq%BO0ElhMRM0BkK5w#Fe zv=C9W5RtPGv9SV z5iFgkl|j79AX3$qNY%MSs#+7NYE7i7J&~&RM5;OxsXB*9)pZ+NXH7U;tQf0C&63sDygIZ9Yhqy(AJz(b%IKqhh*jQp zqWAM2_TPjJxzhVPdPWEE{|)*`+t90)t2umlazFVW>u;U-xLj%QWSiH`KY}~Lw@c8g ze+&Jy4`3VNYa;O7*nYC}j+f&py(@K?0(*CmwK zfoy^|kpBa5?cc!`_yyLExehB(F7_@0T_cax-0+OA7OZ>BwUS4weQLn}9lx11ETWiy zuD?=WfnQVb6Z;%_ycnJ@f9C26V{qp0kYlg=W5Ryi?^8J|=VaS4-;YbY6mWn~vYT-i^>NX~F;f#P(9v*(dt1o>6h4qu-ZR z&)t9pRKRLwEUut=(m5zq>HKgR__1JVp3v&6N~7j~ZL729k)OgB^z10-3(;fXr<_$C zr)pi=YDpDWJ6C@kwW@(NeOq%@656%*Yv7ty$)Di=r{~%6C)f6PBwohduiyS{aNLHUUf6r+ckMz?tF!Dwp$BzCXjtgR(D2Yrp%J0qghrwtbu0Q$TV%fpy{0ci`(=*^ zy_|Q^H+ddCkR|Y#Rz+*trD58lVOog}Z9<2(pF^w9!81J$|NbX2e;`a7$)6{n(H08Q zA3&#tqti->&?fOc@*P?o4(*Kyp1K>bie7pz&`aP|@360LvVR|9|2~X-I7=wVi&jEK zezXDK)@%!2`owGO(dx*b@mNJ?7W=9G2(QH+ zs?9#A%|2+DeNcye&@lU;4*Q@k`=BoSpb_>#qwIr5*}n|4|L3r`Cq2<$0NS96mx%|o zq=!vC;sFha2b@Jbpf2%%Q<(2X!~;r+2jp1>rNjeHB_0qX9*`s+kR=|_f_Q*KJU}NB zkRcM#f=ED^NI-%}K!8YqLL?wTBp^s6Ai*;B=cy!!1T-fSP{vaC=cy!!1T-ZQkRTF} zpf+eV=BXrz1f+-rm_!0v5eaBbB%l?MfYw9;(nJF4)90oM@qqgDxoJY5n-comln@~} zn|66i+U2FR%Zq52ms00SX_uGME|1YJ57RDhLAyLmU3967Htq5-E%MW8k^8oIIc@P4 z)Kr_ccthIaS=!r!0#o2%s=UKEk&!WXymlkJTwg^SEIP2fqs5t|#o3Y;r$bvaLt9g)t(l;$ z8KAAH(AG@Q)=bdWY)V@*!L~-@Y?w0oNtCeNIg>VJIc>@ov?|h2YoeDOw1eUjJ7kAMZyTGq)OKxG^sy6mLj1zc*cs8+Ze}+Vm)R}s z7NVcs(rzg(x6iT95&i9Ub~|x}-NEi42H0KguHs7jLi<87(7waILtJG~uqTK?_PzGK z;+OVI_DkYwdxgD1Tw}j#zls&TRPj8}-X?fo%e8+S!;7*FuvocPSaZ=4p$zMTb%uv! z4}@}}=4awP{Y$Jbb}d31tOhn1K9>?lZ^zYr+hZl)Td~I2ZxPPJ7uu&_jj>n78hpoY zEkcF(7s5s2J%pa(eX&toEH)urB0dsZL~pShp^w<7M8q%H%HGU&^cJ>`qm>(#8^x{4 zOUg@P4A9R{#czRo-Yjmz3SgtfLi+eEQg6emxzEF+?;f#Oy;r?gEK#SZQ^X7E{p$T< zDObmRQGHZ>RJ^4AUH!XQuFhAV5ihHY)qjdt)ECv4#jEP;>g!^ax>{W=-cr}8{}QX! z_tf{r8g;9>O}wk_RCkGgsh_EziTBjK>Rz!y{Zc(3-d7K*2gQf#A=ML`HC0o^4lSSs z#HX66nPMl3*cH39q?Q$*Yjw1GV!u|Zm5Q&mhFU{$P`gaKOnjqVu3at;Y1e4iif^?W zv>U{C+RfU{;)wQJ?YH76kk;9vQv0j+SA{bV6+@e+%~MS68SNP*gt@REE0%2Yl-7D! z4=d;BF+Hxd(NlU#X{TrOta7eiq!%e2^i%W_rK8?LKU?XdpR2c5E=0T7UHLiMzn)58 zy|><5xk8R0M?G5TuHsr%*1Y3f0&oc6MM2tH}+ zHDPWr-`Aq%2j*5S4zIIMw4Axq{9G%NK4#jfSOcv@YlPOTq1INec&4?-if1jf4p`-^ zwRXN-Ust;j-eu=&z2RMUq1Gq#^H7C$nOwzByBz(%E3~Ua14E;=Yoz~}HV&R+(IW#>qUHg6Lq0mFxROw5m{UP*P=q>G` z&^w{E+JD2NY`ykp_>z67%?^DO`bhhm^d-}tl)hxz9P}-BY4bv#g$`=}urgKy?FF=y z?X*=^d#jhW5pCoE?F(z5b-i|2wvoDlHgcMtv8G$I^d{D9>m~ghw12PYzd+0PhTc!M zdHNM-^VaDDr9YT{H88br^lRY#bwt0(?qYY*Z?-S9FVjca{q6qxE%qRLkUrYJ#=b_s z)gEjQ*2mb_+1Kg6wQsa<)W_N*>=F8H_V4T|`Z)VO`vHACe7payPXrRTSf3<&Yx?hi zyS=IZ-hRt|OaCL*3KR-f$Wfj=?tgY+|EK&vint4F#9sn_d0}6sU6qF9|4)9$-`oF5 z8uFZZ$6debo@?GOJTa8j$Hl9<-bTo4{IZKU1%5639m44@_< zW*khBXDO3Orv|>zr@1AOdZETnxzjUL7HSol>I`y*hT6GrL`Q@=ITM_5&V9~gQ~9_PYbZMR#=RbT@^DM|Qj8 zL!;a^*1XWz(4E%8h;XO4k2A~(O^Uki)u@rv-2>Ed>^ z`dfpn!PZc>4(_in_D8rYq9fcX)@aoI3C<2{vUQ)eJhIt(FtW^g6n8P(deWMQ;|tw6 z_K?UjcVu{_wFGyy+*%&(j{D1_ZCGusi|mf}v^H8>oc593)(&frwck2q>sF;ziAmj- z9d+k~=SGIxX@oj+}pS2rmvVbEkw?I$Of4BBk!*;kD5b;SJEH-r>#R?M@$e zQ+RiHpJPS@wC0GTI{h8fah+sDL18aQmJx z^g5xmX50a&<(n9;c83(gu!3Y0+U$|h+0h9n50T+j$J{YK=gx7*M}oBi98P3#C$hDl zR%q{M7XJO#|9CpX#qp}NqZ8aYQDn3`Us7_5+@bPh#%@jO7WH}L)+Ny1fdgJ%zrOj z5IH_nME;%7JM!b9U!-4T;7giE+Lr zPpUHC#0W~hBK-^Lf0k6A%cB`e29Zyb-%LJEzAfn) zq-#lMNvip|^k0JKwCx0b*U*D(+3iD9ChVr*ayO36pvZS;DC(<6Iw@Ip9C#l+u)UR>V$un2g9OJMCDbyeJ8A_->`o9?S7o>NQ z-bZ>r>0U`SzoZUP!m?9Gk-uG1rINg#lj|vIM9ODZp61c>NLP_kqMRdn?IqIrr2inb zCDrGVXU=QXF0G-IsD9~RPTqg&UrAnhkP^03YAN~tq?eKYjqMN?tW6sVk}Ynw0qlR7<9x zAf@K00m*CUlQNf7whSs$Q_p71-;h$Tl&0kUw&+aBtJG~ZDtWDh5{q%(B>yZa&(io5 z8Z}2_E1@;#(SD5oI{AyqXUY5b!#G+@O0*p1DN;V2?(4jN|0?B+Nbe+_Eve>f(Ib@b zDvB>}AtlOk%Da)CLdu$@e!`=lQsV2wNXcs~YmFtPUBja{l1`OW93mel?L^v|)W4Rm z;WtQ$@;AyaB0ZOqC8VP$36p<_^nOY%A^n4-T7di}^3;DdO-Xx6b-%S?i3F&cJ<}bA#sw+Xp)by9B!iy9F-@{w&x%cvgBchnYY3$a#hrM3oseV+Q?V!b*?yIna;yHlH}+Sbas}q14w^#25UVN!~zr={urzA$K&I!~B z)K{Ml{4DS@_1VBb0t?iI5*=0-OLSQMC(z+f)fa&a?^WLbE__&B6Yzqrx;~f)Cei?7&uD)7gxcWPx#-YagT8ZT9 z?*hr~roRvL_D}j=iMIwihhDY*6}ZrP&)O912gLJeaG=CGgCiu$8T^ezIfEl5rWw3h zVw%BGHbU?gAe#>aN85k1pA6n*KW+a#csG#E7lM<3X}%SlDp%9S=!L-Ch#&v;F2pJP z7bUSju+~q3*TiBVZdpkyhe@hT_MQ(;PYrdN#TKN-r8XwFJ1tZ5QcE%i(>=2HrPd|J z;QzJMj?|v$rreBFWjdWMNp;U{a@sf@(>>BXoUW<;=>f?n(!-n{PA{i#YH)gHdQN(2 z`i=A^XFx{e=Vs!`&6&oTdXdS_)tRzPr(8*LnKLAvc7{14(~I23&X{~zc2K@7Gdy!= zY;)#LXMAEsVnu9iW_o6BzAQH_v(}mDOv&ua>`Tsy&dBB?gCdiYlcH9(i!;rc;mnLo zjx5Oz&W_7Y$ljM5;yj+}pNppYr~2dG=Hxcz+C*ljONv})ey*dl$XOcg>a57k$ON4? zoHfq++?Y&RdcL#C*%lj~8JK8PRGz*%Gc`BU*_nIX*_-c|Tf}EPkXw=Jk}h!$r}ns_ zs7r2BGVG2@4Z`3;*p0{MxmmZKTbkROTa+D?+UPdU_jb$NR*^$)yS&I;pSj-c8^D*q!*QRlI=2ckwep zMp%~_n;DxjBOBwzxgn7)axdC3vLmu5w=FjyeIPqHvOjVtQW@2wRx}z-N9#mOqK%@> zqAjzNqiv!cqg|s1qCKL$qJ5(SQnRC1XI4drB!kgmnO4z}(UJL9`AN|+$==cN(ZhI# zDbZ<2eMW9+wm2iAGoz2^7DeYo=jS^`7e$vAshMCD`kHd1d-G$XZ=?q#)<@Sw*XOg* zP0?-X0hzMs&gfoT?Ql$FlgWeGL)k;IV19G#iC8#4Fcwdi<~OJM$I4>alp3oSI~Xg? zm&F=qm&eMo>oU{xi(@Nu(O9ckJA_WL1?e8K#VIp(L9R|#ja9^Y$NI$v=4Paa#IB89 z9~+)q6dM&Ai*xVHF3(JlJ%KYP#b(8(#-_&}!T%HaS+TjX1=;0@u{gF2X%xp+##Y7F z#`eWFWXp5u*yh;w*lz3{%vQ#Zq2mxw~u#;chAp__l)<6_m2;X4~`FwkBE6Tj{$zY!ZhcXg_`>*-_>$br`11H`@zwEl`Bk}1@s05<@g4C! zsR{A@@k8;-gr2Yx(cFMUI#DN4l4z7@1}$!xXp_rSdrV5cq6eUu|BaW(>Sp$Hzct$ zy)>~maUgLRA3_c$!zrnO@k}t8P1egaPL>vRNj6TFC0jw-DU(fJfVQ9_^;%K)Wbb6Z zs0$x7pC7mYi9eWdZzlM`WLlNPdh%OhNh>b2Bl}zkXQ579L_^~ z{=Ksck{LL^R!9v+x-(KE5Jux&GA=bCH92)3!h?mdF|{YPG4*I_Ho}uK6!k3XkGIXd z%=X$LwIsC=?;t7NT_VN=NZl z^|v~OdzzUpA;-H(H%d1vYEnDoHl$E3%nhars2=F^gwVaoTy#1lw-*)(X(%wyqk^c4KC6g>t%EW-c% z8heZI_VR-~z9PMb;SJFB2nW6wHl+_!wvE*1Wvps$XL|3kvks$&uqh)l^)kB=_Q^WN z5YL1&@l3WL-<`qxu&7T_>)IhxCigRws-&4#axc><)6SPm-anSPAXAYYS~FyN=jLYz zA@t7<%Jj?jNB?J_>;WNMi~kda>nXi6GYW6JJN>KT3X^i^v7oOsEAt3`47p~RCo*$0 z3lJ7(mhmdQ4zJcf+ds21v#O|l?T}ep)IPH|v!Pll`ON0({gZR3bNdSUcQA7V?};Q@ zAC=V1y4)|$%Fr;IC-wOz+45}bZ2N4NY;z{%OkxoO!43*o+FT$M!rKUxU0v$L~LX6I!W;+?o8yF7m|`&xE&c3pN|t|YrL zyCu6LyC*jUZ|wc}Rp#`Zm5b)mxjMO$T%%mGT+3XWA~)AD*EQE8*XsnKZ|?XoAb0X` zb?(GrNbdMBGB+$Y^4|&*a}#r8a^pdNI82fKtlW&B6lUfg&&@eWn4ddwSd?3uTY>P# zv6go|>fg3P*ohwWf!yJ|$OrRbgbn$4KAW$1ER@QgGx}!j@}2S*|fHIi39^{JF7 zUsB$Papp@JJeRZ`o z%yWlIUK6BclB!pc_fz|faVAl6iR85~CCn@3G4e?sWopU_$t%mHTzP@=B1&dRDi%}z z5$QjqME#lM#W$36kW?GOv#z7$VbU`wpU8CHlDwLy{Bg>4rgJH2Ige7Jc$CmySARwM z0H*L2`AbRfkyK;;C`>`2T$#(556Yv;U&*hPRI5+giIOi!r!x-AS6#t0OC%Mq%cCkW z1BE3nzL0Y5Y|5|Xxi>O?lJplm`jnJw-%4KZ$(VkgPhgyfDe29aXOTZmQsprD-;kdp zsh~vhQ;SK7mL|PIQq@oSV)8ty4kSNZQsqzNsR?2~`D;mC9;K!!d&yr-@9mJ3Yj^VK5lYgMs(VSPy(;BuF(r3Os&ysrr#xMrtIm-UojR;B1oW&+17!oV1uRTk_~WNwprNt4I%1&J>heC9mwFWVKIu#m-FO zM)FKw8%F+3($zY71mH?2IXx@&tM9_WoqwJa+DI2l5a?Fk#c<; zX`GTpj9EdRI;QTHyv8(@d6fIPp-{e(l68{m)F15^N{L=1dA*27*~+OC$=}U56G`tP zy_xh1pUODOmy%bGknZPMXHxzhCErP2OOfA3$~U-LkNn4sf41b+hspcp)rJzDr8cAd zJV}*ndDeJJt|g_;s0NRoEvYytHNhIdG9QJy3J9OYk= zHl>7Zl(LkPnUtFtM2fJYx8_3wvmzTzKkA?3Fvl2l{9iKCKN*lMY)7ixlW9+asmf8)`|NZU}t7EgJYlG7M7 zD5>yUiAyM{!=tQ&>K`Pp`F-q7yw6`O! zHC&mAFW;+SYa;pBxbt0>NQeKnC zOaUQ(m?2P0z6Ja8C)1MgC4aK>s^V0|lX0u!V@3eJ3n(xf79YUC>;N4<3%@9SY5eLO z_bb7^{59fF@t)TQV*IkBq?Hp%?*+p*<=?WRABVHAI8;NVZRe4z7(s8^O!t35M$|z_mw=?yGT+okMwCurjy=9 z`Z8&2(z8g9ND5uRmnqeQ`bzM>#9Y49gJ%Sr2Ac<41kVhf6+An5POvTJ_H~4o^a%bu zSP{G^*fV%>uvhSsVDI3i!9KxX1p8vX-yZ1CSHT0puY(7J-vo~Yj|MA)o+0pgMAgtR zH7f|6GL4X788&7CMvSPO1qj_LHcmB4jQU2Y(ZFbEoMtpKPB$9csvWQm+k`K^V@K?` zorEWT1G}l++%B`vw9kSMLTkIN-QGUm?hb$ZyX=YfB>O4&)GxJ{K~q;ktNZ<1o(WgO zGePRq6fm8@Ics&d34PR9tbVrd(cWf z8?)xl!%V#Q)d$p>N;CB@>R*-CSXKW?rL8(g-J!HocWMolo3-<_&dNgVe673kympax zk@BK;vDQafruEhOD=W1D+5qKEZJ;(#S*2a44OQOKhH1l;ceLT!aAmFb8|^pByV@vi zl(J46t&LIs1%HMG$_DAlpzNa$LwEWx^ra8OP<^kySG_^6)GO6t^k=w{{tUwdodaFe z(ePwgrjCIp!z=1-@MPEu?=Q@AQ}3of!#(t8_&xm@?x!ciU+BZ|q@2~JJ|$R-tjVu6O3A+|1XmYm-e_&|C)1U5-;hrmAR&k*=h&d~}Sk)8~JN;x|V zlc6y?D`;W{r%)dMk>8XbIsRkk|EG^n`;q&Zf#Wkz5@!zNHGlK5U-erAerb*08{kj& zL%is4|931-;an-FOzz9`k4uxsS3oX*wbL)m#wyIf8W#8o^R3u&y(XzLRUa4(CHtj& zg_9U>9S_L1IK3C+tTV$6Q$sVm!%Z;4Iv>6qYr^H>)~RG-XW~G(eKP2*2zN=wF^W1j zz9l^++&$SYRxjK$+|!AM`*8GhEJjQpNiK-4kYi=ZwW-PB{+a9LC}?<4cyMZ5{86`e ze0F$fctpA+c?5nKl^6wWl4_EvNHqzMPL)T;hsR|e!Pw{M*pbwT=%RSf)P2dtnYkE) zoR?Z2o)DfKzAv>g{9yRe@N79Am$ux4>5_EIh#7t|JTJU3{ve(}`oHuFFVE~w_YJ>> zQT%%8XsR;2I=n8ThBv08$!vOBcuRCx+6wQ07sR6Qp78$Y#Pr1UqVS>g(r{&ZO?*o% z>*$V!G3S~@M`rqePL zciO~yI~`;FF!Iz2|jC97t#>S2~Iq1xH7R9$@+Br*;otzcEr%dLF_zq`NdPrusvkhbHBa>sDoz7n8fO8nW09#y< z9P0*CeKNaKuJnnCrjzyP6*Cv()&t#A+;v%GPimdpSbD~|?P3euPO1ITXv9sfaxaLO zZbfXLyIXq4xC60wZDy`}JyP244oCTxN6h4Q=?{|{;ZA~Qj11FJ9%|ZxPsoBuljN%S zWI48)NK4b`yZ0;bnMNJ2m$EMXOKR8WUOL`9Yu7zYtq184}C1O_k&BBC;Yh!}Qc-(-_rc6^8q zgR$nmf89`a{K7%hoDFl)|(&4TqvUy_gh17NT0@udY^#9i?82@df;9UQ|97Oe3TizUop z0f)su&gQTLC)g~OA2=+`V3|t%6)+?Iop{LKs=OOqk`jzRv!u}YFtG7TxR=84yG07% z$Jh)RnA%Dk({Q_Cx2V1b1(J$MTtyP5~@Vip#7Six@LXPjc2xIm|Zy@R>nO2n98L3uYZ zsNzD0$NEe5;rBIxWksy33B<5MpE8+oNtkoB8QdyjSApY894j`j28k&}9H|f490}(% zA{AmTMcicDc_$b8IdFnCbC{At5fg%|3Rp|vEhT2zCe$b~D|A!D7qP)ZvJIF>%t5*@ z%r@%JETdQ~1moyZX5b~R*bJp;9EVZb#f&9{B>GAYXSPx-Z|MgHQ?$!wGpz!9>GFnH zPP;E*c3vW!7*FrRd{o>YoxqzX!hkvl7L%)(wUkPXC5(=mz%*uTM&n1wy(4%=*sbzAeGADN8{$0*QLN#C z?{CU&drqM#hgakD_cK1ixSOvYTt*bLb-|E zsf^z+enIHaoRc*D6ylub@Meq+_-a2!$#HJxoZC7;uaWyw`i{0%oI1{TJ;OMF?>)ut zt&HCgmZ1AMhp)cH$ogBh2BhNpDna)#opLBY;XK3E7`Zf88qakx%Z zGCj-Im{hk@xHv`VV2Qb!>V+t&kMCN;IEGMJ?A}55if;)WH7Ue#4TpctX>x0kyD20_ z+j0fRVcDwt`L3#j>RJx}g7JSCc}`MJX7_wfa{)ig);g-p;kIs>o7|2KbX5&ycP_j6 zS+>^Zg%2s**4-3?InK?5lG}siIZwHbv4}$c_cc@HXPm3foX@sg-jOV+NF&FQ(aD&M<~ z-JAL9c-kMx!DU3BK=HTH)e9_1*^FbhVjM(R>n9P@L+Xq{U55r-6Ud|UpheAl}i zpZgza|Kj#?%=(0aOC-3JVuv)!LlR>X#tg#$LmIj)6SarR1-&|wDr znrMArVfUGr(E3%0e3zf|P@nV6TBMF16du+9zMb9aget&pt`WuUU){pt7wHG96bhHz zv&jMMeumM0r@zkb^)~X;=dwGjNzCrvjQM=EfZcNl)peZDM(nP~$o+|}ZLI9ybvX)m zRI!ocn;et1kd?vinvB;FO4du3e`EJa4qwZ6T}DTlV@7K#a}S`}(Y@kJPR-9)o>OC~ zDqDK>D8x~Qu155j*J7uH=Bgh+%vHL zeGBxRTVbcqj-o4coBKhVd1yGE5+^7L$C~70#d+~! zP17TSB3Y!1y4ZvCHgN~mf!jlSx;u2F9}$DZFp(fsW}AD`#4TBwccNJL;AO@u;qjRu^GbPVRud zj*g{4M9gz^DGm0cG_y;Ec|CF+IsGoe{**#qX)vGa<>@FK6y>(VhlfM@o;(W%P$=^bWB9d(%^ND>4;exY~+}Sn3u-^-SSbQQeppm$IAQxm&XE<{JPR0`WA&A z0&ElX7K$;Ak$Ro7_}x2&3%!pUJ%k6p;4%m+hhHY0;-dJZIl8YB-vQPN+@z;3(L)fX zFeU;Oe3jW2Y}-DGUpHO57QGtT)zAaced&xo#tMw((F2P+Pz7qFnyBWfHEJhz6#iJ9c4+Jc zTovV_r@8Tu{#bX8^Y@PP_lWg78?zs0`=RGaL!VY3sknr2&I{WoZ5umNbGU0%*xnbF z4&ob0asKFh(Dmi9*WL6sh~yjB-C1$|C9!@}#rZqM`8UM+J)tN+UJz&RonJMpNZ-(Fw3@sM`G_zjBYP@Hz$sYx=#MdvHps;MEOyUio@glhvWSJ zi1VL`^Ph_I+odUPzZI#y+3iA!H}H<{A-@v6oE6QQE}!SWT{T>_Tp6yquKKQqu12oL zuHU(Cay4=N-gU%v6l|$8U`?F{d+O9hyeX&aGuOXdpBo|YrpkdcMJ%c^hS%^Jej{K6 zjs5NsW^FUwY-kDWr0&8F>K1lR_Zu2}rYF0;!=C94uy5&&*dzT`>{)uZ)dstkcChZp zzNKBUb7?Q^UE0^`Z{=g>(jnGRYdH2yABDZrcVLh7*HNCX7=v^d+0G#-O8Cb3QIz!?{Rz}XhJ<4lXY z(Yv;mZDd>7PG-t`WtMD@-nNU(mj!Zw94P-R2g$*5h_V@n;C2TG#f#-Eze=k=$eNMmAbb6g0r{yf;EUVAz3r^vbPK9mTwbSKv zV{YbKeNKPpEa$w&nc#Ff4eatv@0z~y2AtE;f^*bM_LdLHhtd1z%Dm|E$`@pjTq#$} zHFCZ7>j0Lz2Izsh2;*1!jkJdCgP|Q|B;I~@npDOgPcbe%+zSRX(u$-;DbwFCktU_m z3Q|h|{xT`rX;G@bR30VzQHgH{+ZK?66b{tQN=W)jWMJ14_2;eeZ>M`@wX_m>BPj+T z(YE*oy7E*prqO4e_IO94ZQALFU}wT~x@u1-j~;|}waUVm(A6qM62^TU!luhrdrC6i zxAzqrdjDQ&O_y9Jsd#gCj50m0+LO{Sn(P!~vai~+s$fjsInMOHYEP?*@l}_L%tKe< zc?z=%DbApNUB9Mx>HYeEenaomf7QG7-}D~6LoSdDAe{JFGr42@EiZRJv#MZ)bG%j3AIOhGb=n3Ck*Hv`T)LxuB>0Pd&{;+r9(LVD@l(= z56E9}Poyx*lMF`nas9cRC+F)g^f&sX{!*XPU+I6zNpiBBBB#n}_)eNBXUW-ej-0Dc z>#y}0IYUB+{|erO_LKSTe))HM{G0t3c=FG0EkFOgqA(A2o$RCM>jiqD?5F3&jEaWD zjfh5KG_*!<(cAQPy;iT+>+}Y_NpIG#=#6@-UZfZ6B^Wh5iP6-f`X#+oFVoBQ3XEM| z)~obt)UgL+9h@$T@lFm#9l0`BXpDLCFlc&876})|I@?5u#ya9!BKC-CG~yA}^%Ol9 zU#AN1r0ROE?5(Hk8G5FEQReDtr5-iT(zEp(JdMX%_IL^@x{xEx?;$V!kc&KtFCu<^ zGCx06&(ljVUSlcgxq5Zv$tcyd7)eFOk5OY-ITqtr8f~qW>*RX5LB1k4%1!vT-y*kS zrI+)Al1(j^6w4_rrRY56K>=haa^}Y9ut@k=N=cl{bk1c>mQpRwYlbbQlE$&|V@uQ* zq#Kb4WXWSpNOxXwYoPk_<=7ja_4Y-^4+*IB`uC(l^(n( z8s>niV9uvH+GS15*w)7UbREo}G~jvOrkD?Y5Z`^-n5E9a+-*O685LlDdmv`GM`6d! zXT)gixH*Q$|E2s2iEiiIvZB6>_)CFW+Ir}t&^H2F6-r>g1=@z+0yW@IF|If6Hd-0? z7;TKUMmr@7|ezhZ<4 zlwOLOmrK=HW~?>V8S9N#jE%-7W3#a(_Boy;Pl_kiQ^`}=Q^j+wr>dtKJkWZtU)eIFgT?p#yDo6JbuXP zdQ5-aWweJp|7dg;Wl;l9il8yom@Pt()H0E3tTDETO2#&07i7E7*oTwf-ZPGfhLBoF z+=3JR8j4%_B+fffLywD=eD3C*kYbW(=Shb2GCk>@x;WKsxwRZKj`paYMg&iDomKpm z@_|$C1fAn1piMPbcxZ=L;iVJ4gby`*y$C=mogsssMo&?jPcqCvZ9XaLux#p~9+!z5 zAe&v{M%31R(Tb(g#yDY|5ceDBjq{?D+v#?S2T*4r(V6Acg-*P#P)3}Ed;;xyYxtNi~^u3x8ZDy-)w{q(Vjm&bE_Y$Q>3}>W56h3`F?mv+!OD|TZdFbNRFXHKVg%aKc><)&4Lh0HAvz^<3y|8|-L~rL?z=y#wP@>Se zA2?La!dLcJU^6I`?xavC+X;r|;v;q;g`Hq9g{S2pJZ ztC$Ob)y+k~baOGVhWQe(rnwZDVJ-vK!7r9tPa*6UnBgpk(Q!lYIB>uy+yU@cs8uxr z_6F|+`9VC56^w|hCtNhwrolmpgf&14-SFK&=fYROOxfGmwE@3(-PtjN#wFHzYo+xv zPQhDkt+Ccx>rBHmO}FVX%bMlPYrry*xa$Ehao)wQ|DUQ+81IyHj{OBwn9@`x=)I=X zOvGI-`8JUHHHALPD*0)+rg#J;cmzcwBp1%mwC|&pNOI}O)A{P(!&eo)q%ilVJICc% MBJ=^|GbTs>0dZJj*Z=?k literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/fonts/Lato-Regular.ttf b/docs/_build/html/_static/fonts/Lato-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0f3d0f837d24834b9b5b0a6b735459c56f5e75c3 GIT binary patch literal 656568 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 w-9m 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

    {e2O2{s7^`lE9GCT@J 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

    !ump=_U%Ldm`W#$)AJp;TW26LE?`p+s$fwQ%}C zp)_rQwQ)K?p(H1P_0%F@igOy6s+I!NoUefC&M9C6wFH=<76ThvZvvY*{{a5nItcs^ z>kx2;>^F8t-t)kF7Gu}(Iq=tW!9}md%KUEJpnCxN3f`xq*oQn#=g@kB*o}-q`$^)u zTpul{CH5unLL2Ic74wJvRzy#OTE(FEdNF$;R^!fr?71;>)f!{+DR-watweX5F|jN0 zl9&muSz|1A6WxtsCb^r%tmVEeX0p3U%-Zf2G3&Tn#Y}Oxi<#=~5Hro)C1$$2Tg(i1 zkC+W{b_4b2&Ik5Fe=+qp2AGSJ6R0P(+SHH>M9x-#M)B?fV1kR9L!J!9lUzgVTCSmN zvTLYZ+g%2%<1PoLxGRCF?kZrKy9Suwpc>V@>NRhS^5;=+bbGr4@4Kpm)dO z{EBH+&4K-J?gaTkIE@{QQd@7>%vzhq8I)4LCWxUI<_!ubLTX^8>@UzIf&Q+01m+pO z#M|a=@HTocdz)~x;1+MIYrC%NxIwp~Tgm+yPMI)%^(ao_yn}lWK2{UJ?~HNAg5N3Q z>&@-kkZvcJUF8N`?OJYi{3^(Q0h)15VT|%FpBmmN;h;!3D8nHIaUZGwJF`=4Y`Qdc adHZ(BTZNev*8X&ls2nqVA4NVRa`gZB#}q{X literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/fonts/RobotoSlab-Bold.ttf b/docs/_build/html/_static/fonts/RobotoSlab-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..df5d1df2730433013f41bf2698cbe249b075aa02 GIT binary patch literal 170616 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 literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/fonts/fontawesome-webfont.eot b/docs/_build/html/_static/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..c7b00d2ba8896fd29de846b19f89fcf0d56ad152 GIT binary patch literal 76518 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%0DyT{kunD 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⋙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 z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_static/fonts/fontawesome-webfont.ttf b/docs/_build/html/_static/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f221e50a2ef60738ba30932d834530cdfe55cb3e GIT binary patch literal 152796 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

    =v_rA!CPE#mM^4@)}X7qf;At+v)G*FZd&; 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 ALjV8( literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/fonts/fontawesome-webfont.woff b/docs/_build/html/_static/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..6e7483cf61b490c08ed644d6ef802c69472eb247 GIT binary patch literal 90412 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%0Ded40&XED0GYFoIh+AV*?9!MR5pBW?X_8Bp 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 IH>08b4`|GD*#H0l literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/js/modernizr.min.js b/docs/_build/html/_static/js/modernizr.min.js new file mode 100644 index 000000000..f65d47974 --- /dev/null +++ b/docs/_build/html/_static/js/modernizr.min.js @@ -0,0 +1,4 @@ +/* Modernizr 2.6.2 (Custom Build) | MIT & BSD + * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load + */ +;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f

    "); + + // Add expand links to all parents of nested ul + $('.wy-menu-vertical ul').not('.simple').siblings('a').each(function () { + var link = $(this); + expand = $(''); + expand.on('click', function (ev) { + self.toggleCurrent(link); + ev.stopPropagation(); + return false; + }); + link.prepend(expand); + }); + }; + + nav.reset = function () { + // Get anchor from URL and open up nested nav + var anchor = encodeURI(window.location.hash); + if (anchor) { + try { + var link = $('.wy-menu-vertical') + .find('[href="' + anchor + '"]'); + // If we didn't find a link, it may be because we clicked on + // something that is not in the sidebar (eg: when using + // sphinxcontrib.httpdomain it generates headerlinks but those + // aren't picked up and placed in the toctree). So let's find + // the closest header in the document and try with that one. + if (link.length === 0) { + var doc_link = $('.document a[href="' + anchor + '"]'); + var closest_section = doc_link.closest('div.section'); + // Try again with the closest section entry. + link = $('.wy-menu-vertical') + .find('[href="#' + closest_section.attr("id") + '"]'); + + } + $('.wy-menu-vertical li.toctree-l1 li.current') + .removeClass('current'); + link.closest('li.toctree-l2').addClass('current'); + link.closest('li.toctree-l3').addClass('current'); + link.closest('li.toctree-l4').addClass('current'); + } + catch (err) { + console.log("Error expanding nav for anchor", err); + } + } + }; + + nav.onScroll = function () { + this.winScroll = false; + var newWinPosition = this.win.scrollTop(), + winBottom = newWinPosition + this.winHeight, + navPosition = this.navBar.scrollTop(), + newNavPosition = navPosition + (newWinPosition - this.winPosition); + if (newWinPosition < 0 || winBottom > this.docHeight) { + return; + } + this.navBar.scrollTop(newNavPosition); + this.winPosition = newWinPosition; + }; + + nav.onResize = function () { + this.winResize = false; + this.winHeight = this.win.height(); + this.docHeight = $(document).height(); + }; + + nav.hashChange = function () { + this.linkScroll = true; + this.win.one('hashchange', function () { + this.linkScroll = false; + }); + }; + + nav.toggleCurrent = function (elem) { + var parent_li = elem.closest('li'); + parent_li.siblings('li.current').removeClass('current'); + parent_li.siblings().find('li.current').removeClass('current'); + parent_li.find('> ul li.current').removeClass('current'); + parent_li.toggleClass('current'); + } + + return nav; +}; + +module.exports.ThemeNav = ThemeNav(); + +if (typeof(window) != 'undefined') { + window.SphinxRtdTheme = { StickyNav: module.exports.ThemeNav }; +} + +},{"jquery":"jquery"}]},{},["sphinx-rtd-theme"]); diff --git a/docs/_build/html/_static/sidebar.js b/docs/_build/html/_static/sidebar.js new file mode 100644 index 000000000..4282fe91c --- /dev/null +++ b/docs/_build/html/_static/sidebar.js @@ -0,0 +1,159 @@ +/* + * sidebar.js + * ~~~~~~~~~~ + * + * This script makes the Sphinx sidebar collapsible. + * + * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds + * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton + * used to collapse and expand the sidebar. + * + * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden + * and the width of the sidebar and the margin-left of the document + * are decreased. When the sidebar is expanded the opposite happens. + * This script saves a per-browser/per-session cookie used to + * remember the position of the sidebar among the pages. + * Once the browser is closed the cookie is deleted and the position + * reset to the default (expanded). + * + * :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +$(function() { + + + + + + + + + // global elements used by the functions. + // the 'sidebarbutton' element is defined as global after its + // creation, in the add_sidebar_button function + var bodywrapper = $('.bodywrapper'); + var sidebar = $('.sphinxsidebar'); + var sidebarwrapper = $('.sphinxsidebarwrapper'); + + // for some reason, the document has no sidebar; do not run into errors + if (!sidebar.length) return; + + // original margin-left of the bodywrapper and width of the sidebar + // with the sidebar expanded + var bw_margin_expanded = bodywrapper.css('margin-left'); + var ssb_width_expanded = sidebar.width(); + + // margin-left of the bodywrapper and width of the sidebar + // with the sidebar collapsed + var bw_margin_collapsed = '.8em'; + var ssb_width_collapsed = '.8em'; + + // colors used by the current theme + var dark_color = $('.related').css('background-color'); + var light_color = $('.document').css('background-color'); + + function sidebar_is_collapsed() { + return sidebarwrapper.is(':not(:visible)'); + } + + function toggle_sidebar() { + if (sidebar_is_collapsed()) + expand_sidebar(); + else + collapse_sidebar(); + } + + function collapse_sidebar() { + sidebarwrapper.hide(); + sidebar.css('width', ssb_width_collapsed); + bodywrapper.css('margin-left', bw_margin_collapsed); + sidebarbutton.css({ + 'margin-left': '0', + 'height': bodywrapper.height() + }); + sidebarbutton.find('span').text('»'); + sidebarbutton.attr('title', _('Expand sidebar')); + document.cookie = 'sidebar=collapsed'; + } + + function expand_sidebar() { + bodywrapper.css('margin-left', bw_margin_expanded); + sidebar.css('width', ssb_width_expanded); + sidebarwrapper.show(); + sidebarbutton.css({ + 'margin-left': ssb_width_expanded-12, + 'height': bodywrapper.height() + }); + sidebarbutton.find('span').text('«'); + sidebarbutton.attr('title', _('Collapse sidebar')); + document.cookie = 'sidebar=expanded'; + } + + function add_sidebar_button() { + sidebarwrapper.css({ + 'float': 'left', + 'margin-right': '0', + 'width': ssb_width_expanded - 28 + }); + // create the button + sidebar.append( + '
    «
    ' + ); + var sidebarbutton = $('#sidebarbutton'); + light_color = sidebarbutton.css('background-color'); + // find the height of the viewport to center the '<<' in the page + var viewport_height; + if (window.innerHeight) + viewport_height = window.innerHeight; + else + viewport_height = $(window).height(); + sidebarbutton.find('span').css({ + 'display': 'block', + 'margin-top': (viewport_height - sidebar.position().top - 20) / 2 + }); + + sidebarbutton.click(toggle_sidebar); + sidebarbutton.attr('title', _('Collapse sidebar')); + sidebarbutton.css({ + 'color': '#FFFFFF', + 'border-left': '1px solid ' + dark_color, + 'font-size': '1.2em', + 'cursor': 'pointer', + 'height': bodywrapper.height(), + 'padding-top': '1px', + 'margin-left': ssb_width_expanded - 12 + }); + + sidebarbutton.hover( + function () { + $(this).css('background-color', dark_color); + }, + function () { + $(this).css('background-color', light_color); + } + ); + } + + function set_position_from_cookie() { + if (!document.cookie) + return; + var items = document.cookie.split(';'); + for(var k=0; k + + + + + + + + Moto APIs — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Moto APIs

    +

    Moto provides some internal APIs to view and change the state of the backends.

    +
    +

    Reset API

    +

    This API resets the state of all of the backends. Send an HTTP POST to reset:

    +
    requests.post("http://motoapi.amazonaws.com/moto-api/reset")
    +
    +
    +
    +
    +

    Dashboard

    +

    Moto comes with a dashboard to view the current state of the system:

    +
    http://localhost:5000/moto-api/
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/other_langs.html b/docs/_build/html/other_langs.html new file mode 100644 index 000000000..8ec0b7210 --- /dev/null +++ b/docs/_build/html/other_langs.html @@ -0,0 +1,242 @@ + + + + + + + + + + + Other languages — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Other languages

    +

    You don’t need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server and here are some examples in other languages.

    + +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/moto_apis.rst b/docs/moto_apis.rst new file mode 100644 index 000000000..3414cba1a --- /dev/null +++ b/docs/moto_apis.rst @@ -0,0 +1,21 @@ +.. _moto_apis: + +========= +Moto APIs +========= + +Moto provides some internal APIs to view and change the state of the backends. + +Reset API +--------- + +This API resets the state of all of the backends. Send an HTTP POST to reset:: + + requests.post("http://motoapi.amazonaws.com/moto-api/reset") + +Dashboard +--------- + +Moto comes with a dashboard to view the current state of the system:: + + http://localhost:5000/moto-api/ diff --git a/docs/other_langs.rst b/docs/other_langs.rst new file mode 100644 index 000000000..6fb617c39 --- /dev/null +++ b/docs/other_langs.rst @@ -0,0 +1,15 @@ +.. _other_langs: + +=============== +Other languages +=============== + +You don't need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server and here are some examples in other languages. + +* `Java`_ +* `Ruby`_ +* `Javascript`_ + +.. _Java: https://github.com/spulec/moto/blob/master/other_langs/sqsSample.java +.. _Ruby: https://github.com/spulec/moto/blob/master/other_langs/test.rb +.. _Javascript: https://github.com/spulec/moto/blob/master/other_langs/test.js From 6346e44c9d83d83ac26a88178b9dba0d2157411a Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Tue, 14 Mar 2017 19:52:36 +0000 Subject: [PATCH 084/274] Be flexible with Route53 Hosted Zone IDs with /hostedzone/ prefix We will continue to store just the unique ID, but since the AWS API returns /hostedzone/, we should accept attempts to pass that back. For example, both just the ID as well as /hostedzone/ work for specifying the HostedZoneId of a ResourceRecordSet in CloudFormation. So we should support that too. Signed-off-by: Scott Greene --- moto/route53/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 15679f0e3..e3896a1c3 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -277,7 +277,7 @@ class Route53Backend(BaseBackend): return self.zones.values() def get_hosted_zone(self, id_): - return self.zones.get(id_) + return self.zones.get(id_.lstrip("/hostedzone/")) def get_hosted_zone_by_name(self, name): for zone in self.get_all_hosted_zones(): @@ -285,10 +285,7 @@ class Route53Backend(BaseBackend): return zone def delete_hosted_zone(self, id_): - zone = self.zones.get(id_) - if zone: - del self.zones[id_] - return zone + return self.zones.pop(id_.lstrip("/hostedzone/"), None) def create_health_check(self, health_check_args): health_check_id = str(uuid.uuid4()) From d0cde0218c812136f2f18fa0474bd87826e41ec8 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 14 Mar 2017 23:20:17 -0400 Subject: [PATCH 085/274] Rearrange docs. --- README.md | 1 + docs/_build/doctrees/docs/ec2_tut.doctree | Bin 0 -> 8287 bytes .../doctrees/docs/getting_started.doctree | Bin 0 -> 12246 bytes docs/_build/doctrees/docs/moto_apis.doctree | Bin 0 -> 4727 bytes docs/_build/doctrees/docs/other_langs.doctree | Bin 0 -> 5297 bytes docs/_build/doctrees/docs/server_mode.doctree | Bin 0 -> 10556 bytes docs/_build/doctrees/environment.pickle | Bin 10880 -> 11476 bytes docs/_build/doctrees/index.doctree | Bin 30998 -> 30440 bytes docs/_build/doctrees/other_langs.doctree | Bin 5298 -> 5292 bytes .../html/_sources/docs/ec2_tut.rst.txt} | 0 .../_sources/docs/getting_started.rst.txt | 114 ++++++ .../html/_sources/docs/moto_apis.rst.txt} | 0 .../html/_sources/docs/other_langs.rst.txt} | 2 +- .../html/_sources/docs/server_mode.rst.txt | 67 ++++ docs/_build/html/_sources/index.rst.txt | 21 +- docs/_build/html/_sources/other_langs.rst.txt | 2 +- docs/_build/html/docs/ec2_tut.html | 306 ++++++++++++++++ docs/_build/html/docs/getting_started.html | 343 ++++++++++++++++++ docs/_build/html/docs/moto_apis.html | 256 +++++++++++++ docs/_build/html/docs/other_langs.html | 243 +++++++++++++ docs/_build/html/docs/server_mode.html | 283 +++++++++++++++ docs/_build/html/genindex.html | 8 +- docs/_build/html/index.html | 36 +- docs/_build/html/objects.inv | Bin 372 -> 398 bytes docs/_build/html/other_langs.html | 12 +- docs/_build/html/search.html | 8 +- docs/_build/html/searchindex.js | 2 +- docs/docs/ec2_tut.rst | 74 ++++ docs/{ => docs}/getting_started.rst | 2 + docs/docs/moto_apis.rst | 21 ++ docs/docs/server_mode.rst | 67 ++++ docs/index.rst | 21 +- 32 files changed, 1827 insertions(+), 62 deletions(-) create mode 100644 docs/_build/doctrees/docs/ec2_tut.doctree create mode 100644 docs/_build/doctrees/docs/getting_started.doctree create mode 100644 docs/_build/doctrees/docs/moto_apis.doctree create mode 100644 docs/_build/doctrees/docs/other_langs.doctree create mode 100644 docs/_build/doctrees/docs/server_mode.doctree rename docs/{ec2_tut.rst => _build/html/_sources/docs/ec2_tut.rst.txt} (100%) create mode 100644 docs/_build/html/_sources/docs/getting_started.rst.txt rename docs/{moto_apis.rst => _build/html/_sources/docs/moto_apis.rst.txt} (100%) rename docs/{other_langs.rst => _build/html/_sources/docs/other_langs.rst.txt} (78%) create mode 100644 docs/_build/html/_sources/docs/server_mode.rst.txt create mode 100644 docs/_build/html/docs/ec2_tut.html create mode 100644 docs/_build/html/docs/getting_started.html create mode 100644 docs/_build/html/docs/moto_apis.html create mode 100644 docs/_build/html/docs/other_langs.html create mode 100644 docs/_build/html/docs/server_mode.html create mode 100644 docs/docs/ec2_tut.rst rename docs/{ => docs}/getting_started.rst (99%) create mode 100644 docs/docs/moto_apis.rst create mode 100644 docs/docs/server_mode.rst diff --git a/README.md b/README.md index 5485c63cd..2b4daeda6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/spulec/moto.png?branch=master)](https://travis-ci.org/spulec/moto) [![Coverage Status](https://coveralls.io/repos/spulec/moto/badge.png?branch=master)](https://coveralls.io/r/spulec/moto) +[![Docs](https://readthedocs.org/projects/pip/badge/?version=stable)](http://docs.getmoto.org) # In a nutshell diff --git a/docs/_build/doctrees/docs/ec2_tut.doctree b/docs/_build/doctrees/docs/ec2_tut.doctree new file mode 100644 index 0000000000000000000000000000000000000000..d9f63cfa2c607446b0504c6f849a7a9fc00f1a2c GIT binary patch literal 8287 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 literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/docs/getting_started.doctree b/docs/_build/doctrees/docs/getting_started.doctree new file mode 100644 index 0000000000000000000000000000000000000000..96db7c269c1fc6bbb195a3a10ea47c869849d3f4 GIT binary patch literal 12246 zcmeHN2Y4OTm3EUYJzKId7-O(uVr=vb){_eaBH{!>5k|2n_FzGxjOM*J(j95un|II5 zSXPorAcVvY=?ST%kq)UOq>@e=*_!Rynr+#h?b*KjpL1tkkz}tS;rn*K50^JH_nv$1 z>F1Vn`?5+ktR%4?M5TII^`ZiQ*1URL&`WpRp_iq4dC~5vyEQMKYj}}fk?OAE*rp{* zmP~nZ?ANEPD0V6Ks@AL@E2|d9p9bG}dFw(qgbZj}A$(_~RqhlNKivD_a zzzxE>XGIQ7`ZT+*k`O@( zP823o@$~vpyO5#7Xnp#qz1~gYa54|txF&xbvLyi73L z#?FAGOC`&hr-UJUB?Jo*>g{Toy)py8VC-^$587ZQ8jND^h&OmOeuA*UHaEz$55fW? z^D_@+iK1Bd+pA)CiiNC;_DVyW-eDKqI3~ZG#L(b{slKRaFJ~p}9Jjm6Ze@m8-2?cj zCJi1N>C(8psuH+Sg!jR5yUVoh!W&@ys@|=(B=!neuc!Awt=+j_=)G#0I&VBF&lgkV zRoJo$`eMlGl2kvnsJ7Wl6u4IxsD68sceDWr2iQG`rQ%hc@|+`lT$<|3)Ec!lQbl`J zhDJY4fsLV}DvhhbaWycmc8=@Ii)y3V;;CKZ`spw&wZ==-o{=sElI)F(;s+!9QvD3I zg|S1s&s?ZoKMuTtz5+yFnd)bOXz}jh%)1Y^d-nnM?yJDkCCn12?SelAp1zuCy{3a! z(AcDnC%$&RxDLUy3wro}ne4i|?$G;FeLcuhJn@xD*-grdMLd$~XXhf$=uslH1096u zQFWHf61zdIa+#!V78jFrV^J~JH>LV!=9)v+{#M8mUJqi*5yPtzCJ%Ms&X{aswAJvU zXl?-)hf_VqTrkR;gp2tSfxEa<%P4l7ok6}T|{*dj0R+z8tbQSPsd@&`H3n>9E*4Y$@ z;3NVOXob1`R8Iqe_*oTFWe>_`&@(WU;jxPV{TO7W17NMCy3S-h#<7NB#f*iiZZL*! zVa$l1InyWPj6mxOKr^h4Kt!DCBxfxY)|BCBDWHY`a3s~UJcoRnxxEw0+bIWOWu~By zg1EUR6d26Kzerf&xyccl6yIiqvNzuErpO!Z5d$^lThf6|(R3(UIpxLudIVOx_F z)-01^VB~kTm{4NbFIy<}JtKWNaW7}f=H=}CdcZ(JJqjb z==th@)j~orhzLHw$uT&je%!Kjf0UPaPz3C)E{6$`UCEHvg~g_sg~andL? z=5otvlebvOn~N${iy6eaT;^{9yFXhR^jjf?x25{+xzO$rQ)0$BTil!KcjP@C+2WlY zD$(y!ns_q9CX{!~A}b6$!$uMNraj=Hr*;90YtrQ9Q)g#XDf2*O!jYge#5 zQ6B@&qdtw zYmJa%tIW0~KFn{WXm4r8gK?S85pS+gkd>NsVD12z3yNWR8cV4iLj?!Q8I9ZiJgq%z~h0O8&eRQaT4XoT&y?VRjx7G` zapbVzumK?`UnngZ#@iGwPWfp`jdiacM*5-Sc6Z>`rxM;oeP(PW49mE7hu_+L%AZmJ z{>KjX0FnM|%4<;*+c)K=@;R8wle%O(vaBacm-_SYr!S=Xi*OuS>pa+s4iBgLOYBeM zP~stly@iEs7pw*gKsT_kZ&zWJlV&wUpzx!e)^vh(;*95U;KdL`x$5tGEbr*R&51ZD{l*QAWv91cXVT**7LBvB|_k%MuD z;4J9W<{Ume2^@*)Swuf+G(X=u>{)dW^26-LLqz0RgpYh|ECJf^Nh1z6xO^f zn{#cYBd($GvYZj`5(9oD{m9h0}&lq8zpDXNE#s}p7w z)t*Im=ce|%FtsQ3ZoymGKL20nUH=|j`S(-(2k==*N7|4 zIJu2IPMi{x+oSN`wNWim`0p2^`kGu0|G@jx?Wa1w{zowLPpSTAruvjefubL0d)J?& z`d@gg*}EEMcHj0u{|OXg6hDD1RWUodI2%wW5zK#K z?0=`U1o59KI61T-IosIMQjAGySvGPHo+pp`Osk|uS-G2?L>`+@y0$-+;LV60k0UP} z*Ac|I`w0%@xifJfr{yBeQ;y}snZeQujDtx$sf5RqO{ojgyXy{GiAPGSLi3;2C(Nv_S@Mo6d0a#LeJKX9~zh z12VQ7!6QEicY`Q2H;)#s5b+i^_?i&QEv$I?IGp1x*2pC~3!^)U=8#L91nEMeZ^?ym zHjf*AoRgk|!C-MS9w}{+skTjLlrU3P&SrZ!Ba2}R`8@= z9>>05fnI1UqzSh;ACHu_iAqdpV=w3kZQBLpf)+?;XzQ1bHt(e(TGZJR+IH}3kAyZq z*W-mUfJ56wcuQ%ge4FEZTyZW|g+K$0XZtjh(jM+gGPG^(EZjBc8sq`yM&U%LcL4-~ z*o{X@oF}o|na<{80$U+)uYg=^K*sJy2sDQ^*!4xGlO1b+%==ea%i@X==RZ6~?h~=Vty{jieyN7_{#foHZbR804ga5G=Q5B5wLA@vl!kNuS3*hbfK!9D|{AlRRpAbKVcK@?ZukNc}jUOmK8Eh0PKlqEAfhw90&WjghcBzT|G5 zc9{dr5?zfsI>jWWc8yHE(9|fG$h876EZ2ySi8dO>NQmG%JW|>(Xv}ToV?qO(Sx9Dc zMb`_mkqnvHVi^(eqU71qKA#zb02?hjfPQFK>;ZD4QGA2K8}LZ!Mj33B#M3}Y&3)Ha zDCQJoe3QXP%3LexNC+*XcDG+O zb#)Zs8%*PLzEyG;QH<7cO7Ls<=1Mf`J%YBBX1Qk_?l*km8#=>GL`P-V9Cvo(9*t`l zBYHkQ+e)KwrH0bOdbMy1-yuc?dI9=FdLe#ON*UH(pBtmY^<^xsh+c#q(0MxM3#p14 z!v=65{P|u)FGk;}y@n<$7=U`igaW!9ZT4nP2@!Ha9EPaE^u2hp6w=h7ifh3j-N7I| zu)2yTb!;!e_c7EgdW}{kSwwdV@;<%Y+(y;NFz#}<@`b^awy^7(~)uZ-is5~Tx^F$TeaLlK|q9(xWs{v-OV+JA&RG2@2Fz7Yh z+*S3;$rQboG1=?9S{YS!Q9KuTk@CElUWfMM_7<#r8cBR2{I6$3E19%POmEa3(x8t3 z>NwoeEjzP*HCFUdG>mlHeVvv}A7gA?_MvM*_s98tRaQ_;pTHNYLZvzvm5}VTD5;9+ zlW0F?_e^=5a#*1cqG8nT6XR=%`cu-nCIUUNqZ5=MNAzj59ka zSeWhQto$uOiL5NObMv!(V)x`pCZcZxbez6}Uwb8+YBi$o;v=O;@vF9s)A#tf)y2G} F{{aN*l~(`& literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/docs/moto_apis.doctree b/docs/_build/doctrees/docs/moto_apis.doctree new file mode 100644 index 0000000000000000000000000000000000000000..32d9752f64311f0df85931dae3e5895005a69890 GIT binary patch literal 4727 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 Fe*g+bTVMbH literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/docs/server_mode.doctree b/docs/_build/doctrees/docs/server_mode.doctree new file mode 100644 index 0000000000000000000000000000000000000000..0cd41aa5686b1c07d53f09ea0723ddf0fb41d4cd GIT binary patch literal 10556 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%0AXSGhy94^edDJJEg9>P~sPi0{Zd@tli4 z?3>cslv$_tib zX()>poz8eVSIjuc0(M%C&im?JmU_2n4m*H)k1cN%%`1A>^sZc??!xSB=Rrx`?7fzH zAG<*lw!Fhq@0WMl{WAh}w|#7M_#{@U4_NAhB9<(T4qx44iw+t+;}y7k$Wk9Bfvto` zFWTfC@=nnVjYT`BJ`$*p+M<(#gqb*S)1l~(S?c4Wh1OM6sZT(6DA&m$kmv{MlSZ@x zlF=f~NuL@JZ5}$K#@O@Kr_nPb^wehnaST(clc#CMSD&@i=R}*0`J98UK960ThEVhz z^#ysCn1ju6v7o*zhu;x6SBj|B}T3`a-EU=7|8RZ?w^*h1A4z=LB)Gq42YSh=j**{w@>7ON6_s^8;@aykyDT)Q*$a2sVOT+Q&;pqN`h>du~e1=f; z)i-H6_*UiW`|WzW%ENw-v?Qj8nkw&dI0As~;KB85x~)^W4#Vb+1uBmK~P*NusgR`|9uW?yu+215+J( z(D6lUq%fm?8lv?xF&nn{>gPuN0@l}_08+(V&Iyt^*T+HNmzYr=GV0;-?(A0yd6Feh zw&W?6Jk^poP|*JR*R?{3JL#gM$JB2i;t`{MTNd%?R1sbBArkYuL}_BPGnw#Ul=j97 zb~->O*Y>Ek3o@pD4}p&v^@p;+Kh_iY2nqaCtz9?;;iQ&NJ2CZVh1kS;aPEMY@bxrHG=r_ET|}C_CP-*dabfn*J8;@@df4J2U+qo6dh2^-pM% zXM^eSa9i0{Hc0&|tXVL{WA$&Wh!lt?jQUU6+W$6m`hN*Ym_KRMQzdHMG^44h9>Wu` zUC%(H1gd%_GDgp0+VJ3|XE$Jck{IU@BXHEs%-B-N*h&T35tjk&?5^2DP7~lPnMLZ_ zC|%4u)GgJ!dmTg5p@X^|iP3ZM56|ln%C0Qg%23M?aS3?ou-8`Zvw9wr5Q=&}OY8_G z@>x`Oa-nt3oLN#rUN51PFh2(w zqt9jX2=nu(P=)!4-I$no$7knyv=i=lrj0b2M>-RfF6K#peOX+5up~||MLXaD4FD*X!bh(m;i9#)RNa%pPZ&F`~+U{)PYBp~%ajJ1}#@@lJh zmMf<&xcVAyWVi>BHF^mD5El-iU0i7Fyx?|PJg#Nt-IdJ44VXy}xpp0;g!=W!7<~hi zM~e4wVX9(ML%bHHg!+xh7@cJDi1#KcRG}X0#suaBbjLJZ{K+O#WK$aAk6FtJ=t@=D3VR2NKqQ1xTz#VYg7V7 zABoWc{!1v9Nq{0*))2*P2puL9Q8n8XnTh6@kbq{Euns2|npyU-&i$dz5a)3&)Zz@= zC(yvbc_uSGtCHzJU8b%q$tS-Jw^JW~HgZNkhszPdH*=v@V^@~+5c5GwDLT(Z#^~oU zdBppCDpW;(Z0B1oA79 zG5S?Z9`U}K3RNI4>dx6kZ1Zs=*5l@J1A*g}KXowF2cE1Wm9L?6@$d{lD&*SjRzo11o%Es4xuLZCtp*lItzG!IB#- z8Gb6!Z$bk{>&?vcmP)3#)@AC-l6-RiZQM@bx*a*A@8EI-`JG&-)!3CKJ;eNWN-13L zK*s2IGI_-NE-F-o3p=UkIJ=IFhYO+PP1sdco@=vY35kCGz--A-M~Q(eK4S zjRSoC$&wWeK0acWj@$Zu<;pti?fbcr0lynrqd$OuhzkM#AQu|De-GLj_YX1ehbwtM z(twxLkZ&KQlwkiDGDd%#$s@g=;KEeBq=tAuNh!hpDP)ZPG?PcXpP@n(?48{nE>(3; z=?>1HZWzciQukR(!_)5$k`ymX(w{>ULh*SdMt=eSG&<-*PA%aVNk&(e3=W4!IV{;Q z1~sejmr8`yOUakHnubl8ARE@<@{5*|uYkbluj0QP#jkN;hb4Df@)}DHS~C1#r@xK{ z4&OJJ>6?{I->S>hl_mLv$+x+k;`bfojQ%c{qZoXT3$+@%vZRNYzfUQ}?+3^j{X-^? zcz;BNs`wonHvMEgTnHr()eal7gv8%V>C=3R)ISChBKH#{M&E~j8btW)m?fL045ItX zm30Qu1Kh~)KZvZ+KgE9;{-1H7F&IDRcG|T5f|-9=$^1|QX3|5hK1?Y^;aA8Q{c9$V zko*l7rYa^i#QO-P6oua+WAvj;9`XK;3RO|)?B+EghqKivK3N7{c@#ZORLC+?_j^ji zQM8rTDwf3Gh4o`-LM;A(#OOcbzch+!2oW8!H5@?YkYq#rYX;Dtn2@{opIPqXq1~JpCu71nj?%G5X(39s&CV6{>)3?RiatIM zOczr!iZuO)(#69uOgMD?8D}B-zo-SA|3hN*llUh%t7(8F8CM@k?#WLvchgMf4ndp2 zg{h#G{GQ3})RSi+XY}k!<~a?RNe{WwjCStHEzH|m$=lX|m(&n%j8X!;9U0t0GI<2} zJStQH9_z+8Q4gO)Jw08#$tF@XpVH{yP?DiLPzks@kr=%I{{%N3zc!UffHqlHA8pzf zRdm*cEarqzOo;m;E=!~4Qn#4Wa1y|$Q<4;qBLSrzOpq;@yhk4JfC6aiW<5~KU@PoPDO0E+CG7Emi#z{*fS2-GSrGzMxl+8L-d z%zHs4?}ZI`Ney{%5v2sw#mE?a36n=at))U0P^Wa$(I(Sl;{%|>x9r%>(W{y1f=3pT z+Dj=7&zM8UB*w#cIQ+s&uLB7Hy$lKN`tVPH(j&443;>yYcsh`$+zDLHOtg^;$3Eq1 z>u6>O>+n^RuWO_GRDA`Tx1P=Gu;da;uD0YlOZL+%L#(X)IaY6As*NGlmi70pTDCIt z=nlDrvgap9Zt0S%!))e`Yvej67{BM?hh;x!m+rcIEu`U+iQbL*dJ{|AOwz=O>6~Zp zO*(jvl}qAc9`{*z`*=hw#;4@5MwY4ZlL<-pc~D|Z~&y?$_D>%f)+ zn+FGX3?3NTeC_7J%}e`kSkbe_+Ot$|14n<;nL|0)m>k%y8$^oTfk?D zBsxj-ut)DftEP;*xAS96VOPKefQe%#61o|;$U416aa+lsn^HED~2 zqqaC1Kc?n`r6rR}r6J6B3aM1gvr*leJTRi#(xPqBDp(V?_EftO%~r9fVnbq~wzX|# zQ&xVDHRY)e$d-5@Yk5nwp$K*50c+Gg&u7`n}$Qo3*_d)5X+c=w4#f z(u8Q&)*jCdbjDW4i6#-Vd|x~1z=yD9MlDZ>wro%&!#%YkDOxjm%kz*wJ}KJLR%VyB z$31mIQZzeRPo0<)^Fz{;l7VQ;NT8(cf;u^=PN_BI8hbA~yK?M!*t)~@-A(ZwZe~}! z|GLrmsI7N987O($f&J=Kqv8p9+%f4+6D>sxihR{2mjr1`wEETr8KSzyeD1M2JxGsr zK$YuDTc`3?VZu|ZB*AcoQD;JdXp-n}qO!lMEBbp@Wq;2u_j66u^PXfN(aT=Ba;y_t zc9NDjX$jy$PxYZAYmHh*9ck3QUUtbdV7M&V3v1gUXA;1qtfJ$o4Q%U1qs}SYdTyny z=T+Exe%ZVWBJ(y?T7O|^>qW4YbP)LQ{^9MOx)@eoV$`K%Wp&GCuy13HebkQ$+sDYA zNO``ceLJiA*~-mEU0$|wOQn?q6;^I7Te&T=@`_3;w})0<2`f*8rfs37_-LppzQ^$; zMvU*OL0Em2Q9H=$V^a>H7n>`4p>_i2R!Fxq>r;N6(G` zOu0d{)41y4ab-Z>n{>0Gu&n4PPW6uv-z$1Ny1*d*FBj&J!Wc}BF9dgde~`Xg^V)`UMOxof5Pns+l+ z-j1cN(N@9Bv&3QT?wyLMTn)+O6pdJpJiEmF?HnTW)BDw=+$Hk|RAD-OPy!%nRgeFJ z61d3Bu{?QD)&eARa+g@J!_5YHJ07ZXp{fW~?g6Dp6(h%V9ixn8d>G+EX^?p!(+8AK zWI$3*G>RJs0jQhK*%{wcfq{dxJK+{XL<2D=W7O`142%TWftXvIGHMT%MJwD0tGrT6 z4kK#sQOaiR`9@7uRAh=pqn?!z9olvMls7e*cJn1@!x8KCf+jY~Eiv_M#MOO9nS@y6 z+ml83#2&V^HR-A6U=j|A1*O`4jX3<=A<>bwv9f6Pow{iVi>RC70KR`n#GEWbyEE?C zT0L(FVKzupkK{HJ1Y+St-c6@`H{)v?f%f@W5Y-E0r-e22fM{_hIs8sbT5Au6i{+s- z%`&FXI$9kbTaS6%hl#AcKF_yz+kIQ`$-6~b>HBD=^nu*#<8?Yq`B2ph#j8ZS}UITT!qfxIFOQb)Umv+9G zN?RUpPt@x~D}_~bQ^cq>g{2G&>FdSfkSnGAY*hCK5u3CCnNERPy|EO7ZV?N^4MoZe z#>bt#>P<=YW-+f9qe{KS5=$`uoWi(EOaU9F-kMamCgd$*9tZT>#8OJ*TcY-GP~0v$ zM#($zVT|02xb{_G^b^}kn%~x-? z5O&<+ly)Yhuihb+tY5qCoHbZh%A&<$dv7KmWbITDdm&pFJoV0`dY5Pk2Y`CFC2tlj zYkN2Lu3M|_L@c!mprlp)uB3VocY_9vygjMjEAOxd=lJS<*3o(3kyxqTpHv?Zu~cau zc zr8OB<>f_KI%C+-wB*uaIgb{6}y@~qdkZ5-?7&T^@r#^*|8KI{>4Txiim3Dz5wx>Rm zRG$^?76LUpU40JwEDfO;IqLKBPB9t8hLO9|O&!g2Vz0iE@CTS7c$hD?;Ei=vNh!tczk*42S)u+ zb|%$*m8<#v4J~<~t_Kg+^x%l?iMB{|Ry`EL@gp%0T0Qk+qkaNAYd7KPAfLDWRNnEh z7yl_j#=}Pati1L4c|xv8%Hxyrgrqz%DY2)gJ?<}Rg%Eej!AOp&UqZwqMm<^<@vC|f zUGiZP^Xo(@a#`7I_-~SNCyQ3b$0*j;gtm$@rhWr~j~VsbvcTUp68H!S{C%xm_%*@L zQXyl<)E^+`aijiN7V{_GPi3U5X_di$PYb96?ZB1jDf%0B{3Ps1odN{`D}N}!UhOf` z^yg^vN3*igZ_8h>`TMI;e}gu88kimr_nhrzgVf){nk98TR{y}_Mvi#GsDGBN{nw10 z{&zwW=1&^+RGIoeji{d>!~ZLRr*4|l)MWG=d`m2PE*EN@-6fv{mu^O*1cG`VGtRGM zY@q^eP0LU&?v5r=PxMnTOK5Xs7O88cH2jUK+4So+G#%Kl+mRR@!xtUk*-}f`K{C2> zWN^5-4gr@ym;SNZ%R^Q#U=jjRFGSYpMfjF+>EuE^E`+9Dj7A1@2{SIOWIV1hV~%tX z?=ngW)8)t*y@JWZe^7lq6{;|eb<>K%8}`|vNj8z96DY0u(bFfQ5;&fO#ORapB^;~% z`1C2n*Oepl8X%affNdp9d0Hri|DyG&T$lm2I5*NjJdHWKDml9wbLL11b?0ZcO$m&uFT5!Xgl1Mc(a9&%p;u%N>`38!`7_7EG|A$5~o+A9cZ3~ z#OSl}B{XNGUPIEla%6ukur%>?4~yvy#e{J7aiI>*GAL`&UKdQG#U`fLp%rr1BQbgd ziz2|fa-<1+K|cG@8&McrGppTaAO8^2c21uIF3hLaNSQvD%FB#C4=MipORF^g^`ZkB z{HcaVP#*qerw-F0*IulXz8st@=FwLEP;LBejkW_xeLe{tlJ`&R3y>R<4@~P#oOy6s zU&xsw)A}N0Y|$c}ELPq_IIpsJSBPJZXldlLxum`rbP0LCd_X=ZkJw^i)Ee>~4#Yg# zSL#bZHE3ZFgmI&zV!hOtqAf_0vZsCS8rh=n;G| zlO)c@a%4#qy331bSgv0}Bf4^kuXi1m*bVHBQC4(Ks3@Fkf(z#*<@rf@K~io?%J8^M zUyBCz#&ygzR>}1AhD=>Kl22WJ2Dg(tu1C)38@L=nn&d*Q#;zRcA?6z?C3mEdG5VQI z9`RaKsCI{hB(}O7<~D?Vsc{EcLgLeu)-DhkRKgKiBu3l#k|QpIBglqYN9gf#WxY4_ z1UE9=5?P}id?7A`JI95YJHLzDX?4ys^JFD+VFqT>LtS$zCDe<^7_FE*Qmna9ub9*j zuSY4N?jvJ#z~m9{ZYorv9_z;HV*9j0&lZ2Oi4^UjG{hfEitW=;tlo=C;6H`L=x5{E&d+d{~G7KZ^4&W}Pnyb%r>7lN^lTz}+yO1&Z z-Ao=Kc_$a@6_Xm`y^B)v!h4W0`n^mZ@xG4=RbE)!?G*51mB;pE0;lmfXPTu~$TCv* zeoDjTcY9e9-(lzvpb5VCAQGeR#+T*`UGvls!aHPZIBUu-$%XjW#GVf^ArJ2lv)qq_ za>F_CQ7+VDQ^NIQ+)luLoS8pS$^6L~m`M-y^iz})u%AZ8=+7{D1ng(2Pz7v9_>UEM zILezXreqXp`W&Sz$6|nr&IIBtM1LN&fb$DTjQ%3N1ZOo3kR;<8BgrHAOU(V{kUIqJ zD_p1tt<>+YayyOWuQBu2E1ADB12gHNu6&bH0{mOZ7<~_uN1%P13-yXg4e{PfDFOZ+ zWQ_hUlSjPYqe2znv2L9CyEyW9^=$Dbn@G|3DUG6iNrwIbDgpNokr;g+z63X|nOjOE zK$|RUj5dYviotq6i+Lav6XO0L7wWrTg6;^~8M=p<_eYhyKc0b?)KHIpLMcJ_Q)G;O zn8_pPeny2V=oWY5w4biAdZIT1vjvbWBXvKgGz(o1C=2^lC@ci?*Ibwx%-^7$!F-H)e_P4>yBT;%4fXE#loFVKK*s3DnLGmX zk5s4vb6z)IK+G0EvW8UriPDv0&9o)6_UJ#O2+;n5#OS}`OQ1!K0E+CG6;OX;0e=q# zgh2g+3o`@t1lk#>e=_gCDtZ4s123teUOY)D0reCzM*oM&BcT3Eg({#<=*GfR$o5z` z|F(N@UxqV$x)7dqxFicnZPQ$&;r;pOQHk+TVw@!EIUoU`bCDR`j4uI77wI!F0A%XJ z{{y*hBR3DC!=LzY?$eg8wvHl0Scijs+T6`&t6D;FqBi`evQ03bOo4etL zn>(_E#4n@tm~SBTa*)7>E07p{JihRu#Dj+%S=8854DSidaAL?1!g~@IX723CXlIm9 zVcwOMyic2fm();CPNkGUjw56AX-pmg)y0K+y`+YCyD24*Pe;b+RZJf7odu7^j*Kh2JCo*b>iMHin3M#pseY}UMnHFqZEs-)bIl>JFLOfSN)Z{>?Q-NRJ9 zA=S42`_?^l_gVLzB3DuN$Wu2gUm-Ve?OnIt^Qi2n?EUSr(+|txFxz&=6_AF%WqN(& z={}aWmZXX0nY?T5O4)culTYFJ7eD5BuXac*DB9sIcPh|%5tqJS^fvYN?b)-Zw>afX zx6teAiN378yAOn3pI!%PxB<8FxE`bT0E6?zQjcC*1ZlqeOYI$Z_2gmP7UPaT8{VyZ z(zgA>VkPb`CsX5iDp7EKJ5@fhN_n{6vhh3scav2z7VWV#-t5pjD!lHSES=@jyS?by z-gP0tD7VM-dKiaMw}Jb;k^9{YJ^CDEGa64}=wb;E4rBUU)QGchNF=rlY!=)0Z6DgY zbKiCSBSYJVw(T1j8QD3qZ*<_=fsujLeb=w;*_gaxwLT9VgH6*T`g~$-Bh$t)YV`#u z($#J=9zx2wVu2pd@T&pF^OO>vzU$(xp=0~wj3-@A_vuX}X$76U;_ee+p595|n%7F_ z?d&1ZNf$=gg0RuJ|2rfWIfY{2gCaw(k0wNW;E(s5-{UzG`a*~b@D9ZurzaQ0rY}Mj zZddtW7x%%yfnHQ*^~F@LFTs}%dRvqFQe^pQh`x++N2sp9T(_BW4^Z9Z<+?4Ddywh| z%5__j!v!{uyi*tt(Gw574YfvJfv;E^-EEUY`GOCQ*^Y+8qBB)+{GyZDmBQN)7u8py V8qyDp=t1IaPnFTW3T2?(`#-8@&e0y&aINBrX-p7`Q9Jj^Vq7cy61*db-TJ%)9N~+ zZ+j}&UMys?`N>_mR61YBYG=BuP#3PW!I5F#DD2me7}AyOOc$1Rr}JU|C>+qxGO|yf zJ{{>oA=A}SpD!d;A)N{b&Tbii=1f;Ay(Ap8tYuYHlBratkjZr=v-K*S&lOcWObnjg zG8(eZTp?GV$>)pd`hrTfFHEa&$g-BPMl!9wGuOVbKDjtwpHHhr87kIkMYd(S7N}&t zpo;BWz@WI<-`bMu~oYaN6${w zrPA#=m85pK%Iw7GWU-K2kjoXia)orO^i;TNe5J%7c4#J*5648|Y7PFt#7NVdlS;ig z)*qA@p!;%le{f>eo}Qf4)05-;A&JrK$4SzUlS=)#hF_OhP1@Oslh9$4%AL5TKNMl1 z#p%NF{xGAX*}zS?11I>y6Kj>bZ<5u0J$BbBCI+|rnQRJu9IibttZ#5L3wZ*Tb>TY5 ztBFy#F7iqt5Xp)#)>4NdnmoC_RY*@u3`CD(ghJ2nn;2+7>$PkQ(3AwoJY*W5_QQ!L1o&C1&rl}QMge< zq92E2<9Ug}ZOBE%fuDp*s@P54d^mYtVo-ZFna|^R%Dlt?-MR^mfX$@BP5sr2iT>G4 zS2}FO01qymM&VSyufO)ZVw>9=`E)z8q%PbH-LZKTZqeY!5`BEo_1E*)PmD`1=?0<| z*3;;R_H?SXZE36U*fI*Y@(1}7^L|5OkTDZ(?Ssa&27mHAf66?6(ma3TdEqt<{+j;k zX@8S>;kFHaoj)jD^c&*?e1w!3Th%^@Y!`*w`>WGB262bVLCh4g>AG-71U)?pcS6u| z%FaU9PphBBQ+E2k&GpTtQ+8(%+MI+C>j0cr*>pIAk?ztfQbbsejBa;zJ8N!w2@p+Eh|4tuOe= zLVYru&0(flnk&|0qDm+88O$6qDdmcVdL&LEU*DF@V+z42&L`{MXVm=G$rkw`Wyj%c zcEFz1I$#ioh8?h1gWuG$?f)^RaBuX;oG5H2!T+ls7tTd*CZcd3Ivaz? ztM*7@Fek6NUt=+8>+b$+*BDm}I^WEe&@^gU=Jn zkHQRH@ao!RuCC4I>Kedhn?8|zIfEAVvTe?wvc)VSuV$tbOuC{l2RA-V`kE$Bh3+T} z>HSi8pSgf}zy6|heHZ3eRbOP9&!5i;cK-bOw)BFWN^dNuM&8dAvngC{lPR1w>AGo{ z_@>QwqOV_^DfpNja2{?p>^Iq! zs?Xr8EAkr5ldIGraz?3Y9WuPs#d)4l%*(3O!UFoG7=?@20X0$Z*j;%JWHW`dN@nZA z#puK(QMi;g2E$V+ZszpCmBGOnbMg?x)gNf~ZFnfch@$W?n2;=7g5lvTZ+wJ@BjcJ{ zCIG&|K?sk4og<_0cMRn>(S}yO+s||@nT#6^eIwYN?n>eQu5}4cx4Q86Aa_(09!-l| z!vFu{IETm3o4Ftivt5M8vY(cLEd23av!x&0AX~!a(zH0Ky29g@$!uXRRcB_Fxn~RJQhT zg|)!|FV3V2etaw~-UExA=8TE(5lnNm@@N$PrEKM~6<8V5W5q8iKOTiol$D=cf%3Y& zl`A^Q$F;34dOVtcK&CY$fqI2=} zMlG!iUxAIkM&YYv8-J^2;|1Dy&F4k^^(cG;;Fe*?ag)vUv-xG{O?3NTO+w4%^Y4|G z8slSV=`AF4h5p;f+Muk72mHmy|u4F3wx$@4W(3VRfX@0>d{5J}JEpb$Ra1UDjL~3O*5DScb z24VL(Gg*xm<@TL*ICH&Jxg z)w(UKfFG8@()GB7?9OGnaL=B~b){8|a4(=Eo1!Obt`|dbJ%XERnp_`ZuxvmPsSU(X zEHHi;l+6-nj*pYZXL4^S3L9w!O?G3cxM6s{B=;l{G7Tn+<`i3Vlj@oQoiGD8Wy$1j z#6hH{3fv?&76|k0N!(O+sLcrT0y?m}XLEtOaW%%r(exGso8-1kZHZ=d%T_2NwY3y=q;2f47?UUYZRx-BdBB)1jNjs$xF-Pqiwi<0ZZ zaNCLCrWyu!h7b&Apor8iVkQtSZdl=4yTo~>yx^b;rGZ*(D z+zaT)<~U2#TrY;>Y=Ud2_nr`gKVMfH$$IzFQ6xzVMg>^H-=$Aa8qrY3n2&3EQ&~Vilta!95Jj4o?U{> zX{6@aEfqI_9-blO!c&QEUhCFO^#b8uKu0#sqNurE49!IZ*G~1t5QF6s6p>mghGK#7 z!=P-IcB&sN3WsO~P4-Z!xM7%7k3`5+KTI?aw>6Kbt{Kn?lkrHFO!eR4AX2{%52G*^mprb42%Ctt+P+*6%az?p?}x z#Crif*&NRoJ=cxl_$Pw(DO}a&1(1X1g(xC*kywfa#u3A^dD_W+u_#=k6*S#TrQ*h6 zlKV0dGPy4o%`0rpE30b;bix$8iY1f#Y8*uB8iB`JhGQO|kt`(Z_sVpqx%#E9CD04# z!tSB#M8S=$F}^m9|C!*XD#osd09|tfib&lkCSrl{z(DM-krcm4kT+|j?vq=j;(GSV z%x;BTnB6A2w`*Pb`lV;_6!>+Q=-olI7toJQ?@rNjofvv|5v;$^Rk3(Cq~LfDib&ln zc4C2X!;mUC-Y3ZWHBxhYKq{{L9*z$}E*u{c-G{Yq?KFRccrTzQo8_aT=ejX0|3dHz z()=;V!Sitxk$OTb#RB7qVc9(GG=EYQp3(}M?$c6n<1lIdj0lTk|oKxewI5<@Kw^$OeZ_4u#ECI)Je zdKI-6{O>c#Sxn>V@;Z#Rvz(Hpzd9*tOOepUD3Df?%_hRF30IG=k+Dk%M!~#Q^R^S)@Mq~ zOzLY<`$ntj9RF4-ZU|mEQ@?{eFxmLMsQ*w__W~N!go086{z!4~+llud!I`fA36)6w z%qFB4@7m1_Cd5;1>KBOE|Nj+8Ww;EDo%SnB_J5zgw73#JVKESOFQ6N{|NDyAwPup6 zAB5=tD#7%J1cNXDMWhCbjj{{+bq1l7UxNfcSmWi-482JEzlfqPD75 z)44WADsCV>xwabQrCb{;>Z_O4y?_RtTpLI6|037c5S+=iHBpJwcs3!|YONDZfSg0I zmf$L5W_(PzwOMi~>TwXMbp##@MBNMM%N~k}B6h8rgj<*3+Fwp$5Mwwz6p>m_43+)R zxicK4tXp634I1B4k5kK87Tc&E&xI5+cs@=ZSxV>C1^|=$hN9#~>jfq58xcqD8{;5S zlLQ_MMBNMM*ycW2#I7}yhEoV``rn1O39#shO;JRuQLL7oG7_gFdh*h~+)owl&9t^o z&&{Rc252t#TZoXC`z=LtD_e8x>Y4$aFiEDNbg!{^urJdkPbQj6{x$%?{I)0}wH-c@ zo;tAYrBV@Cz({C*2b8~I-dxRh1QI@`qlnZ__~a?D8FFHmog3>W)lZ)_6Q8^6)Qr!W zU3d-<4}Ilv+?>XfcbUS{`X>IdjxBSO9eLcjF<3puiB%m!;!*8P08(j&_;M*U#v2&_ zE)=>1@I(V{KX!!(0XLzD)NW!VCV!Pjz-lJy2!d=(5dEp8c4q^e#sy!)=^g|%m2J&J z6-hN4MWpr=#aLh*(7Pm+8)FaUI<*%-h;(l;Fh?8E!`CboHw+Uu=+A|`H0>oseV?+r z7to+)SH8sVOYw@QJ&*m`A~-Yc?T1RF_Gc5uoDVi&wABHqv#So2_R1I;4^7Qu$y5Cx z97L*B;ITl|y@0muQ+>XOU2BbTi7H93?s!JfhAKL~9Yv&4qFA;|XU^GNn%2^SU!d`N z6nAP?9jKFzFYRssUQnWwA&ztc97JlNz+-`^djW0Rbh09Ltu@BiW6yLFZ1M;ZbfF45 zITVrV7R9n%I;%k^6ui=Sx%Vuee=3hwk}8PQ4WoxtkvNiCgo8*e7I-WWbuXX?o755! zyVe@x>yy+{g7v*;PaFrM5!4Pr5vfB(y=Yfy^>f0)M0ZI3gZsKX&6!6O9YhSWpw zNa9HFcR0Yxm%w9zsCxnZ*aVLfv1`ptIY$$$Z+qNmj)4d?k3|uwWn!f4gN|#^TrT+I zG+rL3;vA`tM+3Q?AObg<9&RTRM{a+>L8ML+cq|ZgFQ9Ro+sPt!tu@9s;NYD?uzsRT zLO2z55IPM-q)r#jvSE622&RZ_oeCfMs+?xUO@A9Z~RHXuItTP{$aAt zxht#LhG&B7o#n!+x>a!MDu(0*BxCpA)e@S^#Pr`a1ULPE4f|SzfYG`RMWp^L!Nvl^ zg`I1UmVJ+Ry(rwE74)FpC>58gxyQRnguKVQSu}64HE*r18PEyy>^7D>fp5n_r0x)S z>0YH--AS<*(6HV0cL~gmr!l?}P2Np#=@z9~-GeH+;a(Jxx=$2ifw4=cb~nf><^6(u zKqEE(2c_cr^U5q9f?QZUEV_?q-Cn0*UOh^X7tp-T38Gut{K;cpp`C!v`oL z^`R)n0%Mm>?QW0+{z#A?YozA?iBw#FUYW(GkPC}{itcAx*G}MUoBEs}EXT2sABOmR z{!IN#z^-k>kOcR(bt0fMk6(&Z>8dT?P2sC83%VJ6P-LZw9mGs38=40S(*T zQYSDsp2qkTnjA{7Nne;4hAO&YIEqM(5Jjwq5rZ{16m~aA`i>OjD2>#Ey|PqXe_olz zXvl@dDx$lp*0s~OQ>ifoc>&GaTvijXYuj)cOK`9Bg`w5a2u9;jL~0FDj|Ij$1E^rM zrXa^_q-HchDz4WaMr%PXjMf(2daYYCeb*rz3ui2>^qnYbt{1~?U4m<;Zwz9v^iW{o zObo>W2?}Rve^a)k=j<^`p>xfCcMLN+KxakpbMMR_M+h0 zH=K4LSpONfx3L`|0JG^Ru(~BCVuA6%Kq{E+EXWxeshRB}71whQvt1zK=skmNy z7neiJI%haLW^1JADffgJls#q!x*xSYZ4xD4V67zKca+ ziB`~LmrBJA!=&%QB4qj=BASQVno)JlfKHfehp}Y(9*zSnObI;JG7z)z9?8zOR8k#D zgcs1Z-7~)vq#IIWd~;g;J;6=3m7~yt&Nv!Hq>d5YSYS-kwcQz##K#J9nMUefSS}UU zrB~*19OS~~c+ovU>&nk5^z@n4j7}uZ3+TaS^asJa<_)8h2-ZK%=w<6%h~rVJd?v51A_NOVEUde(s|D)D)EM8AX0IVw-&&N+T#F`j z$8{*MP$SA%s3Fb^=-loON$2YYd4opkez;L8u1~Mb<|fF6&CQ~Fi`JEgx^xOIG@NcF z5bFReEKawHf@|M!x}9MA)}mzW4hX>PP85;4OH9N9Xcp~>2)jE`81&4(-<`V_y z2GkhejyBgNSpUgVY{XDUM|dc(WFwkbvLVO|=+^ECN#OMb*`SfS2R4w3>kv;bp`AWA zWJw+y;UH2Q3taz>Lf=Rr?;6!4qVV;fg~evFpk3pJ%@l(5?Z)`a6PmMkoC(?renV#sYnaP1V{ z7Gkj64n?H47eleY_+d~sOFM;k5QQDJf+jm%DsC7ig?ADmQ+Q|5oMCJ3Qe88k6At~Z zEIIT|IKXp70@sf<&q=59er7>I%_IgZH!Q5a*t~&Xg5xg*9xb24mj~0oISYTW*lnQPK2=YLU)ZFGt#dY1o?I6g7TdU~K z*Sa-RI!U+}(2-5BP1Ia3hG09vwNp9;F<7QiL~4N;iUr0GgR)uLDcvCozE;p=Gg5KG zFex2~kSV=TG_$s5XLZehPMCUKESb_d9N@_#f$J}MbMUgT^yX7lNC*~lSXg7P1nS1r z7@tnFd4lzqypov$n$R6Z6j;C!Wh~$j=LK|bcZVeOVnHs^NZk)hrQ-VZ%4`mXT-Y2U zx`%3A>q{PU_3C83TD3- z9=SsAJ&s8WQb+u^60%M%+EBIU^$ZIuH^SMqcuG1bqe}-K6TraveXx*Brdn4gq zKu0#Un?%j^VyN9raP8E+1!Azg6-A_O6GO4U_+d~sOFMOM7lk{tf+l;XRNOF3>fR+n zrtaOMd5^7mZ*|RpPMB!-v1IDrkAp}(AaMQlu6dVc^&rJqwqaq7_(KA7<7tfVLX!^@ ztiRsT#3QJp8y-c0Wg1b$G7T|!CRW>F{hx)!3GS7?F!Tc&!RSX6 zk@}CQ#{y%W0aP&hNsvElq-OMsR9vq;jQ$I`F#1(=``|^_{EKR>-!H8MIk?f0&8@Gf zxn2yne$=U*zWpHv%K<3xvo|pm3ydEIWwW%?caSIy)(V>J5UIFfnDng^A=7uLXb!VA zhga7O=!Cg8f+f>;Bn~1qO5i4a_ik1zQ;bC$7S@oD7ML4PV|*q}u0pU$Uzk`GRdmA` z6j-DYMJ&<~gGX{u*xexMJ64dZYoz8sPAaZHJVu0ex?F=LS*(cz{8&xk`kiFvk{{;P z1cLD0pM}L`Edjf>4VSeE?v=hURF6h5S_eg>CW<$zljU8>0@EcTm_Y z?ev`@3Y%yJO?FeMxM7&|Z4@EXcdBS^W@~OJX4f!;Ix$!i{<21Pq!6toSVp~+v4cno>0*xqQfrc17hJ(WH21(x?1i7O|YW~xu z;`;N-EOvriSnMpiGqkQf`xf$Q7lQCTpM}L`R{^`W4VNZ@d!;W7?S@7$nu#J(yNh}( zFxDAB1*5ni_s~eqXqHr5uRV-rLoST=6y3eFZq4-Fn{Y3nBb(bCQFFZ*Zp{SOPT#o@ zgJl9mr1lX*vB3CYP&P|DefJdwzFD@sw7;KJ+%Qb~?k_^7?*XEDpshKtx@JHp%(a79 zGJRWd5UKeBH|e|Yt}00}7H3#kL*6DZH=f4$EShX5*rYE^q)LJpo0ibx$MmSTZ%#IS6hCV^4l-I9ETq&i#>Y#3OOSH`L%N;>#m z65oNYjzk02ZSbixz5LpLc@w(&y$Gz})lq2i$MFTd>S!JgmUr|vn|IZ!V?<#`SUwg< z_$`>!W72EWdv+C>Q`%KTEvFGLU>v5`_?mQeoakAO&6^?1j*h2-?zyUVPk!6UHEB%7>Btf05QOFKEp7Jk9PocQy7o;0jc{@m=)D5Q!*8Ga}G_iiV zwk|J`o-glIZgSXxazSt)3{Lgn6v*~u(1w>syyOlcts0-QNBpU}*H?J-N0^@x# zib!38Pm|99Iq1_w54jnXO9gnD28v-lC^Kbeo4I;toB6GE)d%Bp(W~gb@?cy6xeUgY z;^r#jMmAUw0*)JXH5I&oQQ53WK- z9U3abzz})AIYd)r%bL8pfu^gB)QxP{FI1}Zpl*T`qjWQhNZo?Z(kPLFHA=EAuQy^*x5Er~udpYZ4Vb}qi2eP2Za$&zM8zMU zv^Swpcd>1#%ycd06gL6h4N0W#5d*Q7?M#4`%yodZGv9P;P&3AR#l`-C>34M>Lt3ME znD?`7h=jRc?=T;LBvKEGjabVxJIsn%=(j?x9pgh{fB(Ro#p+?kGog2kkFaf+#JGPs zM)w^7*b1aOSL{mFsYd}v9{dGGq#na(DPejp7}&qaz}4e4jsbl_fO6WXCvlt@S;(~) zR60FrG4_z@#_s~2LaPLZ6qa3V%BSwrkhHs-Dj4-T^$etl=UEhydJdnZco?BRbFp)g zcBYkxWwSDVrLt2F{NHaJ>DlVJk!;lEvC|bCY4Hesh7~Y9B++25ie9vc5{;< z^)i}fC;E3}b8YGs952&nT%u04VB`*gd4R6B~=EHw5se0Qg!T0d!*13%&yxPg{xjEfMo& z<08h-j$5%6mwH=-|G;NDG0?Dl2eoC1VTSsGOctTOi#Gi54WA7G0}ALgpy_}n(uu*H z$t9_DcfnWhqaN?eH*p<^DNtq%%oY5{7k?y%Vk@n#JYQUjG5!G9*@@xheY`LgSUz5yII_L$JS%v%k|coVOG;ODRJq|)UVSBie-khS z+hxehTl4B`Y5fMDJmC_PL$*o139mK%Oy_SKtun4Q6}zZ7=8Le|O-T-blBKX(vG^q)C< z*FDwG(B<|&>K7cy#q_^8h}5rQ)4b-T4<0YC_KsV|Lfw@h$K_}vq~>lgt-yIVg$7V&NxH4^fi8ih~1L7mvd=u!%{ zavL>vQeKUQ1aYnce>s)Hrh44=1zVbNa#;nk*@`{D zVsa*3STH%KI;QxA&g>L5hL9o1wf3}Z*R~q!%dkmby1Qq?w7eQC#z$bAz)W{5=5}rp zmshJx>u}7iR*d7&=)ap7o=tXj6tTMy;Fz=4Kx?8erqnfgFa$4&#I9e-&TchcYz)&w z(<&*cCZH{jvjPOMgJ73_;qh7mtn-suyj+l(tJWrLKw`LwN^dEr>WTAb%ubBQK<88| zx+f>Q@93(JEmN{MTnDhkC{oTt(T9Khlq;S>(WwUR2r8ECdyLGi+(y? zP%*SGPfWm{?z@YHTJ}8}8pzOam0ORM;oM*j7o655oD9|aXv0{JOcgu3Tg&HqUNxW{ zQzT|>Y=V|E;0-i*1h?+tPC~Go#!qcF6zmud7N>2Fc`K#?P(hY&1Yu$@WRtv2HcP3E zSzS53BvWA0V}}LPi<4-2Kw@Bfu4_T2LroTwGSbNDMH$)nQB9H7F?iiKAOFUEhXhLYkFmKu{l(4X^5A2bS+d{2zUg~g7V~%S6fQ! zNN2>9n>+DdKB<2qxC=gu^3Dbr;Svf75QA)+!VkM7!*^^vWqOrAT(AlIc6 zw-enVm|waPMP`ZGp0GZN3GF^M&1(lU-T7e28NanNsTLNy)eb-{$H|y%Yn3fs)Q)I~ z4^E8eoylrCtqn-b-4$`~#N$C`-%PbLYS{8n_Q1qOe>_#u`vo-v?bz_CBaK(iCb5&J z+64`>6C)(~db)a7X&shFJcZVC+6cP}mY z*f5UsN-Di*3efo}Y9?X0qIMOMOIrO*ho8m&g4&(U{j)i|r$5fa@xA-FwfBj-2Oxo* zKqELkxAt`EEGqZSq||I4jKCRVC&!*>T%K4{PY!9lNogMaTJT%V?vrXSKya#=QIk_B zul7dcyu@m9K4rRa-zEE(rn@-4DK!VuW!L~KxfmzDZfZsoFCNmD&#sHLQW+r~%tVZtjoM--6b{_DE}@Iu@Ip=IL*$y&s(0EcV|~~tDO@@ z_GXK{r7DkilX_!}xl`BG7>3@5JY8efrShVoH3sk5A3BP#bqrF;q8i-X9SG$wL_x<+=xQQIzS2)2WNYHQYXmRz{`VKn2qhPD4+v8Ha13rxc1zb)a~P+cbu*KPHI_dS1PdW_Luud59~&rE}UG!y=o9chAah4qv!I9B$~3bpyfRVIW(=jS$Di zahOCLT`ln2-k$6h!?PKbzn;H-dTc4vFP1h*kI`!3^mI$6R8sk#atU3vX&i3WnqEDh zjl#_rr^oeRP!UHIZh=a+JVf0n+;VYxY;PeGML2FMeUp~q40O@BUO!*Rf?wFa7^CQKk{%oB4h?7eh+^ATf6K*w>tcV4#s21t z!|htr9=mG>Q)&hVXht=qX8GIX{H+#;+qe2H{@7gEZ%d8#>0<4M9f|mMh{GNIZTyWR zzkPAIQ)^v+=1YZKOSm(-bC)>WmEFmdo)7o4lk=I@ujZzJmti2uQ;58kbJt`rQDM#_qdjju$g|98S-1g zy}@ChIGjrk3&3GcvXJjl8MQK5@-wAmrcfy2Ok7zkCvhU@GEu&;QlGTNaw&;?C`HMh zOq9zek>}k@NPGBMyQJMBKfdZX+?O4&U!x8f%b{Th?BD8lbnX7Xj43<-Ju)v24Ezu>-r>PALpGS`4$0Kg`q1<= z=JN1P4Hsl2ThK`h(MOzU>w7+21m@{DJPcvVtdo*l0Dd*#+aWwW4!g)f>Y7{5&^say zkEE!bP|Onb=4U7UQfX-R%$W$UJu{Fwwm8_{TO63_Ypy2WHQ%FJ(__#ZWt9()mN~qM zDo*3YoLaUEkIC?~MkjUqx;OO6(PDA~QF2aQ5{H=zyNW}Z8+wK%R@j@KVlHGeDi^XW zdoaxTV=_GHCRK8-uhr*S-5-ZO>GKk}zqtf1G?&0UdTNP2n|(QrgJI6hX|4Z28VMj<9LVGc(R-Wo@|>Ok9b9S)`QChKmAPu;W`J`79h_<1 zQ=Y}saSF%eZ1<{_ALN}vOLz`?;oLYp553@@!l1ut#x*>@H9d+w6<*-4%IW(8IUPO! zBwD->!?rF+H9aT7i!fvt$KkgsLw~Lw`Z3ydcu8dj-GGDtC1*=`DGXf}hnI7tz6z{| z-e=}s5r-rM5vc7);6#Z59GZlaVjJ-j`;12Nwjhj&$a!^DGIFv`QbnL7702&=Oa)xC9w z`&F?#)DqqY%lF6O1KKjoe799+dM&p{^%e_JYV|ULypy^2VBJk50l1BC2_J&BhvV>( zs~K36sHlST%fWas|0 zu5*!(YGq6KGuZff9R8weaQSZd_CEVXi zxNpYcTMU=qeCOtRiz9~mL`boV#6Y8H3Y7;AeM$CVS z!@n|STKve4Ifp)89T^pVidObFZjLh9@U!}xh*D26i=_D+qwu#l{Ckz-Kk8#LnfuRH zpK1C<9R90{{&yqv-&b(mb;>$R+EbLUDjN)zJQJz2_ZXDt&W3Ojnax; z6VRP=4w;wSKU2yr&#Y9V3G@8_T3xBe2-daRmRg5)$5NrA&dxYApK_uak0MqrVjvM{ z;{hYEJCb#yXiNi)*#jk z=)&eTO>|uQhS!<|cQm%Q7GxloKoKiXtRw>CgyGbYTU(IpXrv~$u2fv-hTM7(3c2+~ zaRaT`wI;~n+{`6$GdEPs5901QTO7=(4GH%Gda?O!Bxe<*ivV zr?+{lIgt=2eDct@I=>SB*H$0VmXVi>;oAt468JeUO*pq z4;>~NZfI?(4QTyvf;$>n<2nvH=Li(BI#O&T0^@?A*qtLuev}}O)=1qei>2ba_Nv^D zfn2zCi|!JwEB^-2(|7N1h8g0$fSzoIJ)-BjF${YN?r3f^3pse^P{gWFEF}Wth+)<7 z>=&f3k(y^-DsBKZo&n^-bE)VSv~JT>A0XTd=*XryC~B@3L$gS5^Hd*#7%W2+u~K3v z5g0!V%4TV&dL#-Zt)R)4rQ(KRQhk{Snd-|$bA_$Bvax1BCrrj;Su)j+!$GXRCUE_~ z1xfYgnNqK>Vxqi&_U+y|UeIn>ZK;iD`RfFC$bTs;ODCWeopK_ISbanE6M-?$5bREo z6hBFjCu^kcjZ>uJy7j7@PK8`JohG`gw61kWAt|nzoldM5(2LFN4AF627-nY@Y~P_+ z_RfL~{LV%Zt8>IkA}~%EP948<1$mxEYJT68itD__?|jIG-vy$3q1Kht4J-NiUiU8L zBI3P(o@|a6i=OMoaQqg*`V?+x^AgCx^HLPCx=bu30^^8b**xv!zFZWp&@9=CzGA13F;}UdNKjeLW6hb%VeYU6U}6&&`xF$wTr(Ij%^l8wvCR zy0ClbCQ)!BYfB|*{M!U~G%$8E1n8PuP{isxVj>Y34-CZa8cFe61$moB>OQ$$Dz0a* z%Iprvh1s2=dzaRgZ&!L2&w}4jh2Gsndjb8}^zIQY*NLHbFTwizS_6ysK?;udqlnc5 zVkZ$8Hw>wc<97x5phjwr4@t#!U*q^N-?G~Fkq;>Kap{Kq0>nm;9)PurT$G}a90 zgqip(N_PRKr(7uHO5KHgRBBPr0RYoKK@qE;;!}Tf^#VH6eU+YIX{evE9bW{0PHcLN z7O7vL##@vTGn}v}672YUK7OP<;<;~-YQ5qKgHbuXX;I|p77v1`o){VKs7!^3zDZ5a30 zQN-#E(XJY&$I7^;$5+{@-%5L6IKC;WZ)sJ%LSs12Qg5S`B;OIK8vuBv+h4QfI`B?13Jaq!iN_gBH0uKyI3 zSbfGOq!;hn%?u{Qvpwo_h}i#s6G(ly42_-kcb4q`f8Zci{}gy45Opu08@vC%5V32` zB-y_R);Il5F#m=GgYX{|vHGvrsJftEXAmm+HDV<3tKpL*x%5BW?kqJDGO}DWWIIaTZbiwVqF}>YCVA`0#Ww@`m%>&eG$9XOu}tIaPu!G8$yiX*a$_e zHWouwKXmR4M8S(TSt@mb1&oB|cR~3T=FQc7S0LeIHi}s7hEJXXJ0K_a-Dg(IjO3j8^YFRv z-V5ka$$P6M$6OLwvau z+EQCF{tgOV0(iCnw;y{#gn;)#5vw_3Bq9GKN5E=t)DZ;Pm>@<`OYOr3IGro_CQkPy zsH19YKU9%a`=f}}0iu`)j01X?q;g}dQEpN5079e(ih-0ipoi}uskmX7xIuqD>(6?`LwqM`}I)4nQ3nUDzRF~CX6|sGr(x8MX0l@($ZcZBjcf| z!&vfEKO6_K>JoS&5Opu0ZTnO|Ld34Mwvd< z){YT;x5n#H+g`3o3Mah$Ed|9K<1U{iuSDk0MriQLNgfvnF%`!7tT#x%aG|f2x31k{S@H8%B-PAaNvB#6hfv1fB>) z-3#c!CKZa%9p?sfv9@{{n!L!5xdsRlyf}6`nJc7=IaoF<_Rcbb)p!l`k><` zG`}JElQdo)Y~mcLPDTT{ogxA^ni{uLi6ghua1g6i0#5{@?gcb%b30wcuC=z*)*QSu z2-eS5NeE}64nk+4h}GGmSv5>=4k3NLOt;h~H+7Dnj9^MlAXhGRuC`%)&o~b)vHB)H ztJkperQ$~HRWFejKrZ|*6y1xouAB+_zBJc6OI=Km7tp-j8{ZPJYr8GAIeX(0f^{m_ z3|)#wbjf8XVs*KwCjw)g0aUuA`pxMIL0+kmdRo6qDz4WWqpKkoM%Re$wOY4pdHo$2 z|1sI(+?6$L!_&b@XSuMUZUdaUjv;vg$=LmOy@cj6G5vP~!5#l!!@dzAV6<*R5vy-Y zu!+EMVdvVTW#8l7EDE=11wClrk%~*z+~eIULf+%uCYra~ns+qT4CsV;b|*`oz<1#w zR(A`$a<8&b-9xb#(6HV0_X^C7r!Cb+llKu^xkXv1?nf2f@BoTfeOD9{fw4=cb~nf> z<%5EJNFz1>ho$2B^QtT!fm~QTD!Pwp-C?I;q#h^83uxZv@;w2&whfmj2p)bKM(X=$ z1fw6Ih}928JrNk|44{tDj|BOoMruYsmWu1O#^@=?h0)WZ`;69Yz9#uB@m@erHo50S z&vj$S{e<8zS(E%J83cu6}n(nWp;>KYT_<0dBfnN~K z7j4a#8fyl0!fg9BOD6EkIKcXxz)b=lw6}VNVlSX!yI)=vm>W-9s+}fZBiJM`OuUXN zy5S8JvHGnjCIVxZPVH`x1b$PHZ)v3F|F%?Ie_oZvJCF;DcSZL-t!pQ6p+~(>5SHUu z$WKIkK3=ANCt%mM;qn2&!x9*VK13rJ{T>CD<3v3X80!q6j?qVg{G&!{MjuPX^;%=} z3FN})Pon#0t=n|r;xB|_WsZf#?XRNddNJHSCAj&@!)FkK<>x4{0w;zNf$_tjY?gNV z{#_LQp%paQe@ev-!=&#QB4qmhOEmv&YyPLPWH>4)X-jQOlcT6((ibL1ql#`AgCbUAMG@;^#9+-0h20I3zT*TrUL!UC z7OA-Yyef+ckPC~6qB}|J+UYx>)MSFZfaYy3Qv~eVHe99>JS=@-Xbm)i(KHmXT2s^$ zfw9g2>KLsh$b?2}MxIn$uQf(%LoSTg5#4pQZqxK#k8muUv9Qv2eNl707;YO7+&q0Z zgcvM0LV<-dF_Z|59|mQ!w9_{!3Y%yJO?J9e+%Qb~ZYn~i?`ER8xvjZHW6gk0m}^_I zWcs$^AXZxmJkd24cZ>TMGQIhnQd<+^1vGAV%{BscV`@vyqS-crJBFE=fhKfEJBnD% z6y-!fBGM9uYL_{}A_c{=Y4F<9=0B3Ap0A*@tU#|s#g&C*Wi14Ln- zR?uV*l!_aMN#~RZna&4^=6qYTv$1ACCrp5YSuz0*!9lDJ6}Y~cm^Wv>T0pTE(6HSt z3kBxJ)0WzSCKnN`XJeX3ql#`g3`MLC7sW(i?9!>-4U)!OB#PA$8majoDHYeBS7mV& z(@+MDVAO*mR=uL02#j?GP{$}M z$ec!MMtxFoz1A4@LoSSb(ame!rs*3H?geyYb6YBEt{20tKydT)9e@}t2T{bTD25V& z@x!2OmUj9Mi9)CqG+8ATHw=@$kqDW-CDAO~n#&q%26Vz)Th5Z{y8;JTm=buRYYb-N zgE9j>*^D}t2rr;*yJwCQq#IIOYA0I#8o?d5l^8APjN?(n>g%GL2#jgEwmU&mYs^z>QKj7}xa3+TaSbeiB@^M=tXg7q&ihS@qD z?cj9=iddZ~77~GRz%c4~oh8V#HB$3BM=Gx48n1I97hdOy?l-k=^DlPi6YmA|Wb?Z~ z^jtTF--QH!$rrneAP3KjQN-$7Vkr?AM-0p6X(#g~qHw8J&~z`8iW`SX=F3IMWWGW) zue3Fs5evQ=3 z9*~ObxyJ0fkPEX1MfV}C+jMSzm~bzkBb(nNqUL%r{2nE^`P}>%#9;Y2idcP53?%~N zhe6pa?R0)Z6uz$&G}#|W#SO!x^AAPHbpDZOK51+IxUptHC(OI2STdcT#zCx}5xBm! zNX=iUo~0P;H!Q4Se@{1jDm!_QD)xkePRTtkc((5c-GlE%Lf z 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 diff --git a/docs/_build/doctrees/other_langs.doctree b/docs/_build/doctrees/other_langs.doctree index 7070542cb3c7599e4ecafe69d6ef8c90f307c965..eb8fa04730391ca1ebedd044f845d2a5e9bab295 100644 GIT binary patch delta 48 qcmdm_xkhsX3k&0l$*e5(%z6qQle<_n7_BC6X0b;X+q{rfkRJe5NewLk delta 54 tcmZ3Zxk+;a3k&1w$sbti*%T7Kx3g$4>P_CjVvj|5^K4c@egOQJ5pn` guide to get familiar +:doc:`Getting Started with Moto ` guide to get familiar with ``moto`` and its usage. Currently implemented Services: +------------------------------- +-----------------------+---------------------+-----------------------------------+ | Service Name | Decorator | Development Status | @@ -79,11 +78,6 @@ Currently implemented Services: +-----------------------+---------------------+-----------------------------------+ -Moto APIs ---------- -some stuff - - Additional Resources -------------------- @@ -91,6 +85,7 @@ Additional Resources * `Moto Source Repository`_ * `Moto Issue Tracker`_ +.. _AWS infrastructure: http://aws.amazon.com/ .. _Moto Issue Tracker: https://github.com/spulec/moto/issues .. _Moto Source Repository: https://github.com/spulec/moto @@ -99,7 +94,7 @@ Additional Resources :hidden: :glob: - index - getting_started - other_langs - moto_apis + docs/getting_started + docs/server_mode + docs/moto_apis + docs/ec2_tut diff --git a/docs/_build/html/_sources/other_langs.rst.txt b/docs/_build/html/_sources/other_langs.rst.txt index 6fb617c39..664ce50b1 100644 --- a/docs/_build/html/_sources/other_langs.rst.txt +++ b/docs/_build/html/_sources/other_langs.rst.txt @@ -4,7 +4,7 @@ Other languages =============== -You don't need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server and here are some examples in other languages. +You don't need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server. Here are some examples in other languages: * `Java`_ * `Ruby`_ diff --git a/docs/_build/html/docs/ec2_tut.html b/docs/_build/html/docs/ec2_tut.html new file mode 100644 index 000000000..63b3adc24 --- /dev/null +++ b/docs/_build/html/docs/ec2_tut.html @@ -0,0 +1,306 @@ + + + + + + + + + + + Use Moto as EC2 backend — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Use Moto as EC2 backend

    +

    This tutorial explains moto.ec2‘s features and how to use it. This +tutorial assumes that you have already downloaded and installed boto and moto. +Before all code examples the following snippet is launched:

    +
    >>> import boto.ec2, moto
    +>>> mock_ec2 = moto.mock_ec2()
    +>>> mock_ec2.start()
    +>>> conn = boto.ec2.connect_to_region("eu-west-1")
    +
    +
    +
    +

    Launching instances

    +

    After mock is started, the behavior is the same than previously:

    +
    >>> reservation = conn.run_instances('ami-f00ba4')
    +>>> reservation.instances[0]
    +Instance:i-91dd2f32
    +
    +
    +

    Moto set static or generate random object’s attributes:

    +
    >>> vars(reservation.instances[0])
    +{'_in_monitoring_element': False,
    + '_placement': None,
    + '_previous_state': None,
    + '_state': pending(0),
    + 'ami_launch_index': u'0',
    + 'architecture': u'x86_64',
    + 'block_device_mapping': None,
    + 'client_token': '',
    + 'connection': EC2Connection:ec2.eu-west-1.amazonaws.com,
    + 'dns_name': u'ec2-54.214.135.84.compute-1.amazonaws.com',
    + 'ebs_optimized': False,
    + 'eventsSet': None,
    + 'group_name': None,
    + 'groups': [],
    + 'hypervisor': u'xen',
    + 'id': u'i-91dd2f32',
    + 'image_id': u'f00ba4',
    + 'instance_profile': None,
    + 'instance_type': u'm1.small',
    + 'interfaces': [NetworkInterface:eni-ed65f870],
    + 'ip_address': u'54.214.135.84',
    + 'item': u'\n        ',
    + 'kernel': u'None',
    + 'key_name': u'None',
    + 'launch_time': u'2015-07-27T05:59:57Z',
    + 'monitored': True,
    + 'monitoring': u'\n          ',
    + 'monitoring_state': u'enabled',
    + 'persistent': False,
    + 'platform': None,
    + 'private_dns_name': u'ip-10.136.187.180.ec2.internal',
    + 'private_ip_address': u'10.136.187.180',
    + 'product_codes': [],
    + 'public_dns_name': u'ec2-54.214.135.84.compute-1.amazonaws.com',
    + 'ramdisk': None,
    + 'reason': '',
    + 'region': RegionInfo:eu-west-1,
    + 'requester_id': None,
    + 'root_device_name': None,
    + 'root_device_type': None,
    + 'sourceDestCheck': u'true',
    + 'spot_instance_request_id': None,
    + 'state_reason': None,
    + 'subnet_id': None,
    + 'tags': {},
    + 'virtualization_type': u'paravirtual',
    + 'vpc_id': None}
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/docs/getting_started.html b/docs/_build/html/docs/getting_started.html new file mode 100644 index 000000000..5ab53fe72 --- /dev/null +++ b/docs/_build/html/docs/getting_started.html @@ -0,0 +1,343 @@ + + + + + + + + + + + Getting Started with Moto — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Getting Started with Moto

    +
    +

    Installing Moto

    +

    You can use pip to install the latest released version of moto:

    +
    pip install moto
    +
    +
    +

    If you want to install moto from source:

    +
    git clone git://github.com/spulec/moto.git
    +cd moto
    +python setup.py install
    +
    +
    +
    +
    +

    Moto usage

    +

    For example we have the following code we want to test:

    +
    import boto
    +from boto.s3.key import Key
    +
    +class MyModel(object):
    +    def __init__(self, name, value):
    +        self.name = name
    +        self.value = value
    +
    +    def save(self):
    +        conn = boto.connect_s3()
    +        bucket = conn.get_bucket('mybucket')
    +        k = Key(bucket)
    +        k.key = self.name
    +        k.set_contents_from_string(self.value)
    +
    +
    +

    There are several method to do this, just keep in mind Moto creates a full blank environment.

    +
    +

    Decorator

    +

    With a decorator wrapping all the calls to S3 are automatically mocked out.

    +
    import boto
    +from moto import mock_s3
    +from mymodule import MyModel
    +
    +@mock_s3
    +def test_my_model_save():
    +    conn = boto.connect_s3()
    +    # We need to create the bucket since this is all in Moto's 'virtual' AWS account
    +    conn.create_bucket('mybucket')
    +
    +    model_instance = MyModel('steve', 'is awesome')
    +    model_instance.save()
    +
    +    assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
    +
    +
    +
    +
    +

    Context manager

    +

    Same as decorator, every call inside with statement are mocked out.

    +
    def test_my_model_save():
    +    with mock_s3():
    +        conn = boto.connect_s3()
    +        conn.create_bucket('mybucket')
    +
    +        model_instance = MyModel('steve', 'is awesome')
    +        model_instance.save()
    +
    +        assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
    +
    +
    +
    +
    +

    Raw

    +

    You can also start and stop manually the mocking.

    +
    def test_my_model_save():
    +    mock = mock_s3()
    +    mock.start()
    +
    +    conn = boto.connect_s3()
    +    conn.create_bucket('mybucket')
    +
    +    model_instance = MyModel('steve', 'is awesome')
    +    model_instance.save()
    +
    +    assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
    +
    +    mock.stop()
    +
    +
    +
    +
    +

    Stand-alone server mode

    +

    Moto comes with a stand-alone server allowing you to mock out an AWS HTTP endpoint. It is very useful to test even if you don’t use Python.

    +
    $ moto_server ec2 -p3000
    + * Running on http://127.0.0.1:3000/
    +
    +
    +

    This method isn’t encouraged if you’re using boto, best is to use decorator method.

    +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/docs/moto_apis.html b/docs/_build/html/docs/moto_apis.html new file mode 100644 index 000000000..690a8069e --- /dev/null +++ b/docs/_build/html/docs/moto_apis.html @@ -0,0 +1,256 @@ + + + + + + + + + + + Moto APIs — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Moto APIs

    +

    Moto provides some internal APIs to view and change the state of the backends.

    +
    +

    Reset API

    +

    This API resets the state of all of the backends. Send an HTTP POST to reset:

    +
    requests.post("http://motoapi.amazonaws.com/moto-api/reset")
    +
    +
    +
    +
    +

    Dashboard

    +

    Moto comes with a dashboard to view the current state of the system:

    +
    http://localhost:5000/moto-api/
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/docs/other_langs.html b/docs/_build/html/docs/other_langs.html new file mode 100644 index 000000000..3b5a91b53 --- /dev/null +++ b/docs/_build/html/docs/other_langs.html @@ -0,0 +1,243 @@ + + + + + + + + + + + Other languages — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Other languages

    +

    You don’t need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server. Here are some examples in other languages:

    + +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/docs/server_mode.html b/docs/_build/html/docs/server_mode.html new file mode 100644 index 000000000..c226df021 --- /dev/null +++ b/docs/_build/html/docs/server_mode.html @@ -0,0 +1,283 @@ + + + + + + + + + + + Server mode — Moto 0.4.10 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Server mode

    +

    Moto has a stand-alone server mode. This allows you to utilize +the backend structure of Moto even if you don’t use Python.

    +

    It uses flask, which isn’t a default dependency. You can install the +server ‘extra’ package with:

    +
    pip install moto[server]
    +
    +
    +

    You can then start it running a service:

    +
    $ moto_server ec2
    +
    +
    +

    You can also pass the port:

    +
    $ moto_server ec2 -p3000
    + * Running on http://127.0.0.1:3000/
    +
    +
    +

    If you want to be able to use the server externally you can pass an IP +address to bind to as a hostname or allow any of your external +interfaces with 0.0.0.0:

    +
    $ moto_server ec2 -H 0.0.0.0
    + * Running on http://0.0.0.0:5000/
    +
    +
    +

    Please be aware this might allow other network users to access your +server.

    +

    Then go to localhost to see a list of running instances (it will be empty since you haven’t added any yet).

    +

    If you want to use boto3 with this, you can pass an endpoint_url to the resource

    +
    boto3.resource(
    +    service_name='s3',
    +    region_name='us-west-1',
    +    endpoint_url='http://localhost:5000',
    +)
    +
    +
    +
    +

    Other languages

    +

    You don’t need to use Python to use Moto; it can be used with any language. Here are some examples to run it with other languages:

    + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html index 1a7b34098..d0445ccd1 100644 --- a/docs/_build/html/genindex.html +++ b/docs/_build/html/genindex.html @@ -90,10 +90,10 @@ diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html index cab7668b9..9b6474caf 100644 --- a/docs/_build/html/index.html +++ b/docs/_build/html/index.html @@ -36,7 +36,7 @@ href="genindex.html"/> - + @@ -89,21 +89,11 @@ -
      -
    • Moto: Mock AWS Services -
    • -
    • Getting Started with Moto
    • -
    • Other languages
    • -
    • Moto APIs
    • + @@ -170,13 +160,15 @@

      Moto: Mock AWS Services

      A library that allows you to easily mock out tests based on -AWS infrastructure.

      +AWS infrastructure.

      Getting Started

      If you’ve never used moto before, you should read the -Getting Started with Moto guide to get familiar +Getting Started with Moto guide to get familiar with moto and its usage.

      -

      Currently implemented Services:

      +
      +
      +

      Currently implemented Services:

      @@ -321,10 +313,6 @@ all endpoints done
      -
      -

      Moto APIs

      -

      some stuff

      -

      Additional Resources

        @@ -346,7 +334,7 @@ all endpoints done diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv index 157697e1afd26af9b70476a677479fd9a0902829..9d86687f9f2ad7b680f6b17a3f9d25a5f6533ab2 100644 GIT binary patch delta 286 zcmV+(0pb4i0*(Wad4E#DZo?o9z4H~8*p*G|vA1fPrXD6$il!YS3RV&|300u&_V)`A z7D&hi!L#49dA3wSzo6Ka=4L<}xtB@+dj%Qc&$a1V_|gcU$|&F(-rsg`=Hi0ABrdZ; zW17Aywc&$7xl&rhz2b)lp_oXI#?-Ea*X+hy$*+xGsr^U1j(_Xwa5`$w(>UC5C_5!_ z#f)N}TQ~qLKw=wOe#SOy1RWpDrfnHgVc+x;e^Q-*`m#rRB{|p9!a3jQ1hPQe_HpiG zG9hZgChW&kT;T9sI3pT?nq+#PS@da>H(HpXUVm8#gtGG1;$%QN;`Hn3kg0t#mpYGI kBOgSaay=G?>lflDULjd-MOtjR7t{mHrN=w+2jL`A7MQ2+n{ delta 259 zcmV+e0sQ`s1M~urd4G>fYr`-Qgzx?p3+`2g=9pV3rQ}c=Fr`PMNQ*5ZOTk+A@$V~H ze#mNa5z_2@GbKE$Cf$`9}eNok_GPI<#`8ZE-q+ffeEMi8FfyU z{=tIdUP6OL8HqpJh*^#8$8hydB4wJ|5$_3Qx@$Iy)N2%>ntc4MCIRX*J1IxBCW*Izad=2|&uoPOH*teRhyOLttvbx$~)jxKXA z6|w~cdO83A diff --git a/docs/_build/html/other_langs.html b/docs/_build/html/other_langs.html index 8ec0b7210..c329485c5 100644 --- a/docs/_build/html/other_langs.html +++ b/docs/_build/html/other_langs.html @@ -91,6 +91,16 @@
          +
        • Moto: Mock AWS Services +
        • Getting Started with Moto
        • Other languages
        • Moto APIs
        • @@ -159,7 +169,7 @@

          Other languages

          -

          You don’t need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server and here are some examples in other languages.

          +

          You don’t need to use Python to use Moto; it can be used with any language. To use it with another language, run moto_server. Here are some examples in other languages:

          • Java
          • Ruby
          • diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html index 311903b7e..f78097c23 100644 --- a/docs/_build/html/search.html +++ b/docs/_build/html/search.html @@ -89,10 +89,10 @@ diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js index f66b2d15b..73a7045f9 100644 --- a/docs/_build/html/searchindex.js +++ b/docs/_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["ec2_tut","getting_started","index","moto_apis","other_langs"],envversion:50,filenames:["ec2_tut.rst","getting_started.rst","index.rst","moto_apis.rst","other_langs.rst"],objects:{},objnames:{},objtypes:{},terms:{"27t05":0,"57z":0,"91dd2f32":0,"class":1,"import":[0,1],"static":0,"true":0,"var":0,AWS:1,EBS:2,ECS:2,For:1,KMS:2,RDS:2,SES:2,SNS:2,SQS:2,STS:2,There:1,With:1,__init__:1,_in_monitoring_el:0,_placement:0,_previous_st:0,_state:0,access:[],account:1,administr:[],after:0,all:[0,1,2,3],allow:[1,2],alreadi:0,also:1,amazonaw:[0,3],ami:[0,2],ami_launch_index:0,analyt:[],ani:4,anoth:4,api:[],applic:[],architectur:0,assert:1,assum:0,attribut:0,auto:[],automat:1,autosc:2,awesom:1,backend:3,base:2,basic:2,befor:[0,2],behavior:0,best:1,blank:1,block:[],block_device_map:0,bodi:[],boto:[0,1],bucket:1,call:1,can:[1,4],cell:[],chang:3,client_token:0,clone:1,cloud:[],cloudform:2,cloudwatch:2,code:[0,1],column:[],com:[0,1,3],come:[1,3],comput:0,conn:[0,1],connect:0,connect_s3:1,connect_to_region:0,contain:[],content:[],core:2,creat:1,create_bucket:1,current:[2,3],data:2,databas:[],decor:2,def:1,deliveri:[],deploy:[],develop:2,dns_name:0,don:[1,4],done:2,download:0,dynamodb2:2,dynamodb:2,easili:2,ebs_optim:0,ec2:[1,2],ec2connect:0,ed65f870:0,elast:[],elb:2,emr:2,enabl:0,encourag:1,endpoint:[1,2],eni:0,environ:1,even:1,eventsset:0,everi:1,exampl:[0,1,4],explain:0,f00ba4:0,fals:0,familiar:2,featur:0,follow:[0,1],from:1,full:1,gatewai:2,gener:0,get_bucket:1,get_contents_as_str:1,get_kei:1,git:1,github:1,glacier:2,grid:[],group:[0,2],group_nam:0,guid:2,have:[0,1],header:[],here:4,how:0,http:[1,3],hypervisor:0,iam:2,ident:[],image_id:0,implement:2,index:2,infrastructur:2,insid:1,instal:0,instanc:2,instance_profil:0,instance_typ:0,interfac:0,intern:[0,3],ip_address:0,isn:1,issu:2,item:0,its:2,java:4,javascript:4,just:1,keep:1,kei:1,kernel:0,key_nam:0,kinesi:2,lambda:2,languag:[],latest:1,launch_tim:0,librari:2,localhost:3,mai:[],manag:[],manual:1,method:1,mind:1,mobil:[],mock:[0,1],mock_apigatewai:2,mock_autosc:2,mock_cloudform:2,mock_cloudwatch:2,mock_datapipelin:2,mock_dynamodb2:2,mock_dynamodb:2,mock_ec2:[0,2],mock_ec:2,mock_elb:2,mock_emr:2,mock_glaci:2,mock_iam:2,mock_kinesi:2,mock_km:2,mock_lambda:2,mock_rd:2,mock_rds2:2,mock_redshift:2,mock_route53:2,mock_s3:[1,2],mock_s:2,mock_sfw:2,mock_sn:2,mock_sq:2,mock_st:2,model_inst:1,modul:[],monitor:0,monitoring_st:0,moto:4,moto_serv:[1,4],motoapi:3,mybucket:1,mymodel:1,mymodul:1,name:[1,2],need:[1,4],network:[],networkinterfac:0,never:2,none:0,object:[0,1],other:[],out:[1,2],p3000:1,page:[],paravirtu:0,partial:2,pend:0,persist:0,pip:1,pipelin:2,platform:0,post:3,previous:0,private_dns_nam:0,private_ip_address:0,product_cod:0,provid:3,public_dns_nam:0,python:[1,4],ramdisk:0,random:0,rds2:2,read:2,reason:0,redshift:2,region:0,regioninfo:0,releas:1,repositori:2,request:3,requester_id:0,reserv:0,root_device_nam:0,root_device_typ:0,route53:2,row:[],rubi:4,run:[1,4],run_inst:0,same:[0,1],save:1,scale:[],search:[],secur:2,self:1,send:3,set:0,set_contents_from_str:1,setup:1,sever:1,should:2,sinc:1,small:0,snippet:0,some:[2,3,4],sourc:[1,2],sourcedestcheck:0,span:[],spot_instance_request_id:0,spulec:1,start:0,state:3,state_reason:0,statement:1,statu:2,steve:1,stop:1,storag:[],stuff:2,subnet_id:0,swf:2,system:3,tabl:[],tag:[0,2],test:[1,2],test_my_model_sav:1,than:0,thi:[0,1,3],tracker:2,tutori:0,usag:2,use:[0,1,4],used:[2,4],useful:1,using:1,valu:1,veri:1,version:1,view:3,virtual:1,virtualization_typ:0,vpc_id:0,want:1,west:0,wrap:1,x86_64:0,xen:0,you:[0,1,2,4]},titles:["Use Moto as EC2 backend","Getting Started with Moto","Moto: Mock AWS Services","Moto APIs","Other languages"],titleterms:{AWS:2,Use:0,addit:2,alon:1,ani:[],anoth:[],api:[2,3],backend:0,boto:[],can:[],context:1,current:[],dashboard:3,decor:1,don:[],ec2:0,exampl:[],get:[1,2],here:[],implement:[],indic:[],instal:1,instanc:0,languag:4,launch:0,librari:[],manag:1,mock:2,mode:1,moto:[0,1,2,3],moto_serv:[],need:[],other:4,python:[],raw:1,reset:3,resourc:2,run:[],server:1,servic:2,some:[],stand:1,start:[1,2],tabl:[],usag:1,use:[],used:[],you:[]}}) \ No newline at end of file +Search.setIndex({docnames:["docs/ec2_tut","docs/getting_started","docs/moto_apis","docs/server_mode","index"],envversion:50,filenames:["docs/ec2_tut.rst","docs/getting_started.rst","docs/moto_apis.rst","docs/server_mode.rst","index.rst"],objects:{},objnames:{},objtypes:{},terms:{"27t05":0,"57z":0,"91dd2f32":0,"class":1,"default":3,"import":[0,1],"static":0,"true":0,"var":0,AWS:1,EBS:4,ECS:4,For:1,KMS:4,RDS:4,SES:4,SNS:4,SQS:4,STS:4,Then:3,There:1,With:1,__init__:1,_in_monitoring_el:0,_placement:0,_previous_st:0,_state:0,abl:3,abov:[],access:3,account:1,action:[],added:3,address:3,administr:[],after:0,all:[0,1,2,4],allow:[1,3,4],alon:3,alreadi:0,also:[1,3],amazonaw:[0,2],ami:[0,4],ami_launch_index:0,analyt:[],ani:3,anoth:[],api:4,applic:[],architectur:0,assert:1,assum:0,attribut:0,auto:[],automat:1,autosc:4,awar:3,awesom:1,backend:[2,3],base:4,basic:4,befor:[0,4],behavior:0,best:1,bind:3,blank:1,block:[],block_device_map:0,bodi:[],boto3:3,boto:[0,1],bucket:1,call:1,can:[1,3],cell:[],chang:2,client_token:0,clone:1,cloud:[],cloudform:4,cloudwatch:4,code:[0,1],column:[],com:[0,1,2],come:[1,2],comput:0,config:[],conn:[0,1],connect:0,connect_s3:1,connect_to_region:0,contain:[],content:[],core:4,creat:1,create_bucket:1,current:2,data:4,databas:[],decor:4,def:1,deliveri:[],depend:3,deploy:[],describeinst:[],develop:4,dns_name:0,don:[1,3],done:4,download:0,dynamodb2:4,dynamodb:4,easiest:[],easili:4,ebs_optim:0,ec2:[1,3,4],ec2connect:0,ed65f870:0,elast:[],elb:4,empti:3,emr:4,enabl:0,encourag:1,endpoint:[1,4],endpoint_url:3,eni:0,environ:1,even:[1,3],eventsset:0,everi:1,exampl:[0,1,3],explain:0,extern:3,extra:3,f00ba4:0,fals:0,familiar:4,featur:0,file:[],flask:3,follow:[0,1],from:1,full:1,gatewai:4,gener:0,get_bucket:1,get_contents_as_str:1,get_kei:1,git:1,github:1,glacier:4,grid:[],group:[0,4],group_nam:0,guid:4,has:3,have:[0,1],haven:3,header:[],here:3,hostnam:3,how:0,http:[1,2,3],https_validate_certif:[],hypervisor:0,iam:4,ident:[],image_id:0,implement:[],index:4,infrastructur:4,insid:1,instal:[0,3],instanc:[3,4],instance_profil:0,instance_typ:0,instead:[],interfac:[0,3],intern:[0,2],ip_address:0,is_secur:[],isn:[1,3],issu:4,item:0,its:4,java:3,javascript:3,just:1,keep:1,kei:1,kernel:0,key_nam:0,kinesi:4,lambda:4,languag:[],latest:1,launch_tim:0,librari:4,list:3,localhost:[2,3],mai:[],manag:[],manual:1,method:1,might:3,mind:1,mobil:[],mock:[0,1],mock_apigatewai:4,mock_autosc:4,mock_cloudform:4,mock_cloudwatch:4,mock_datapipelin:4,mock_dynamodb2:4,mock_dynamodb:4,mock_ec2:[0,4],mock_ec:4,mock_elb:4,mock_emr:4,mock_glaci:4,mock_iam:4,mock_kinesi:4,mock_km:4,mock_lambda:4,mock_rd:4,mock_rds2:4,mock_redshift:4,mock_route53:4,mock_s3:[1,4],mock_s:4,mock_sfw:4,mock_sn:4,mock_sq:4,mock_st:4,model_inst:1,modul:[],monitor:0,monitoring_st:0,moto:3,moto_serv:[1,3],motoapi:2,mybucket:1,mymodel:1,mymodul:1,name:[1,4],need:[1,3],network:3,networkinterfac:0,never:4,none:0,object:[0,1],other:[],out:[1,4],p3000:[1,3],packag:3,page:[],paravirtu:0,partial:4,pass:3,pend:0,persist:0,pip:[1,3],pipelin:4,platform:0,pleas:3,port:3,post:2,previous:0,private_dns_nam:0,private_ip_address:0,product_cod:0,provid:2,proxi:[],proxy_port:[],public_dns_nam:0,python:[1,3],ramdisk:0,random:0,rds2:4,read:4,reason:0,redshift:4,region:0,region_nam:3,regioninfo:0,releas:1,repositori:4,request:2,requester_id:0,reserv:0,resourc:3,root_device_nam:0,root_device_typ:0,route53:4,row:[],rubi:3,run:[1,3],run_inst:0,same:[0,1],save:1,scale:[],search:[],secur:4,see:3,self:1,send:2,servic:3,service_nam:3,set:0,set_contents_from_str:1,setup:1,sever:1,should:4,simpler:[],sinc:[1,3],small:0,snippet:0,some:[2,3],sourc:[1,4],sourcedestcheck:0,span:[],spot_instance_request_id:0,spulec:1,stand:3,start:[0,3],state:2,state_reason:0,statement:1,statu:4,steve:1,stop:1,storag:[],strongli:[],structur:3,stuff:[],subnet_id:0,swf:4,system:2,tabl:[],tag:[0,4],test:[1,4],test_my_model_sav:1,than:0,thi:[0,1,2,3],tracker:4,tutori:0,usag:4,use:[0,1,3],used:[3,4],useful:1,user:3,uses:3,using:1,util:3,valu:1,veri:1,version:1,view:2,virtual:1,virtualization_typ:0,vpc_id:0,wai:[],want:[1,3],west:[0,3],which:3,wrap:1,x86_64:0,xen:0,yet:3,you:[0,1,3,4],your:3},titles:["Use Moto as EC2 backend","Getting Started with Moto","Moto APIs","Server mode","Moto: Mock AWS Services"],titleterms:{AWS:4,Use:0,addit:4,alon:1,ani:[],anoth:[],api:2,backend:0,boto:[],can:[],context:1,current:4,dashboard:2,decor:1,don:[],ec2:0,exampl:[],get:[1,4],here:[],implement:4,indic:[],instal:1,instanc:0,languag:3,launch:0,librari:[],manag:1,mock:4,mode:[1,3],moto:[0,1,2,4],moto_serv:[],need:[],other:3,python:[],raw:1,reset:2,resourc:4,run:[],server:[1,3],servic:4,some:[],stand:1,start:[1,4],tabl:[],usag:1,use:[],used:[],you:[]}}) \ No newline at end of file diff --git a/docs/docs/ec2_tut.rst b/docs/docs/ec2_tut.rst new file mode 100644 index 000000000..86d6ae313 --- /dev/null +++ b/docs/docs/ec2_tut.rst @@ -0,0 +1,74 @@ +.. _ec2_tut: + +======================= +Use Moto as EC2 backend +======================= + +This tutorial explains ``moto.ec2``'s features and how to use it. This +tutorial assumes that you have already downloaded and installed boto and moto. +Before all code examples the following snippet is launched:: + + >>> import boto.ec2, moto + >>> mock_ec2 = moto.mock_ec2() + >>> mock_ec2.start() + >>> conn = boto.ec2.connect_to_region("eu-west-1") + +Launching instances +------------------- + +After mock is started, the behavior is the same than previously:: + + >>> reservation = conn.run_instances('ami-f00ba4') + >>> reservation.instances[0] + Instance:i-91dd2f32 + +Moto set static or generate random object's attributes:: + + >>> vars(reservation.instances[0]) + {'_in_monitoring_element': False, + '_placement': None, + '_previous_state': None, + '_state': pending(0), + 'ami_launch_index': u'0', + 'architecture': u'x86_64', + 'block_device_mapping': None, + 'client_token': '', + 'connection': EC2Connection:ec2.eu-west-1.amazonaws.com, + 'dns_name': u'ec2-54.214.135.84.compute-1.amazonaws.com', + 'ebs_optimized': False, + 'eventsSet': None, + 'group_name': None, + 'groups': [], + 'hypervisor': u'xen', + 'id': u'i-91dd2f32', + 'image_id': u'f00ba4', + 'instance_profile': None, + 'instance_type': u'm1.small', + 'interfaces': [NetworkInterface:eni-ed65f870], + 'ip_address': u'54.214.135.84', + 'item': u'\n ', + 'kernel': u'None', + 'key_name': u'None', + 'launch_time': u'2015-07-27T05:59:57Z', + 'monitored': True, + 'monitoring': u'\n ', + 'monitoring_state': u'enabled', + 'persistent': False, + 'platform': None, + 'private_dns_name': u'ip-10.136.187.180.ec2.internal', + 'private_ip_address': u'10.136.187.180', + 'product_codes': [], + 'public_dns_name': u'ec2-54.214.135.84.compute-1.amazonaws.com', + 'ramdisk': None, + 'reason': '', + 'region': RegionInfo:eu-west-1, + 'requester_id': None, + 'root_device_name': None, + 'root_device_type': None, + 'sourceDestCheck': u'true', + 'spot_instance_request_id': None, + 'state_reason': None, + 'subnet_id': None, + 'tags': {}, + 'virtualization_type': u'paravirtual', + 'vpc_id': None} diff --git a/docs/getting_started.rst b/docs/docs/getting_started.rst similarity index 99% rename from docs/getting_started.rst rename to docs/docs/getting_started.rst index e0a4fb10e..97f667d26 100644 --- a/docs/getting_started.rst +++ b/docs/docs/getting_started.rst @@ -1,3 +1,5 @@ +.. _getting_started: + ========================= Getting Started with Moto ========================= diff --git a/docs/docs/moto_apis.rst b/docs/docs/moto_apis.rst new file mode 100644 index 000000000..3414cba1a --- /dev/null +++ b/docs/docs/moto_apis.rst @@ -0,0 +1,21 @@ +.. _moto_apis: + +========= +Moto APIs +========= + +Moto provides some internal APIs to view and change the state of the backends. + +Reset API +--------- + +This API resets the state of all of the backends. Send an HTTP POST to reset:: + + requests.post("http://motoapi.amazonaws.com/moto-api/reset") + +Dashboard +--------- + +Moto comes with a dashboard to view the current state of the system:: + + http://localhost:5000/moto-api/ diff --git a/docs/docs/server_mode.rst b/docs/docs/server_mode.rst new file mode 100644 index 000000000..e8139e04d --- /dev/null +++ b/docs/docs/server_mode.rst @@ -0,0 +1,67 @@ +.. _server_mode: + +=========== +Server mode +=========== + +Moto has a stand-alone server mode. This allows you to utilize +the backend structure of Moto even if you don't use Python. + +It uses flask, which isn't a default dependency. You can install the +server 'extra' package with: + +.. code:: bash + + pip install moto[server] + + +You can then start it running a service: + +.. code:: bash + + $ moto_server ec2 + +You can also pass the port: + +.. code-block:: bash + + $ moto_server ec2 -p3000 + * Running on http://127.0.0.1:3000/ + +If you want to be able to use the server externally you can pass an IP +address to bind to as a hostname or allow any of your external +interfaces with 0.0.0.0: + +.. code-block:: bash + + $ moto_server ec2 -H 0.0.0.0 + * Running on http://0.0.0.0:5000/ + +Please be aware this might allow other network users to access your +server. + +Then go to localhost_ to see a list of running instances (it will be empty since you haven't added any yet). + +If you want to use boto3 with this, you can pass an `endpoint_url` to the resource + +.. code-block:: python + + boto3.resource( + service_name='s3', + region_name='us-west-1', + endpoint_url='http://localhost:5000', + ) + +Other languages +--------------- + +You don't need to use Python to use Moto; it can be used with any language. Here are some examples to run it with other languages: + +* `Java`_ +* `Ruby`_ +* `Javascript`_ + +.. _Java: https://github.com/spulec/moto/blob/master/other_langs/sqsSample.java +.. _Ruby: https://github.com/spulec/moto/blob/master/other_langs/test.rb +.. _Javascript: https://github.com/spulec/moto/blob/master/other_langs/test.js +.. _localhost: http://localhost:5000/?Action=DescribeInstances diff --git a/docs/index.rst b/docs/index.rst index 560ebc661..2ce31febd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,18 +5,17 @@ Moto: Mock AWS Services ============================= A library that allows you to easily mock out tests based on -_`AWS infrastructure`. - -.. _AWS infrastructure: http://aws.amazon.com/ +`AWS infrastructure`_. Getting Started --------------- If you've never used ``moto`` before, you should read the -:doc:`Getting Started with Moto ` guide to get familiar +:doc:`Getting Started with Moto ` guide to get familiar with ``moto`` and its usage. Currently implemented Services: +------------------------------- +-----------------------+---------------------+-----------------------------------+ | Service Name | Decorator | Development Status | @@ -79,11 +78,6 @@ Currently implemented Services: +-----------------------+---------------------+-----------------------------------+ -Moto APIs ---------- -some stuff - - Additional Resources -------------------- @@ -91,6 +85,7 @@ Additional Resources * `Moto Source Repository`_ * `Moto Issue Tracker`_ +.. _AWS infrastructure: http://aws.amazon.com/ .. _Moto Issue Tracker: https://github.com/spulec/moto/issues .. _Moto Source Repository: https://github.com/spulec/moto @@ -99,7 +94,7 @@ Additional Resources :hidden: :glob: - index - getting_started - other_langs - moto_apis + docs/getting_started + docs/server_mode + docs/moto_apis + docs/ec2_tut From 2bd4567801a733297588ec7a8167c1af5c2edd75 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 14 Mar 2017 23:26:27 -0400 Subject: [PATCH 086/274] Do not use flask outside of server mode. --- moto/core/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/core/responses.py b/moto/core/responses.py index ebc4e1743..2d0e485ba 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -12,7 +12,6 @@ from jinja2 import Environment, DictLoader, TemplateNotFound import six from six.moves.urllib.parse import parse_qs, urlparse -from flask import render_template import xmltodict from pkg_resources import resource_filename from werkzeug.exceptions import HTTPException @@ -375,6 +374,7 @@ class MotoAPIResponse(BaseResponse): return 200, {"Content-Type": "application/javascript"}, json.dumps(results) def dashboard(self, request, full_url, headers): + from flask import render_template return render_template('dashboard.html') From 09a4d177f5f4e47ec8eb24fc7bef236c6fb4f9c0 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 14 Mar 2017 23:42:47 -0400 Subject: [PATCH 087/274] Add kms boto3 test. --- moto/kms/responses.py | 1 - tests/test_kms/test_kms.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/moto/kms/responses.py b/moto/kms/responses.py index 7ed8927a2..0f544e954 100644 --- a/moto/kms/responses.py +++ b/moto/kms/responses.py @@ -231,7 +231,6 @@ class KmsResponse(BaseResponse): def decrypt(self): value = self.parameters.get("CiphertextBlob") - print("value 3", value) return json.dumps({"Plaintext": base64.b64decode(value).decode("utf-8")}) diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index e1468cce0..8d034c7ff 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals import re +import boto3 import boto.kms from boto.exception import JSONResponseError from boto.kms.exceptions import AlreadyExistsException, NotFoundException import sure # noqa -from moto import mock_kms_deprecated +from moto import mock_kms, mock_kms_deprecated from nose.tools import assert_raises @@ -600,3 +601,12 @@ def test__assert_default_policy(): "not-default").should.throw(JSONResponseError) _assert_default_policy.when.called_with( "default").should_not.throw(JSONResponseError) + + +@mock_kms +def test_kms_encrypt_boto3(): + client = boto3.client('kms', region_name='us-east-1') + response = client.encrypt(KeyId='foo', Plaintext=b'bar') + + response = client.decrypt(CiphertextBlob=response['CiphertextBlob']) + response['Plaintext'].should.equal(b'bar') From 3cdb4afad0f6a42c790c39c56e2b6f0680d54ff7 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 21:58:37 -0400 Subject: [PATCH 088/274] Fix redshift responses to work with json or xml. --- moto/redshift/responses.py | 34 +++++++++++++++++----------- setup.py | 1 + tests/test_redshift/test_redshift.py | 17 +++++++++++++- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index 23c653332..cc0e72793 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import json +import dicttoxml from moto.core.responses import BaseResponse from .models import redshift_backends @@ -12,6 +13,13 @@ class RedshiftResponse(BaseResponse): def redshift_backend(self): return redshift_backends[self.region] + def get_response(self, response): + if self.request_json: + return json.dumps(response) + else: + xml = dicttoxml.dicttoxml(response, attr_type=False, root=False) + return xml + def create_cluster(self): cluster_kwargs = { "cluster_identifier": self._get_param('ClusterIdentifier'), @@ -37,7 +45,7 @@ class RedshiftResponse(BaseResponse): } cluster = self.redshift_backend.create_cluster(**cluster_kwargs) - return json.dumps({ + return self.get_response({ "CreateClusterResponse": { "CreateClusterResult": { "Cluster": cluster.to_json(), @@ -52,7 +60,7 @@ class RedshiftResponse(BaseResponse): cluster_identifier = self._get_param("ClusterIdentifier") clusters = self.redshift_backend.describe_clusters(cluster_identifier) - return json.dumps({ + return self.get_response({ "DescribeClustersResponse": { "DescribeClustersResult": { "Clusters": [cluster.to_json() for cluster in clusters] @@ -84,7 +92,7 @@ class RedshiftResponse(BaseResponse): } cluster = self.redshift_backend.modify_cluster(**cluster_kwargs) - return json.dumps({ + return self.get_response({ "ModifyClusterResponse": { "ModifyClusterResult": { "Cluster": cluster.to_json(), @@ -99,7 +107,7 @@ class RedshiftResponse(BaseResponse): cluster_identifier = self._get_param("ClusterIdentifier") cluster = self.redshift_backend.delete_cluster(cluster_identifier) - return json.dumps({ + return self.get_response({ "DeleteClusterResponse": { "DeleteClusterResult": { "Cluster": cluster.to_json() @@ -121,7 +129,7 @@ class RedshiftResponse(BaseResponse): subnet_ids=subnet_ids, ) - return json.dumps({ + return self.get_response({ "CreateClusterSubnetGroupResponse": { "CreateClusterSubnetGroupResult": { "ClusterSubnetGroup": subnet_group.to_json(), @@ -137,7 +145,7 @@ class RedshiftResponse(BaseResponse): subnet_groups = self.redshift_backend.describe_cluster_subnet_groups( subnet_identifier) - return json.dumps({ + return self.get_response({ "DescribeClusterSubnetGroupsResponse": { "DescribeClusterSubnetGroupsResult": { "ClusterSubnetGroups": [subnet_group.to_json() for subnet_group in subnet_groups] @@ -152,7 +160,7 @@ class RedshiftResponse(BaseResponse): subnet_identifier = self._get_param("ClusterSubnetGroupName") self.redshift_backend.delete_cluster_subnet_group(subnet_identifier) - return json.dumps({ + return self.get_response({ "DeleteClusterSubnetGroupResponse": { "ResponseMetadata": { "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", @@ -170,7 +178,7 @@ class RedshiftResponse(BaseResponse): description=description, ) - return json.dumps({ + return self.get_response({ "CreateClusterSecurityGroupResponse": { "CreateClusterSecurityGroupResult": { "ClusterSecurityGroup": security_group.to_json(), @@ -187,7 +195,7 @@ class RedshiftResponse(BaseResponse): security_groups = self.redshift_backend.describe_cluster_security_groups( cluster_security_group_name) - return json.dumps({ + return self.get_response({ "DescribeClusterSecurityGroupsResponse": { "DescribeClusterSecurityGroupsResult": { "ClusterSecurityGroups": [security_group.to_json() for security_group in security_groups] @@ -203,7 +211,7 @@ class RedshiftResponse(BaseResponse): self.redshift_backend.delete_cluster_security_group( security_group_identifier) - return json.dumps({ + return self.get_response({ "DeleteClusterSecurityGroupResponse": { "ResponseMetadata": { "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", @@ -222,7 +230,7 @@ class RedshiftResponse(BaseResponse): description, ) - return json.dumps({ + return self.get_response({ "CreateClusterParameterGroupResponse": { "CreateClusterParameterGroupResult": { "ClusterParameterGroup": parameter_group.to_json(), @@ -238,7 +246,7 @@ class RedshiftResponse(BaseResponse): parameter_groups = self.redshift_backend.describe_cluster_parameter_groups( cluster_parameter_group_name) - return json.dumps({ + return self.get_response({ "DescribeClusterParameterGroupsResponse": { "DescribeClusterParameterGroupsResult": { "ParameterGroups": [parameter_group.to_json() for parameter_group in parameter_groups] @@ -254,7 +262,7 @@ class RedshiftResponse(BaseResponse): self.redshift_backend.delete_cluster_parameter_group( cluster_parameter_group_name) - return json.dumps({ + return self.get_response({ "DeleteClusterParameterGroupResponse": { "ResponseMetadata": { "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", diff --git a/setup.py b/setup.py index a09438d69..37eb78ccf 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ install_requires = [ "cookies", "requests>=2.0", "xmltodict", + "dicttoxml", "six", "werkzeug", "pytz", diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 41be8f022..045e30246 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import boto +import boto3 from boto.redshift.exceptions import ( ClusterNotFound, ClusterParameterGroupNotFound, @@ -10,7 +11,21 @@ from boto.redshift.exceptions import ( ) import sure # noqa -from moto import mock_ec2_deprecated, mock_redshift_deprecated +from moto import mock_ec2_deprecated, mock_redshift_deprecated, mock_redshift + + +@mock_redshift +def test_create_cluster_boto3(): + client = boto3.client('redshift', region_name='us-east-1') + response = client.create_cluster( + DBName='test', + ClusterIdentifier='test', + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='user', + MasterUserPassword='password', + ) + response['Cluster']['NodeType'].should.equal('ds2.xlarge') @mock_redshift_deprecated From 6666351757c2c2083a88158a132f446112109b9d Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 22:05:11 -0400 Subject: [PATCH 089/274] Fix redshift server to default to xml. --- tests/test_redshift/test_server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_redshift/test_server.py b/tests/test_redshift/test_server.py index ba407ab4c..4e950fc74 100644 --- a/tests/test_redshift/test_server.py +++ b/tests/test_redshift/test_server.py @@ -18,7 +18,5 @@ def test_describe_clusters(): res = test_client.get('/?Action=DescribeClusters') - json_data = json.loads(res.data.decode("utf-8")) - clusters = json_data['DescribeClustersResponse'][ - 'DescribeClustersResult']['Clusters'] - list(clusters).should.equal([]) + result = res.data.decode("utf-8") + result.should.contain(" Date: Wed, 15 Mar 2017 22:12:16 -0400 Subject: [PATCH 090/274] Fix py3 redshift encoding. --- CHANGELOG.md | 1 + moto/redshift/responses.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5938acbb6..ebf621dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Latest * The normal @mock_ decorators will no longer work with boto. It is suggested that you upgrade to boto3 or use the standalone-server mode. If you would still like to use boto, you must use the @mock__deprecated decorators which will be removed in a future release. * The @mock_s3bucket_path decorator is now deprecated. Use the @mock_s3 decorator instead. * Drop support for Python 2.6 + * Redshift server defaults to returning XML instead of JSON Added * Reset API: a reset API has been added to flush all of the current data ex: `requests.post("http://motoapi.amazonaws.com/moto-api/reset")` diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index cc0e72793..ba28b1343 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -18,7 +18,7 @@ class RedshiftResponse(BaseResponse): return json.dumps(response) else: xml = dicttoxml.dicttoxml(response, attr_type=False, root=False) - return xml + return xml.decode("utf-8") def create_cluster(self): cluster_kwargs = { From 5f3fbff627029c0c260546a674a3b372cacdae82 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 22:21:04 -0400 Subject: [PATCH 091/274] Standardize on one account id (123456789012). --- moto/ec2/responses/amis.py | 2 +- moto/ec2/responses/elastic_block_store.py | 4 ++-- moto/ec2/responses/instances.py | 16 ++++++++-------- moto/ec2/responses/security_groups.py | 6 +++--- moto/ec2/responses/vpc_peering_connections.py | 2 +- moto/s3/responses.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py index 42bfba209..ab5256976 100755 --- a/moto/ec2/responses/amis.py +++ b/moto/ec2/responses/amis.py @@ -100,7 +100,7 @@ DESCRIBE_IMAGES_RESPONSE = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE {{ reservation.id }} - 111122223333 + 123456789012 sg-245f6a01 @@ -280,7 +280,7 @@ EC2_RUN_INSTANCES = """{{ vpc_pcx.vpc.cidr_block }} - 111122223333 + 123456789012 {{ vpc_pcx.peer_vpc.id }} diff --git a/moto/s3/responses.py b/moto/s3/responses.py index e123d76e1..9cc94ca03 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -985,7 +985,7 @@ S3_ALL_MULTIPARTS = """ {{ upload.key_name }} {{ upload.id }} - arn:aws:iam::111122223333:user/user1-11111a31-17b5-4fb7-9df5-b111111f13de + arn:aws:iam::123456789012:user/user1-11111a31-17b5-4fb7-9df5-b111111f13de user1-11111a31-17b5-4fb7-9df5-b111111f13de From 8a803cdbaf3dfc2c515e3d71df5412bc1dbe16b8 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 22:45:28 -0400 Subject: [PATCH 092/274] Better EC2 duplicate SG error. --- moto/ec2/exceptions.py | 8 ++++++++ moto/ec2/models.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index d32118b82..e5432baf7 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -156,6 +156,14 @@ class InvalidPermissionNotFoundError(EC2ClientError): "The specified rule does not exist in this security group") +class InvalidPermissionDuplicateError(EC2ClientError): + + def __init__(self): + super(InvalidPermissionDuplicateError, self).__init__( + "InvalidPermission.Duplicate", + "The specified rule already exists") + + class InvalidRouteTableIdError(EC2ClientError): def __init__(self, route_table_id): diff --git a/moto/ec2/models.py b/moto/ec2/models.py index a0a4c93f1..989ec5572 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -35,6 +35,7 @@ from .exceptions import ( InvalidSecurityGroupDuplicateError, InvalidSecurityGroupNotFoundError, InvalidPermissionNotFoundError, + InvalidPermissionDuplicateError, InvalidRouteTableIdError, InvalidRouteError, InvalidInstanceIdError, @@ -1311,7 +1312,7 @@ class SecurityGroup(TaggedEC2Resource): def add_ingress_rule(self, rule): if rule in self.ingress_rules: - raise InvalidParameterValueError('security_group') + raise InvalidPermissionDuplicateError() else: self.ingress_rules.append(rule) From 3899eee648966921d4cd3901d47cc6bdcbcfad6e Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 22:53:27 -0400 Subject: [PATCH 093/274] Fix S3 filtering by unicode prefix. Closes #838 --- moto/s3/responses.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 9cc94ca03..954bf6706 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -233,6 +233,8 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] + if prefix: + prefix = prefix.decode("utf-8") delimiter = querystring.get('delimiter', [None])[0] result_keys, result_folders = self.backend.prefix_query( bucket, prefix, delimiter) @@ -250,6 +252,8 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] + if prefix: + prefix = prefix.decode("utf-8") delimiter = querystring.get('delimiter', [None])[0] result_keys, result_folders = self.backend.prefix_query( bucket, prefix, delimiter) @@ -278,7 +282,7 @@ class ResponseObject(_TemplateEnvironmentMixin): return template.render( bucket=bucket, - prefix=prefix or '', + prefix=prefix or b'', delimiter=delimiter, result_keys=result_keys, result_folders=result_folders, From e25d1499c2bfe80f2942901d6f4f0e0c1b4848e3 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 23:02:10 -0400 Subject: [PATCH 094/274] Update cloudformation for new list types. --- moto/cloudformation/parsing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index fbf34b6f1..dafe30436 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -345,7 +345,8 @@ class ResourceMap(collections.Mapping): # Set any input parameters that were passed for key, value in self.input_parameters.items(): if key in self.resolved_parameters: - if parameter_slots[key].get('Type', 'String') == 'CommaDelimitedList': + value_type = parameter_slots[key].get('Type', 'String') + if value_type == 'CommaDelimitedList' or value_type.startswith("List"): value = value.split(',') self.resolved_parameters[key] = value From 446843e756093e3aa7d8140a7c409981caf33aea Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 23:13:09 -0400 Subject: [PATCH 095/274] Fix py3 s3 prefix decoding. --- moto/s3/responses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 954bf6706..449fed0a9 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -233,7 +233,7 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] - if prefix: + if prefix and isinstance(prefix, six.binary_type): prefix = prefix.decode("utf-8") delimiter = querystring.get('delimiter', [None])[0] result_keys, result_folders = self.backend.prefix_query( @@ -252,7 +252,7 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) prefix = querystring.get('prefix', [None])[0] - if prefix: + if prefix and isinstance(prefix, six.binary_type): prefix = prefix.decode("utf-8") delimiter = querystring.get('delimiter', [None])[0] result_keys, result_folders = self.backend.prefix_query( @@ -282,7 +282,7 @@ class ResponseObject(_TemplateEnvironmentMixin): return template.render( bucket=bucket, - prefix=prefix or b'', + prefix=prefix or '', delimiter=delimiter, result_keys=result_keys, result_folders=result_folders, From 25e2af0320e2f1718d68f11d170dc0ab459131a8 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 23:39:36 -0400 Subject: [PATCH 096/274] Fix camelcase_to_underscore. Closes #767. --- moto/core/utils.py | 16 ++++++++--- moto/elb/models.py | 2 +- moto/elb/responses.py | 4 +-- moto/rds/responses.py | 24 ++++++++-------- moto/rds2/responses.py | 53 ----------------------------------- tests/test_core/test_utils.py | 1 + 6 files changed, 28 insertions(+), 72 deletions(-) diff --git a/moto/core/utils.py b/moto/core/utils.py index 54622d0d7..946dd5895 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -12,10 +12,18 @@ def camelcase_to_underscores(argument): python underscore variable like the_new_attribute''' result = '' prev_char_title = True - for char in argument: - if char.istitle() and not prev_char_title: - # Only add underscore if char is capital, not first letter, and prev - # char wasn't capital + for index, char in enumerate(argument): + try: + next_char_title = argument[index + 1].istitle() + except IndexError: + next_char_title = True + + upper_to_lower = char.istitle() and not next_char_title + lower_to_upper = char.istitle() and not prev_char_title + + if index and (upper_to_lower or lower_to_upper): + # Only add underscore if char is capital, not first letter, and next + # char is not capital result += "_" prev_char_title = char.istitle() if not char.isspace(): # Only add non-whitespace diff --git a/moto/elb/models.py b/moto/elb/models.py index 41df8a649..234e5ea58 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -85,7 +85,7 @@ class FakeLoadBalancer(BaseModel): instance_port=( port.get('instance_port') or port['InstancePort']), ssl_certificate_id=port.get( - 'sslcertificate_id', port.get('SSLCertificateId')), + 'ssl_certificate_id', port.get('SSLCertificateId')), ) self.listeners.append(listener) diff --git a/moto/elb/responses.py b/moto/elb/responses.py index e90de260e..5402ea964 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -94,7 +94,7 @@ class ELBResponse(BaseResponse): load_balancer_name, instance_ids) return template.render(load_balancer=load_balancer) - def set_load_balancer_listener_sslcertificate(self): + def set_load_balancer_listener_ssl_certificate(self): load_balancer_name = self._get_param('LoadBalancerName') ssl_certificate_id = self.querystring['SSLCertificateId'][0] lb_port = self.querystring['LoadBalancerPort'][0] @@ -188,7 +188,7 @@ class ELBResponse(BaseResponse): template = self.response_template(CREATE_LOAD_BALANCER_POLICY_TEMPLATE) return template.render() - def create_lbcookie_stickiness_policy(self): + def create_lb_cookie_stickiness_policy(self): load_balancer_name = self._get_param('LoadBalancerName') policy = AppCookieStickinessPolicy() diff --git a/moto/rds/responses.py b/moto/rds/responses.py index 6b51c8fe6..a6a580e25 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -72,27 +72,27 @@ class RDSResponse(BaseResponse): count += 1 return unpacked_list - def create_dbinstance(self): + def create_db_instance(self): db_kwargs = self._get_db_kwargs() database = self.backend.create_database(db_kwargs) template = self.response_template(CREATE_DATABASE_TEMPLATE) return template.render(database=database) - def create_dbinstance_read_replica(self): + def create_db_instance_read_replica(self): db_kwargs = self._get_db_replica_kwargs() database = self.backend.create_database_replica(db_kwargs) template = self.response_template(CREATE_DATABASE_REPLICA_TEMPLATE) return template.render(database=database) - def describe_dbinstances(self): + def describe_db_instances(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') databases = self.backend.describe_databases(db_instance_identifier) template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) return template.render(databases=databases) - def modify_dbinstance(self): + def modify_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') db_kwargs = self._get_db_kwargs() database = self.backend.modify_database( @@ -100,13 +100,13 @@ class RDSResponse(BaseResponse): template = self.response_template(MODIFY_DATABASE_TEMPLATE) return template.render(database=database) - def delete_dbinstance(self): + def delete_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') database = self.backend.delete_database(db_instance_identifier) template = self.response_template(DELETE_DATABASE_TEMPLATE) return template.render(database=database) - def create_dbsecurity_group(self): + def create_db_security_group(self): group_name = self._get_param('DBSecurityGroupName') description = self._get_param('DBSecurityGroupDescription') tags = self.unpack_complex_list_params('Tags.Tag', ('Key', 'Value')) @@ -115,21 +115,21 @@ class RDSResponse(BaseResponse): template = self.response_template(CREATE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - def describe_dbsecurity_groups(self): + def describe_db_security_groups(self): security_group_name = self._get_param('DBSecurityGroupName') security_groups = self.backend.describe_security_groups( security_group_name) template = self.response_template(DESCRIBE_SECURITY_GROUPS_TEMPLATE) return template.render(security_groups=security_groups) - def delete_dbsecurity_group(self): + def delete_db_security_group(self): security_group_name = self._get_param('DBSecurityGroupName') security_group = self.backend.delete_security_group( security_group_name) template = self.response_template(DELETE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - def authorize_dbsecurity_group_ingress(self): + def authorize_db_security_group_ingress(self): security_group_name = self._get_param('DBSecurityGroupName') cidr_ip = self._get_param('CIDRIP') security_group = self.backend.authorize_security_group( @@ -137,7 +137,7 @@ class RDSResponse(BaseResponse): template = self.response_template(AUTHORIZE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - def create_dbsubnet_group(self): + def create_db_subnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') description = self._get_param('DBSubnetGroupDescription') subnet_ids = self._get_multi_param('SubnetIds.member') @@ -149,13 +149,13 @@ class RDSResponse(BaseResponse): template = self.response_template(CREATE_SUBNET_GROUP_TEMPLATE) return template.render(subnet_group=subnet_group) - def describe_dbsubnet_groups(self): + def describe_db_subnet_groups(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_groups = self.backend.describe_subnet_groups(subnet_name) template = self.response_template(DESCRIBE_SUBNET_GROUPS_TEMPLATE) return template.render(subnet_groups=subnet_groups) - def delete_dbsubnet_group(self): + def delete_db_subnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_group = self.backend.delete_subnet_group(subnet_name) template = self.response_template(DELETE_SUBNET_GROUP_TEMPLATE) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 96b98463d..f3032ea8b 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -99,18 +99,12 @@ class RDS2Response(BaseResponse): count += 1 return unpacked_list - def create_dbinstance(self): - return self.create_db_instance() - def create_db_instance(self): db_kwargs = self._get_db_kwargs() database = self.backend.create_database(db_kwargs) template = self.response_template(CREATE_DATABASE_TEMPLATE) return template.render(database=database) - def create_dbinstance_read_replica(self): - return self.create_db_instance_read_replica() - def create_db_instance_read_replica(self): db_kwargs = self._get_db_replica_kwargs() @@ -118,18 +112,12 @@ class RDS2Response(BaseResponse): template = self.response_template(CREATE_DATABASE_REPLICA_TEMPLATE) return template.render(database=database) - def describe_dbinstances(self): - return self.describe_db_instances() - def describe_db_instances(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') databases = self.backend.describe_databases(db_instance_identifier) template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) return template.render(databases=databases) - def modify_dbinstance(self): - return self.modify_db_instance() - def modify_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') db_kwargs = self._get_db_kwargs() @@ -138,18 +126,12 @@ class RDS2Response(BaseResponse): template = self.response_template(MODIFY_DATABASE_TEMPLATE) return template.render(database=database) - def delete_dbinstance(self): - return self.delete_db_instance() - def delete_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') database = self.backend.delete_database(db_instance_identifier) template = self.response_template(DELETE_DATABASE_TEMPLATE) return template.render(database=database) - def reboot_dbinstance(self): - return self.reboot_db_instance() - def reboot_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') database = self.backend.reboot_db_instance(db_instance_identifier) @@ -176,9 +158,6 @@ class RDS2Response(BaseResponse): template = self.response_template(REMOVE_TAGS_FROM_RESOURCE_TEMPLATE) return template.render() - def create_dbsecurity_group(self): - return self.create_db_security_group() - def create_db_security_group(self): group_name = self._get_param('DBSecurityGroupName') description = self._get_param('DBSecurityGroupDescription') @@ -188,9 +167,6 @@ class RDS2Response(BaseResponse): template = self.response_template(CREATE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - def describe_dbsecurity_groups(self): - return self.describe_db_security_groups() - def describe_db_security_groups(self): security_group_name = self._get_param('DBSecurityGroupName') security_groups = self.backend.describe_security_groups( @@ -198,9 +174,6 @@ class RDS2Response(BaseResponse): template = self.response_template(DESCRIBE_SECURITY_GROUPS_TEMPLATE) return template.render(security_groups=security_groups) - def delete_dbsecurity_group(self): - return self.delete_db_security_group() - def delete_db_security_group(self): security_group_name = self._get_param('DBSecurityGroupName') security_group = self.backend.delete_security_group( @@ -208,9 +181,6 @@ class RDS2Response(BaseResponse): template = self.response_template(DELETE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - def authorize_dbsecurity_group_ingress(self): - return self.authorize_db_security_group_ingress() - def authorize_db_security_group_ingress(self): security_group_name = self._get_param('DBSecurityGroupName') cidr_ip = self._get_param('CIDRIP') @@ -219,9 +189,6 @@ class RDS2Response(BaseResponse): template = self.response_template(AUTHORIZE_SECURITY_GROUP_TEMPLATE) return template.render(security_group=security_group) - def create_dbsubnet_group(self): - return self.create_db_subnet_group() - def create_db_subnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') description = self._get_param('DBSubnetGroupDescription') @@ -234,18 +201,12 @@ class RDS2Response(BaseResponse): template = self.response_template(CREATE_SUBNET_GROUP_TEMPLATE) return template.render(subnet_group=subnet_group) - def describe_dbsubnet_groups(self): - return self.describe_db_subnet_groups() - def describe_db_subnet_groups(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_groups = self.backend.describe_subnet_groups(subnet_name) template = self.response_template(DESCRIBE_SUBNET_GROUPS_TEMPLATE) return template.render(subnet_groups=subnet_groups) - def delete_dbsubnet_group(self): - return self.delete_db_subnet_group() - def delete_db_subnet_group(self): subnet_name = self._get_param('DBSubnetGroupName') subnet_group = self.backend.delete_subnet_group(subnet_name) @@ -307,8 +268,6 @@ class RDS2Response(BaseResponse): template = self.response_template(MODIFY_OPTION_GROUP_TEMPLATE) return template.render(option_group=option_group) - def create_dbparameter_group(self): - return self.create_db_parameter_group() def create_db_parameter_group(self): kwargs = self._get_db_parameter_group_kwargs() @@ -316,9 +275,6 @@ class RDS2Response(BaseResponse): template = self.response_template(CREATE_DB_PARAMETER_GROUP_TEMPLATE) return template.render(db_parameter_group=db_parameter_group) - def describe_dbparameter_groups(self): - return self.describe_db_parameter_groups() - def describe_db_parameter_groups(self): kwargs = self._get_db_parameter_group_kwargs() kwargs['max_records'] = self._get_param('MaxRecords') @@ -328,9 +284,6 @@ class RDS2Response(BaseResponse): DESCRIBE_DB_PARAMETER_GROUPS_TEMPLATE) return template.render(db_parameter_groups=db_parameter_groups) - def modify_dbparameter_group(self): - return self.modify_db_parameter_group() - def modify_db_parameter_group(self): db_parameter_group_name = self._get_param('DBParameterGroupName') db_parameter_group_parameters = self._get_db_parameter_group_paramters() @@ -353,9 +306,6 @@ class RDS2Response(BaseResponse): return parameter_group_parameters.values() - def describe_dbparameters(self): - return self.describe_db_parameters() - def describe_db_parameters(self): db_parameter_group_name = self._get_param('DBParameterGroupName') db_parameter_groups = self.backend.describe_db_parameter_groups( @@ -366,9 +316,6 @@ class RDS2Response(BaseResponse): template = self.response_template(DESCRIBE_DB_PARAMETERS_TEMPLATE) return template.render(db_parameter_group=db_parameter_groups[0]) - def delete_dbparameter_group(self): - return self.delete_db_parameter_group() - def delete_db_parameter_group(self): kwargs = self._get_db_parameter_group_kwargs() db_parameter_group = self.backend.delete_db_parameter_group(kwargs[ diff --git a/tests/test_core/test_utils.py b/tests/test_core/test_utils.py index 76f0645af..8dbf21716 100644 --- a/tests/test_core/test_utils.py +++ b/tests/test_core/test_utils.py @@ -11,6 +11,7 @@ def test_camelcase_to_underscores(): "theNewAttribute": "the_new_attribute", "attri bute With Space": "attribute_with_space", "FirstLetterCapital": "first_letter_capital", + "ListMFADevices": "list_mfa_devices", } for arg, expected in cases.items(): camelcase_to_underscores(arg).should.equal(expected) From a5da348fbab0ceb6264abf6b918f0a9af17c3008 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 15 Mar 2017 23:43:48 -0400 Subject: [PATCH 097/274] Fix lint. --- moto/rds2/responses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index f3032ea8b..7d6f48aa4 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -268,7 +268,6 @@ class RDS2Response(BaseResponse): template = self.response_template(MODIFY_OPTION_GROUP_TEMPLATE) return template.render(option_group=option_group) - def create_db_parameter_group(self): kwargs = self._get_db_parameter_group_kwargs() db_parameter_group = self.backend.create_db_parameter_group(kwargs) From 83084bf2af39dbc95da7249988b1ac47a60cb89a Mon Sep 17 00:00:00 2001 From: michael_lerch Date: Thu, 16 Mar 2017 13:43:45 -0700 Subject: [PATCH 098/274] Prevent 100% cpu usage while SQS long polling on an empty queue While using moto server with a test SQS client, I noticed significant CPU usage while the client was long polling. I narrowed this down to the `receive_messages` call of the SQS service sitting in a `while True:` statement with no work to be done, thus looping forever. To produce this issue, I do: ``` $ python3 -m venv venv $ . ./venv/bin/activate (venv) $ pip install moto moto[server] boto3 Collecting moto Downloading moto-0.4.31-py2.py3-none-any.whl (303kB) --snip-- (venv) $ moto_server sqs & [1] 31727 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) (venv) $ python3 Python 3.6.0 (default, Dec 24 2016, 08:01:42) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import boto3 >>> client = boto3.client('sqs', region_name='us-east-1', endpoint_url='http://127.0.0.1:5000'); >>> client.create_queue(QueueName='testing') 127.0.0.1 - - [16/Mar/2017 13:34:20] "POST / HTTP/1.1" 200 - {'QueueUrl': 'http://sqs.us-east-1.amazonaws.com/123456789012/testing', 'ResponseMetadata': {'RequestId': '7a62c49f-347e-4fc4-9331-6e8e7a96aa73', 'HTTPStatusCode': 200, 'HTTPHeaders': {'content-type': 'text/html; charset=utf-8', 'content-length': '343', 'server': 'Werkzeug/0.12.1 Python/3.6.0', 'date': 'Thu, 16 Mar 2017 20:34:20 GMT'}, 'RetryAttempts': 0}} >>> client.receive_message(QueueUrl='http://sqs.us-east-1.amazonaws.com/123456789012/testing', MaxNumberOfMessages=10, WaitTimeSeconds=10) ``` At this point the moto server will run at 100% cpu for 10 seconds until the request times out waiting for a message. If multiple clients are continuously reconnected (as in mocking a normal sqs worker setup) the server will sit at 100% cpu indefinitely. This pull request adds a simple sleep statement to the SQS `receive_messages` call when there are no messages to process. In doing so, the loop will be limited to executing once per 0.001 seconds when the queue is empty. The CPU usage is nearly 0% after this change. --- moto/sqs/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 61093aa82..9dff700e7 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -289,6 +289,11 @@ class SQSBackend(BaseBackend): # queue.messages only contains visible messages while True: + if len(queue.messages) == 0: + import time + time.sleep(0.001) + continue + for message in queue.messages: if not message.visible: continue From e3bff8b926aaf8a0737063fbc1a4e7bac888b0e9 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 16 Mar 2017 21:19:53 -0400 Subject: [PATCH 099/274] Fix cloudformation NoValue parsing to not add attribute. Closes #870 --- moto/cloudformation/parsing.py | 6 +++++- .../test_cloudformation_stack_integration.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index dafe30436..6d38289c7 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -158,7 +158,11 @@ def clean_json(resource_json, resources_map): cleaned_json = {} for key, value in resource_json.items(): - cleaned_json[key] = clean_json(value, resources_map) + cleaned_val = clean_json(value, resources_map) + if cleaned_val is None: + # If we didn't find anything, don't add this attribute + continue + cleaned_json[key] = cleaned_val return cleaned_json elif isinstance(resource_json, list): return [clean_json(val, resources_map) for val in resource_json] diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index e5d231865..2480ee051 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -382,6 +382,7 @@ def test_stack_elb_integration_with_update(): "Protocol": "HTTP", } ], + "Policies": {"Ref" : "AWS::NoValue"}, } }, }, From e7a3f3408e0e6e59766fdaa5811c730b0976a73e Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 16 Mar 2017 22:00:57 -0400 Subject: [PATCH 100/274] Add Lambda header for invoking error. Closes #770. --- moto/awslambda/models.py | 12 ++++++---- tests/test_awslambda/test_lambda.py | 37 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 477537d10..a3b1f715f 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -138,6 +138,7 @@ class LambdaFunction(BaseModel): except Exception as ex: print("Exception %s", ex) + errored = False try: original_stdout = sys.stdout original_stderr = sys.stderr @@ -152,26 +153,29 @@ class LambdaFunction(BaseModel): if exec_err: result = "\n".join([exec_out.strip(), self.convert(exec_err)]) except Exception as ex: + errored = True result = '%s\n\n\nException %s' % (mycode, ex) finally: codeErr.close() codeOut.close() sys.stdout = original_stdout sys.stderr = original_stderr - return self.convert(result) + return self.convert(result), errored def invoke(self, body, request_headers, response_headers): payload = dict() # Get the invocation type: - r = self._invoke_lambda(code=self.code, event=body) + res, errored = self._invoke_lambda(code=self.code, event=body) if request_headers.get("x-amz-invocation-type") == "RequestResponse": - encoded = base64.b64encode(r.encode('utf-8')) + encoded = base64.b64encode(res.encode('utf-8')) response_headers["x-amz-log-result"] = encoded.decode('utf-8') payload['result'] = response_headers["x-amz-log-result"] - result = r.encode('utf-8') + result = res.encode('utf-8') else: result = json.dumps(payload) + if errored: + response_headers['x-amz-function-error'] = "Handled" return result diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index d967c8bad..007516f56 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -432,3 +432,40 @@ def test_list_create_list_get_delete_list(): conn.delete_function(FunctionName='testFunction') conn.list_functions()['Functions'].should.have.length_of(0) + + +@mock_lambda +def test_invoke_lambda_error(): + lambda_fx = """ + def lambda_handler(event, context): + raise Exception('failsauce') + """ + zip_output = io.BytesIO() + zip_file = zipfile.ZipFile(zip_output, 'w', zipfile.ZIP_DEFLATED) + zip_file.writestr('lambda_function.zip', lambda_fx) + zip_file.close() + zip_output.seek(0) + + client = boto3.client('lambda', region_name='us-east-1') + client.create_function( + FunctionName='test-lambda-fx', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_function.lambda_handler', + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + Code={ + 'ZipFile': zip_output.read() + }, + ) + + result = client.invoke( + FunctionName='test-lambda-fx', + InvocationType='RequestResponse', + LogType='Tail' + ) + + assert 'FunctionError' in result + assert result['FunctionError'] == 'Handled' From c207963a86b7dcc68006c760cab1f4169519b94a Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 16 Mar 2017 22:28:30 -0400 Subject: [PATCH 101/274] Cleanup SNS exceptions. Closes #751. --- moto/sns/exceptions.py | 16 +++++++ moto/sns/models.py | 13 +++++- tests/test_sns/test_application.py | 2 +- tests/test_sns/test_application_boto3.py | 59 +++++++++++++++++++++++- 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/moto/sns/exceptions.py b/moto/sns/exceptions.py index 76e0bccb1..092bb9d69 100644 --- a/moto/sns/exceptions.py +++ b/moto/sns/exceptions.py @@ -8,3 +8,19 @@ class SNSNotFoundError(RESTError): def __init__(self, message): super(SNSNotFoundError, self).__init__( "NotFound", message) + + +class DuplicateSnsEndpointError(RESTError): + code = 400 + + def __init__(self, message): + super(DuplicateSnsEndpointError, self).__init__( + "DuplicateEndpoint", message) + + +class SnsEndpointDisabled(RESTError): + code = 400 + + def __init__(self, message): + super(SnsEndpointDisabled, self).__init__( + "EndpointDisabled", message) diff --git a/moto/sns/models.py b/moto/sns/models.py index 64352d545..5289c8bcd 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -12,7 +12,9 @@ from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.sqs import sqs_backends -from .exceptions import SNSNotFoundError +from .exceptions import ( + SNSNotFoundError, DuplicateSnsEndpointError, SnsEndpointDisabled +) from .utils import make_arn_for_topic, make_arn_for_subscription DEFAULT_ACCOUNT_ID = 123456789012 @@ -136,6 +138,10 @@ class PlatformEndpoint(BaseModel): if 'Enabled' not in self.attributes: self.attributes['Enabled'] = True + @property + def enabled(self): + return json.loads(self.attributes.get('Enabled', 'true').lower()) + @property def arn(self): return "arn:aws:sns:{region}:123456789012:endpoint/{platform}/{name}/{id}".format( @@ -146,6 +152,9 @@ class PlatformEndpoint(BaseModel): ) def publish(self, message): + if not self.enabled: + raise SnsEndpointDisabled("Endpoint %s disabled" % self.id) + # This is where we would actually send a message message_id = six.text_type(uuid.uuid4()) self.messages[message_id] = message @@ -251,6 +260,8 @@ class SNSBackend(BaseBackend): self.applications.pop(platform_arn) def create_platform_endpoint(self, region, application, custom_user_data, token, attributes): + if any(token == endpoint.token for endpoint in self.platform_endpoints.values()): + raise DuplicateSnsEndpointError("Duplicate endpoint token: %s" % token) platform_endpoint = PlatformEndpoint( region, application, custom_user_data, token, attributes) self.platform_endpoints[platform_endpoint.arn] = platform_endpoint diff --git a/tests/test_sns/test_application.py b/tests/test_sns/test_application.py index 613b11af5..319e4a6f8 100644 --- a/tests/test_sns/test_application.py +++ b/tests/test_sns/test_application.py @@ -297,7 +297,7 @@ def test_publish_to_platform_endpoint(): token="some_unique_id", custom_user_data="some user data", attributes={ - "Enabled": False, + "Enabled": True, }, ) diff --git a/tests/test_sns/test_application_boto3.py b/tests/test_sns/test_application_boto3.py index 968240b15..99c378fe4 100644 --- a/tests/test_sns/test_application_boto3.py +++ b/tests/test_sns/test_application_boto3.py @@ -142,6 +142,35 @@ def test_create_platform_endpoint(): "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/my-application/") +@mock_sns +def test_create_duplicate_platform_endpoint(): + conn = boto3.client('sns', region_name='us-east-1') + platform_application = conn.create_platform_application( + Name="my-application", + Platform="APNS", + Attributes={}, + ) + application_arn = platform_application['PlatformApplicationArn'] + + endpoint = conn.create_platform_endpoint( + PlatformApplicationArn=application_arn, + Token="some_unique_id", + CustomUserData="some user data", + Attributes={ + "Enabled": 'false', + }, + ) + + endpoint = conn.create_platform_endpoint.when.called_with( + PlatformApplicationArn=application_arn, + Token="some_unique_id", + CustomUserData="some user data", + Attributes={ + "Enabled": 'false', + }, + ).should.throw(ClientError) + + @mock_sns def test_get_list_endpoints_by_platform_application(): conn = boto3.client('sns', region_name='us-east-1') @@ -256,7 +285,7 @@ def test_publish_to_platform_endpoint(): Token="some_unique_id", CustomUserData="some user data", Attributes={ - "Enabled": 'false', + "Enabled": 'true', }, ) @@ -264,3 +293,31 @@ def test_publish_to_platform_endpoint(): conn.publish(Message="some message", MessageStructure="json", TargetArn=endpoint_arn) + + +@mock_sns +def test_publish_to_disabled_platform_endpoint(): + conn = boto3.client('sns', region_name='us-east-1') + platform_application = conn.create_platform_application( + Name="my-application", + Platform="APNS", + Attributes={}, + ) + application_arn = platform_application['PlatformApplicationArn'] + + endpoint = conn.create_platform_endpoint( + PlatformApplicationArn=application_arn, + Token="some_unique_id", + CustomUserData="some user data", + Attributes={ + "Enabled": 'false', + }, + ) + + endpoint_arn = endpoint['EndpointArn'] + + conn.publish.when.called_with( + Message="some message", + MessageStructure="json", + TargetArn=endpoint_arn, + ).should.throw(ClientError) From f2b7ba03b4cd8d46621dafbe4da98a69c1b775ee Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Fri, 17 Mar 2017 02:45:58 +0000 Subject: [PATCH 102/274] Forgot that lstrip works on character sets, not substrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I suppose this is one way to do it. I could have also split and taken the last element. Not sure which is best. 🤔 --- moto/route53/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index e3896a1c3..b823cb915 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -277,7 +277,7 @@ class Route53Backend(BaseBackend): return self.zones.values() def get_hosted_zone(self, id_): - return self.zones.get(id_.lstrip("/hostedzone/")) + return self.zones.get(id_.replace("/hostedzone/", "")) def get_hosted_zone_by_name(self, name): for zone in self.get_all_hosted_zones(): @@ -285,7 +285,7 @@ class Route53Backend(BaseBackend): return zone def delete_hosted_zone(self, id_): - return self.zones.pop(id_.lstrip("/hostedzone/"), None) + return self.zones.pop(id_.replace("/hostedzone/", ""), None) def create_health_check(self, health_check_args): health_check_id = str(uuid.uuid4()) From 6f4cb512ac39daa659e1e56f0b5243ea878b8566 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Fri, 17 Mar 2017 23:57:57 +0000 Subject: [PATCH 103/274] Allow CloudFormation stack tags to be updated Limitations: * does not update the tags of the resources in the stack. that can be implemented later. * does not support the supposed feature of clearing tags by passing an empty value that boto3 mentions in its documentation. I could not find anything in the request body to indicate when an empty value was passed. --- moto/cloudformation/models.py | 10 ++++++--- moto/cloudformation/responses.py | 11 +++++++++- .../test_cloudformation_stack_crud.py | 22 ++++++++++++++++++- .../test_cloudformation_stack_crud_boto3.py | 6 ++++- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index df9f4a139..b58d1dcf0 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -82,7 +82,7 @@ class FakeStack(BaseModel): def stack_outputs(self): return self.output_map.values() - def update(self, template, role_arn=None, parameters=None): + def update(self, template, role_arn=None, parameters=None, tags=None): self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") self.template = template self.resource_map.update(json.loads(template), parameters) @@ -90,6 +90,10 @@ class FakeStack(BaseModel): self._add_stack_event("UPDATE_COMPLETE") self.status = "UPDATE_COMPLETE" self.role_arn = role_arn + # only overwrite tags if passed + if tags is not None: + self.tags = tags + # TODO: update tags in the resource map def delete(self): self._add_stack_event("DELETE_IN_PROGRESS", @@ -164,9 +168,9 @@ class CloudFormationBackend(BaseBackend): if stack.name == name_or_stack_id: return stack - def update_stack(self, name, template, role_arn=None, parameters=None): + def update_stack(self, name, template, role_arn=None, parameters=None, tags=None): stack = self.get_stack(name) - stack.update(template, role_arn, parameters=parameters) + stack.update(template, role_arn, parameters=parameters, tags=tags) return stack def list_stack_resources(self, stack_name_or_id): diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 64923c7e6..f1e6d0415 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -152,6 +152,14 @@ class CloudFormationResponse(BaseResponse): for parameter in self._get_list_prefix("Parameters.member") ]) + # boto3 is supposed to let you clear the tags by passing an empty value, but the request body doesn't + # end up containing anything we can use to differentiate between passing an empty value versus not + # passing anything. so until that changes, moto won't be able to clear tags, only update them. + tags = dict((item['key'], item['value']) + for item in self._get_list_prefix("Tags.member")) + # so that if we don't pass the parameter, we don't clear all the tags accidentally + if not tags: + tags = None stack = self.cloudformation_backend.get_stack(stack_name) if stack.status == 'ROLLBACK_COMPLETE': @@ -162,7 +170,8 @@ class CloudFormationResponse(BaseResponse): name=stack_name, template=stack_body, role_arn=role_arn, - parameters=parameters + parameters=parameters, + tags=tags, ) if self.request_json: stack_body = { diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 4085e6d8f..eb3798f82 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -424,7 +424,7 @@ def test_update_stack(): @mock_cloudformation_deprecated -def test_update_stack(): +def test_update_stack_with_previous_template(): conn = boto.connect_cloudformation() conn.create_stack( "test_stack", @@ -482,6 +482,26 @@ def test_update_stack_with_parameters(): assert stack.parameters[0].value == "192.168.0.1/16" +@mock_cloudformation_deprecated +def test_update_stack_replace_tags(): + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + tags={"foo": "bar"}, + ) + conn.update_stack( + "test_stack", + template_body=dummy_template_json, + tags={"foo": "baz"}, + ) + + stack = conn.describe_stacks()[0] + stack.stack_status.should.equal("UPDATE_COMPLETE") + # since there is one tag it doesn't come out as a list + dict(stack.tags).should.equal({"foo": "baz"}) + + @mock_cloudformation_deprecated def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index c69766209..9a531010f 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -258,12 +258,15 @@ def test_describe_updated_stack(): cf_conn.create_stack( StackName="test_stack", TemplateBody=dummy_template_json, + Tags=[{'Key': 'foo', 'Value': 'bar'}], ) cf_conn.update_stack( StackName="test_stack", RoleARN='arn:aws:iam::123456789012:role/moto', - TemplateBody=dummy_update_template_json) + TemplateBody=dummy_update_template_json, + Tags=[{'Key': 'foo', 'Value': 'baz'}], + ) stack = cf_conn.describe_stacks(StackName="test_stack")['Stacks'][0] stack_id = stack['StackId'] @@ -272,6 +275,7 @@ def test_describe_updated_stack(): stack_by_id['StackName'].should.equal("test_stack") stack_by_id['StackStatus'].should.equal("UPDATE_COMPLETE") stack_by_id['RoleARN'].should.equal('arn:aws:iam::123456789012:role/moto') + stack_by_id['Tags'].should.equal([{'Key': 'foo', 'Value': 'baz'}]) @mock_cloudformation From a0a205328d44f6f423fe8f4f25be6b2e0af8b7b1 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 19 Mar 2017 11:03:22 -0400 Subject: [PATCH 104/274] Cleanup SQS body encoding. Closes #458, #460. --- moto/core/responses.py | 3 ++- tests/test_sqs/test_sqs.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/moto/core/responses.py b/moto/core/responses.py index 2d0e485ba..a5a1f3880 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -125,6 +125,7 @@ class BaseResponse(_TemplateEnvironmentMixin): for key, value in request.form.items(): querystring[key] = [value, ] + raw_body = self.body if isinstance(self.body, six.binary_type): self.body = self.body.decode('utf-8') @@ -143,7 +144,7 @@ class BaseResponse(_TemplateEnvironmentMixin): for key, value in flat.items(): querystring[key] = [value] elif self.body: - querystring.update(parse_qs(self.body, keep_blank_values=True)) + querystring.update(parse_qs(raw_body, keep_blank_values=True)) if not querystring: querystring.update(headers) diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 2889e520f..0df4c2dc9 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals import boto @@ -54,6 +55,20 @@ def test_message_send(): messages.should.have.length_of(1) +@mock_sqs +def test_send_message_with_unicode_characters(): + body_one = 'Héllo!😀' + + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + msg = queue.send_message(MessageBody=body_one) + + messages = queue.receive_messages() + message_body = messages[0].body + + message_body.should.equal(body_one) + + @mock_sqs def test_set_queue_attributes(): sqs = boto3.resource('sqs', region_name='us-east-1') From 2d05f8a79a0aa45776ee47f47615e68ea3d5602c Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 19 Mar 2017 11:09:30 -0400 Subject: [PATCH 105/274] Add functionality for iam get-user with current user. Closes #480. --- moto/iam/responses.py | 9 +++++++-- tests/test_iam/test_iam.py | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 318c04f3a..71e2993f9 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse -from .models import iam_backend +from .models import iam_backend, User class IamResponse(BaseResponse): @@ -234,7 +234,12 @@ class IamResponse(BaseResponse): def get_user(self): user_name = self._get_param('UserName') - user = iam_backend.get_user(user_name) + if user_name: + user = iam_backend.get_user(user_name) + else: + user = User(name='default_user') + # If no user is specific, IAM returns the current user + template = self.response_template(USER_TEMPLATE) return template.render(action='Get', user=user) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 076f33916..021d7c041 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -213,6 +213,14 @@ def test_get_user(): conn.get_user('my-user') +@mock_iam_deprecated() +def test_get_current_user(): + """If no user is specific, IAM returns the current user""" + conn = boto.connect_iam() + user = conn.get_user()['get_user_response']['get_user_result']['user'] + user['user_name'].should.equal('default_user') + + @mock_iam() def test_list_users(): path_prefix = '/' From bba197e29f7fdd635fcbcb2cd18993d839f93f39 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 19 Mar 2017 11:58:24 -0400 Subject: [PATCH 106/274] Make IAM ARNs more dynamic. Closes #663. --- moto/iam/models.py | 26 ++++++++++++++++++++--- moto/iam/responses.py | 48 +++++++++++++++++++------------------------ 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index ba6985895..c150d1c99 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -8,6 +8,8 @@ from moto.core import BaseBackend, BaseModel from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id +ACCOUNT_ID = 123456789012 + class Policy(BaseModel): @@ -82,6 +84,10 @@ class Role(BaseModel): return role + @property + def arn(self): + return "arn:aws:iam::{0}:role{1}{2}".format(ACCOUNT_ID, self.path, self.name) + def put_policy(self, policy_name, policy_json): self.policies[policy_name] = policy_json @@ -115,6 +121,10 @@ class InstanceProfile(BaseModel): role_ids=role_ids, ) + @property + def arn(self): + return "arn:aws:iam::{0}:instance-profile{1}{2}".format(ACCOUNT_ID, self.path, self.name) + @property def physical_resource_id(self): return self.name @@ -132,13 +142,17 @@ class Certificate(BaseModel): self.cert_name = cert_name self.cert_body = cert_body self.private_key = private_key - self.path = path + self.path = path if path else "/" self.cert_chain = cert_chain @property def physical_resource_id(self): return self.name + @property + def arn(self): + return "arn:aws:iam::{0}:server-certificate{1}{2}".format(ACCOUNT_ID, self.path, self.cert_name) + class AccessKey(BaseModel): @@ -179,6 +193,10 @@ class Group(BaseModel): raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') raise UnformattedGetAttTemplateException() + @property + def arn(self): + return "arn:aws:iam::{0}:group/{1}".format(ACCOUNT_ID, self.path) + def get_policy(self, policy_name): try: policy_json = self.policies[policy_name] @@ -208,12 +226,14 @@ class User(BaseModel): datetime.utcnow(), "%Y-%m-%d-%H-%M-%S" ) - self.arn = 'arn:aws:iam::123456789012:user{0}{1}'.format( - self.path, name) self.policies = {} self.access_keys = [] self.password = None + @property + def arn(self): + return "arn:aws:iam::{0}:user{1}{2}".format(ACCOUNT_ID, self.path, self.name) + def get_policy(self, policy_name): policy_json = None try: diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 71e2993f9..27e69537d 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -439,7 +439,7 @@ CREATE_INSTANCE_PROFILE_TEMPLATE = """ {% if certificate.path %} {{ certificate.path }} {% endif %} - arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} + {{ certificate.arn }} 2010-05-08T01:02:03.004Z ASCACKCEVSQ6C2EXAMPLE 2012-05-08T01:02:03.004Z @@ -623,11 +623,9 @@ LIST_SERVER_CERTIFICATES_TEMPLATE = """ {{ certificate.cert_name }} {% if certificate.path %} - {{ certificate.path }} - arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} - {% else %} - arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} + {{ certificate.path }} {% endif %} + {{ certificate.arn }} 2010-05-08T01:02:03.004Z ASCACKCEVSQ6C2EXAMPLE 2012-05-08T01:02:03.004Z @@ -646,11 +644,9 @@ GET_SERVER_CERTIFICATE_TEMPLATE = """ {{ certificate.cert_name }} {% if certificate.path %} - {{ certificate.path }} - arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} - {% else %} - arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} + {{ certificate.path }} {% endif %} + {{ certificate.arn }} 2010-05-08T01:02:03.004Z ASCACKCEVSQ6C2EXAMPLE 2012-05-08T01:02:03.004Z @@ -669,7 +665,7 @@ CREATE_GROUP_TEMPLATE = """ {{ group.path }} {{ group.name }} {{ group.id }} - arn:aws:iam::123456789012:group/{{ group.path }} + {{ group.arn }} @@ -683,7 +679,7 @@ GET_GROUP_TEMPLATE = """ {{ group.path }} {{ group.name }} {{ group.id }} - arn:aws:iam::123456789012:group/{{ group.path }} + {{ group.arn }} {% for user in group.users %} @@ -691,9 +687,7 @@ GET_GROUP_TEMPLATE = """ {{ user.path }} {{ user.name }} {{ user.id }} - - arn:aws:iam::123456789012:user/{{ user.path }}/{{ user.name}} - + {{ user.arn }} {% endfor %} @@ -712,7 +706,7 @@ LIST_GROUPS_TEMPLATE = """ {{ group.path }} {{ group.name }} {{ group.id }} - arn:aws:iam::123456789012:group/{{ group.path }} + {{ group.arn }} {% endfor %} @@ -731,7 +725,7 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """ {{ group.path }} {{ group.name }} {{ group.id }} - arn:aws:iam::123456789012:group/{{ group.path }} + {{ group.arn }} {% endfor %} @@ -778,7 +772,7 @@ USER_TEMPLATE = """<{{ action }}UserResponse> {{ user.path }} {{ user.name }} {{ user.id }} - arn:aws:iam::123456789012:user/{{ user.path }}/{{ user.name }} + {{ user.arn }} @@ -908,7 +902,7 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """ {{ role.path }} - arn:aws:iam::123456789012:role{{ role.path }}S3Access + {{ role.arn }} {{ role.name }} {{ role.assume_policy_document }} 2012-05-09T15:45:35Z @@ -918,7 +912,7 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """ {{ profile.name }} {{ profile.path }} - arn:aws:iam::123456789012:instance-profile{{ profile.path }}Webserver + {{ profile.arn }} 2012-05-09T16:27:11Z {% endfor %} From 6e209bb14cb2edc5719fd27177574423eaf0b27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valent=C3=ADn=20Guti=C3=A9rrez?= Date: Wed, 22 Mar 2017 14:33:19 +0100 Subject: [PATCH 107/274] Implement availability-zone filter for DescribeNetworkInterfaces --- moto/ec2/models.py | 2 ++ .../test_elastic_network_interfaces.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 989ec5572..64cc02e09 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -256,6 +256,8 @@ class NetworkInterface(TaggedEC2Resource): return self.subnet.vpc_id elif filter_name == 'group-id': return [group.id for group in self._group_set] + elif filter_name == 'availability-zone': + return self.subnet.availability_zone filter_value = super( NetworkInterface, self).get_filter_value(filter_name) diff --git a/tests/test_ec2/test_elastic_network_interfaces.py b/tests/test_ec2/test_elastic_network_interfaces.py index 4ec23b919..828f9d917 100644 --- a/tests/test_ec2/test_elastic_network_interfaces.py +++ b/tests/test_ec2/test_elastic_network_interfaces.py @@ -228,6 +228,37 @@ def test_elastic_network_interfaces_get_by_tag_name(): enis.should.have.length_of(0) +@mock_ec2 +def test_elastic_network_interfaces_get_by_availability_zone(): + ec2 = boto3.resource('ec2', region_name='us-west-2') + ec2_client = boto3.client('ec2', region_name='us-west-2') + + vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') + subnet1 = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.0.0/24', AvailabilityZone='us-west-2a') + + subnet2 = ec2.create_subnet( + VpcId=vpc.id, CidrBlock='10.0.1.0/24', AvailabilityZone='us-west-2b') + + eni1 = ec2.create_network_interface( + SubnetId=subnet1.id, PrivateIpAddress='10.0.0.15') + + eni2 = ec2.create_network_interface( + SubnetId=subnet2.id, PrivateIpAddress='10.0.1.15') + + # The status of the new interface should be 'available' + waiter = ec2_client.get_waiter('network_interface_available') + waiter.wait(NetworkInterfaceIds=[eni1.id, eni2.id]) + + filters = [{'Name': 'availability-zone', 'Values': ['us-west-2a']}] + enis = list(ec2.network_interfaces.filter(Filters=filters)) + enis.should.have.length_of(1) + + filters = [{'Name': 'availability-zone', 'Values': ['us-west-2c']}] + enis = list(ec2.network_interfaces.filter(Filters=filters)) + enis.should.have.length_of(0) + + @mock_ec2 def test_elastic_network_interfaces_get_by_private_ip(): ec2 = boto3.resource('ec2', region_name='us-west-2') From 8b9d685f1c37b33c1b36f986d9e62a9dd5d6d5f5 Mon Sep 17 00:00:00 2001 From: Sean Marlow Date: Mon, 27 Mar 2017 12:08:57 -0600 Subject: [PATCH 108/274] Add mfa device endpoints to iam backend. - Add mfa device class - Add mfa devices dictionary to user class - Add responses, endpoints and tests --- moto/iam/models.py | 60 ++++++++++++++++++++++++++++++++++++++ moto/iam/responses.py | 46 +++++++++++++++++++++++++++++ tests/test_iam/test_iam.py | 23 +++++++++++++++ 3 files changed, 129 insertions(+) diff --git a/moto/iam/models.py b/moto/iam/models.py index c150d1c99..c219c1afc 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -11,6 +11,19 @@ from .utils import random_access_key, random_alphanumeric, random_resource_id, r ACCOUNT_ID = 123456789012 +class MFADevice(object): + """MFA Device class.""" + + def __init__(self, + serial_number, + authentication_code_1, + authentication_code_2): + self.enable_date = datetime.now(pytz.utc) + self.serial_number = serial_number + self.authentication_code_1 = authentication_code_1 + self.authentication_code_2 = authentication_code_2 + + class Policy(BaseModel): is_attachable = False @@ -226,6 +239,7 @@ class User(BaseModel): datetime.utcnow(), "%Y-%m-%d-%H-%M-%S" ) + self.mfa_devices = {} self.policies = {} self.access_keys = [] self.password = None @@ -251,6 +265,9 @@ class User(BaseModel): def put_policy(self, policy_name, policy_json): self.policies[policy_name] = policy_json + def deactivate_mfa_device(self, serial_number): + self.mfa_devices.pop(serial_number) + def delete_policy(self, policy_name): if policy_name not in self.policies: raise IAMNotFoundException( @@ -263,6 +280,16 @@ class User(BaseModel): self.access_keys.append(access_key) return access_key + def enable_mfa_device(self, + serial_number, + authentication_code_1, + authentication_code_2): + self.mfa_devices[serial_number] = MFADevice( + serial_number, + authentication_code_1, + authentication_code_2 + ) + def get_all_access_keys(self): return self.access_keys @@ -724,6 +751,39 @@ class IAMBackend(BaseBackend): user = self.get_user(user_name) user.delete_access_key(access_key_id) + def enable_mfa_device(self, + user_name, + serial_number, + authentication_code_1, + authentication_code_2): + """Enable MFA Device for user.""" + user = self.get_user(user_name) + if serial_number in user.mfa_devices: + raise IAMConflictException( + "EntityAlreadyExists", + "Device {0} already exists".format(serial_number) + ) + + user.enable_mfa_device( + serial_number, + authentication_code_1, + authentication_code_2 + ) + + def deactivate_mfa_device(self, user_name, serial_number): + """Deactivate and detach MFA Device from user if device exists.""" + user = self.get_user(user_name) + if serial_number not in user.mfa_devices: + raise IAMNotFoundException( + "Device {0} not found".format(serial_number) + ) + + user.deactivate_mfa_device(serial_number) + + def list_mfa_devices(self, user_name): + user = self.get_user(user_name) + return user.mfa_devices.values() + def delete_user(self, user_name): try: del self.users[user_name] diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 27e69537d..0757d7eee 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -326,6 +326,35 @@ class IamResponse(BaseResponse): template = self.response_template(GENERIC_EMPTY_TEMPLATE) return template.render(name='DeleteAccessKey') + def deactivate_mfa_device(self): + user_name = self._get_param('UserName') + serial_number = self._get_param('SerialNumber') + + iam_backend.deactivate_mfa_device(user_name, serial_number) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name='DeactivateMFADevice') + + def enable_mfa_device(self): + user_name = self._get_param('UserName') + serial_number = self._get_param('SerialNumber') + authentication_code_1 = self._get_param('AuthenticationCode1') + authentication_code_2 = self._get_param('AuthenticationCode2') + + iam_backend.enable_mfa_device( + user_name, + serial_number, + authentication_code_1, + authentication_code_2 + ) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name='EnableMFADevice') + + def list_mfa_devices(self): + user_name = self._get_param('UserName') + devices = iam_backend.list_mfa_devices(user_name) + template = self.response_template(LIST_MFA_DEVICES_TEMPLATE) + return template.render(user_name=user_name, devices=devices) + def delete_user(self): user_name = self._get_param('UserName') iam_backend.delete_user(user_name) @@ -922,3 +951,20 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """6a8c3992-99f4-11e1-a4c3-27EXAMPLE804 """ + +LIST_MFA_DEVICES_TEMPLATE = """ + + + {% for device in devices %} + + {{ user_name }} + {{ device.serial_number }} + + {% endfor %} + + false + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 021d7c041..1ae892f62 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -292,6 +292,29 @@ def test_delete_access_key(): conn.delete_access_key(access_key_id, 'my-user') +@mock_iam() +def test_mfa_devices(): + # Test enable device + conn = boto3.client('iam', region_name='us-east-1') + conn.create_user(UserName='my-user') + conn.enable_mfa_device( + UserName='my-user', + SerialNumber='123456789', + AuthenticationCode1='234567', + AuthenticationCode2='987654' + ) + + # Test list mfa devices + response = conn.list_mfa_devices(UserName='my-user') + device = response['MFADevices'][0] + device['SerialNumber'].should.equal('123456789') + + # Test deactivate mfa device + conn.deactivate_mfa_device(UserName='my-user', SerialNumber='123456789') + response = conn.list_mfa_devices(UserName='my-user') + len(response['MFADevices']).should.equal(0) + + @mock_iam_deprecated() def test_delete_user(): conn = boto.connect_iam() From a5727bf64a682c279e6b04a76391d0476f11fea2 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Thu, 6 Apr 2017 21:09:58 +1000 Subject: [PATCH 109/274] fix SQS message polling to abort after wait_seconds_timeout --- moto/sqs/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 9dff700e7..cedf03199 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -289,6 +289,10 @@ class SQSBackend(BaseBackend): # queue.messages only contains visible messages while True: + + if result or (wait_seconds_timeout and unix_time() > polling_end): + break + if len(queue.messages) == 0: import time time.sleep(0.001) @@ -304,9 +308,6 @@ class SQSBackend(BaseBackend): if len(result) >= count: break - if result or unix_time() > polling_end: - break - return result def delete_message(self, queue_name, receipt_handle): From b8a41c56058f1ac4f6ba4738e9d412f612b5dd23 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 8 Apr 2017 15:28:51 +1000 Subject: [PATCH 110/274] fix domain handling for local domain names in S3 API --- moto/s3/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 449fed0a9..59cd9d322 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -53,8 +53,8 @@ class ResponseObject(_TemplateEnvironmentMixin): if not host: host = urlparse(request.url).netloc - if not host or host.startswith("localhost"): - # For localhost, default to path-based buckets + if not host or host.startswith("localhost") or re.match(r"^[^.]+$", host): + # For localhost or local domain names, default to path-based buckets return False match = re.match(r'^([^\[\]:]+)(:\d+)?$', host) From 880f3fb9506c9dd70c50e75a53a78148b402cba4 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Wed, 12 Apr 2017 13:30:32 +0100 Subject: [PATCH 111/274] Container Instance Resource implementation --- moto/ecs/models.py | 163 +++++++++++++++++--- moto/ecs/responses.py | 4 +- tests/test_ecs/test_ecs_boto3.py | 247 ++++++++++++++++++++++++++++++- 3 files changed, 384 insertions(+), 30 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index e5a2e9f96..aadc76bce 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals import uuid -from random import randint, random +from random import random from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends @@ -130,7 +130,6 @@ class TaskDefinition(BaseObject): original_resource.volumes != volumes): # currently TaskRoleArn isn't stored at TaskDefinition # instances - ecs_backend = ecs_backends[region_name] ecs_backend.deregister_task_definition(original_resource.arn) return ecs_backend.register_task_definition( @@ -142,7 +141,8 @@ class TaskDefinition(BaseObject): class Task(BaseObject): - def __init__(self, cluster, task_definition, container_instance_arn, overrides={}, started_by=''): + def __init__(self, cluster, task_definition, container_instance_arn, + resource_requirements, overrides={}, started_by=''): self.cluster_arn = cluster.arn self.task_arn = 'arn:aws:ecs:us-east-1:012345678910:task/{0}'.format( str(uuid.uuid1())) @@ -154,6 +154,7 @@ class Task(BaseObject): self.containers = [] self.started_by = started_by self.stopped_reason = '' + self.resource_requirements = resource_requirements @property def response_object(self): @@ -240,12 +241,57 @@ class ContainerInstance(BaseObject): def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id self.status = 'ACTIVE' - self.registeredResources = [] + self.registeredResources = [ + {'doubleValue': 0.0, + 'integerValue': 4096, + 'longValue': 0, + 'name': 'CPU', + 'type': 'INTEGER'}, + {'doubleValue': 0.0, + 'integerValue': 7482, + 'longValue': 0, + 'name': 'MEMORY', + 'type': 'INTEGER'}, + {'doubleValue': 0.0, + 'integerValue': 0, + 'longValue': 0, + 'name': 'PORTS', + 'stringSetValue': ['22', '2376', '2375', '51678', '51679'], + 'type': 'STRINGSET'}, + {'doubleValue': 0.0, + 'integerValue': 0, + 'longValue': 0, + 'name': 'PORTS_UDP', + 'stringSetValue': [], + 'type': 'STRINGSET'}] self.agentConnected = True self.containerInstanceArn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( str(uuid.uuid1())) self.pendingTaskCount = 0 - self.remainingResources = [] + self.remainingResources = [ + {'doubleValue': 0.0, + 'integerValue': 4096, + 'longValue': 0, + 'name': 'CPU', + 'type': 'INTEGER'}, + {'doubleValue': 0.0, + 'integerValue': 7482, + 'longValue': 0, + 'name': 'MEMORY', + 'type': 'INTEGER'}, + {'doubleValue': 0.0, + 'integerValue': 0, + 'longValue': 0, + 'name': 'PORTS', + 'stringSetValue': ['22', '2376', '2375', '51678', '51679'], + 'type': 'STRINGSET'}, + {'doubleValue': 0.0, + 'integerValue': 0, + 'longValue': 0, + 'name': 'PORTS_UDP', + 'stringSetValue': [], + 'type': 'STRINGSET'} + ] self.runningTaskCount = 0 self.versionInfo = { 'agentVersion': "1.0.0", @@ -380,20 +426,72 @@ class EC2ContainerServiceBackend(BaseBackend): container_instances = list( self.container_instances.get(cluster_name, {}).keys()) if not container_instances: - raise Exception( - "No instances found in cluster {}".format(cluster_name)) + raise Exception("No instances found in cluster {}".format(cluster_name)) active_container_instances = [x for x in container_instances if self.container_instances[cluster_name][x].status == 'ACTIVE'] - for _ in range(count or 1): - container_instance_arn = self.container_instances[cluster_name][ - active_container_instances[randint(0, len(active_container_instances) - 1)] - ].containerInstanceArn - task = Task(cluster, task_definition, container_instance_arn, - overrides or {}, started_by or '') - tasks.append(task) - self.tasks[cluster_name][task.task_arn] = task + resource_requirements = self._calculate_task_resource_requirements(task_definition) + # TODO: return event about unable to place task if not able to place enough tasks to meet count + placed_count = 0 + for container_instance in active_container_instances: + container_instance = self.container_instances[cluster_name][container_instance] + container_instance_arn = container_instance.containerInstanceArn + try_to_place = True + while try_to_place: + can_be_placed, message = self._can_be_placed(container_instance, resource_requirements) + if can_be_placed: + task = Task(cluster, task_definition, container_instance_arn, + resource_requirements, overrides or {}, started_by or '') + self.update_container_instance_resources(container_instance, resource_requirements) + tasks.append(task) + self.tasks[cluster_name][task.task_arn] = task + placed_count += 1 + if placed_count == count: + return tasks + else: + try_to_place = False return tasks + @staticmethod + def _calculate_task_resource_requirements(task_definition): + resource_requirements = {"CPU": 0, "MEMORY": 0, "PORTS": [], "PORTS_UDP": []} + for container_definition in task_definition.container_definitions: + resource_requirements["CPU"] += container_definition.get('cpu') + resource_requirements["MEMORY"] += container_definition.get("memory") + for port_mapping in container_definition.get("portMappings", []): + resource_requirements["PORTS"].append(port_mapping.get('hostPort')) + return resource_requirements + + @staticmethod + def _can_be_placed(container_instance, task_resource_requirements): + """ + + :param container_instance: The container instance trying to be placed onto + :param task_resource_requirements: The calculated resource requirements of the task in the form of a dict + :return: A boolean stating whether the given container instance has enough resources to have the task placed on + it as well as a description, if it cannot be placed this will describe why. + """ + # TODO: Implement default and other placement strategies as well as constraints: + # docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement.html + remaining_cpu = 0 + remaining_memory = 0 + reserved_ports = [] + for resource in container_instance.remainingResources: + if resource.get("name") == "CPU": + remaining_cpu = resource.get("integerValue") + elif resource.get("name") == "MEMORY": + remaining_memory = resource.get("integerValue") + elif resource.get("name") == "PORTS": + reserved_ports = resource.get("stringSetValue") + if task_resource_requirements.get("CPU") > remaining_cpu: + return False, "Not enough CPU credits" + if task_resource_requirements.get("MEMORY") > remaining_memory: + return False, "Not enough memory" + ports_needed = task_resource_requirements.get("PORTS") + for port in ports_needed: + if str(port) in reserved_ports: + return False, "Port clash" + return True, "Can be placed" + def start_task(self, cluster_str, task_definition_str, container_instances, overrides, started_by): cluster_name = cluster_str.split('/')[-1] if cluster_name in self.clusters: @@ -409,14 +507,15 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance_ids = [x.split('/')[-1] for x in container_instances] - + resource_requirements = self._calculate_task_resource_requirements(task_definition) for container_instance_id in container_instance_ids: - container_instance_arn = self.container_instances[cluster_name][ + container_instance = self.container_instances[cluster_name][ container_instance_id - ].containerInstanceArn - task = Task(cluster, task_definition, container_instance_arn, - overrides or {}, started_by or '') + ] + task = Task(cluster, task_definition, container_instance.containerInstanceArn, + resource_requirements, overrides or {}, started_by or '') tasks.append(task) + self.update_container_instance_resources(container_instance, resource_requirements) self.tasks[cluster_name][task.task_arn] = task return tasks @@ -470,6 +569,10 @@ class EC2ContainerServiceBackend(BaseBackend): "Cluster {} has no registered tasks".format(cluster_name)) for task in tasks.keys(): if task.endswith(task_id): + container_instance_arn = tasks[task].container_instance_arn + container_instance = self.container_instances[cluster_name][container_instance_arn.split('/')[-1]] + self.update_container_instance_resources(container_instance, tasks[task].resource_requirements, + removing=True) tasks[task].last_status = 'STOPPED' tasks[task].desired_status = 'STOPPED' tasks[task].stopped_reason = reason @@ -566,6 +669,7 @@ class EC2ContainerServiceBackend(BaseBackend): failures = [] container_instance_objects = [] for container_instance_id in list_container_instance_ids: + container_instance_id = container_instance_id.split('/')[-1] container_instance = self.container_instances[ cluster_name].get(container_instance_id, None) if container_instance is not None: @@ -582,7 +686,8 @@ class EC2ContainerServiceBackend(BaseBackend): raise Exception("{0} is not a cluster".format(cluster_name)) status = status.upper() if status not in ['ACTIVE', 'DRAINING']: - raise Exception("An error occurred (InvalidParameterException) when calling the UpdateContainerInstancesState operation: Container instances status should be one of [ACTIVE,DRAINING]") + raise Exception("An error occurred (InvalidParameterException) when calling the UpdateContainerInstancesState operation: \ + Container instances status should be one of [ACTIVE,DRAINING]") failures = [] container_instance_objects = [] for container_instance_id in list_container_instance_ids: @@ -595,6 +700,22 @@ class EC2ContainerServiceBackend(BaseBackend): return container_instance_objects, failures + def update_container_instance_resources(self, container_instance, task_resources, removing=False): + resource_multiplier = 1 + if removing: + resource_multiplier = -1 + for resource in container_instance.remainingResources: + if resource.get("name") == "CPU": + resource["integerValue"] -= task_resources.get('CPU') * resource_multiplier + elif resource.get("name") == "MEMORY": + resource["integerValue"] -= task_resources.get('MEMORY') * resource_multiplier + elif resource.get("name") == "PORTS": + for port in task_resources.get("PORTS"): + if removing: + resource["stringSetValue"].remove(str(port)) + else: + resource["stringSetValue"].append(str(port)) + def deregister_container_instance(self, cluster_str, container_instance_str): pass diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 338cfec28..9c4524816 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -225,7 +225,9 @@ class EC2ContainerServiceResponse(BaseResponse): cluster_str = self._get_param('cluster') list_container_instance_arns = self._get_param('containerInstances') status_str = self._get_param('status') - container_instances, failures = self.ecs_backend.update_container_instances_state(cluster_str, list_container_instance_arns, status_str) + container_instances, failures = self.ecs_backend.update_container_instances_state(cluster_str, + list_container_instance_arns, + status_str) return json.dumps({ 'failures': [ci.response_object for ci in failures], 'containerInstances': [ci.response_object for ci in container_instances] diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 82b6be195..e76be8cbe 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -531,8 +531,8 @@ def test_register_container_instance(): 'arn:aws:ecs:us-east-1:012345678910:container-instance') arn_part[1].should.equal(str(UUID(arn_part[1]))) response['containerInstance']['status'].should.equal('ACTIVE') - len(response['containerInstance']['registeredResources']).should.equal(0) - len(response['containerInstance']['remainingResources']).should.equal(0) + len(response['containerInstance']['registeredResources']).should.equal(4) + len(response['containerInstance']['remainingResources']).should.equal(4) response['containerInstance']['agentConnected'].should.equal(True) response['containerInstance']['versionInfo'][ 'agentVersion'].should.equal('1.0.0') @@ -622,6 +622,7 @@ def test_describe_container_instances(): for arn in test_instance_arns: response_arns.should.contain(arn) + @mock_ec2 @mock_ecs def test_update_container_instances_state(): @@ -653,26 +654,33 @@ def test_update_container_instances_state(): test_instance_arns.append(response['containerInstance']['containerInstanceArn']) test_instance_ids = list(map((lambda x: x.split('/')[1]), test_instance_arns)) - response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, + containerInstances=test_instance_ids, + status='DRAINING') len(response['failures']).should.equal(0) len(response['containerInstances']).should.equal(instance_to_create) response_statuses = [ci['status'] for ci in response['containerInstances']] for status in response_statuses: status.should.equal('DRAINING') - response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, + containerInstances=test_instance_ids, + status='DRAINING') len(response['failures']).should.equal(0) len(response['containerInstances']).should.equal(instance_to_create) response_statuses = [ci['status'] for ci in response['containerInstances']] for status in response_statuses: status.should.equal('DRAINING') - response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='ACTIVE') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, + containerInstances=test_instance_ids, + status='ACTIVE') len(response['failures']).should.equal(0) len(response['containerInstances']).should.equal(instance_to_create) response_statuses = [ci['status'] for ci in response['containerInstances']] for status in response_statuses: status.should.equal('ACTIVE') - ecs_client.update_container_instances_state.when.called_with(cluster=test_cluster_name, containerInstances=test_instance_ids, status='test_status').should.throw(Exception) - + ecs_client.update_container_instances_state.when.called_with(cluster=test_cluster_name, + containerInstances=test_instance_ids, + status='test_status').should.throw(Exception) @mock_ec2 @@ -838,7 +846,7 @@ def test_list_tasks(): ec2_utils.generate_instance_identity_document(test_instance) ) - response = client.register_container_instance( + _ = client.register_container_instance( cluster=test_cluster_name, instanceIdentityDocument=instance_id_document ) @@ -1042,6 +1050,88 @@ def test_stop_task(): stop_response['task']['stoppedReason'].should.equal('moto testing') +@mock_ec2 +@mock_ecs +def test_resource_reservation_and_release(): + client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + + _ = client.create_cluster( + clusterName=test_cluster_name + ) + + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + _ = client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'}, + 'portMappings': [ + { + 'hostPort': 80, + 'containerPort': 8080 + } + ] + } + ] + ) + run_response = client.run_task( + cluster='test_ecs_cluster', + overrides={}, + taskDefinition='test_ecs_task', + count=1, + startedBy='moto' + ) + container_instance_arn = run_response['tasks'][0].get('containerInstanceArn') + container_instance_description = client.describe_container_instances( + cluster='test_ecs_cluster', + containerInstances=[container_instance_arn] + )['containerInstances'][0] + remaining_resources, registered_resources = _fetch_container_instance_resources(container_instance_description) + remaining_resources['CPU'].should.equal(registered_resources['CPU'] - 1024) + remaining_resources['MEMORY'].should.equal(registered_resources['MEMORY'] - 400) + registered_resources['PORTS'].append('80') + remaining_resources['PORTS'].should.equal(registered_resources['PORTS']) + client.stop_task( + cluster='test_ecs_cluster', + task=run_response['tasks'][0].get('taskArn'), + reason='moto testing' + ) + container_instance_description = client.describe_container_instances( + cluster='test_ecs_cluster', + containerInstances=[container_instance_arn] + )['containerInstances'][0] + remaining_resources, registered_resources = _fetch_container_instance_resources(container_instance_description) + remaining_resources['CPU'].should.equal(registered_resources['CPU']) + remaining_resources['MEMORY'].should.equal(registered_resources['MEMORY']) + remaining_resources['PORTS'].should.equal(registered_resources['PORTS']) + + @mock_ecs @mock_cloudformation def test_create_cluster_through_cloudformation(): @@ -1141,6 +1231,133 @@ def test_create_task_definition_through_cloudformation(): len(resp['taskDefinitionArns']).should.equal(1) +@mock_ec2 +@mock_ecs +def test_task_definitions_unable_to_be_placed(): + client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + + _ = client.create_cluster( + clusterName=test_cluster_name + ) + + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 5000, + 'memory': 40000, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.run_task( + cluster='test_ecs_cluster', + overrides={}, + taskDefinition='test_ecs_task', + count=2, + startedBy='moto' + ) + len(response['tasks']).should.equal(0) + + +@mock_ec2 +@mock_ecs +def test_task_definitions_with_port_clash(): + client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + + _ = client.create_cluster( + clusterName=test_cluster_name + ) + + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 256, + 'memory': 512, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'}, + 'portMappings': [ + { + 'hostPort': 80, + 'containerPort': 8080 + } + ] + } + ] + ) + response = client.run_task( + cluster='test_ecs_cluster', + overrides={}, + taskDefinition='test_ecs_task', + count=2, + startedBy='moto' + ) + len(response['tasks']).should.equal(1) + response['tasks'][0]['taskArn'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:task/') + response['tasks'][0]['clusterArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:cluster/test_ecs_cluster') + response['tasks'][0]['taskDefinitionArn'].should.equal( + 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') + response['tasks'][0]['containerInstanceArn'].should.contain( + 'arn:aws:ecs:us-east-1:012345678910:container-instance/') + response['tasks'][0]['overrides'].should.equal({}) + response['tasks'][0]['lastStatus'].should.equal("RUNNING") + response['tasks'][0]['desiredStatus'].should.equal("RUNNING") + response['tasks'][0]['startedBy'].should.equal("moto") + response['tasks'][0]['stoppedReason'].should.equal("") + + @mock_ecs @mock_cloudformation def test_update_task_definition_family_through_cloudformation_should_trigger_a_replacement(): @@ -1294,3 +1511,17 @@ def test_update_service_through_cloudformation_should_trigger_replacement(): ecs_conn = boto3.client('ecs', region_name='us-west-1') resp = ecs_conn.list_services(cluster='testcluster') len(resp['serviceArns']).should.equal(1) + + +def _fetch_container_instance_resources(container_instance_description): + remaining_resources = {} + registered_resources = {} + remaining_resources_list = container_instance_description['remainingResources'] + registered_resources_list = container_instance_description['registeredResources'] + remaining_resources['CPU'] = [x['integerValue'] for x in remaining_resources_list if x['name'] == 'CPU'][0] + remaining_resources['MEMORY'] = [x['integerValue'] for x in remaining_resources_list if x['name'] == 'MEMORY'][0] + remaining_resources['PORTS'] = [x['stringSetValue'] for x in remaining_resources_list if x['name'] == 'PORTS'][0] + registered_resources['CPU'] = [x['integerValue'] for x in registered_resources_list if x['name'] == 'CPU'][0] + registered_resources['MEMORY'] = [x['integerValue'] for x in registered_resources_list if x['name'] == 'MEMORY'][0] + registered_resources['PORTS'] = [x['stringSetValue'] for x in registered_resources_list if x['name'] == 'PORTS'][0] + return remaining_resources, registered_resources From 03c4d9fe204264cfe1c4378835db8c149ff1f281 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 12 Apr 2017 20:40:55 -0400 Subject: [PATCH 112/274] Fix standalone server headers not having HTTP_AUTHORIZATION. Closes #874. --- moto/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/server.py b/moto/server.py index a23580065..e5426bc7a 100644 --- a/moto/server.py +++ b/moto/server.py @@ -59,7 +59,7 @@ class DomainDispatcherApplication(object): try: _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[ 1].split("/") - except ValueError: + except (KeyError, ValueError): region = 'us-east-1' service = 's3' if service == 'dynamodb': From bf935c210bf2fa419af8d088402f84122f44bdb2 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 12 Apr 2017 22:15:18 -0400 Subject: [PATCH 113/274] Fix cloudwatch events delete_rule. Closes #884. --- moto/__init__.py | 1 + moto/events/responses.py | 3 ++- tests/test_events/test_events.py | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/moto/__init__.py b/moto/__init__.py index 546603b00..7cc9594fb 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -17,6 +17,7 @@ from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa from .ecs import mock_ecs, mock_ecs_deprecated # flake8: noqa from .elb import mock_elb, mock_elb_deprecated # flake8: noqa from .emr import mock_emr, mock_emr_deprecated # flake8: noqa +from .events import mock_events # flake8: noqa from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa from .opsworks import mock_opsworks, mock_opsworks_deprecated # flake8: noqa from .iam import mock_iam, mock_iam_deprecated # flake8: noqa diff --git a/moto/events/responses.py b/moto/events/responses.py index d03befe12..8f433844a 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -29,10 +29,11 @@ class EventsHandler(BaseResponse): def delete_rule(self): body = self.load_body() - name = body.get('NamePrefix') + name = body.get('Name') if not name: return self.error('ValidationException', 'Parameter Name is required.') + events_backend.delete_rule(name) return '', self.response_headers diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 537b741f2..da8238f72 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -132,6 +132,15 @@ def test_list_rules(): assert(len(rules['Rules']) == len(RULES)) +@mock_events +def test_delete_rule(): + client = generate_environment() + + client.delete_rule(Name=RULES[0]['Name']) + rules = client.list_rules() + assert(len(rules['Rules']) == len(RULES) - 1) + + @mock_events def test_list_targets_by_rule(): rule_name = get_random_rule()['Name'] From b62015b27dc5a90077d9d188533b0b1f326d5699 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 12 Apr 2017 22:19:48 -0400 Subject: [PATCH 114/274] Add Iam User CreateDate. Closes #886. --- moto/iam/responses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 0757d7eee..17fed1c2a 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -817,6 +817,7 @@ LIST_USERS_TEMPLATE = """<{{ action }}UsersResponse> {{ user.id }} {{ user.path }} {{ user.name }} + {{ user.created }} {{ user.arn }} {% endfor %} From 30b1de507cb08794edade449505fbddd0a8a043b Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 12 Apr 2017 22:25:07 -0400 Subject: [PATCH 115/274] Make ELB created_time dynamic. Closes #887. --- moto/elb/models.py | 2 ++ moto/elb/responses.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/moto/elb/models.py b/moto/elb/models.py index 234e5ea58..4136fdc78 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import datetime from boto.ec2.elb.attributes import ( LbAttributes, ConnectionSettingAttribute, @@ -66,6 +67,7 @@ class FakeLoadBalancer(BaseModel): self.zones = zones self.listeners = [] self.backends = [] + self.created_time = datetime.datetime.now() self.scheme = scheme self.attributes = FakeLoadBalancer.get_default_attributes() self.policies = Policies() diff --git a/moto/elb/responses.py b/moto/elb/responses.py index 5402ea964..262038a43 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -391,7 +391,7 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ 0: + raise Exception("Found running tasks on the instance.") + # Currently assume that people might want to do something based around deregistered instances + # with tasks left running on them - but nothing if deregistration is forced or no tasks were + # running already + elif force and container_instance.running_tasks_count > 0: + if not self.container_instances.get('orphaned'): + self.container_instances['orphaned'] = {} + self.container_instances['orphaned'][container_instance_id] = container_instance + del(self.container_instances[cluster_name][container_instance_id]) + self._respond_to_cluster_state_update(cluster_str) + pass + + def _respond_to_cluster_state_update(self, cluster_str): + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) pass diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 9c4524816..6c8fd8e2d 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -203,6 +203,17 @@ class EC2ContainerServiceResponse(BaseResponse): 'containerInstance': container_instance.response_object }) + def deregister_container_instance(self): + cluster_str = self._get_param('cluster', 'default'), + container_instance_str = self._get_param('containerInstance') + force = self._get_param('force', False) + container_instance = self.ecs_backend.deregister_container_instance( + cluster_str, container_instance_str, force + ) + return json.dumps({ + 'containerInstance': container_instance.response_object + }) + def list_container_instances(self): cluster_str = self._get_param('cluster') container_instance_arns = self.ecs_backend.list_container_instances( From acb6c3ce01d36a3a90d80d7b10eb8e9899ae3a40 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 17:46:15 +0100 Subject: [PATCH 117/274] Implement container instance deregistration --- AUTHORS.md | 1 + moto/ecs/models.py | 9 ++-- moto/ecs/responses.py | 8 +-- tests/test_ecs/test_ecs_boto3.py | 87 ++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 08757d2bb..5d5b99a06 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -45,3 +45,4 @@ Moto is written by Steve Pulec with contributions from: * [Tom Viner](https://github.com/tomviner) * [Justin Wiley](https://github.com/SectorNine50) * [Adam Stauffer](https://github.com/adamstauffer) +* [Guy Templeton](https://github.com/gjtempleton) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index e4258cee1..22835ecbb 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -652,6 +652,7 @@ class EC2ContainerServiceBackend(BaseBackend): '/')[-1] self.container_instances[cluster_name][ container_instance_id] = container_instance + self.clusters[cluster_name].registered_container_instances_count += 1 return container_instance def list_container_instances(self, cluster_str): @@ -715,8 +716,10 @@ class EC2ContainerServiceBackend(BaseBackend): resource["stringSetValue"].remove(str(port)) else: resource["stringSetValue"].append(str(port)) + container_instance.runningTaskCount += resource_multiplier * 1 def deregister_container_instance(self, cluster_str, container_instance_str, force): + failures = [] cluster_name = cluster_str.split('/')[-1] if cluster_name not in self.clusters: raise Exception("{0} is not a cluster".format(cluster_name)) @@ -724,18 +727,18 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = self.container_instances[cluster_name].get(container_instance_id) if container_instance is None: raise Exception("{0} is not a container id in the cluster") - if not force and container_instance.running_tasks_count > 0: + if not force and container_instance.runningTaskCount > 0: raise Exception("Found running tasks on the instance.") # Currently assume that people might want to do something based around deregistered instances # with tasks left running on them - but nothing if deregistration is forced or no tasks were # running already - elif force and container_instance.running_tasks_count > 0: + elif force and container_instance.runningTaskCount > 0: if not self.container_instances.get('orphaned'): self.container_instances['orphaned'] = {} self.container_instances['orphaned'][container_instance_id] = container_instance del(self.container_instances[cluster_name][container_instance_id]) self._respond_to_cluster_state_update(cluster_str) - pass + return container_instance, failures def _respond_to_cluster_state_update(self, cluster_str): cluster_name = cluster_str.split('/')[-1] diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 6c8fd8e2d..50d9e3cd4 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -204,10 +204,12 @@ class EC2ContainerServiceResponse(BaseResponse): }) def deregister_container_instance(self): - cluster_str = self._get_param('cluster', 'default'), + cluster_str = self._get_param('cluster') + if not cluster_str: + cluster_str = 'default' container_instance_str = self._get_param('containerInstance') - force = self._get_param('force', False) - container_instance = self.ecs_backend.deregister_container_instance( + force = self._get_param('force') + container_instance, failures = self.ecs_backend.deregister_container_instance( cluster_str, container_instance_str, force ) return json.dumps({ diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index e76be8cbe..7be782740 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -11,6 +11,7 @@ from uuid import UUID from moto import mock_cloudformation from moto import mock_ecs from moto import mock_ec2 +from nose.tools import assert_raises @mock_ecs @@ -542,6 +543,92 @@ def test_register_container_instance(): 'dockerVersion'].should.equal('DockerVersion: 1.5.0') +@mock_ec2 +@mock_ecs +def test_deregister_container_instance(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + container_instance_id = response['containerInstance']['containerInstanceArn'] + response = ecs_client.deregister_container_instance( + cluster=test_cluster_name, + containerInstance=container_instance_id + ) + container_instances_response = ecs_client.list_container_instances( + cluster=test_cluster_name + ) + len(container_instances_response['containerInstanceArns']).should.equal(0) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document + ) + container_instance_id = response['containerInstance']['containerInstanceArn'] + _ = ecs_client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + + response = ecs_client.start_task( + cluster='test_ecs_cluster', + taskDefinition='test_ecs_task', + overrides={}, + containerInstances=[container_instance_id], + startedBy='moto' + ) + with assert_raises(Exception) as e: + ecs_client.deregister_container_instance( + cluster=test_cluster_name, + containerInstance=container_instance_id + ).should.have.raised(Exception) + container_instances_response = ecs_client.list_container_instances( + cluster=test_cluster_name + ) + len(container_instances_response['containerInstanceArns']).should.equal(1) + ecs_client.deregister_container_instance( + cluster=test_cluster_name, + containerInstance=container_instance_id, + force=True + ) + container_instances_response = ecs_client.list_container_instances( + cluster=test_cluster_name + ) + len(container_instances_response['containerInstanceArns']).should.equal(0) + + @mock_ec2 @mock_ecs def test_list_container_instances(): From f3aff0f356196f29c3b7598499bf452fcd05e650 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 17:53:23 +0100 Subject: [PATCH 118/274] Switch ContainerInstance model to snake case --- moto/ecs/models.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 22835ecbb..06600cab5 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -241,7 +241,7 @@ class ContainerInstance(BaseObject): def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id self.status = 'ACTIVE' - self.registeredResources = [ + self.registered_resources = [ {'doubleValue': 0.0, 'integerValue': 4096, 'longValue': 0, @@ -264,11 +264,11 @@ class ContainerInstance(BaseObject): 'name': 'PORTS_UDP', 'stringSetValue': [], 'type': 'STRINGSET'}] - self.agentConnected = True - self.containerInstanceArn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( + self.agent_connected = True + self.container_instance_arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( str(uuid.uuid1())) - self.pendingTaskCount = 0 - self.remainingResources = [ + self.pending_task_count = 0 + self.remaining_resources = [ {'doubleValue': 0.0, 'integerValue': 4096, 'longValue': 0, @@ -292,8 +292,8 @@ class ContainerInstance(BaseObject): 'stringSetValue': [], 'type': 'STRINGSET'} ] - self.runningTaskCount = 0 - self.versionInfo = { + self.running_task_count = 0 + self.version_info = { 'agentVersion': "1.0.0", 'agentHash': '4023248', 'dockerVersion': 'DockerVersion: 1.5.0' @@ -434,7 +434,7 @@ class EC2ContainerServiceBackend(BaseBackend): placed_count = 0 for container_instance in active_container_instances: container_instance = self.container_instances[cluster_name][container_instance] - container_instance_arn = container_instance.containerInstanceArn + container_instance_arn = container_instance.container_instance_arn try_to_place = True while try_to_place: can_be_placed, message = self._can_be_placed(container_instance, resource_requirements) @@ -475,7 +475,7 @@ class EC2ContainerServiceBackend(BaseBackend): remaining_cpu = 0 remaining_memory = 0 reserved_ports = [] - for resource in container_instance.remainingResources: + for resource in container_instance.remaining_resources: if resource.get("name") == "CPU": remaining_cpu = resource.get("integerValue") elif resource.get("name") == "MEMORY": @@ -512,7 +512,7 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = self.container_instances[cluster_name][ container_instance_id ] - task = Task(cluster, task_definition, container_instance.containerInstanceArn, + task = Task(cluster, task_definition, container_instance.container_instance_arn, resource_requirements, overrides or {}, started_by or '') tasks.append(task) self.update_container_instance_resources(container_instance, resource_requirements) @@ -648,7 +648,7 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = ContainerInstance(ec2_instance_id) if not self.container_instances.get(cluster_name): self.container_instances[cluster_name] = {} - container_instance_id = container_instance.containerInstanceArn.split( + container_instance_id = container_instance.container_instance_arn.split( '/')[-1] self.container_instances[cluster_name][ container_instance_id] = container_instance @@ -660,7 +660,7 @@ class EC2ContainerServiceBackend(BaseBackend): container_instances_values = self.container_instances.get( cluster_name, {}).values() container_instances = [ - ci.containerInstanceArn for ci in container_instances_values] + ci.container_instance_arn for ci in container_instances_values] return sorted(container_instances) def describe_container_instances(self, cluster_str, list_container_instance_ids): @@ -705,7 +705,7 @@ class EC2ContainerServiceBackend(BaseBackend): resource_multiplier = 1 if removing: resource_multiplier = -1 - for resource in container_instance.remainingResources: + for resource in container_instance.remaining_resources: if resource.get("name") == "CPU": resource["integerValue"] -= task_resources.get('CPU') * resource_multiplier elif resource.get("name") == "MEMORY": @@ -716,7 +716,7 @@ class EC2ContainerServiceBackend(BaseBackend): resource["stringSetValue"].remove(str(port)) else: resource["stringSetValue"].append(str(port)) - container_instance.runningTaskCount += resource_multiplier * 1 + container_instance.running_task_count += resource_multiplier * 1 def deregister_container_instance(self, cluster_str, container_instance_str, force): failures = [] @@ -727,12 +727,11 @@ class EC2ContainerServiceBackend(BaseBackend): container_instance = self.container_instances[cluster_name].get(container_instance_id) if container_instance is None: raise Exception("{0} is not a container id in the cluster") - if not force and container_instance.runningTaskCount > 0: + if not force and container_instance.running_task_count > 0: raise Exception("Found running tasks on the instance.") # Currently assume that people might want to do something based around deregistered instances - # with tasks left running on them - but nothing if deregistration is forced or no tasks were - # running already - elif force and container_instance.runningTaskCount > 0: + # with tasks left running on them - but nothing if no tasks were running already + elif force and container_instance.running_task_count > 0: if not self.container_instances.get('orphaned'): self.container_instances['orphaned'] = {} self.container_instances['orphaned'][container_instance_id] = container_instance From 69b86b2c7a25b225fc9da2f76f6dbb9f5adfee23 Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Thu, 13 Apr 2017 18:41:29 +0100 Subject: [PATCH 119/274] Fix indentation of ContainerInstance response object --- moto/ecs/models.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 06600cab5..359cddec6 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -299,11 +299,10 @@ class ContainerInstance(BaseObject): 'dockerVersion': 'DockerVersion: 1.5.0' } - @property - def response_object(self): - response_object = self.gen_response_object() - del response_object['name'], response_object['arn'] - return response_object + @property + def response_object(self): + response_object = self.gen_response_object() + return response_object class ContainerInstanceFailure(BaseObject): From 9a2f2fcd4b34ecfca8961274ed183c65dca84e14 Mon Sep 17 00:00:00 2001 From: Sean Marlow Date: Thu, 13 Apr 2017 15:09:23 -0600 Subject: [PATCH 120/274] Add list user policies endpoint to iam backend. - Add response and endpoint methods. - Add test covering put, get, delete and list user policy. --- moto/iam/models.py | 4 ++++ moto/iam/responses.py | 20 ++++++++++++++++++++ tests/test_iam/test_iam.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/moto/iam/models.py b/moto/iam/models.py index c219c1afc..456dce4aa 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -729,6 +729,10 @@ class IAMBackend(BaseBackend): policy = user.get_policy(policy_name) return policy + def list_user_policies(self, user_name): + user = self.get_user(user_name) + return user.policies.keys() + def put_user_policy(self, user_name, policy_name, policy_json): user = self.get_user(user_name) user.put_policy(policy_name, policy_json) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 0757d7eee..7febb22a7 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -287,6 +287,12 @@ class IamResponse(BaseResponse): policy_document=policy_document ) + def list_user_policies(self): + user_name = self._get_param('UserName') + policies = iam_backend.list_user_policies(user_name) + template = self.response_template(LIST_USER_POLICIES_TEMPLATE) + return template.render(policies=policies) + def put_user_policy(self): user_name = self._get_param('UserName') policy_name = self._get_param('PolicyName') @@ -854,6 +860,20 @@ GET_USER_POLICY_TEMPLATE = """ """ +LIST_USER_POLICIES_TEMPLATE = """ + + + {% for policy in policies %} + {{ policy }} + {% endfor %} + + + false + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + CREATE_ACCESS_KEY_TEMPLATE = """ diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 1ae892f62..e039f8f61 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -234,6 +234,39 @@ def test_list_users(): user['Arn'].should.equal('arn:aws:iam::123456789012:user/my-user') +@mock_iam() +def test_user_policies(): + policy_name = 'UserManagedPolicy' + policy_document = "{'mypolicy': 'test'}" + user_name = 'my-user' + conn = boto3.client('iam', region_name='us-east-1') + conn.create_user(UserName=user_name) + conn.put_user_policy( + UserName=user_name, + PolicyName=policy_name, + PolicyDocument=policy_document + ) + + policy_doc = conn.get_user_policy( + UserName=user_name, + PolicyName=policy_name + ) + test = policy_document in policy_doc['PolicyDocument'] + test.should.equal(True) + + policies = conn.list_user_policies(UserName=user_name) + len(policies['PolicyNames']).should.equal(1) + policies['PolicyNames'][0].should.equal(policy_name) + + conn.delete_user_policy( + UserName=user_name, + PolicyName=policy_name + ) + + policies = conn.list_user_policies(UserName=user_name) + len(policies['PolicyNames']).should.equal(0) + + @mock_iam_deprecated() def test_create_login_profile(): conn = boto.connect_iam() From 34c711189f4961eeee6a5de32e8106ec0bdb48bf Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 13 Apr 2017 21:39:00 -0400 Subject: [PATCH 121/274] Cleanup IAM user create format. Closes #898. --- moto/core/utils.py | 4 ++++ moto/iam/models.py | 12 +++++++----- moto/iam/responses.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/moto/core/utils.py b/moto/core/utils.py index 946dd5895..7d4a9d412 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -170,6 +170,10 @@ def iso_8601_datetime_with_milliseconds(datetime): return datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z' +def iso_8601_datetime_without_milliseconds(datetime): + return datetime.strftime("%Y-%m-%dT%H:%M:%S") + 'Z' + + def rfc_1123_datetime(datetime): RFC1123 = '%a, %d %b %Y %H:%M:%S GMT' return datetime.strftime(RFC1123) diff --git a/moto/iam/models.py b/moto/iam/models.py index 456dce4aa..c7142fb5d 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -4,6 +4,7 @@ from datetime import datetime import pytz from moto.core import BaseBackend, BaseModel +from moto.core.utils import iso_8601_datetime_without_milliseconds from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id @@ -235,10 +236,7 @@ class User(BaseModel): self.name = name self.id = random_resource_id() self.path = path if path else "/" - self.created = datetime.strftime( - datetime.utcnow(), - "%Y-%m-%d-%H-%M-%S" - ) + self.created = datetime.utcnow() self.mfa_devices = {} self.policies = {} self.access_keys = [] @@ -248,6 +246,10 @@ class User(BaseModel): def arn(self): return "arn:aws:iam::{0}:user{1}{2}".format(ACCOUNT_ID, self.path, self.name) + @property + def created_iso_8601(self): + return iso_8601_datetime_without_milliseconds(self.created) + def get_policy(self, policy_name): policy_json = None try: @@ -310,7 +312,7 @@ class User(BaseModel): def to_csv(self): date_format = '%Y-%m-%dT%H:%M:%S+00:00' - date_created = datetime.strptime(self.created, '%Y-%m-%d-%H-%M-%S') + date_created = self.created # aagrawal,arn:aws:iam::509284790694:user/aagrawal,2014-09-01T22:28:48+00:00,true,2014-11-12T23:36:49+00:00,2014-09-03T18:59:00+00:00,N/A,false,true,2014-09-01T22:28:48+00:00,false,N/A,false,N/A,false,N/A if not self.password: password_enabled = 'false' diff --git a/moto/iam/responses.py b/moto/iam/responses.py index c56090cf4..8e19b3aa7 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -823,7 +823,7 @@ LIST_USERS_TEMPLATE = """<{{ action }}UsersResponse> {{ user.id }} {{ user.path }} {{ user.name }} - {{ user.created }} + {{ user.created_iso_8601 }} {{ user.arn }} {% endfor %} From 6e61ee4caa75efd7391cd6db4df75f8d61416a59 Mon Sep 17 00:00:00 2001 From: Dmytro Milinevskyy Date: Fri, 14 Apr 2017 13:32:52 +0200 Subject: [PATCH 122/274] s3: handle WebsiteRedirectLocation spulec/moto#821 --- moto/s3/models.py | 4 ++++ moto/s3/responses.py | 1 + tests/test_s3/test_s3.py | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/moto/s3/models.py b/moto/s3/models.py index 04220c142..cdd96015e 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -25,6 +25,7 @@ class FakeKey(BaseModel): self.value = value self.last_modified = datetime.datetime.utcnow() self.acl = get_canned_acl('private') + self.website_redirect_location = None self._storage_class = storage if storage else "STANDARD" self._metadata = {} self._expiry = None @@ -103,6 +104,9 @@ class FakeKey(BaseModel): if self._is_versioned: res['x-amz-version-id'] = str(self._version_id) + if self.website_redirect_location: + res['x-amz-website-redirect-location'] = self.website_redirect_location + return res @property diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 59cd9d322..5309e7ffc 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -570,6 +570,7 @@ class ResponseObject(_TemplateEnvironmentMixin): metadata = metadata_from_headers(request.headers) new_key.set_metadata(metadata) new_key.set_acl(acl) + new_key.website_redirect_location = request.headers.get('x-amz-website-redirect-location') template = self.response_template(S3_OBJECT_RESPONSE) response_headers.update(new_key.response_dict) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 36d4bdbc4..406494eba 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1046,6 +1046,19 @@ def test_boto3_key_etag(): resp = s3.get_object(Bucket='mybucket', Key='steve') resp['ETag'].should.equal('"d32bda93738f7e03adb22e66c90fbc04"') +@mock_s3 +def test_website_redirect_location(): + s3 = boto3.client('s3', region_name='us-east-1') + s3.create_bucket(Bucket='mybucket') + + s3.put_object(Bucket='mybucket', Key='steve', Body=b'is awesome') + resp = s3.get_object(Bucket='mybucket', Key='steve') + resp.get('WebsiteRedirectLocation').should.be.none + + url = 'https://github.com/spulec/moto' + s3.put_object(Bucket='mybucket', Key='steve', Body=b'is awesome', WebsiteRedirectLocation=url) + resp = s3.get_object(Bucket='mybucket', Key='steve') + resp['WebsiteRedirectLocation'].should.equal(url) @mock_s3 def test_boto3_list_keys_xml_escaped(): From 18ed73292caa5464bfffef6b6302f97648df556d Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Fri, 14 Apr 2017 18:16:24 +0200 Subject: [PATCH 123/274] Return the revision in ecs.register_task_definition This matches boto, see http://boto3.readthedocs.io/en/latest/reference/services/ecs.html#ECS.Client.register_task_definition --- moto/ecs/models.py | 1 + tests/test_ecs/test_ecs_boto3.py | 1 + 2 files changed, 2 insertions(+) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index aadc76bce..35bccd625 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -89,6 +89,7 @@ class TaskDefinition(BaseObject): def __init__(self, family, revision, container_definitions, volumes=None): self.family = family + self.revision = revision self.arn = 'arn:aws:ecs:us-east-1:012345678910:task-definition/{0}:{1}'.format( family, revision) self.container_definitions = container_definitions diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index e76be8cbe..fd5443ab9 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -86,6 +86,7 @@ def test_register_task_definition(): ] ) type(response['taskDefinition']).should.be(dict) + response['taskDefinition']['revision'].should.equal(1) response['taskDefinition']['taskDefinitionArn'].should.equal( 'arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task:1') response['taskDefinition']['containerDefinitions'][ From 0ae6e404d06a739f783e28434b491a02b96eb84e Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Fri, 14 Apr 2017 18:40:47 +0200 Subject: [PATCH 124/274] Add deployments to the ecs services (describe_services) --- moto/ecs/models.py | 23 ++++++++++++++++++++++- tests/test_ecs/test_ecs_boto3.py | 5 +++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index aadc76bce..794d82d02 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals import uuid -from random import random +from datetime import datetime +from random import random, randint +import pytz from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends from copy import copy @@ -174,6 +176,18 @@ class Service(BaseObject): self.task_definition = task_definition.arn self.desired_count = desired_count self.events = [] + self.deployments = [ + { + 'createdAt': datetime.now(pytz.utc), + 'desiredCount': self.desired_count, + 'id': 'ecs-svc/{}'.format(randint(0, 32**12)), + 'pendingCount': self.desired_count, + 'runningCount': 0, + 'status': 'PRIMARY', + 'taskDefinition': task_definition.arn, + 'updatedAt': datetime.now(pytz.utc), + } + ] self.load_balancers = [] self.pending_count = 0 @@ -187,6 +201,13 @@ class Service(BaseObject): del response_object['name'], response_object['arn'] response_object['serviceName'] = self.name response_object['serviceArn'] = self.arn + + for deployment in response_object['deployments']: + if isinstance(deployment['createdAt'], datetime): + deployment['createdAt'] = deployment['createdAt'].isoformat() + if isinstance(deployment['updatedAt'], datetime): + deployment['updatedAt'] = deployment['updatedAt'].isoformat() + return response_object @classmethod diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index e76be8cbe..34fce1c49 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -402,6 +402,11 @@ def test_describe_services(): 'arn:aws:ecs:us-east-1:012345678910:service/test_ecs_service2') response['services'][1]['serviceName'].should.equal('test_ecs_service2') + response['services'][0]['deployments'][0]['desiredCount'].should.equal(2) + response['services'][0]['deployments'][0]['pendingCount'].should.equal(2) + response['services'][0]['deployments'][0]['runningCount'].should.equal(0) + response['services'][0]['deployments'][0]['status'].should.equal('PRIMARY') + @mock_ecs def test_update_service(): From 121a68be4950eed0464c235eee8ba1a591c02570 Mon Sep 17 00:00:00 2001 From: Ambrus Adrian Date: Sat, 15 Apr 2017 01:06:28 +0300 Subject: [PATCH 125/274] Fixed compatibility issue with the Java AWS SDK Issue is described here: https://github.com/spulec/moto/issues/900 --- moto/s3/responses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 59cd9d322..0c9126aa7 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -506,7 +506,7 @@ class ResponseObject(_TemplateEnvironmentMixin): upload_id = query['uploadId'][0] part_number = int(query['partNumber'][0]) if 'x-amz-copy-source' in request.headers: - src = request.headers.get("x-amz-copy-source") + src = request.headers.get("x-amz-copy-source").lstrip("/") src_bucket, src_key = src.split("/", 1) src_range = request.headers.get( 'x-amz-copy-source-range', '').split("bytes=")[-1] @@ -541,7 +541,7 @@ class ResponseObject(_TemplateEnvironmentMixin): if 'x-amz-copy-source' in request.headers: # Copy key src_key_parsed = urlparse(request.headers.get("x-amz-copy-source")) - src_bucket, src_key = src_key_parsed.path.split("/", 1) + src_bucket, src_key = src_key_parsed.path.lstrip("/").split("/", 1) src_version_id = parse_qs(src_key_parsed.query).get( 'versionId', [None])[0] self.backend.copy_key(src_bucket, src_key, bucket_name, key_name, From 47bc23f4810051a7b6670f276ed5229fd00baa6a Mon Sep 17 00:00:00 2001 From: GuyTempleton Date: Sat, 15 Apr 2017 16:24:30 +0100 Subject: [PATCH 126/274] Move agent_connected assignation --- moto/ecs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 359cddec6..f73cd353e 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -240,6 +240,7 @@ class ContainerInstance(BaseObject): def __init__(self, ec2_instance_id): self.ec2_instance_id = ec2_instance_id + self.agent_connected = True self.status = 'ACTIVE' self.registered_resources = [ {'doubleValue': 0.0, @@ -264,7 +265,6 @@ class ContainerInstance(BaseObject): 'name': 'PORTS_UDP', 'stringSetValue': [], 'type': 'STRINGSET'}] - self.agent_connected = True self.container_instance_arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format( str(uuid.uuid1())) self.pending_task_count = 0 From 783a1d73b4ce31ee80ccd45931bcf9b08aa72b3e Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Tue, 18 Apr 2017 19:09:10 +0200 Subject: [PATCH 127/274] Implement support for SSM parameter store This commit adds initial support for the Simple System Manager client. It currently only mocks the following api endpoints: - delete_parameter() - put_parameter() - get_parameters() --- AUTHORS.md | 1 + moto/__init__.py | 1 + moto/backends.py | 2 + moto/ssm/__init__.py | 6 ++ moto/ssm/models.py | 65 ++++++++++++++++++ moto/ssm/responses.py | 56 +++++++++++++++ moto/ssm/urls.py | 10 +++ tests/test_ssm/test_ssm_boto3.py | 114 +++++++++++++++++++++++++++++++ 8 files changed, 255 insertions(+) create mode 100644 moto/ssm/__init__.py create mode 100644 moto/ssm/models.py create mode 100644 moto/ssm/responses.py create mode 100644 moto/ssm/urls.py create mode 100644 tests/test_ssm/test_ssm_boto3.py diff --git a/AUTHORS.md b/AUTHORS.md index 5d5b99a06..f4160146c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -46,3 +46,4 @@ Moto is written by Steve Pulec with contributions from: * [Justin Wiley](https://github.com/SectorNine50) * [Adam Stauffer](https://github.com/adamstauffer) * [Guy Templeton](https://github.com/gjtempleton) +* [Michael van Tellingen](https://github.com/mvantellingen) diff --git a/moto/__init__.py b/moto/__init__.py index 7cc9594fb..8101a4332 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -31,6 +31,7 @@ from .ses import mock_ses, mock_ses_deprecated # flake8: noqa from .sns import mock_sns, mock_sns_deprecated # flake8: noqa from .sqs import mock_sqs, mock_sqs_deprecated # flake8: noqa from .sts import mock_sts, mock_sts_deprecated # flake8: noqa +from .ssm import mock_ssm # flake8: noqa from .route53 import mock_route53, mock_route53_deprecated # flake8: noqa from .swf import mock_swf, mock_swf_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index 94c7f4849..eae94db75 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -27,6 +27,7 @@ from moto.s3 import s3_backends from moto.ses import ses_backends from moto.sns import sns_backends from moto.sqs import sqs_backends +from moto.ssm import ssm_backends from moto.sts import sts_backends BACKENDS = { @@ -56,6 +57,7 @@ BACKENDS = { 'ses': ses_backends, 'sns': sns_backends, 'sqs': sqs_backends, + 'ssm': ssm_backends, 'sts': sts_backends, 'route53': route53_backends, 'lambda': lambda_backends, diff --git a/moto/ssm/__init__.py b/moto/ssm/__init__.py new file mode 100644 index 000000000..c42f3b780 --- /dev/null +++ b/moto/ssm/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import ssm_backends +from ..core.models import base_decorator + +ssm_backend = ssm_backends['us-east-1'] +mock_ssm = base_decorator(ssm_backends) diff --git a/moto/ssm/models.py b/moto/ssm/models.py new file mode 100644 index 000000000..3344623dd --- /dev/null +++ b/moto/ssm/models.py @@ -0,0 +1,65 @@ +from __future__ import unicode_literals + +from moto.core import BaseBackend, BaseModel +from moto.ec2 import ec2_backends + + +class Parameter(BaseModel): + def __init__(self, name, value, type, description, keyid): + self.name = name + self.type = type + self.description = description + self.keyid = keyid + + if self.type == 'SecureString': + self.value = self.encrypt(value) + else: + self.value = value + + def encrypt(self, value): + return 'kms:{}:'.format(self.keyid or 'default') + value + + def decrypt(self, value): + if self.type != 'SecureString': + return value + + prefix = 'kms:{}:'.format(self.keyid or 'default') + if value.startswith(prefix): + return value[len(prefix):] + + def response_object(self, decrypt=False): + return { + 'Name': self.name, + 'Type': self.type, + 'Value': self.decrypt(self.value) if decrypt else self.value + } + + +class SimpleSystemManagerBackend(BaseBackend): + + def __init__(self): + self._parameters = {} + + def delete_parameter(self, name): + try: + del self._parameters[name] + except KeyError: + pass + + def get_parameters(self, names, with_decryption): + result = [] + for name in names: + if name in self._parameters: + result.append(self._parameters[name]) + return result + + def put_parameter(self, name, description, value, type, keyid, overwrite): + if not overwrite and name in self._parameters: + return + self._parameters[name] = Parameter( + name, value, type, description, keyid) + + +ssm_backends = {} +for region, ec2_backend in ec2_backends.items(): + ssm_backends[region] = SimpleSystemManagerBackend() diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py new file mode 100644 index 000000000..ee21d7380 --- /dev/null +++ b/moto/ssm/responses.py @@ -0,0 +1,56 @@ +from __future__ import unicode_literals +import json + +from moto.core.responses import BaseResponse +from .models import ssm_backends + + +class SimpleSystemManagerResponse(BaseResponse): + + @property + def ssm_backend(self): + return ssm_backends[self.region] + + @property + def request_params(self): + try: + return json.loads(self.body) + except ValueError: + return {} + + def _get_param(self, param, default=None): + return self.request_params.get(param, default) + + def delete_parameter(self): + name = self._get_param('Name') + self.ssm_backend.delete_parameter(name) + return json.dumps({}) + + def get_parameters(self): + names = self._get_param('Names') + with_decryption = self._get_param('WithDecryption') + + result = self.ssm_backend.get_parameters(names, with_decryption) + + response = { + 'Parameters': [], + 'InvalidParameters': [], + } + + for parameter in result: + param_data = parameter.response_object(with_decryption) + response['Parameters'].append(param_data) + + return json.dumps(response) + + def put_parameter(self): + name = self._get_param('Name') + description = self._get_param('Description') + value = self._get_param('Value') + type_ = self._get_param('Type') + keyid = self._get_param('KeyId') + overwrite = self._get_param('Overwrite', False) + + self.ssm_backend.put_parameter( + name, description, value, type_, keyid, overwrite) + return json.dumps({}) diff --git a/moto/ssm/urls.py b/moto/ssm/urls.py new file mode 100644 index 000000000..d22866486 --- /dev/null +++ b/moto/ssm/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import SimpleSystemManagerResponse + +url_bases = [ + "https?://ssm.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': SimpleSystemManagerResponse.dispatch, +} diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py new file mode 100644 index 000000000..6b8a1a369 --- /dev/null +++ b/tests/test_ssm/test_ssm_boto3.py @@ -0,0 +1,114 @@ +from __future__ import unicode_literals + +import boto3 +import sure # noqa + +from moto import mock_ssm + + +@mock_ssm +def test_delete_parameter(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='String') + + response = client.get_parameters(Names=['test']) + len(response['Parameters']).should.equal(1) + + client.delete_parameter(Name='test') + + response = client.get_parameters(Names=['test']) + len(response['Parameters']).should.equal(0) + + +@mock_ssm +def test_put_parameter(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='String') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=False) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('value') + response['Parameters'][0]['Type'].should.equal('String') + + +@mock_ssm +def test_put_parameter_secure_default_kms(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='SecureString') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=False) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('kms:default:value') + response['Parameters'][0]['Type'].should.equal('SecureString') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=True) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('value') + response['Parameters'][0]['Type'].should.equal('SecureString') + + +@mock_ssm +def test_put_parameter_secure_custom_kms(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='SecureString', + KeyId='foo') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=False) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('kms:foo:value') + response['Parameters'][0]['Type'].should.equal('SecureString') + + response = client.get_parameters( + Names=[ + 'test' + ], + WithDecryption=True) + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Value'].should.equal('value') + response['Parameters'][0]['Type'].should.equal('SecureString') From e7498580417ee4eb57d9f92d35af72168f36ca33 Mon Sep 17 00:00:00 2001 From: Andrii Piasetskyi Date: Fri, 21 Apr 2017 21:24:52 +0300 Subject: [PATCH 128/274] Added DataPipeline Tags. Implemented delete_pipeline. Added tests for delete_pipeline --- moto/datapipeline/models.py | 13 ++++++++----- moto/datapipeline/responses.py | 8 +++++++- tests/test_datapipeline/test_datapipeline.py | 13 +++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index 77c84924d..19b73b5c9 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -23,7 +23,7 @@ class PipelineObject(BaseModel): class Pipeline(BaseModel): - def __init__(self, name, unique_id): + def __init__(self, name, unique_id, **kwargs): self.name = name self.unique_id = unique_id self.description = "" @@ -31,6 +31,7 @@ class Pipeline(BaseModel): self.creation_time = datetime.datetime.utcnow() self.objects = [] self.status = "PENDING" + self.tags = kwargs.get('tags', []) @property def physical_resource_id(self): @@ -78,8 +79,7 @@ class Pipeline(BaseModel): }], "name": self.name, "pipelineId": self.pipeline_id, - "tags": [ - ] + "tags": self.tags } def set_pipeline_objects(self, pipeline_objects): @@ -113,8 +113,8 @@ class DataPipelineBackend(BaseBackend): def __init__(self): self.pipelines = {} - def create_pipeline(self, name, unique_id): - pipeline = Pipeline(name, unique_id) + def create_pipeline(self, name, unique_id, tags=[]): + pipeline = Pipeline(name, unique_id, tags=tags) self.pipelines[pipeline.pipeline_id] = pipeline return pipeline @@ -129,6 +129,9 @@ class DataPipelineBackend(BaseBackend): def get_pipeline(self, pipeline_id): return self.pipelines[pipeline_id] + def delete_pipeline(self, pipeline_id): + self.pipelines.pop(pipeline_id, None) + def put_pipeline_definition(self, pipeline_id, pipeline_objects): pipeline = self.get_pipeline(pipeline_id) pipeline.set_pipeline_objects(pipeline_objects) diff --git a/moto/datapipeline/responses.py b/moto/datapipeline/responses.py index f3644fd5c..9250bce54 100644 --- a/moto/datapipeline/responses.py +++ b/moto/datapipeline/responses.py @@ -23,7 +23,8 @@ class DataPipelineResponse(BaseResponse): def create_pipeline(self): name = self.parameters['name'] unique_id = self.parameters['uniqueId'] - pipeline = self.datapipeline_backend.create_pipeline(name, unique_id) + tags = self.parameters.get('tags', []) + pipeline = self.datapipeline_backend.create_pipeline(name, unique_id, tags=tags) return json.dumps({ "pipelineId": pipeline.pipeline_id, }) @@ -48,6 +49,11 @@ class DataPipelineResponse(BaseResponse): ] }) + def delete_pipeline(self): + pipeline_id = self.parameters["pipelineId"] + self.datapipeline_backend.delete_pipeline(pipeline_id) + return json.dumps({}) + def put_pipeline_definition(self): pipeline_id = self.parameters["pipelineId"] pipeline_objects = self.parameters["pipelineObjects"] diff --git a/tests/test_datapipeline/test_datapipeline.py b/tests/test_datapipeline/test_datapipeline.py index 520142c2e..490e3bfa4 100644 --- a/tests/test_datapipeline/test_datapipeline.py +++ b/tests/test_datapipeline/test_datapipeline.py @@ -136,6 +136,19 @@ def test_activate_pipeline(): get_value_from_fields('@pipelineState', fields).should.equal("SCHEDULED") +@mock_datapipeline_deprecated +def test_delete_pipeline(): + conn = boto.datapipeline.connect_to_region("us-west-2") + res = conn.create_pipeline("mypipeline", "some-unique-id") + pipeline_id = res["pipelineId"] + + conn.delete_pipeline(pipeline_id) + + response = conn.list_pipelines() + + response["pipelineIdList"].should.have.length_of(0) + + @mock_datapipeline_deprecated def test_listing_pipelines(): conn = boto.datapipeline.connect_to_region("us-west-2") From d414ecd2116a5a36d17bc0900488dd363bb9fd03 Mon Sep 17 00:00:00 2001 From: Andrii Piasetskyi Date: Fri, 21 Apr 2017 21:29:40 +0300 Subject: [PATCH 129/274] More consistent **kwargs --- moto/datapipeline/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index 19b73b5c9..e7abe439b 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -113,8 +113,8 @@ class DataPipelineBackend(BaseBackend): def __init__(self): self.pipelines = {} - def create_pipeline(self, name, unique_id, tags=[]): - pipeline = Pipeline(name, unique_id, tags=tags) + def create_pipeline(self, name, unique_id, **kwargs): + pipeline = Pipeline(name, unique_id, **kwargs) self.pipelines[pipeline.pipeline_id] = pipeline return pipeline From 748eb138b259cff4fc1c9a2b1d2190e473ead185 Mon Sep 17 00:00:00 2001 From: Ian Auld Date: Fri, 21 Apr 2017 14:54:27 -0700 Subject: [PATCH 130/274] Started passing QueryFilters to query method in responses.py --- moto/dynamodb2/responses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 3ceda0be1..95d52ebdd 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -329,6 +329,7 @@ class DynamoHandler(BaseResponse): else: # 'KeyConditions': {u'forum_name': {u'ComparisonOperator': u'EQ', u'AttributeValueList': [{u'S': u'the-key'}]}} key_conditions = self.body.get('KeyConditions') + query_filters = self.body.get("QueryFilter") if key_conditions: hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name( name, key_conditions.keys()) @@ -357,6 +358,8 @@ class DynamoHandler(BaseResponse): else: range_comparison = None range_values = [] + if query_filters: + filter_kwargs.update(query_filters) index_name = self.body.get('IndexName') exclusive_start_key = self.body.get('ExclusiveStartKey') limit = self.body.get("Limit") From cdc007fc63240d093afaf6e2e79542126928c31f Mon Sep 17 00:00:00 2001 From: Ian Auld Date: Fri, 21 Apr 2017 14:55:25 -0700 Subject: [PATCH 131/274] Added test for query using an attribute that is not a range/hash key --- .../test_dynamodb_table_with_range_key.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index 58e0d66d1..c740350ef 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -838,6 +838,47 @@ def test_query_filter_gte(): list(results).should.have.length_of(2) +@requires_boto_gte("2.9") +@mock_dynamodb2_deprecated +def test_query_non_hash_range_key(): + table = create_table_with_local_indexes() + item_data = [ + { + 'forum_name': 'Cool Forum', + 'subject': 'Check this out!', + 'version': '1', + 'threads': 1, + }, + { + 'forum_name': 'Cool Forum', + 'subject': 'Read this now!', + 'version': '3', + 'threads': 5, + }, + { + 'forum_name': 'Cool Forum', + 'subject': 'Please read this... please', + 'version': '2', + 'threads': 0, + } + ] + for data in item_data: + item = Item(table, data) + item.save(overwrite=True) + + results = table.query( + forum_name__eq='Cool Forum', version__gt="2" + ) + results = list(results) + results.should.have.length_of(1) + + results = table.query( + forum_name__eq='Cool Forum', version__lt="3" + ) + results = list(results) + results.should.have.length_of(2) + + @mock_dynamodb2_deprecated def test_reverse_query(): conn = boto.dynamodb2.layer1.DynamoDBConnection() From ee96c20034f1693521fbbc9927e860ad621a2d65 Mon Sep 17 00:00:00 2001 From: Andrii Piasetskyi Date: Sun, 23 Apr 2017 19:20:10 +0300 Subject: [PATCH 132/274] Added description and tags for create_pipeline --- moto/datapipeline/models.py | 2 +- moto/datapipeline/responses.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index e7abe439b..20fc4b12b 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -26,7 +26,7 @@ class Pipeline(BaseModel): def __init__(self, name, unique_id, **kwargs): self.name = name self.unique_id = unique_id - self.description = "" + self.description = kwargs.get('description', '') self.pipeline_id = get_random_pipeline_id() self.creation_time = datetime.datetime.utcnow() self.objects = [] diff --git a/moto/datapipeline/responses.py b/moto/datapipeline/responses.py index 9250bce54..e75367c49 100644 --- a/moto/datapipeline/responses.py +++ b/moto/datapipeline/responses.py @@ -21,10 +21,11 @@ class DataPipelineResponse(BaseResponse): return datapipeline_backends[self.region] def create_pipeline(self): - name = self.parameters['name'] - unique_id = self.parameters['uniqueId'] + name = self.parameters.get('name') + unique_id = self.parameters.get('uniqueId') + description = self.parameters.get('description', '') tags = self.parameters.get('tags', []) - pipeline = self.datapipeline_backend.create_pipeline(name, unique_id, tags=tags) + pipeline = self.datapipeline_backend.create_pipeline(name, unique_id, description=description, tags=tags) return json.dumps({ "pipelineId": pipeline.pipeline_id, }) From 3fecd7f8e9625e1238d37bb547284ed5ddaea67e Mon Sep 17 00:00:00 2001 From: Sean Marlow Date: Mon, 24 Apr 2017 17:10:02 -0600 Subject: [PATCH 133/274] Add create date to user response template. --- moto/iam/responses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 8e19b3aa7..64f4e3297 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -807,6 +807,7 @@ USER_TEMPLATE = """<{{ action }}UserResponse> {{ user.path }} {{ user.name }} {{ user.id }} + {{ user.created_iso_8601 }} {{ user.arn }} From 0945765537e440d67615f2e3381d57f4558bacd7 Mon Sep 17 00:00:00 2001 From: Hugo Picado Date: Thu, 27 Apr 2017 13:57:18 +0100 Subject: [PATCH 134/274] Fixing metadata key on s3 operation response ETag metadata key is being returned as "Etag" instead of "ETag". This leads to issues in some AWS SDKs using MotoServer. This change fixes the issue by updating the key to the correct format. This closes #920 --- moto/s3/models.py | 2 +- tests/test_s3/test_server.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index cdd96015e..1cf183d56 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -91,7 +91,7 @@ class FakeKey(BaseModel): @property def response_dict(self): res = { - 'Etag': self.etag, + 'ETag': self.etag, 'last-modified': self.last_modified_RFC1123, 'content-length': str(len(self.value)), } diff --git a/tests/test_s3/test_server.py b/tests/test_s3/test_server.py index f6b8f889c..44a085bea 100644 --- a/tests/test_s3/test_server.py +++ b/tests/test_s3/test_server.py @@ -34,6 +34,7 @@ def test_s3_server_bucket_create(): res = test_client.put( '/bar', 'http://foobaz.localhost:5000/', data='test value') res.status_code.should.equal(200) + assert 'ETag' in dict(res.headers) res = test_client.get('/bar', 'http://foobaz.localhost:5000/') res.status_code.should.equal(200) From 06d65fd3da8788066c859640ba16f6173c46ea3f Mon Sep 17 00:00:00 2001 From: Abhinav I Date: Fri, 28 Apr 2017 21:26:32 +0530 Subject: [PATCH 135/274] Added test cases that covers route53 client's function. Also added validation to throw a ClientError when the record set does not match the hosted zone's config --- moto/route53/responses.py | 11 ++ tests/test_route53/test_route53.py | 156 +++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/moto/route53/responses.py b/moto/route53/responses.py index 984f305ab..2419f896d 100644 --- a/moto/route53/responses.py +++ b/moto/route53/responses.py @@ -112,6 +112,17 @@ class Route53(BaseResponse): for value in change_list: action = value['Action'] record_set = value['ResourceRecordSet'] + + cleaned_record_name = record_set['Name'].strip('.') + cleaned_hosted_zone_name = the_zone.name.strip('.') + + if not cleaned_record_name.endswith(cleaned_hosted_zone_name): + error_msg = """ + An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: + RRSet with DNS name %s is not permitted in zone %s + """ % (record_set['Name'], the_zone.name) + return 400, headers, error_msg + if action in ('CREATE', 'UPSERT'): if 'ResourceRecords' in record_set: resource_records = list( diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py index b64c63a30..ac8d6e7ad 100644 --- a/tests/test_route53/test_route53.py +++ b/tests/test_route53/test_route53.py @@ -9,6 +9,9 @@ import sure # noqa import uuid +import botocore +from nose.tools import assert_raises + from moto import mock_route53, mock_route53_deprecated @@ -491,3 +494,156 @@ def test_list_hosted_zones_by_name(): zones["HostedZones"][0]["Name"].should.equal("test.b.com.") zones["HostedZones"][1]["Name"].should.equal("test.a.org.") zones["HostedZones"][2]["Name"].should.equal("test.a.org.") + + +@mock_route53 +def test_change_resource_record_sets_crud_valid(): + conn = boto3.client('route53', region_name='us-east-1') + conn.create_hosted_zone( + Name="db.", + CallerReference=str(hash('foo')), + HostedZoneConfig=dict( + PrivateZone=True, + Comment="db", + ) + ) + + zones = conn.list_hosted_zones_by_name(DNSName="db.") + len(zones["HostedZones"]).should.equal(1) + zones["HostedZones"][0]["Name"].should.equal("db.") + hosted_zone_id = zones["HostedZones"][0]["Id"] + + # Create A Record. + a_record_endpoint_payload = { + 'Comment': 'create A record prod.redis.db', + 'Changes': [ + { + 'Action': 'CREATE', + 'ResourceRecordSet': { + 'Name': 'prod.redis.db', + 'Type': 'A', + 'TTL': 10, + 'ResourceRecords': [{ + 'Value': '127.0.0.1' + }] + } + } + ] + } + conn.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch=a_record_endpoint_payload) + + response = conn.list_resource_record_sets(HostedZoneId=hosted_zone_id) + len(response['ResourceRecordSets']).should.equal(1) + a_record_detail = response['ResourceRecordSets'][0] + a_record_detail['Name'].should.equal('prod.redis.db') + a_record_detail['Type'].should.equal('A') + a_record_detail['TTL'].should.equal(10) + a_record_detail['ResourceRecords'].should.equal([{'Value': '127.0.0.1'}]) + + # Update type to CNAME + cname_record_endpoint_payload = { + 'Comment': 'Update to CNAME prod.redis.db', + 'Changes': [ + { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': 'prod.redis.db', + 'Type': 'CNAME', + 'TTL': 60, + 'ResourceRecords': [{ + 'Value': '192.168.1.1' + }] + } + } + ] + } + conn.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch=cname_record_endpoint_payload) + + response = conn.list_resource_record_sets(HostedZoneId=hosted_zone_id) + len(response['ResourceRecordSets']).should.equal(1) + cname_record_detail = response['ResourceRecordSets'][0] + cname_record_detail['Name'].should.equal('prod.redis.db') + cname_record_detail['Type'].should.equal('CNAME') + cname_record_detail['TTL'].should.equal(60) + cname_record_detail['ResourceRecords'].should.equal([{'Value': '192.168.1.1'}]) + + # Delete record. + delete_payload = { + 'Comment': 'delete prod.redis.db', + 'Changes': [ + { + 'Action': 'DELETE', + 'ResourceRecordSet': { + 'Name': 'prod.redis.db', + 'Type': 'CNAME', + } + } + ] + } + conn.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch=delete_payload) + response = conn.list_resource_record_sets(HostedZoneId=hosted_zone_id) + len(response['ResourceRecordSets']).should.equal(0) + + +@mock_route53 +def test_change_resource_record_invalid(): + conn = boto3.client('route53', region_name='us-east-1') + conn.create_hosted_zone( + Name="db.", + CallerReference=str(hash('foo')), + HostedZoneConfig=dict( + PrivateZone=True, + Comment="db", + ) + ) + + zones = conn.list_hosted_zones_by_name(DNSName="db.") + len(zones["HostedZones"]).should.equal(1) + zones["HostedZones"][0]["Name"].should.equal("db.") + hosted_zone_id = zones["HostedZones"][0]["Id"] + + invalid_a_record_payload = { + 'Comment': 'this should fail', + 'Changes': [ + { + 'Action': 'CREATE', + 'ResourceRecordSet': { + 'Name': 'prod.scooby.doo', + 'Type': 'A', + 'TTL': 10, + 'ResourceRecords': [{ + 'Value': '127.0.0.1' + }] + } + } + ] + } + + with assert_raises(botocore.exceptions.ClientError): + conn.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch=invalid_a_record_payload) + + response = conn.list_resource_record_sets(HostedZoneId=hosted_zone_id) + len(response['ResourceRecordSets']).should.equal(0) + + invalid_cname_record_payload = { + 'Comment': 'this should also fail', + 'Changes': [ + { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': 'prod.scooby.doo', + 'Type': 'CNAME', + 'TTL': 10, + 'ResourceRecords': [{ + 'Value': '127.0.0.1' + }] + } + } + ] + } + + with assert_raises(botocore.exceptions.ClientError): + conn.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch=invalid_cname_record_payload) + + response = conn.list_resource_record_sets(HostedZoneId=hosted_zone_id) + len(response['ResourceRecordSets']).should.equal(0) From 819a308e2b1a17123904a7c4f49c924e61252a26 Mon Sep 17 00:00:00 2001 From: georgepsarakis Date: Sat, 29 Apr 2017 21:56:48 +0300 Subject: [PATCH 136/274] Add failing test for S3 list_object_versions --- tests/test_s3/test_s3.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 406494eba..193399917 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1293,6 +1293,30 @@ def test_boto3_multipart_etag(): resp['ETag'].should.equal(EXPECTED_ETAG) +@mock_s3 +def test_boto3_list_object_versions(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-versions' + s3.create_bucket(Bucket=bucket_name) + for body in ('v1', 'v2'): + s3.put_object( + Bucket=bucket_name, + Key=key, + Body=body + ) + response = s3.list_object_versions( + Bucket=bucket_name + ) + # Two object versions should be returned + len(response['Versions']).should.equal(2) + keys = set([item['Key'] for item in response['Versions']]) + keys.should.equal({key}) + # Test latest object version is returned + response = s3.get_object(Bucket=bucket_name, Key=key) + response['Body'].read().should.equal('v2') + + TEST_XML = """\ From 617e994ac65860351f83834c37397155eddf8d16 Mon Sep 17 00:00:00 2001 From: georgepsarakis Date: Sat, 29 Apr 2017 22:35:25 +0300 Subject: [PATCH 137/274] Specify integer value for MaxKeys in S3 response --- moto/s3/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 4f8e6e993..1642a5697 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -224,7 +224,7 @@ class ResponseObject(_TemplateEnvironmentMixin): key_list=versions, bucket=bucket, prefix='', - max_keys='', + max_keys=1000, delimiter='', is_truncated='false', ) From 2714fb76f1cbea381a1078996a3fcc900523c895 Mon Sep 17 00:00:00 2001 From: georgepsarakis Date: Sun, 30 Apr 2017 08:03:46 +0300 Subject: [PATCH 138/274] Python 2/3 compatibility fixes --- tests/test_s3/test_s3.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 193399917..09ef235a8 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -14,6 +14,7 @@ from boto.exception import S3CreateError, S3ResponseError from boto.s3.connection import S3Connection from boto.s3.key import Key from freezegun import freeze_time +import six import requests import tests.backport_assert_raises # noqa from nose.tools import assert_raises @@ -1299,7 +1300,8 @@ def test_boto3_list_object_versions(): bucket_name = 'mybucket' key = 'key-with-versions' s3.create_bucket(Bucket=bucket_name) - for body in ('v1', 'v2'): + items = (six.b('v1'), six.b('v2')) + for body in items: s3.put_object( Bucket=bucket_name, Key=key, @@ -1314,7 +1316,7 @@ def test_boto3_list_object_versions(): keys.should.equal({key}) # Test latest object version is returned response = s3.get_object(Bucket=bucket_name, Key=key) - response['Body'].read().should.equal('v2') + response['Body'].read().should.equal(items[-1]) TEST_XML = """\ From bf3fff6e2c7a93b28a4852a6881e33059b933f34 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Mon, 1 May 2017 11:28:35 -0700 Subject: [PATCH 139/274] Allow yaml templates for cloud formation Fixes #912 --- moto/cloudformation/models.py | 9 ++++++++- tests/test_cloudformation/test_stack_parsing.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index b58d1dcf0..824d568a2 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import datetime import json +import yaml import uuid import boto.cloudformation @@ -17,7 +18,7 @@ class FakeStack(BaseModel): self.stack_id = stack_id self.name = name self.template = template - self.template_dict = json.loads(self.template) + self._parse_template() self.parameters = parameters self.region_name = region_name self.notification_arns = notification_arns if notification_arns else [] @@ -70,6 +71,12 @@ class FakeStack(BaseModel): resource_properties=resource_properties, )) + def _parse_template(self): + try: + self.template_dict = json.loads(self.template) + except json.JSONDecodeError: + self.template_dict = yaml.load(self.template) + @property def stack_parameters(self): return self.resource_map.resolved_parameters diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index c2af6363a..610b02325 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import json +import yaml from mock import patch import sure # noqa @@ -126,6 +127,20 @@ def test_parse_stack_with_name_type_resource(): queue.should.be.a(Queue) +def test_parse_stack_with_yaml_template(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=yaml.dump(name_type_template), + parameters={}, + region_name='us-west-1') + + stack.resource_map.should.have.length_of(1) + list(stack.resource_map.keys())[0].should.equal('Queue') + queue = list(stack.resource_map.values())[0] + queue.should.be.a(Queue) + + def test_parse_stack_with_outputs(): stack = FakeStack( stack_id="test_id", From a2fd72d2f8b72453bb6a7e44b16a9cc6148cfd41 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Mon, 1 May 2017 12:13:12 -0700 Subject: [PATCH 140/274] Require content-length header fixes #908 --- moto/s3/responses.py | 4 ++++ tests/test_s3/test_server.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 4f8e6e993..a2c0f3dd5 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -294,6 +294,8 @@ class ResponseObject(_TemplateEnvironmentMixin): ) def _bucket_response_put(self, request, body, region_name, bucket_name, querystring, headers): + if not request.headers.get('Content-Length'): + return 411, {}, "Content-Length required" if 'versioning' in querystring: ver = re.search('([A-Za-z]+)', body) if ver: @@ -355,6 +357,8 @@ class ResponseObject(_TemplateEnvironmentMixin): return 409, {}, template.render(bucket=removed_bucket) def _bucket_response_post(self, request, body, bucket_name, headers): + if not request.headers.get('Content-Length'): + return 411, {}, "Content-Length required" path = request.path if hasattr(request, 'path') else request.path_url if self.is_delete_keys(request, path, bucket_name): return self._bucket_response_delete_keys(request, body, bucket_name, headers) diff --git a/tests/test_s3/test_server.py b/tests/test_s3/test_server.py index f6b8f889c..e0440ce2f 100644 --- a/tests/test_s3/test_server.py +++ b/tests/test_s3/test_server.py @@ -66,3 +66,14 @@ def test_s3_server_post_to_bucket(): res = test_client.get('/the-key', 'http://tester.localhost:5000/') res.status_code.should.equal(200) res.data.should.equal(b"nothing") + + +def test_s3_server_post_without_content_length(): + backend = server.create_backend_app("s3") + test_client = backend.test_client() + + res = test_client.put('/', 'http://tester.localhost:5000/', environ_overrides={'CONTENT_LENGTH': ''}) + res.status_code.should.equal(411) + + res = test_client.post('/', "https://tester.localhost:5000/", environ_overrides={'CONTENT_LENGTH': ''}) + res.status_code.should.equal(411) From 02edc6fa00fd700e38dc854dfbf1090cdc16804c Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Mon, 1 May 2017 12:31:31 -0700 Subject: [PATCH 141/274] Idempotent Dynamodb2 deletes Fixes #873 --- moto/dynamodb2/responses.py | 14 +++++--------- .../test_dynamodb_table_with_range_key.py | 3 ++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 3ceda0be1..3811bbb73 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -419,16 +419,12 @@ class DynamoHandler(BaseResponse): keys = self.body['Key'] return_values = self.body.get('ReturnValues', '') item = dynamodb_backend2.delete_item(name, keys) - if item: - if return_values == 'ALL_OLD': - item_dict = item.to_json() - else: - item_dict = {'Attributes': {}} - item_dict['ConsumedCapacityUnits'] = 0.5 - return dynamo_json_dump(item_dict) + if item and return_values == 'ALL_OLD': + item_dict = item.to_json() else: - er = 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException' - return self.error(er) + item_dict = {'Attributes': {}} + item_dict['ConsumedCapacityUnits'] = 0.5 + return dynamo_json_dump(item_dict) def update_item(self): name = self.body['TableName'] diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index 58e0d66d1..402424f07 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -314,7 +314,8 @@ def test_delete_item(): response.should.equal(True) table.count().should.equal(0) - item.delete().should.equal(False) + # Deletes are idempotent + item.delete().should.equal(True) @requires_boto_gte("2.9") From f23a6954cc45edd2fa2a296b880aad1cd20f469e Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Tue, 2 May 2017 16:19:57 +0100 Subject: [PATCH 142/274] bundled httpretty: re-normalize headers after executing callable_body --- moto/packages/httpretty/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/packages/httpretty/core.py b/moto/packages/httpretty/core.py index b409711cf..0974f38dd 100644 --- a/moto/packages/httpretty/core.py +++ b/moto/packages/httpretty/core.py @@ -597,6 +597,7 @@ class Entry(BaseClass): if self.body_is_callable: status, headers, self.body = self.callable_body( self.request, self.info.full_url(), headers) + headers = self.normalize_headers(headers) if self.request.method != "HEAD": headers.update({ 'content-length': len(self.body) From 835fe2d742022ea1281501ccf42554a21abd34ba Mon Sep 17 00:00:00 2001 From: graham-hargreaves Date: Sun, 7 May 2017 16:02:53 +0100 Subject: [PATCH 143/274] Update list IAM AccessKeys Add the creation date, including timezone info, to the data returned when requesting all AccessKeys for an IAM user. This fixes #75 --- moto/iam/models.py | 2 +- moto/iam/responses.py | 1 + tests/test_iam/test_iam.py | 10 ++++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index c7142fb5d..0f11022ee 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -177,7 +177,7 @@ class AccessKey(BaseModel): self.status = 'Active' self.create_date = datetime.strftime( datetime.utcnow(), - "%Y-%m-%d-%H-%M-%S" + "%Y-%m-%dT%H:%M:%SZ" ) def get_cfn_attribute(self, attribute_name): diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 8e19b3aa7..d27ee2e36 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -900,6 +900,7 @@ LIST_ACCESS_KEYS_TEMPLATE = """ {{ user_name }} {{ key.access_key_id }} {{ key.status }} + {{ key.create_date }} {% endfor %} diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index e039f8f61..798c516a6 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -299,6 +299,8 @@ def test_create_access_key(): @mock_iam_deprecated() def test_get_all_access_keys(): + """If no access keys exist there should be none in the response, + if an access key is present it should have the correct fields present""" conn = boto.connect_iam() conn.create_user('my-user') response = conn.get_all_access_keys('my-user') @@ -309,10 +311,10 @@ def test_get_all_access_keys(): ) conn.create_access_key('my-user') response = conn.get_all_access_keys('my-user') - assert_not_equals( - response['list_access_keys_response'][ - 'list_access_keys_result']['access_key_metadata'], - [] + assert_equals( + sorted(response['list_access_keys_response'][ + 'list_access_keys_result']['access_key_metadata'][0].keys()), + sorted(['status', 'create_date', 'user_name', 'access_key_id']) ) From 8e3d46fb05fe9328e54322b1ae3a5c63991cf5d9 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Mon, 8 May 2017 17:25:59 -1000 Subject: [PATCH 144/274] Deleting from an unknown table raises error If the table exists then we deletes are idempotent --- moto/dynamodb2/models.py | 2 +- moto/dynamodb2/responses.py | 5 +++++ .../test_dynamodb2/test_dynamodb_table_without_range_key.py | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 2ee5da203..f516d14a0 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -659,7 +659,7 @@ class DynamoDBBackend(BaseBackend): return item def delete_item(self, table_name, keys): - table = self.tables.get(table_name) + table = self.get_table(table_name) if not table: return None hash_key, range_key = self.get_keys_value(table, keys) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 3811bbb73..1e23c832a 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -418,6 +418,11 @@ class DynamoHandler(BaseResponse): name = self.body['TableName'] keys = self.body['Key'] return_values = self.body.get('ReturnValues', '') + table = dynamodb_backend2.get_table(name) + if not table: + er = 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException' + return self.error(er) + item = dynamodb_backend2.delete_item(name, keys) if item and return_values == 'ALL_OLD': item_dict = item.to_json() diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 36e1b6c61..5ea242116 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -199,7 +199,8 @@ def test_delete_item(): table.count().should.equal(0) - item.delete().should.equal(False) + # Deletes are idempotent and 'False' here would imply an error condition + item.delete().should.equal(True) @requires_boto_gte("2.9") From 6d8aa9d8f14b9a0d460276d0678a218887a65ef0 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Mon, 8 May 2017 20:05:46 -1000 Subject: [PATCH 145/274] support default ports in RDS --- moto/rds2/models.py | 18 ++++++++++++++++++ tests/test_rds2/test_rds2.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index eecb608dd..5e3dfc9c7 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -57,6 +57,8 @@ class Database(BaseModel): self.source_db_identifier = kwargs.get("source_db_identifier") self.db_instance_class = kwargs.get('db_instance_class') self.port = kwargs.get('port') + if self.port is None: + self.port = Database.default_port(self.engine) self.db_instance_identifier = kwargs.get('db_instance_identifier') self.db_name = kwargs.get("db_name") self.publicly_accessible = kwargs.get("publicly_accessible") @@ -241,6 +243,22 @@ class Database(BaseModel): return self.port raise UnformattedGetAttTemplateException() + @staticmethod + def default_port(engine): + return { + 'mysql': 3306, + 'mariadb': 3306, + 'postgres': 5432, + 'oracle-ee': 1521, + 'oracle-se2': 1521, + 'oracle-se1': 1521, + 'oracle-se': 1521, + 'sqlserver-ee': 1433, + 'sqlserver-ex': 1433, + 'sqlserver-se': 1433, + 'sqlserver-web': 1433, + }[engine] + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 1e2e0abdf..c5f99c326 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -973,6 +973,20 @@ def test_create_db_instance_with_parameter_group(): 'ParameterApplyStatus'].should.equal('in-sync') +@disable_on_py3() +@mock_rds2 +def test_create_database_with_default_port(): + conn = boto3.client('rds', region_name='us-west-2') + database = conn.create_db_instance(DBInstanceIdentifier='db-master-1', + AllocatedStorage=10, + Engine='postgres', + DBInstanceClass='db.m1.small', + MasterUsername='root', + MasterUserPassword='hunter2', + DBSecurityGroups=["my_sg"]) + database['DBInstance']['Endpoint']['Port'].should.equal(5432) + + @disable_on_py3() @mock_rds2 def test_modify_db_instance_with_parameter_group(): From 408a70992cf68adfb66492f7d550279efe1214fd Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 10 May 2017 21:33:30 -0400 Subject: [PATCH 146/274] Fix filter wildcards. Closes #910. --- moto/ec2/utils.py | 9 +++++++-- tests/test_ec2/test_amis.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 8cba650a6..dfca51ca8 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals + +import fnmatch import random import re import six @@ -460,7 +462,11 @@ def is_filter_matching(obj, filter, filter_value): value = obj.get_filter_value(filter) if isinstance(value, six.string_types): - return value in filter_value + if not isinstance(filter_value, list): + filter_value = [filter_value] + if any(fnmatch.fnmatch(value, pattern) for pattern in filter_value): + return True + return False try: value = set(value) @@ -479,7 +485,6 @@ def generic_filter(filters, objects): def simple_aws_filter_to_re(filter_string): - import fnmatch tmp_filter = filter_string.replace('\?', '[?]') tmp_filter = tmp_filter.replace('\*', '[*]') tmp_filter = fnmatch.translate(tmp_filter) diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 40cc5fe24..c9570e1a6 100755 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -5,11 +5,12 @@ from nose.tools import assert_raises import boto import boto.ec2 +import boto3 from boto.exception import EC2ResponseError, EC2ResponseError import sure # noqa -from moto import mock_emr_deprecated +from moto import mock_emr_deprecated, mock_ec2 from tests.helpers import requires_boto_gte @@ -558,3 +559,17 @@ def test_ami_attribute_error_cases(): cm.exception.code.should.equal('InvalidAMIID.NotFound') cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none + + +""" +Boto3 +""" + +@mock_ec2 +def test_ami_filter_wildcard(): + ec2 = boto3.resource('ec2', region_name='us-west-1') + instance = ec2.create_instances(ImageId='ami-1234abcd', MinCount=1, MaxCount=1)[0] + image = instance.create_image(Name='test-image') + filter_result = list(ec2.images.filter(Owners=['111122223333'], Filters=[{'Name':'name', 'Values':['test*']}])) + assert filter_result == [image] + From 0adebeed2482ffd27ed16b0cfbbb1e014d8b0a40 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 10 May 2017 21:58:42 -0400 Subject: [PATCH 147/274] Merge #913. --- moto/autoscaling/models.py | 5 +- moto/autoscaling/responses.py | 40 +++++- moto/cloudformation/models.py | 5 +- moto/cloudformation/responses.py | 17 ++- moto/datapipeline/models.py | 3 +- moto/datapipeline/responses.py | 21 ++- moto/dynamodb2/models.py | 20 ++- moto/dynamodb2/responses.py | 31 ++++- moto/ec2/models.py | 22 ++- moto/ec2/responses/amis.py | 5 +- moto/ec2/responses/instances.py | 16 ++- moto/ec2/utils.py | 14 +- moto/elb/models.py | 3 +- moto/elb/responses.py | 19 ++- moto/emr/models.py | 6 +- moto/kinesis/models.py | 2 +- moto/kinesis/responses.py | 18 ++- moto/rds/responses.py | 19 ++- moto/rds2/models.py | 3 +- moto/rds2/responses.py | 19 ++- setup.py | 1 + tests/test_autoscaling/test_autoscaling.py | 24 ++++ .../test_launch_configurations.py | 21 +++ .../test_cloudformation_stack_crud_boto3.py | 20 +++ tests/test_datapipeline/test_datapipeline.py | 13 ++ tests/test_dynamodb2/test_dynamodb.py | 85 ++++++++++++ .../test_dynamodb_table_with_range_key.py | 6 +- .../test_dynamodb_table_without_range_key.py | 1 + tests/test_ec2/test_amis.py | 129 ++++++++++++++++-- tests/test_ec2/test_elastic_block_store.py | 5 + tests/test_ec2/test_instances.py | 37 ++++- tests/test_elb/test_elb.py | 21 +++ tests/test_emr/test_emr_boto3.py | 12 ++ tests/test_kinesis/test_kinesis.py | 22 ++- tests/test_rds/test_rds.py | 23 +++- tests/test_rds2/test_rds2.py | 19 +++ 36 files changed, 669 insertions(+), 58 deletions(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 0fdd82ddb..ec46d1182 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping +from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends from moto.elb import elb_backends @@ -284,8 +285,8 @@ class FakeAutoScalingGroup(BaseModel): class AutoScalingBackend(BaseBackend): def __init__(self, ec2_backend, elb_backend): - self.autoscaling_groups = {} - self.launch_configurations = {} + self.autoscaling_groups = OrderedDict() + self.launch_configurations = OrderedDict() self.policies = {} self.ec2_backend = ec2_backend self.elb_backend = elb_backend diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py index b1d160320..2c3bddd79 100644 --- a/moto/autoscaling/responses.py +++ b/moto/autoscaling/responses.py @@ -40,11 +40,22 @@ class AutoScalingResponse(BaseResponse): def describe_launch_configurations(self): names = self._get_multi_param('LaunchConfigurationNames.member') - launch_configurations = self.autoscaling_backend.describe_launch_configurations( - names) + all_launch_configurations = self.autoscaling_backend.describe_launch_configurations(names) + marker = self._get_param('NextToken') + all_names = [lc.name for lc in all_launch_configurations] + if marker: + start = all_names.index(marker) + 1 + else: + start = 0 + max_records = self._get_param('MaxRecords', 50) # the default is 100, but using 50 to make testing easier + launch_configurations_resp = all_launch_configurations[start:start + max_records] + next_token = None + if len(all_launch_configurations) > start + max_records: + next_token = launch_configurations_resp[-1].name + template = self.response_template( DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE) - return template.render(launch_configurations=launch_configurations) + return template.render(launch_configurations=launch_configurations_resp, next_token=next_token) def delete_launch_configuration(self): launch_configurations_name = self.querystring.get( @@ -78,9 +89,22 @@ class AutoScalingResponse(BaseResponse): def describe_auto_scaling_groups(self): names = self._get_multi_param("AutoScalingGroupNames.member") - groups = self.autoscaling_backend.describe_autoscaling_groups(names) + token = self._get_param("NextToken") + all_groups = self.autoscaling_backend.describe_autoscaling_groups(names) + all_names = [group.name for group in all_groups] + if token: + start = all_names.index(token) + 1 + else: + start = 0 + max_records = self._get_param("MaxRecords", 50) + if max_records > 100: + raise ValueError + groups = all_groups[start:start + max_records] + next_token = None + if max_records and len(all_groups) > start + max_records: + next_token = groups[-1].name template = self.response_template(DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE) - return template.render(groups=groups) + return template.render(groups=groups, next_token=next_token) def update_auto_scaling_group(self): self.autoscaling_backend.update_autoscaling_group( @@ -239,6 +263,9 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """ {% endfor %} + {% if next_token %} + {{ next_token }} + {% endif %} d05a22f8-b690-11e2-bf8e-2113fEXAMPLE @@ -331,6 +358,9 @@ DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE = """ {% endfor %} + {% if next_token %} + {{ next_token }} + {% endif %} 0f02a07d-b677-11e2-9eb0-dd50EXAMPLE diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index b58d1dcf0..892f85174 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -4,6 +4,7 @@ import json import uuid import boto.cloudformation +from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from .parsing import ResourceMap, OutputMap @@ -121,7 +122,7 @@ class FakeEvent(BaseModel): class CloudFormationBackend(BaseBackend): def __init__(self): - self.stacks = {} + self.stacks = OrderedDict() self.deleted_stacks = {} def create_stack(self, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None): @@ -152,7 +153,7 @@ class CloudFormationBackend(BaseBackend): return [stack] raise ValidationError(name_or_stack_id) else: - return stacks + return list(stacks) def list_stacks(self): return self.stacks.values() diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index f1e6d0415..60f647efa 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -72,10 +72,20 @@ class CloudFormationResponse(BaseResponse): stack_name_or_id = None if self._get_param('StackName'): stack_name_or_id = self.querystring.get('StackName')[0] + token = self._get_param('NextToken') stacks = self.cloudformation_backend.describe_stacks(stack_name_or_id) - + stack_ids = [stack.stack_id for stack in stacks] + if token: + start = stack_ids.index(token) + 1 + else: + start = 0 + max_results = 50 # using this to mske testing of paginated stacks more convenient than default 1 MB + stacks_resp = stacks[start:start + max_results] + next_token = None + if len(stacks) > (start + max_results): + next_token = stacks_resp[-1].stack_id template = self.response_template(DESCRIBE_STACKS_TEMPLATE) - return template.render(stacks=stacks) + return template.render(stacks=stacks_resp, next_token=next_token) def describe_stack_resource(self): stack_name = self._get_param('StackName') @@ -270,6 +280,9 @@ DESCRIBE_STACKS_TEMPLATE = """ {% endfor %} + {% if next_token %} + {{ next_token }} + {% endif %} """ diff --git a/moto/datapipeline/models.py b/moto/datapipeline/models.py index 20fc4b12b..bb8417a20 100644 --- a/moto/datapipeline/models.py +++ b/moto/datapipeline/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import boto.datapipeline +from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from .utils import get_random_pipeline_id, remove_capitalization_of_dict_keys @@ -111,7 +112,7 @@ class Pipeline(BaseModel): class DataPipelineBackend(BaseBackend): def __init__(self): - self.pipelines = {} + self.pipelines = OrderedDict() def create_pipeline(self, name, unique_id, **kwargs): pipeline = Pipeline(name, unique_id, **kwargs) diff --git a/moto/datapipeline/responses.py b/moto/datapipeline/responses.py index e75367c49..e462e3981 100644 --- a/moto/datapipeline/responses.py +++ b/moto/datapipeline/responses.py @@ -31,12 +31,25 @@ class DataPipelineResponse(BaseResponse): }) def list_pipelines(self): - pipelines = self.datapipeline_backend.list_pipelines() + pipelines = list(self.datapipeline_backend.list_pipelines()) + pipeline_ids = [pipeline.pipeline_id for pipeline in pipelines] + max_pipelines = 50 + marker = self.parameters.get('marker') + if marker: + start = pipeline_ids.index(marker) + 1 + else: + start = 0 + pipelines_resp = pipelines[start:start + max_pipelines] + has_more_results = False + marker = None + if start + max_pipelines < len(pipeline_ids) - 1: + has_more_results = True + marker = pipelines_resp[-1].pipeline_id return json.dumps({ - "hasMoreResults": False, - "marker": None, + "hasMoreResults": has_more_results, + "marker": marker, "pipelineIdList": [ - pipeline.to_meta_json() for pipeline in pipelines + pipeline.to_meta_json() for pipeline in pipelines_resp ] }) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 2ee5da203..45be1818f 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -200,6 +200,11 @@ class Table(BaseModel): self.global_indexes = global_indexes if global_indexes else [] self.created_at = datetime.datetime.utcnow() self.items = defaultdict(dict) + self.table_arn = self._generate_arn(table_name) + self.tags = [] + + def _generate_arn(self, name): + return 'arn:aws:dynamodb:us-east-1:123456789011:table/' + name def describe(self, base_key='TableDescription'): results = { @@ -209,11 +214,12 @@ class Table(BaseModel): 'TableSizeBytes': 0, 'TableName': self.name, 'TableStatus': 'ACTIVE', + 'TableArn': self.table_arn, 'KeySchema': self.schema, 'ItemCount': len(self), 'CreationDateTime': unix_time(self.created_at), 'GlobalSecondaryIndexes': [index for index in self.global_indexes], - 'LocalSecondaryIndexes': [index for index in self.indexes] + 'LocalSecondaryIndexes': [index for index in self.indexes], } } return results @@ -505,6 +511,18 @@ class DynamoDBBackend(BaseBackend): def delete_table(self, name): return self.tables.pop(name, None) + def tag_resource(self, table_arn, tags): + for table in self.tables: + if self.tables[table].table_arn == table_arn: + self.tables[table].tags.extend(tags) + + def list_tags_of_resource(self, table_arn): + required_table = None + for table in self.tables: + if self.tables[table].table_arn == table_arn: + required_table = self.tables[table] + return required_table.tags + def update_table_throughput(self, name, throughput): table = self.tables[name] table.throughput = throughput diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 95d52ebdd..11d23a830 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -73,7 +73,7 @@ class DynamoHandler(BaseResponse): def list_tables(self): body = self.body - limit = body.get('Limit') + limit = body.get('Limit', 100) if body.get("ExclusiveStartTableName"): last = body.get("ExclusiveStartTableName") start = list(dynamodb_backend2.tables.keys()).index(last) + 1 @@ -124,6 +124,35 @@ class DynamoHandler(BaseResponse): er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' return self.error(er) + def tag_resource(self): + tags = self.body['Tags'] + table_arn = self.body['ResourceArn'] + dynamodb_backend2.tag_resource(table_arn, tags) + return json.dumps({}) + + def list_tags_of_resource(self): + try: + table_arn = self.body['ResourceArn'] + all_tags = dynamodb_backend2.list_tags_of_resource(table_arn) + all_tag_keys = [tag['Key'] for tag in all_tags] + marker = self.body.get('NextToken') + if marker: + start = all_tag_keys.index(marker) + 1 + else: + start = 0 + max_items = 10 # there is no default, but using 10 to make testing easier + tags_resp = all_tags[start:start + max_items] + next_marker = None + if len(all_tags) > start + max_items: + next_marker = tags_resp[-1]['Key'] + if next_marker: + return json.dumps({'Tags': tags_resp, + 'NextToken': next_marker}) + return json.dumps({'Tags': tags_resp}) + except AttributeError: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er) + def update_table(self): name = self.body['TableName'] if 'GlobalSecondaryIndexUpdates' in self.body: diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 64cc02e09..8be13d867 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -12,6 +12,7 @@ from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest from boto.ec2.launchspecification import LaunchSpecification +from moto.compat import OrderedDict from moto.core import BaseBackend from moto.core.models import Model, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores @@ -618,7 +619,7 @@ class Instance(TaggedEC2Resource, BotoInstance): class InstanceBackend(object): def __init__(self): - self.reservations = {} + self.reservations = OrderedDict() super(InstanceBackend, self).__init__() def get_instance(self, instance_id): @@ -1049,12 +1050,22 @@ class AmiBackend(object): self.amis[ami_id] = ami return ami - def describe_images(self, ami_ids=(), filters=None): + def describe_images(self, ami_ids=(), filters=None, exec_users=None): + images = [] + if exec_users: + for ami_id in self.amis: + found = False + for user_id in exec_users: + if user_id in self.amis[ami_id].launch_permission_users: + found = True + if found: + images.append(self.amis[ami_id]) + if images == []: + return images if filters: - images = self.amis.values() + images = images or self.amis.values() return generic_filter(filters, images) else: - images = [] for ami_id in ami_ids: if ami_id in self.amis: images.append(self.amis[ami_id]) @@ -1766,6 +1777,9 @@ class Snapshot(TaggedEC2Resource): if filter_name == 'encrypted': return str(self.encrypted).lower() + if filter_name == 'status': + return self.status + filter_value = super(Snapshot, self).get_filter_value(filter_name) if filter_value is None: diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py index ab5256976..74767aa6b 100755 --- a/moto/ec2/responses/amis.py +++ b/moto/ec2/responses/amis.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_querystring, \ - filters_from_querystring, sequence_from_querystring + filters_from_querystring, sequence_from_querystring, executable_users_from_querystring class AmisResponse(BaseResponse): @@ -43,8 +43,9 @@ class AmisResponse(BaseResponse): def describe_images(self): ami_ids = image_ids_from_querystring(self.querystring) filters = filters_from_querystring(self.querystring) + exec_users = executable_users_from_querystring(self.querystring) images = self.ec2_backend.describe_images( - ami_ids=ami_ids, filters=filters) + ami_ids=ami_ids, filters=filters, exec_users=exec_users) template = self.response_template(DESCRIBE_IMAGES_RESPONSE) return template.render(images=images) diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index 7902dc375..d964fc22b 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -11,6 +11,7 @@ class InstanceResponse(BaseResponse): def describe_instances(self): filter_dict = filters_from_querystring(self.querystring) instance_ids = instance_ids_from_querystring(self.querystring) + token = self._get_param("NextToken") if instance_ids: reservations = self.ec2_backend.get_reservations_by_instance_ids( instance_ids, filters=filter_dict) @@ -18,8 +19,18 @@ class InstanceResponse(BaseResponse): reservations = self.ec2_backend.all_reservations( make_copy=True, filters=filter_dict) + reservation_ids = [reservation.id for reservation in reservations] + if token: + start = reservation_ids.index(token) + 1 + else: + start = 0 + max_results = int(self._get_param('MaxResults', 100)) + reservations_resp = reservations[start:start + max_results] + next_token = None + if max_results and len(reservations) > (start + max_results): + next_token = reservations_resp[-1].id template = self.response_template(EC2_DESCRIBE_INSTANCES) - return template.render(reservations=reservations) + return template.render(reservations=reservations_resp, next_token=next_token) def run_instances(self): min_count = int(self.querystring.get('MinCount', ['1'])[0]) @@ -492,6 +503,9 @@ EC2_DESCRIBE_INSTANCES = """ start + page_size: + next_marker = load_balancers_resp[-1].name + template = self.response_template(DESCRIBE_LOAD_BALANCERS_TEMPLATE) - return template.render(load_balancers=load_balancers) + return template.render(load_balancers=load_balancers_resp, marker=next_marker) def delete_load_balancer_listeners(self): load_balancer_name = self._get_param('LoadBalancerName') @@ -493,6 +505,9 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ start + page_size: + next_marker = instances_resp[-1].db_instance_identifier + template = self.response_template(DESCRIBE_DATABASES_TEMPLATE) - return template.render(databases=databases) + return template.render(databases=instances_resp, marker=next_marker) def modify_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') @@ -187,6 +199,9 @@ DESCRIBE_DATABASES_TEMPLATE = """=2.8", "boto>=2.36.0", + "boto3>=1.2.1", "cookies", "requests>=2.0", "xmltodict", diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 9a6408999..8487ecb49 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -115,6 +115,30 @@ def test_create_autoscaling_groups_defaults(): list(group.tags).should.equal([]) +@mock_autoscaling +def test_list_many_autoscaling_groups(): + conn = boto3.client('autoscaling', region_name='us-east-1') + conn.create_launch_configuration(LaunchConfigurationName='TestLC') + + for i in range(51): + conn.create_auto_scaling_group(AutoScalingGroupName='TestGroup%d' % i, + MinSize=1, + MaxSize=2, + LaunchConfigurationName='TestLC') + + response = conn.describe_auto_scaling_groups() + groups = response["AutoScalingGroups"] + marker = response["NextToken"] + groups.should.have.length_of(50) + marker.should.equal(groups[-1]['AutoScalingGroupName']) + + response2 = conn.describe_auto_scaling_groups(NextToken=marker) + + groups.extend(response2["AutoScalingGroups"]) + groups.should.have.length_of(51) + assert 'NextToken' not in response2.keys() + + @mock_autoscaling_deprecated def test_autoscaling_group_describe_filter(): conn = boto.connect_autoscale() diff --git a/tests/test_autoscaling/test_launch_configurations.py b/tests/test_autoscaling/test_launch_configurations.py index 1c1486421..931fc8a7e 100644 --- a/tests/test_autoscaling/test_launch_configurations.py +++ b/tests/test_autoscaling/test_launch_configurations.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals import boto +import boto3 from boto.ec2.autoscale.launchconfig import LaunchConfiguration from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping import sure # noqa from moto import mock_autoscaling_deprecated +from moto import mock_autoscaling from tests.helpers import requires_boto_gte @@ -208,6 +210,25 @@ def test_launch_configuration_describe_filter(): conn.get_all_launch_configurations().should.have.length_of(3) +@mock_autoscaling +def test_launch_configuration_describe_paginated(): + conn = boto3.client('autoscaling', region_name='us-east-1') + for i in range(51): + conn.create_launch_configuration(LaunchConfigurationName='TestLC%d' % i) + + response = conn.describe_launch_configurations() + lcs = response["LaunchConfigurations"] + marker = response["NextToken"] + lcs.should.have.length_of(50) + marker.should.equal(lcs[-1]['LaunchConfigurationName']) + + response2 = conn.describe_launch_configurations(NextToken=marker) + + lcs.extend(response2["LaunchConfigurations"]) + lcs.should.have.length_of(51) + assert 'NextToken' not in response2.keys() + + @mock_autoscaling_deprecated def test_launch_configuration_delete(): conn = boto.connect_autoscale() diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 9a531010f..85815e9f8 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -144,6 +144,26 @@ def test_create_stack_from_s3_url(): 'TemplateBody'].should.equal(dummy_template) +@mock_cloudformation +def test_describe_stack_pagination(): + conn = boto3.client('cloudformation', region_name='us-east-1') + for i in range(100): + conn.create_stack( + StackName="test_stack", + TemplateBody=dummy_template_json, + ) + + resp = conn.describe_stacks() + stacks = resp['Stacks'] + stacks.should.have.length_of(50) + next_token = resp['NextToken'] + next_token.should_not.be.none + resp2 = conn.describe_stacks(NextToken=next_token) + stacks.extend(resp2['Stacks']) + stacks.should.have.length_of(100) + assert 'NextToken' not in resp2.keys() + + @mock_cloudformation def test_describe_stack_resources(): cf_conn = boto3.client('cloudformation', region_name='us-east-1') diff --git a/tests/test_datapipeline/test_datapipeline.py b/tests/test_datapipeline/test_datapipeline.py index 490e3bfa4..ce190c7e4 100644 --- a/tests/test_datapipeline/test_datapipeline.py +++ b/tests/test_datapipeline/test_datapipeline.py @@ -170,6 +170,19 @@ def test_listing_pipelines(): }) +@mock_datapipeline_deprecated +def test_listing_paginated_pipelines(): + conn = boto.datapipeline.connect_to_region("us-west-2") + for i in range(100): + conn.create_pipeline("mypipeline%d" % i, "some-unique-id%d" % i) + + response = conn.list_pipelines() + + response["hasMoreResults"].should.be(True) + response["marker"].should.equal(response["pipelineIdList"][-1]['id']) + response["pipelineIdList"].should.have.length_of(50) + + # testing a helper function def test_remove_capitalization_of_dict_keys(): result = remove_capitalization_of_dict_keys( diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 860333e50..7fec5c2bd 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -2,11 +2,13 @@ from __future__ import unicode_literals, print_function import six import boto +import boto3 import sure # noqa import requests from moto import mock_dynamodb2, mock_dynamodb2_deprecated from moto.dynamodb2 import dynamodb_backend2 from boto.exception import JSONResponseError +from botocore.exceptions import ClientError from tests.helpers import requires_boto_gte import tests.backport_assert_raises from nose.tools import assert_raises @@ -64,3 +66,86 @@ def test_describe_missing_table(): aws_secret_access_key="sk") with assert_raises(JSONResponseError): conn.describe_table('messages') + + +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_list_table_tags(): + name = 'TestTable' + conn = boto3.client('dynamodb', + region_name='us-west-2', + aws_access_key_id="ak", + aws_secret_access_key="sk") + conn.create_table(TableName=name, + KeySchema=[{'AttributeName':'id','KeyType':'HASH'}], + AttributeDefinitions=[{'AttributeName':'id','AttributeType':'S'}], + ProvisionedThroughput={'ReadCapacityUnits':5,'WriteCapacityUnits':5}) + table_description = conn.describe_table(TableName=name) + arn = table_description['Table']['TableArn'] + tags = [{'Key':'TestTag', 'Value': 'TestValue'}] + conn.tag_resource(ResourceArn=arn, + Tags=tags) + resp = conn.list_tags_of_resource(ResourceArn=arn) + assert resp["Tags"] == tags + + +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_list_table_tags_empty(): + name = 'TestTable' + conn = boto3.client('dynamodb', + region_name='us-west-2', + aws_access_key_id="ak", + aws_secret_access_key="sk") + conn.create_table(TableName=name, + KeySchema=[{'AttributeName':'id','KeyType':'HASH'}], + AttributeDefinitions=[{'AttributeName':'id','AttributeType':'S'}], + ProvisionedThroughput={'ReadCapacityUnits':5,'WriteCapacityUnits':5}) + table_description = conn.describe_table(TableName=name) + arn = table_description['Table']['TableArn'] + tags = [{'Key':'TestTag', 'Value': 'TestValue'}] + # conn.tag_resource(ResourceArn=arn, + # Tags=tags) + resp = conn.list_tags_of_resource(ResourceArn=arn) + assert resp["Tags"] == [] + + +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_list_table_tags_paginated(): + name = 'TestTable' + conn = boto3.client('dynamodb', + region_name='us-west-2', + aws_access_key_id="ak", + aws_secret_access_key="sk") + conn.create_table(TableName=name, + KeySchema=[{'AttributeName':'id','KeyType':'HASH'}], + AttributeDefinitions=[{'AttributeName':'id','AttributeType':'S'}], + ProvisionedThroughput={'ReadCapacityUnits':5,'WriteCapacityUnits':5}) + table_description = conn.describe_table(TableName=name) + arn = table_description['Table']['TableArn'] + for i in range(11): + tags = [{'Key':'TestTag%d' % i, 'Value': 'TestValue'}] + conn.tag_resource(ResourceArn=arn, + Tags=tags) + resp = conn.list_tags_of_resource(ResourceArn=arn) + assert len(resp["Tags"]) == 10 + assert 'NextToken' in resp.keys() + resp2 = conn.list_tags_of_resource(ResourceArn=arn, + NextToken=resp['NextToken']) + assert len(resp2["Tags"]) == 1 + assert 'NextToken' not in resp2.keys() + + +@requires_boto_gte("2.9") +@mock_dynamodb2 +def test_list_not_found_table_tags(): + conn = boto3.client('dynamodb', + region_name='us-west-2', + aws_access_key_id="ak", + aws_secret_access_key="sk") + arn = 'DymmyArn' + try: + conn.list_tags_of_resource(ResourceArn=arn) + except ClientError as exception: + assert exception.response['Error']['Code'] == "ResourceNotFoundException" diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index c740350ef..cf7c958d3 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -77,13 +77,14 @@ def test_create_table(): 'TableSizeBytes': 0, 'TableName': 'messages', 'TableStatus': 'ACTIVE', + 'TableArn': 'arn:aws:dynamodb:us-east-1:123456789011:table/messages', 'KeySchema': [ {'KeyType': 'HASH', 'AttributeName': 'forum_name'}, {'KeyType': 'RANGE', 'AttributeName': 'subject'} ], 'LocalSecondaryIndexes': [], 'ItemCount': 0, 'CreationDateTime': 1326499200.0, - 'GlobalSecondaryIndexes': [], + 'GlobalSecondaryIndexes': [] } } table.describe().should.equal(expected) @@ -109,6 +110,7 @@ def test_create_table_with_local_index(): 'TableSizeBytes': 0, 'TableName': 'messages', 'TableStatus': 'ACTIVE', + 'TableArn': 'arn:aws:dynamodb:us-east-1:123456789011:table/messages', 'KeySchema': [ {'KeyType': 'HASH', 'AttributeName': 'forum_name'}, {'KeyType': 'RANGE', 'AttributeName': 'subject'} @@ -125,7 +127,7 @@ def test_create_table_with_local_index(): ], 'ItemCount': 0, 'CreationDateTime': 1326499200.0, - 'GlobalSecondaryIndexes': [], + 'GlobalSecondaryIndexes': [] } } table.describe().should.equal(expected) diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 36e1b6c61..e38194f36 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -44,6 +44,7 @@ def test_create_table(): 'TableSizeBytes': 0, 'TableName': 'messages', 'TableStatus': 'ACTIVE', + 'TableArn': 'arn:aws:dynamodb:us-east-1:123456789011:table/messages', 'KeySchema': [ {'KeyType': 'HASH', 'AttributeName': 'forum_name'} ], diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index c9570e1a6..ed251f527 100755 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -4,17 +4,18 @@ import tests.backport_assert_raises # noqa from nose.tools import assert_raises import boto +import boto3 import boto.ec2 import boto3 from boto.exception import EC2ResponseError, EC2ResponseError import sure # noqa -from moto import mock_emr_deprecated, mock_ec2 +from moto import mock_ec2_deprecated, mock_ec2 from tests.helpers import requires_boto_gte -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_create_and_delete(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -75,7 +76,7 @@ def test_ami_create_and_delete(): @requires_boto_gte("2.14.0") -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_copy(): conn = boto.ec2.connect_to_region("us-west-1") reservation = conn.run_instances('ami-1234abcd') @@ -134,7 +135,7 @@ def test_ami_copy(): cm.exception.request_id.should_not.be.none -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_tagging(): conn = boto.connect_vpc('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -161,7 +162,7 @@ def test_ami_tagging(): image.tags["a key"].should.equal("some value") -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_create_from_missing_instance(): conn = boto.connect_ec2('the_key', 'the_secret') args = ["i-abcdefg", "test-ami", "this is a test ami"] @@ -173,7 +174,7 @@ def test_ami_create_from_missing_instance(): cm.exception.request_id.should_not.be.none -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_pulls_attributes_from_instance(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -185,7 +186,7 @@ def test_ami_pulls_attributes_from_instance(): image.kernel_id.should.equal('test-kernel') -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_filters(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -242,7 +243,7 @@ def test_ami_filters(): set([ami.id for ami in amis_by_nonpublic]).should.equal(set([imageA.id])) -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_filtering_via_tag(): conn = boto.connect_vpc('the_key', 'the_secret') @@ -268,7 +269,7 @@ def test_ami_filtering_via_tag(): set([ami.id for ami in amis_by_tagB]).should.equal(set([imageB.id])) -@mock_emr_deprecated +@mock_ec2_deprecated def test_getting_missing_ami(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -279,7 +280,7 @@ def test_getting_missing_ami(): cm.exception.request_id.should_not.be.none -@mock_emr_deprecated +@mock_ec2_deprecated def test_getting_malformed_ami(): conn = boto.connect_ec2('the_key', 'the_secret') @@ -290,7 +291,7 @@ def test_getting_malformed_ami(): cm.exception.request_id.should_not.be.none -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_attribute_group_permissions(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -350,7 +351,7 @@ def test_ami_attribute_group_permissions(): **REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError) -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_attribute_user_permissions(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') @@ -422,7 +423,107 @@ def test_ami_attribute_user_permissions(): **REMOVE_USERS_ARGS).should_not.throw(EC2ResponseError) -@mock_emr_deprecated +@mock_ec2_deprecated +def test_ami_describe_executable_users(): + conn = boto3.client('ec2', region_name='us-east-1') + ec2 = boto3.resource('ec2', 'us-east-1') + ec2.create_instances(ImageId='', + MinCount=1, + MaxCount=1) + response = conn.describe_instances(Filters=[{'Name': 'instance-state-name','Values': ['running']}]) + instance_id = response['Reservations'][0]['Instances'][0]['InstanceId'] + image_id = conn.create_image(InstanceId=instance_id, + Name='TestImage',)['ImageId'] + + + USER1 = '123456789011' + + ADD_USER_ARGS = {'ImageId': image_id, + 'Attribute': 'launchPermission', + 'OperationType': 'add', + 'UserIds': [USER1]} + + # Add users and get no images + conn.modify_image_attribute(**ADD_USER_ARGS) + + attributes = conn.describe_image_attribute(ImageId=image_id, + Attribute='LaunchPermissions', + DryRun=False) + attributes['LaunchPermissions'].should.have.length_of(1) + attributes['LaunchPermissions'][0]['UserId'].should.equal(USER1) + images = conn.describe_images(ExecutableUsers=[USER1])['Images'] + images.should.have.length_of(1) + images[0]['ImageId'].should.equal(image_id) + + +@mock_ec2_deprecated +def test_ami_describe_executable_users_negative(): + conn = boto3.client('ec2', region_name='us-east-1') + ec2 = boto3.resource('ec2', 'us-east-1') + ec2.create_instances(ImageId='', + MinCount=1, + MaxCount=1) + response = conn.describe_instances(Filters=[{'Name': 'instance-state-name','Values': ['running']}]) + instance_id = response['Reservations'][0]['Instances'][0]['InstanceId'] + image_id = conn.create_image(InstanceId=instance_id, + Name='TestImage')['ImageId'] + + + USER1 = '123456789011' + USER2 = '113355789012' + + ADD_USER_ARGS = {'ImageId': image_id, + 'Attribute': 'launchPermission', + 'OperationType': 'add', + 'UserIds': [USER1]} + + # Add users and get no images + conn.modify_image_attribute(**ADD_USER_ARGS) + + attributes = conn.describe_image_attribute(ImageId=image_id, + Attribute='LaunchPermissions', + DryRun=False) + attributes['LaunchPermissions'].should.have.length_of(1) + attributes['LaunchPermissions'][0]['UserId'].should.equal(USER1) + images = conn.describe_images(ExecutableUsers=[USER2])['Images'] + images.should.have.length_of(0) + + +@mock_ec2_deprecated +def test_ami_describe_executable_users_and_filter(): + conn = boto3.client('ec2', region_name='us-east-1') + ec2 = boto3.resource('ec2', 'us-east-1') + ec2.create_instances(ImageId='', + MinCount=1, + MaxCount=1) + response = conn.describe_instances(Filters=[{'Name': 'instance-state-name','Values': ['running']}]) + instance_id = response['Reservations'][0]['Instances'][0]['InstanceId'] + image_id = conn.create_image(InstanceId=instance_id, + Name='ImageToDelete',)['ImageId'] + + + USER1 = '123456789011' + + ADD_USER_ARGS = {'ImageId': image_id, + 'Attribute': 'launchPermission', + 'OperationType': 'add', + 'UserIds': [USER1]} + + # Add users and get no images + conn.modify_image_attribute(**ADD_USER_ARGS) + + attributes = conn.describe_image_attribute(ImageId=image_id, + Attribute='LaunchPermissions', + DryRun=False) + attributes['LaunchPermissions'].should.have.length_of(1) + attributes['LaunchPermissions'][0]['UserId'].should.equal(USER1) + images = conn.describe_images(ExecutableUsers=[USER1], + Filters=[{'Name': 'state', 'Values': ['available']}])['Images'] + images.should.have.length_of(1) + images[0]['ImageId'].should.equal(image_id) + + +@mock_ec2_deprecated def test_ami_attribute_user_and_group_permissions(): """ Boto supports adding/removing both users and groups at the same time. @@ -477,7 +578,7 @@ def test_ami_attribute_user_and_group_permissions(): image.is_public.should.equal(False) -@mock_emr_deprecated +@mock_ec2_deprecated def test_ami_attribute_error_cases(): conn = boto.connect_ec2('the_key', 'the_secret') reservation = conn.run_instances('ami-1234abcd') diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index 83c89d129..b238e68f9 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -336,6 +336,11 @@ def test_snapshot_filters(): set([snap.id for snap in snapshots_by_volume_id] ).should.equal(set([snapshot1.id, snapshot2.id])) + snapshots_by_status = conn.get_all_snapshots( + filters={'status': 'completed'}) + set([snap.id for snap in snapshots_by_status] + ).should.equal(set([snapshot1.id, snapshot2.id, snapshot3.id])) + snapshots_by_volume_size = conn.get_all_snapshots( filters={'volume-size': volume1.size}) set([snap.id for snap in snapshots_by_volume_size] diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 49020555b..6687a7e6c 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -7,12 +7,13 @@ import base64 import datetime import boto +import boto3 from boto.ec2.instance import Reservation, InstanceAttribute from boto.exception import EC2ResponseError, EC2ResponseError from freezegun import freeze_time import sure # noqa -from moto import mock_ec2_deprecated +from moto import mock_ec2_deprecated, mock_ec2 from tests.helpers import requires_boto_gte @@ -157,6 +158,26 @@ def test_get_instances_by_id(): cm.exception.request_id.should_not.be.none +@mock_ec2 +def test_get_paginated_instances(): + image_id = 'ami-1234abcd' + client = boto3.client('ec2', region_name='us-east-1') + conn = boto3.resource('ec2', 'us-east-1') + for i in range(100): + conn.create_instances(ImageId=image_id, + MinCount=1, + MaxCount=1) + resp = client.describe_instances(MaxResults=50) + reservations = resp['Reservations'] + reservations.should.have.length_of(50) + next_token = resp['NextToken'] + next_token.should_not.be.none + resp2 = client.describe_instances(NextToken=next_token) + reservations.extend(resp2['Reservations']) + reservations.should.have.length_of(100) + assert 'NextToken' not in resp2.keys() + + @mock_ec2_deprecated def test_get_instances_filtering_by_state(): conn = boto.connect_ec2() @@ -337,6 +358,20 @@ def test_get_instances_filtering_by_architecture(): reservations[0].instances.should.have.length_of(1) +@mock_ec2 +def test_get_instances_filtering_by_image_id(): + image_id = 'ami-1234abcd' + client = boto3.client('ec2', region_name='us-east-1') + conn = boto3.resource('ec2', 'us-east-1') + conn.create_instances(ImageId=image_id, + MinCount=1, + MaxCount=1) + + reservations = client.describe_instances(Filters=[{'Name': 'image-id', + 'Values': [image_id]}])['Reservations'] + reservations[0]['Instances'].should.have.length_of(1) + + @mock_ec2_deprecated def test_get_instances_filtering_by_tag(): conn = boto.connect_ec2() diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 4b5d59d6d..3c991d565 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -109,6 +109,27 @@ def test_create_and_delete_boto3_support(): 'LoadBalancerDescriptions']).should.have.length_of(0) +@mock_elb +def test_describe_paginated_balancers(): + client = boto3.client('elb', region_name='us-east-1') + + for i in range(51): + client.create_load_balancer( + LoadBalancerName='my-lb%d' % i, + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 80, 'InstancePort': 8080}], + AvailabilityZones=['us-east-1a', 'us-east-1b'] + ) + + resp = client.describe_load_balancers() + resp['LoadBalancerDescriptions'].should.have.length_of(50) + resp['NextMarker'].should.equal(resp['LoadBalancerDescriptions'][-1]['LoadBalancerName']) + resp2 = client.describe_load_balancers(Marker=resp['NextMarker']) + resp2['LoadBalancerDescriptions'].should.have.length_of(1) + assert 'NextToken' not in resp2.keys() + + + @mock_elb_deprecated def test_add_listener(): conn = boto.connect_elb() diff --git a/tests/test_emr/test_emr_boto3.py b/tests/test_emr/test_emr_boto3.py index b2877c7f5..830abdb85 100644 --- a/tests/test_emr/test_emr_boto3.py +++ b/tests/test_emr/test_emr_boto3.py @@ -127,6 +127,18 @@ def test_describe_cluster(): cl['VisibleToAllUsers'].should.equal(True) +@mock_emr +def test_describe_cluster_not_found(): + conn = boto3.client('emr', region_name='us-east-1') + raised = False + try: + cluster = conn.describe_cluster(ClusterId='DummyId') + except ClientError as e: + if e.response['Error']['Code'] == "ResourceNotFoundException": + raised = True + raised.should.equal(True) + + @mock_emr def test_describe_job_flows(): client = boto3.client('emr', region_name='us-east-1') diff --git a/tests/test_kinesis/test_kinesis.py b/tests/test_kinesis/test_kinesis.py index 5b2f9ccf3..26a87f35a 100644 --- a/tests/test_kinesis/test_kinesis.py +++ b/tests/test_kinesis/test_kinesis.py @@ -2,9 +2,10 @@ from __future__ import unicode_literals import boto.kinesis from boto.kinesis.exceptions import ResourceNotFoundException, InvalidArgumentException +import boto3 import sure # noqa -from moto import mock_kinesis_deprecated +from moto import mock_kinesis, mock_kinesis_deprecated @mock_kinesis_deprecated @@ -51,6 +52,25 @@ def test_list_and_delete_stream(): "not-a-stream").should.throw(ResourceNotFoundException) +@mock_kinesis +def test_list_many_streams(): + conn = boto3.client('kinesis', region_name="us-west-2") + + for i in range(11): + conn.create_stream(StreamName="stream%d" % i, ShardCount=1) + + resp = conn.list_streams() + stream_names = resp["StreamNames"] + has_more_streams = resp["HasMoreStreams"] + stream_names.should.have.length_of(10) + has_more_streams.should.be(True) + resp2 = conn.list_streams(ExclusiveStartStreamName=stream_names[-1]) + stream_names = resp2["StreamNames"] + has_more_streams = resp2["HasMoreStreams"] + stream_names.should.have.length_of(1) + has_more_streams.should.equal(False) + + @mock_kinesis_deprecated def test_basic_shard_iterator(): conn = boto.kinesis.connect_to_region("us-west-2") diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index 090147d11..0a474ee26 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals +import boto3 import boto.rds import boto.vpc from boto.exception import BotoServerError import sure # noqa -from moto import mock_ec2_deprecated, mock_rds_deprecated +from moto import mock_ec2_deprecated, mock_rds_deprecated, mock_rds from tests.helpers import disable_on_py3 @@ -45,6 +46,26 @@ def test_get_databases(): databases[0].id.should.equal("db-master-1") +@disable_on_py3() +@mock_rds +def test_get_databases_paginated(): + conn = boto3.client('rds', region_name="us-west-2") + + for i in range(51): + conn.create_db_instance(AllocatedStorage=5, + Port=5432, + DBInstanceIdentifier='rds%d' % i, + DBInstanceClass='db.t1.micro', + Engine='postgres') + + resp = conn.describe_db_instances() + resp["DBInstances"].should.have.length_of(50) + resp["Marker"].should.equal(resp["DBInstances"][-1]['DBInstanceIdentifier']) + + resp2 = conn.describe_db_instances(Marker=resp["Marker"]) + resp2["DBInstances"].should.have.length_of(1) + + @mock_rds_deprecated def test_describe_non_existant_database(): conn = boto.rds.connect_to_region("us-west-2") diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 1e2e0abdf..915695ad8 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -65,6 +65,25 @@ def test_get_databases(): 'arn:aws:rds:us-west-2:1234567890:db:db-master-1') +@disable_on_py3() +@mock_rds2 +def test_get_databases_paginated(): + conn = boto3.client('rds', region_name="us-west-2") + + for i in range(51): + conn.create_db_instance(AllocatedStorage=5, + Port=5432, + DBInstanceIdentifier='rds%d' % i, + DBInstanceClass='db.t1.micro', + Engine='postgres') + + resp = conn.describe_db_instances() + resp["DBInstances"].should.have.length_of(50) + resp["Marker"].should.equal(resp["DBInstances"][-1]['DBInstanceIdentifier']) + + resp2 = conn.describe_db_instances(Marker=resp["Marker"]) + resp2["DBInstances"].should.have.length_of(1) + @disable_on_py3() @mock_rds2 def test_describe_non_existant_database(): From 2adc5f2ace5b48ef95135163b36f8ffadf5c595c Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Wed, 10 May 2017 22:44:57 -0400 Subject: [PATCH 148/274] Prefix should not be required for S3 lifecycle config. Closes #930. --- moto/s3/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index 1cf183d56..ec3e69f1b 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -281,7 +281,7 @@ class FakeBucket(BaseModel): transition = rule.get('Transition') self.rules.append(LifecycleRule( id=rule.get('ID'), - prefix=rule['Prefix'], + prefix=rule.get('Prefix'), status=rule['Status'], expiration_days=expiration.get('Days') if expiration else None, expiration_date=expiration.get('Date') if expiration else None, From 94a923ae91227513c939d346af46b45e1446d4da Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 11 May 2017 06:50:33 -0700 Subject: [PATCH 149/274] adding pyaml dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 37eb78ccf..51e1748f5 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ install_requires = [ "dicttoxml", "six", "werkzeug", + "pyaml", "pytz", "python-dateutil", ] From 9801d226294cc2493c4a772fbb66d042c4852185 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 11 May 2017 07:15:07 -0700 Subject: [PATCH 150/274] The name of yaml exceptions is more consistent between Py2->3 --- moto/cloudformation/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 824d568a2..f791406ab 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -73,9 +73,9 @@ class FakeStack(BaseModel): def _parse_template(self): try: - self.template_dict = json.loads(self.template) - except json.JSONDecodeError: self.template_dict = yaml.load(self.template) + except yaml.parser.ParserError: + self.template_dict = json.loads(self.template) @property def stack_parameters(self): From 97b920f6cfcf9e60513d957dcd6494dad5b00d05 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Fri, 12 May 2017 19:48:14 -0400 Subject: [PATCH 151/274] Fix ec2 tags in instance create. Closes #938. --- moto/core/responses.py | 22 ++++++++++++++++++ moto/ec2/models.py | 9 ++++++++ moto/ec2/responses/instances.py | 12 +++++++++- tests/test_ec2/test_instances.py | 38 ++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/moto/core/responses.py b/moto/core/responses.py index a5a1f3880..adad5d1de 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -1,4 +1,6 @@ from __future__ import unicode_literals + +from collections import defaultdict import datetime import json import logging @@ -330,6 +332,26 @@ class BaseResponse(_TemplateEnvironmentMixin): return results + def _parse_tag_specification(self, param_prefix): + tags = self._get_list_prefix(param_prefix) + + results = defaultdict(dict) + for tag in tags: + resource_type = tag.pop("resource_type") + + param_index = 1 + while True: + key_name = 'tag.{0}._key'.format(param_index) + value_name = 'tag.{0}._value'.format(param_index) + + try: + results[resource_type][tag[key_name]] = tag[value_name] + except KeyError: + break + param_index += 1 + + return results + @property def request_json(self): return 'JSON' in self.querystring.get('ContentType', []) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 8be13d867..87d2d59e5 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -141,6 +141,10 @@ class TaggedEC2Resource(BaseModel): def add_tag(self, key, value): self.ec2_backend.create_tags([self.id], {key: value}) + def add_tags(self, tag_map): + for key, value in tag_map.items(): + self.ec2_backend.create_tags([self.id], {key: value}) + def get_filter_value(self, filter_name): tags = self.get_tags() @@ -638,6 +642,10 @@ class InstanceBackend(object): security_groups.extend(self.get_security_group_from_id(sg_id) for sg_id in kwargs.pop("security_group_ids", [])) self.reservations[new_reservation.id] = new_reservation + + tags = kwargs.pop("tags", {}) + instance_tags = tags.get('instance', {}) + for index in range(count): new_instance = Instance( self, @@ -647,6 +655,7 @@ class InstanceBackend(object): **kwargs ) new_reservation.instances.append(new_instance) + new_instance.add_tags(instance_tags) new_instance.setup_defaults() return new_reservation diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index d964fc22b..09ecb172b 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -47,13 +47,15 @@ class InstanceResponse(BaseResponse): associate_public_ip = self.querystring.get( "AssociatePublicIpAddress", [None])[0] key_name = self.querystring.get("KeyName", [None])[0] + tags = self._parse_tag_specification("TagSpecification") if self.is_not_dryrun('RunInstance'): new_reservation = self.ec2_backend.add_instances( image_id, min_count, user_data, security_group_names, instance_type=instance_type, placement=placement, subnet_id=subnet_id, key_name=key_name, security_group_ids=security_group_ids, - nics=nics, private_ip=private_ip, associate_public_ip=associate_public_ip) + nics=nics, private_ip=private_ip, associate_public_ip=associate_public_ip, + tags=tags) template = self.response_template(EC2_RUN_INSTANCES) return template.render(reservation=new_reservation) @@ -282,6 +284,14 @@ EC2_RUN_INSTANCES = """ Date: Fri, 12 May 2017 19:59:26 -0400 Subject: [PATCH 154/274] Version 1.0.0 --- moto/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/__init__.py b/moto/__init__.py index 8101a4332..c93719cb2 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -3,7 +3,7 @@ import logging # logging.getLogger('boto').setLevel(logging.CRITICAL) __title__ = 'moto' -__version__ = '0.4.31' +__version__ = '1.0.0' from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa from .autoscaling import mock_autoscaling, mock_autoscaling_deprecated # flake8: noqa diff --git a/setup.py b/setup.py index 790bec377..1116b1794 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ extras_require = { setup( name='moto', - version='0.4.31', + version='1.0.0', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', From b6d9b4f58421f6f674d140718bc692f0cc408e8a Mon Sep 17 00:00:00 2001 From: Jerome Bosman Date: Sun, 14 May 2017 13:03:43 +0100 Subject: [PATCH 155/274] Replace and delete Network ACL Entries --- moto/ec2/models.py | 90 ++++++++--------------------- moto/ec2/responses/network_acls.py | 46 +++++++++++++-- tests/test_ec2/test_network_acls.py | 55 ++++++++++++++++++ 3 files changed, 121 insertions(+), 70 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 87d2d59e5..6631706f1 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -118,21 +118,18 @@ def validate_resource_ids(resource_ids): class InstanceState(object): - def __init__(self, name='pending', code=0): self.name = name self.code = code class StateReason(object): - def __init__(self, message="", code=""): self.message = message self.code = code class TaggedEC2Resource(BaseModel): - def get_tags(self, *args, **kwargs): tags = self.ec2_backend.describe_tags( filters={'resource-id': [self.id]}) @@ -164,7 +161,6 @@ class TaggedEC2Resource(BaseModel): class NetworkInterface(TaggedEC2Resource): - def __init__(self, ec2_backend, subnet, private_ip_address, device_index=0, public_ip_auto_assign=True, group_ids=None): self.ec2_backend = ec2_backend @@ -277,7 +273,6 @@ class NetworkInterface(TaggedEC2Resource): class NetworkInterfaceBackend(object): - def __init__(self): self.enis = {} super(NetworkInterfaceBackend, self).__init__() @@ -621,7 +616,6 @@ class Instance(TaggedEC2Resource, BotoInstance): class InstanceBackend(object): - def __init__(self): self.reservations = OrderedDict() super(InstanceBackend, self).__init__() @@ -791,7 +785,6 @@ class InstanceBackend(object): class KeyPairBackend(object): - def __init__(self): self.keypairs = defaultdict(dict) super(KeyPairBackend, self).__init__() @@ -830,7 +823,6 @@ class KeyPairBackend(object): class TagBackend(object): - VALID_TAG_FILTERS = ['key', 'resource-id', 'resource-type', @@ -957,7 +949,6 @@ class TagBackend(object): class Ami(TaggedEC2Resource): - def __init__(self, ec2_backend, ami_id, instance=None, source_ami=None, name=None, description=None): self.ec2_backend = ec2_backend @@ -1036,7 +1027,6 @@ class Ami(TaggedEC2Resource): class AmiBackend(object): - def __init__(self): self.amis = {} super(AmiBackend, self).__init__() @@ -1141,14 +1131,12 @@ class AmiBackend(object): class Region(object): - def __init__(self, name, endpoint): self.name = name self.endpoint = endpoint class Zone(object): - def __init__(self, name, region_name): self.name = name self.region_name = region_name @@ -1191,7 +1179,6 @@ class RegionsAndZonesBackend(object): class SecurityRule(object): - def __init__(self, ip_protocol, from_port, to_port, ip_ranges, source_groups): self.ip_protocol = ip_protocol self.from_port = from_port @@ -1214,7 +1201,6 @@ class SecurityRule(object): class SecurityGroup(TaggedEC2Resource): - def __init__(self, ec2_backend, group_id, name, description, vpc_id=None): self.ec2_backend = ec2_backend self.id = group_id @@ -1353,7 +1339,6 @@ class SecurityGroup(TaggedEC2Resource): class SecurityGroupBackend(object): - def __init__(self): # the key in the dict group is the vpc_id or None (non-vpc) self.groups = defaultdict(dict) @@ -1597,7 +1582,6 @@ class SecurityGroupBackend(object): class SecurityGroupIngress(object): - def __init__(self, security_group, properties): self.security_group = security_group self.properties = properties @@ -1656,7 +1640,6 @@ class SecurityGroupIngress(object): class VolumeAttachment(object): - def __init__(self, volume, instance, device, status): self.volume = volume self.attach_time = utc_date_and_time() @@ -1681,7 +1664,6 @@ class VolumeAttachment(object): class Volume(TaggedEC2Resource): - def __init__(self, ec2_backend, volume_id, size, zone, snapshot_id=None, encrypted=False): self.id = volume_id self.size = size @@ -1755,7 +1737,6 @@ class Volume(TaggedEC2Resource): class Snapshot(TaggedEC2Resource): - def __init__(self, ec2_backend, snapshot_id, volume, description, encrypted=False): self.id = snapshot_id self.volume = volume @@ -1799,7 +1780,6 @@ class Snapshot(TaggedEC2Resource): class EBSBackend(object): - def __init__(self): self.volumes = {} self.attachments = {} @@ -1916,7 +1896,6 @@ class EBSBackend(object): class VPC(TaggedEC2Resource): - def __init__(self, ec2_backend, vpc_id, cidr_block, is_default, instance_tenancy='default'): self.ec2_backend = ec2_backend self.id = vpc_id @@ -1972,7 +1951,6 @@ class VPC(TaggedEC2Resource): class VPCBackend(object): - def __init__(self): self.vpcs = {} super(VPCBackend, self).__init__() @@ -2015,7 +1993,7 @@ class VPCBackend(object): if len(route_tables) > 1: raise DependencyViolationError( "The vpc {0} has dependencies and cannot be deleted." - .format(vpc_id) + .format(vpc_id) ) for route_table in route_tables: self.delete_route_table(route_table.id) @@ -2052,7 +2030,6 @@ class VPCBackend(object): class VPCPeeringConnectionStatus(object): - def __init__(self, code='initiating-request', message=''): self.code = code self.message = message @@ -2075,7 +2052,6 @@ class VPCPeeringConnectionStatus(object): class VPCPeeringConnection(TaggedEC2Resource): - def __init__(self, vpc_pcx_id, vpc, peer_vpc): self.id = vpc_pcx_id self.vpc = vpc @@ -2100,7 +2076,6 @@ class VPCPeeringConnection(TaggedEC2Resource): class VPCPeeringConnectionBackend(object): - def __init__(self): self.vpc_pcxs = {} super(VPCPeeringConnectionBackend, self).__init__() @@ -2142,7 +2117,6 @@ class VPCPeeringConnectionBackend(object): class Subnet(TaggedEC2Resource): - def __init__(self, ec2_backend, subnet_id, vpc_id, cidr_block, availability_zone, default_for_az, map_public_ip_on_launch): self.ec2_backend = ec2_backend @@ -2226,7 +2200,6 @@ class Subnet(TaggedEC2Resource): class SubnetBackend(object): - def __init__(self): # maps availability zone to dict of (subnet_id, subnet) self.subnets = defaultdict(dict) @@ -2280,7 +2253,6 @@ class SubnetBackend(object): class SubnetRouteTableAssociation(object): - def __init__(self, route_table_id, subnet_id): self.route_table_id = route_table_id self.subnet_id = subnet_id @@ -2301,7 +2273,6 @@ class SubnetRouteTableAssociation(object): class SubnetRouteTableAssociationBackend(object): - def __init__(self): self.subnet_associations = {} super(SubnetRouteTableAssociationBackend, self).__init__() @@ -2315,7 +2286,6 @@ class SubnetRouteTableAssociationBackend(object): class RouteTable(TaggedEC2Resource): - def __init__(self, ec2_backend, route_table_id, vpc_id, main=False): self.ec2_backend = ec2_backend self.id = route_table_id @@ -2368,7 +2338,6 @@ class RouteTable(TaggedEC2Resource): class RouteTableBackend(object): - def __init__(self): self.route_tables = {} super(RouteTableBackend, self).__init__() @@ -2408,7 +2377,7 @@ class RouteTableBackend(object): if route_table.associations: raise DependencyViolationError( "The routeTable '{0}' has dependencies and cannot be deleted." - .format(route_table_id) + .format(route_table_id) ) self.route_tables.pop(route_table_id) return True @@ -2454,7 +2423,6 @@ class RouteTableBackend(object): class Route(object): - def __init__(self, route_table, destination_cidr_block, local=False, gateway=None, instance=None, interface=None, vpc_pcx=None): self.id = generate_route_id(route_table.id, destination_cidr_block) @@ -2489,7 +2457,6 @@ class Route(object): class RouteBackend(object): - def __init__(self): super(RouteBackend, self).__init__() @@ -2514,7 +2481,8 @@ class RouteBackend(object): instance=self.get_instance( instance_id) if instance_id else None, interface=None, - vpc_pcx=self.get_vpc_peering_connection(vpc_peering_connection_id) if vpc_peering_connection_id else None) + vpc_pcx=self.get_vpc_peering_connection( + vpc_peering_connection_id) if vpc_peering_connection_id else None) route_table.routes[route.id] = route return route @@ -2560,7 +2528,6 @@ class RouteBackend(object): class InternetGateway(TaggedEC2Resource): - def __init__(self, ec2_backend): self.ec2_backend = ec2_backend self.id = random_internet_gateway_id() @@ -2584,7 +2551,6 @@ class InternetGateway(TaggedEC2Resource): class InternetGatewayBackend(object): - def __init__(self): self.internet_gateways = {} super(InternetGatewayBackend, self).__init__() @@ -2613,7 +2579,7 @@ class InternetGatewayBackend(object): if igw.vpc: raise DependencyViolationError( "{0} is being utilized by {1}" - .format(internet_gateway_id, igw.vpc.id) + .format(internet_gateway_id, igw.vpc.id) ) self.internet_gateways.pop(internet_gateway_id) return True @@ -2639,7 +2605,6 @@ class InternetGatewayBackend(object): class VPCGatewayAttachment(BaseModel): - def __init__(self, gateway_id, vpc_id): self.gateway_id = gateway_id self.vpc_id = vpc_id @@ -2663,7 +2628,6 @@ class VPCGatewayAttachment(BaseModel): class VPCGatewayAttachmentBackend(object): - def __init__(self): self.gateway_attachments = {} super(VPCGatewayAttachmentBackend, self).__init__() @@ -2675,7 +2639,6 @@ class VPCGatewayAttachmentBackend(object): class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): - def __init__(self, ec2_backend, spot_request_id, price, image_id, type, valid_from, valid_until, launch_group, availability_zone_group, key_name, security_groups, user_data, instance_type, placement, @@ -2746,7 +2709,6 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): @six.add_metaclass(Model) class SpotRequestBackend(object): - def __init__(self): self.spot_instance_requests = {} super(SpotRequestBackend, self).__init__() @@ -2782,7 +2744,6 @@ class SpotRequestBackend(object): class SpotFleetLaunchSpec(object): - def __init__(self, ebs_optimized, group_set, iam_instance_profile, image_id, instance_type, key_name, monitoring, spot_price, subnet_id, user_data, weighted_capacity): @@ -2800,7 +2761,6 @@ class SpotFleetLaunchSpec(object): class SpotFleetRequest(TaggedEC2Resource): - def __init__(self, ec2_backend, spot_fleet_request_id, spot_price, target_capacity, iam_fleet_role, allocation_strategy, launch_specs): @@ -2857,7 +2817,8 @@ class SpotFleetRequest(TaggedEC2Resource): ] spot_fleet_request = ec2_backend.request_spot_fleet(spot_price, - target_capacity, iam_fleet_role, allocation_strategy, launch_specs) + target_capacity, iam_fleet_role, allocation_strategy, + launch_specs) return spot_fleet_request @@ -2913,7 +2874,6 @@ class SpotFleetRequest(TaggedEC2Resource): class SpotFleetBackend(object): - def __init__(self): self.spot_fleet_requests = {} super(SpotFleetBackend, self).__init__() @@ -2954,7 +2914,6 @@ class SpotFleetBackend(object): class ElasticAddress(object): - def __init__(self, domain): self.public_ip = random_ip() self.allocation_id = random_eip_allocation_id() if domain == "vpc" else None @@ -2995,7 +2954,6 @@ class ElasticAddress(object): class ElasticAddressBackend(object): - def __init__(self): self.addresses = [] super(ElasticAddressBackend, self).__init__() @@ -3102,7 +3060,6 @@ class ElasticAddressBackend(object): class DHCPOptionsSet(TaggedEC2Resource): - def __init__(self, ec2_backend, domain_name_servers=None, domain_name=None, ntp_servers=None, netbios_name_servers=None, netbios_node_type=None): @@ -3153,7 +3110,6 @@ class DHCPOptionsSet(TaggedEC2Resource): class DHCPOptionsSetBackend(object): - def __init__(self): self.dhcp_options_sets = {} super(DHCPOptionsSetBackend, self).__init__() @@ -3220,7 +3176,6 @@ class DHCPOptionsSetBackend(object): class VPNConnection(TaggedEC2Resource): - def __init__(self, ec2_backend, id, type, customer_gateway_id, vpn_gateway_id): self.ec2_backend = ec2_backend @@ -3236,7 +3191,6 @@ class VPNConnection(TaggedEC2Resource): class VPNConnectionBackend(object): - def __init__(self): self.vpn_connections = {} super(VPNConnectionBackend, self).__init__() @@ -3287,7 +3241,6 @@ class VPNConnectionBackend(object): class NetworkAclBackend(object): - def __init__(self): self.network_acls = {} super(NetworkAclBackend, self).__init__() @@ -3338,6 +3291,24 @@ class NetworkAclBackend(object): network_acl.network_acl_entries.append(network_acl_entry) return network_acl_entry + def delete_network_acl_entry(self, network_acl_id, rule_number, egress): + network_acl = self.get_network_acl(network_acl_id) + entry = next(entry for entry in network_acl.netword_acl_entries + if entry.egress == egress and entry.rule_number == rule_number) + if entry is not None: + network_acl.netword_acl_entries.remove(entry) + return entry + + def replace_network_acl_entry(self, network_acl_id, rule_number, protocol, rule_action, egress, + cidr_block, icmp_code, icmp_type, port_range_from, port_range_to): + + self.delete_network_acl_entry(network_acl_id, rule_number, egress) + network_acl_entry = self.create_network_acl_entry(network_acl_id, rule_number, + protocol, rule_action, egress, + cidr_block, icmp_code, icmp_type, + port_range_from, port_range_to) + return network_acl_entry + def replace_network_acl_association(self, association_id, network_acl_id): @@ -3369,7 +3340,6 @@ class NetworkAclBackend(object): class NetworkAclAssociation(object): - def __init__(self, ec2_backend, new_association_id, subnet_id, network_acl_id): self.ec2_backend = ec2_backend @@ -3381,7 +3351,6 @@ class NetworkAclAssociation(object): class NetworkAcl(TaggedEC2Resource): - def __init__(self, ec2_backend, network_acl_id, vpc_id, default=False): self.ec2_backend = ec2_backend self.id = network_acl_id @@ -3410,7 +3379,6 @@ class NetworkAcl(TaggedEC2Resource): class NetworkAclEntry(TaggedEC2Resource): - def __init__(self, ec2_backend, network_acl_id, rule_number, protocol, rule_action, egress, cidr_block, icmp_code, icmp_type, port_range_from, @@ -3429,7 +3397,6 @@ class NetworkAclEntry(TaggedEC2Resource): class VpnGateway(TaggedEC2Resource): - def __init__(self, ec2_backend, id, type): self.ec2_backend = ec2_backend self.id = id @@ -3439,7 +3406,6 @@ class VpnGateway(TaggedEC2Resource): class VpnGatewayAttachment(object): - def __init__(self, vpc_id, state): self.vpc_id = vpc_id self.state = state @@ -3447,7 +3413,6 @@ class VpnGatewayAttachment(object): class VpnGatewayBackend(object): - def __init__(self): self.vpn_gateways = {} super(VpnGatewayBackend, self).__init__() @@ -3491,7 +3456,6 @@ class VpnGatewayBackend(object): class CustomerGateway(TaggedEC2Resource): - def __init__(self, ec2_backend, id, type, ip_address, bgp_asn): self.ec2_backend = ec2_backend self.id = id @@ -3503,7 +3467,6 @@ class CustomerGateway(TaggedEC2Resource): class CustomerGatewayBackend(object): - def __init__(self): self.customer_gateways = {} super(CustomerGatewayBackend, self).__init__() @@ -3534,7 +3497,6 @@ class CustomerGatewayBackend(object): class NatGateway(object): - def __init__(self, backend, subnet_id, allocation_id): # public properties self.id = random_nat_gateway_id() @@ -3583,7 +3545,6 @@ class NatGateway(object): class NatGatewayBackend(object): - def __init__(self): self.nat_gateways = {} @@ -3609,7 +3570,6 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, SpotRequestBackend, ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend, NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend, NatGatewayBackend): - def __init__(self, region_name): super(EC2Backend, self).__init__() self.region_name = region_name diff --git a/moto/ec2/responses/network_acls.py b/moto/ec2/responses/network_acls.py index bf9833d13..6c89d72d1 100644 --- a/moto/ec2/responses/network_acls.py +++ b/moto/ec2/responses/network_acls.py @@ -39,8 +39,32 @@ class NetworkACLs(BaseResponse): return template.render() def delete_network_acl_entry(self): - raise NotImplementedError( - 'NetworkACLs(AmazonVPC).delete_network_acl_entry is not yet implemented') + network_acl_id = self.querystring.get('NetworkAclId')[0] + rule_number = self.querystring.get('RuleNumber')[0] + egress = self.querystring.get('Egress')[0] + self.ec2_backend.delete_network_acl(network_acl_id, rule_number, egress) + template = self.response_template(DELETE_NETWORK_ACL_ENTRY_RESPONSE) + return template.render() + + def replace_network_acl_entry(self): + network_acl_id = self.querystring.get('NetworkAclId')[0] + rule_number = self.querystring.get('RuleNumber')[0] + protocol = self.querystring.get('Protocol')[0] + rule_action = self.querystring.get('RuleAction')[0] + egress = self.querystring.get('Egress')[0] + cidr_block = self.querystring.get('CidrBlock')[0] + icmp_code = self.querystring.get('Icmp.Code', [None])[0] + icmp_type = self.querystring.get('Icmp.Type', [None])[0] + port_range_from = self.querystring.get('PortRange.From')[0] + port_range_to = self.querystring.get('PortRange.To')[0] + + self.ec2_backend.replace_network_acl_entry( + network_acl_id, rule_number, protocol, rule_action, + egress, cidr_block, icmp_code, icmp_type, + port_range_from, port_range_to) + + template = self.response_template(REPLACE_NETWORK_ACL_ENTRY_RESPONSE) + return template.render() def describe_network_acls(self): network_acl_ids = network_acl_ids_from_querystring(self.querystring) @@ -61,9 +85,7 @@ class NetworkACLs(BaseResponse): template = self.response_template(REPLACE_NETWORK_ACL_ASSOCIATION) return template.render(association=association) - def replace_network_acl_entry(self): - raise NotImplementedError( - 'NetworkACLs(AmazonVPC).replace_network_acl_entry is not yet implemented') + CREATE_NETWORK_ACL_RESPONSE = """ @@ -147,6 +169,13 @@ CREATE_NETWORK_ACL_ENTRY_RESPONSE = """ """ +REPLACE_NETWORK_ACL_ENTRY_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +""" + REPLACE_NETWORK_ACL_ASSOCIATION = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE @@ -160,3 +189,10 @@ DELETE_NETWORK_ACL_ASSOCIATION = """ true """ + +DELETE_NETWORK_ACL_ENTRY_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +""" \ No newline at end of file diff --git a/tests/test_ec2/test_network_acls.py b/tests/test_ec2/test_network_acls.py index 91158e0bf..fd2ec105e 100644 --- a/tests/test_ec2/test_network_acls.py +++ b/tests/test_ec2/test_network_acls.py @@ -62,6 +62,61 @@ def test_network_acl_entries(): entries[0].rule_action.should.equal('ALLOW') +@mock_ec2_deprecated +def test_delete_network_acl_entry(): + conn = boto.connect_vpc('the_key', 'the secret') + vpc = conn.create_vpc("10.0.0.0/16") + + network_acl = conn.create_network_acl(vpc.id) + + conn.create_network_acl_entry( + network_acl.id, 110, 6, + 'ALLOW', '0.0.0.0/0', False, + port_range_from='443', + port_range_to='443' + ) + conn.delete_network_acl_entry( + network_acl.id, 110, False + ) + + all_network_acls = conn.get_all_network_acls() + + test_network_acl = next(na for na in all_network_acls + if na.id == network_acl.id) + entries = test_network_acl.network_acl_entries + entries.should.have.length_of(0) + + +@mock_ec2_deprecated +def test_replace_network_acl_entry(): + conn = boto.connect_vpc('the_key', 'the secret') + vpc = conn.create_vpc("10.0.0.0/16") + + network_acl = conn.create_network_acl(vpc.id) + + conn.create_network_acl_entry( + network_acl.id, 110, 6, + 'ALLOW', '0.0.0.0/0', False, + port_range_from='443', + port_range_to='443' + ) + conn.replace_network_acl_entry( + network_acl.id, 110, -1, + 'DENY', '0.0.0.0/0', False, + port_range_from='22', + port_range_to='22' + ) + + all_network_acls = conn.get_all_network_acls() + + test_network_acl = next(na for na in all_network_acls + if na.id == network_acl.id) + entries = test_network_acl.network_acl_entries + entries.should.have.length_of(1) + entries[0].rule_number.should.equal('110') + entries[0].protocol.should.equal('-1') + entries[0].rule_action.should.equal('DENY') + @mock_ec2_deprecated def test_associate_new_network_acl_with_subnet(): conn = boto.connect_vpc('the_key', 'the secret') From 405b8af87032f70fb4204395f2c6e4e784eeacf4 Mon Sep 17 00:00:00 2001 From: Jerome Bosman Date: Sun, 14 May 2017 13:12:28 +0100 Subject: [PATCH 156/274] Fixed: moto/ec2/models.py:1996:21: E131 continuation line unaligned for hanging indent moto/ec2/models.py:2380:21: E131 continuation line unaligned for hanging indent moto/ec2/models.py:2582:21: E131 continuation line unaligned for hanging indent moto/ec2/responses/network_acls.py:91:1: E303 too many blank lines (4) moto/ec2/responses/network_acls.py:198:4: W292 no newline at end of file --- moto/ec2/models.py | 9 +++------ moto/ec2/responses/network_acls.py | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 6631706f1..9b6b6920d 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1992,8 +1992,7 @@ class VPCBackend(object): route_tables = self.get_all_route_tables(filters={'vpc-id': vpc_id}) if len(route_tables) > 1: raise DependencyViolationError( - "The vpc {0} has dependencies and cannot be deleted." - .format(vpc_id) + "The vpc {0} has dependencies and cannot be deleted.".format(vpc_id) ) for route_table in route_tables: self.delete_route_table(route_table.id) @@ -2376,8 +2375,7 @@ class RouteTableBackend(object): route_table = self.get_route_table(route_table_id) if route_table.associations: raise DependencyViolationError( - "The routeTable '{0}' has dependencies and cannot be deleted." - .format(route_table_id) + "The routeTable '{0}' has dependencies and cannot be deleted.".format(route_table_id) ) self.route_tables.pop(route_table_id) return True @@ -2578,8 +2576,7 @@ class InternetGatewayBackend(object): igw = self.get_internet_gateway(internet_gateway_id) if igw.vpc: raise DependencyViolationError( - "{0} is being utilized by {1}" - .format(internet_gateway_id, igw.vpc.id) + "{0} is being utilized by {1}".format(internet_gateway_id, igw.vpc.id) ) self.internet_gateways.pop(internet_gateway_id) return True diff --git a/moto/ec2/responses/network_acls.py b/moto/ec2/responses/network_acls.py index 6c89d72d1..a244583e2 100644 --- a/moto/ec2/responses/network_acls.py +++ b/moto/ec2/responses/network_acls.py @@ -86,8 +86,6 @@ class NetworkACLs(BaseResponse): return template.render(association=association) - - CREATE_NETWORK_ACL_RESPONSE = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE @@ -195,4 +193,4 @@ DELETE_NETWORK_ACL_ENTRY_RESPONSE = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE true -""" \ No newline at end of file +""" From 227318b037fbd519426234ca407b077db0039fc3 Mon Sep 17 00:00:00 2001 From: Jerome Bosman Date: Sun, 14 May 2017 13:20:18 +0100 Subject: [PATCH 157/274] Fixed typo failing the build --- moto/ec2/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 9b6b6920d..7fa7e1009 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -3290,10 +3290,10 @@ class NetworkAclBackend(object): def delete_network_acl_entry(self, network_acl_id, rule_number, egress): network_acl = self.get_network_acl(network_acl_id) - entry = next(entry for entry in network_acl.netword_acl_entries + entry = next(entry for entry in network_acl.network_acl_entries if entry.egress == egress and entry.rule_number == rule_number) if entry is not None: - network_acl.netword_acl_entries.remove(entry) + network_acl.network_acl_entries.remove(entry) return entry def replace_network_acl_entry(self, network_acl_id, rule_number, protocol, rule_action, egress, From d1789624c4d283f547dcc4feb6bd7d49a642ae2f Mon Sep 17 00:00:00 2001 From: Jerome Bosman Date: Sun, 14 May 2017 14:21:18 +0100 Subject: [PATCH 158/274] Fix --- moto/ec2/responses/network_acls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ec2/responses/network_acls.py b/moto/ec2/responses/network_acls.py index a244583e2..440069edc 100644 --- a/moto/ec2/responses/network_acls.py +++ b/moto/ec2/responses/network_acls.py @@ -42,7 +42,7 @@ class NetworkACLs(BaseResponse): network_acl_id = self.querystring.get('NetworkAclId')[0] rule_number = self.querystring.get('RuleNumber')[0] egress = self.querystring.get('Egress')[0] - self.ec2_backend.delete_network_acl(network_acl_id, rule_number, egress) + self.ec2_backend.delete_network_acl_entry(network_acl_id, rule_number, egress) template = self.response_template(DELETE_NETWORK_ACL_ENTRY_RESPONSE) return template.render() From 9a861a367d777865465bd7b92820c0ca4de245ff Mon Sep 17 00:00:00 2001 From: georgepsarakis Date: Sun, 14 May 2017 19:56:25 +0300 Subject: [PATCH 159/274] Enable DeleteMarker support for versioned buckets - Add object version deletion - Add DeleteMarker in versioned key store as latest version - GetObject returns NoSuchKey for objects with DeleteMarker as latest version - Enable IsLatest in response when listing object versions --- moto/s3/models.py | 79 +++++++++++++++++++++++++++++++++++++++----- moto/s3/responses.py | 37 +++++++++++++++++---- 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index ec3e69f1b..3cd50050d 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -18,6 +18,17 @@ UPLOAD_ID_BYTES = 43 UPLOAD_PART_MIN_SIZE = 5242880 +class FakeDeleteMarker(BaseModel): + + def __init__(self, key): + self.key = key + self._version_id = key.version_id + 1 + + @property + def version_id(self): + return self._version_id + + class FakeKey(BaseModel): def __init__(self, name, value, storage="STANDARD", etag=None, is_versioned=False, version_id=0): @@ -33,6 +44,10 @@ class FakeKey(BaseModel): self._version_id = version_id self._is_versioned = is_versioned + @property + def version_id(self): + return self._version_id + def copy(self, new_name=None): r = copy.deepcopy(self) if new_name is not None: @@ -102,7 +117,7 @@ class FakeKey(BaseModel): res['x-amz-restore'] = rhdr.format(self.expiry_date) if self._is_versioned: - res['x-amz-version-id'] = str(self._version_id) + res['x-amz-version-id'] = str(self.version_id) if self.website_redirect_location: res['x-amz-website-redirect-location'] = self.website_redirect_location @@ -356,6 +371,26 @@ class S3Backend(BaseBackend): def get_bucket_versioning(self, bucket_name): return self.get_bucket(bucket_name).versioning_status + def get_bucket_latest_versions(self, bucket_name): + versions = self.get_bucket_versions(bucket_name) + maximum_version_per_key = {} + latest_versions = {} + + for version in versions: + if isinstance(version, FakeDeleteMarker): + name = version.key.name + else: + name = version.name + version_id = version.version_id + maximum_version_per_key[name] = max( + version_id, + maximum_version_per_key.get(name, -1) + ) + if version_id == maximum_version_per_key[name]: + latest_versions[name] = version_id + + return latest_versions + def get_bucket_versions(self, bucket_name, delimiter=None, encoding_type=None, key_marker=None, @@ -423,15 +458,22 @@ class S3Backend(BaseBackend): def get_key(self, bucket_name, key_name, version_id=None): key_name = clean_key_name(key_name) bucket = self.get_bucket(bucket_name) + key = None + if bucket: if version_id is None: if key_name in bucket.keys: - return bucket.keys[key_name] + key = bucket.keys[key_name] else: - for key in bucket.keys.getlist(key_name): - if str(key._version_id) == str(version_id): - return key - raise MissingKey(key_name=key_name) + for key_version in bucket.keys.getlist(key_name): + if str(key_version.version_id) == str(version_id): + key = key_version + break + + if isinstance(key, FakeKey): + return key + else: + raise MissingKey(key_name=key_name) def initiate_multipart(self, bucket_name, key_name, metadata): bucket = self.get_bucket(bucket_name) @@ -510,12 +552,33 @@ class S3Backend(BaseBackend): return key_results, folder_results - def delete_key(self, bucket_name, key_name): + def _set_delete_marker(self, bucket_name, key_name): + bucket = self.get_bucket(bucket_name) + bucket.keys[key_name] = FakeDeleteMarker( + key=bucket.keys[key_name] + ) + + def delete_key(self, bucket_name, key_name, version_id=None): key_name = clean_key_name(key_name) bucket = self.get_bucket(bucket_name) try: - bucket.keys.pop(key_name) + if not bucket.is_versioned: + bucket.keys.pop(key_name) + else: + if version_id is None: + self._set_delete_marker(bucket_name, key_name) + else: + if key_name not in bucket.keys: + raise KeyError + bucket.keys.setlist( + key_name, + [ + key + for key in bucket.keys.getlist(key_name) + if str(key.version_id) != str(version_id) + ] + ) return True except KeyError: return False diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 68530c190..fd33c5ead 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -13,7 +13,7 @@ from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_n from .exceptions import BucketAlreadyExists, S3ClientError, InvalidPartOrder -from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl +from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey from .utils import bucket_name_from_url, metadata_from_headers from xml.dom import minidom @@ -219,9 +219,21 @@ class ResponseObject(_TemplateEnvironmentMixin): max_keys=max_keys, version_id_marker=version_id_marker ) + latest_versions = self.backend.get_bucket_latest_versions( + bucket_name=bucket_name + ) + key_list = [] + delete_marker_list = [] + for version in versions: + if isinstance(version, FakeKey): + key_list.append(version) + else: + delete_marker_list.append(version) template = self.response_template(S3_BUCKET_GET_VERSIONS) return 200, {}, template.render( - key_list=versions, + key_list=key_list, + delete_marker_list=delete_marker_list, + latest_versions=latest_versions, bucket=bucket, prefix='', max_keys=1000, @@ -478,7 +490,7 @@ class ResponseObject(_TemplateEnvironmentMixin): return self._key_response_post(request, body, bucket_name, query, key_name, headers) else: raise NotImplementedError( - "Method {0} has not been impelemented in the S3 backend yet".format(method)) + "Method {0} has not been implemented in the S3 backend yet".format(method)) def _key_response_get(self, bucket_name, query, key_name, headers): response_headers = {} @@ -630,7 +642,8 @@ class ResponseObject(_TemplateEnvironmentMixin): upload_id = query['uploadId'][0] self.backend.cancel_multipart(bucket_name, upload_id) return 204, {}, "" - self.backend.delete_key(bucket_name, key_name) + version_id = query.get('versionId', [None])[0] + self.backend.delete_key(bucket_name, key_name, version_id=version_id) template = self.response_template(S3_DELETE_OBJECT_SUCCESS) return 204, {}, template.render() @@ -851,8 +864,8 @@ S3_BUCKET_GET_VERSIONS = """ {% for key in key_list %} {{ key.name }} - {{ key._version_id }} - false + {{ key.version_id }} + {% if latest_versions[key.name] == key.version_id %}true{% else %}false{% endif %} {{ key.last_modified_ISO8601 }} {{ key.etag }} {{ key.size }} @@ -863,6 +876,18 @@ S3_BUCKET_GET_VERSIONS = """ {% endfor %} + {% for marker in delete_marker_list %} + + {{ marker.key.name }} + {{ marker.version_id }} + {% if latest_versions[marker.key.name] == marker.version_id %}true{% else %}false{% endif %} + {{ marker.key.last_modified_ISO8601 }} + + 75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a + webfile + + + {% endfor %} """ From ac8b8c9a36ca1e87004ac923ed73d146b7402b19 Mon Sep 17 00:00:00 2001 From: georgepsarakis Date: Sun, 14 May 2017 20:00:26 +0300 Subject: [PATCH 160/274] Add tests for DeleteMarker support --- tests/test_s3/test_s3.py | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 09ef235a8..de9c6a7de 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1300,6 +1300,12 @@ def test_boto3_list_object_versions(): bucket_name = 'mybucket' key = 'key-with-versions' s3.create_bucket(Bucket=bucket_name) + s3.put_bucket_versioning( + Bucket=bucket_name, + VersioningConfiguration={ + 'Status': 'Enabled' + } + ) items = (six.b('v1'), six.b('v2')) for body in items: s3.put_object( @@ -1319,6 +1325,58 @@ def test_boto3_list_object_versions(): response['Body'].read().should.equal(items[-1]) +@mock_s3 +def test_boto3_delete_markers(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-versions' + s3.create_bucket(Bucket=bucket_name) + s3.put_bucket_versioning( + Bucket=bucket_name, + VersioningConfiguration={ + 'Status': 'Enabled' + } + ) + items = (six.b('v1'), six.b('v2')) + for body in items: + s3.put_object( + Bucket=bucket_name, + Key=key, + Body=body + ) + s3.delete_object( + Bucket=bucket_name, + Key=key + ) + with assert_raises(ClientError) as e: + s3.get_object( + Bucket=bucket_name, + Key=key + ) + e.response['Error']['Code'].should.equal('NoSuchKey') + + s3.delete_object( + Bucket=bucket_name, + Key=key, + VersionId='2' + ) + response = s3.get_object( + Bucket=bucket_name, + Key=key + ) + response['Body'].read().should.equal(items[-1]) + response = s3.list_object_versions( + Bucket=bucket_name + ) + response['Versions'].should.have.length_of(2) + response['Versions'][-1]['IsLatest'].should.be.true + response['Versions'][0]['IsLatest'].should.be.false + [(key_metadata['Key'], key_metadata['VersionId']) + for key_metadata in response['Versions']].should.equal( + [('key-with-versions', '0'), ('key-with-versions', '1')] + ) + + TEST_XML = """\ @@ -1337,3 +1395,4 @@ TEST_XML = """\ """ + From 2bae587a76aaa4f2b80f8c5566db3c9ade16ad0c Mon Sep 17 00:00:00 2001 From: Pierre Tardy Date: Mon, 15 May 2017 11:28:42 +0200 Subject: [PATCH 161/274] fix warning on py2 as well object takes no constructor argument whatever the python version. we simplify the code to not use constructor arguments --- moto/core/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/moto/core/models.py b/moto/core/models.py index a3a343aa7..6e93f911a 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -229,10 +229,7 @@ class InstanceTrackerMeta(type): @six.add_metaclass(InstanceTrackerMeta) class BaseModel(object): def __new__(cls, *args, **kwargs): - if six.PY2: - instance = super(BaseModel, cls).__new__(cls, *args, **kwargs) - else: - instance = super(BaseModel, cls).__new__(cls) + instance = super(BaseModel, cls).__new__(cls) cls.instances.append(instance) return instance From e307dc38e6f79e82045fa9c965c3979c43ba8dd4 Mon Sep 17 00:00:00 2001 From: Kate Heddleston Date: Mon, 15 May 2017 14:56:28 -0700 Subject: [PATCH 162/274] Implementing IAM policy versions Adding definitions for create, list, and delete policy_versions --- moto/iam/models.py | 42 ++++++++++++++++++++++++++++++++++++++++++ moto/iam/responses.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/moto/iam/models.py b/moto/iam/models.py index c7142fb5d..30674a306 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -43,6 +43,7 @@ class Policy(BaseModel): self.id = random_policy_id() self.path = path or '/' self.default_version_id = default_version_id or 'v1' + self.versions = [] self.create_datetime = datetime.now(pytz.utc) self.update_datetime = datetime.now(pytz.utc) @@ -52,6 +53,20 @@ class Policy(BaseModel): return 'arn:aws:iam::aws:policy{0}{1}'.format(self.path, self.name) +class Version(object): + + def __init__(self, + policy_arn, + document, + is_default_version=False): + self.policy_arn = policy_arn + self.document = document or {} + self.is_default_version = is_default_version + self.version_id = 'v1' + + self.create_datetime = datetime.now(pytz.utc) + + class ManagedPolicy(Policy): """Managed policy.""" @@ -536,6 +551,15 @@ class IAMBackend(BaseBackend): return policies, marker + def get_policy(self, policy_name): + policy = self.managed_policies[policy_name] + if not policy: + raise IAMNotFoundException("Policy {0} not found".format(policy_name)) + return policy + + def get_policies(self): + return self.managed_policies.values() + def create_role(self, role_name, assume_role_policy_document, path): role_id = random_resource_id() role = Role(role_id, role_name, assume_role_policy_document, path) @@ -568,6 +592,24 @@ class IAMBackend(BaseBackend): role = self.get_role(role_name) return role.policies.keys() + def create_policy_version(self, policy_arn, policy_document, set_as_default): + policy_name = policy_arn.split(':')[-1] + policy_name = policy_name.split('/')[1] + policy = self.get_policy(policy_name) + version = Version(policy_arn, policy_document, set_as_default) + policy.versions.append(version) + if set_as_default: + policy.default_version_id = version.version_id + + def delete_policy_version(self, policy_arn, version_id): + policy_name = policy_arn.split(':')[-1] + policy_name = policy_name.split('/')[1] + policy = self.get_policy(policy_name) + for i, v in enumerate(policy.versions): + if v.version_id == version_id: + del policy.versions[i] + return + def create_instance_profile(self, name, path, role_ids): instance_profile_id = random_resource_id() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 8e19b3aa7..407592f8d 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -93,6 +93,30 @@ class IamResponse(BaseResponse): template = self.response_template(GENERIC_EMPTY_TEMPLATE) return template.render(name="UpdateAssumeRolePolicyResponse") + def create_policy_version(self): + policy_arn = self._get_param('PolicyArn') + policy_document = self._get_param('PolicyDocument') + set_as_default = self._get_param('SetAsDefault') + policy_version = iam_backend.create_policy_version(policy_arn, policy_document, set_as_default) + + template = self.response_template(LIST_POLICY_VERSIONS_TEMPLATE) + return template.render(policy_versions=[policy_version]) + + def list_policy_versions(self): + policy_arn = self._get_param('PolicyArn') + policy_versions = iam_backend.list_policy_versions(policy_arn) + + template = self.response_template(LIST_POLICY_VERSIONS_TEMPLATE) + return template.render(policy_versions=policy_versions) + + def delete_policy_version(self): + policy_arn = self._get_param('PolicyArn') + version_id = self._get_param('VersionId') + + iam_backend.delete_policy_version(policy_arn, version_id) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name='DeletePolicyVersion') + def create_instance_profile(self): profile_name = self._get_param('InstanceProfileName') path = self._get_param('Path') @@ -600,6 +624,25 @@ LIST_ROLE_POLICIES = """ + + false + + {% for version in policy_versions %} + + {{ version.document }} + {{ version.version_id }} + {{ version.is_default_version }} + 2012-05-09T15:45:35Z + + {% endfor %} + + + + 20f7279f-99ee-11e1-a4c3-27EXAMPLE804 + +""" + LIST_INSTANCE_PROFILES_TEMPLATE = """ false From 992b4750931dcb3956196d2b71fbc9b35ddf82c8 Mon Sep 17 00:00:00 2001 From: Kate Heddleston Date: Mon, 15 May 2017 14:56:30 -0700 Subject: [PATCH 163/274] testing create, get, list, delete policy versions --- moto/iam/models.py | 45 +++++++++++++++------- moto/iam/responses.py | 46 ++++++++++++++++++++--- tests/test_iam/test_iam.py | 77 ++++++++++++++++++++++++++++++++++++++ tests/test_sqs/test_sqs.py | 2 +- 4 files changed, 150 insertions(+), 20 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index 30674a306..eef5fed2a 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -53,15 +53,15 @@ class Policy(BaseModel): return 'arn:aws:iam::aws:policy{0}{1}'.format(self.path, self.name) -class Version(object): +class PolicyVersion(object): def __init__(self, policy_arn, document, - is_default_version=False): + is_default=False): self.policy_arn = policy_arn self.document = document or {} - self.is_default_version = is_default_version + self.is_default = is_default self.version_id = 'v1' self.create_datetime = datetime.now(pytz.utc) @@ -506,6 +506,9 @@ class IAMBackend(BaseBackend): self.managed_policies[policy.name] = policy return policy + def get_policy(self, policy_name): + return self.managed_policies.get(policy_name) + def list_attached_role_policies(self, role_name, marker=None, max_items=100, path_prefix='/'): policies = self.get_role(role_name).managed_policies.values() @@ -551,15 +554,6 @@ class IAMBackend(BaseBackend): return policies, marker - def get_policy(self, policy_name): - policy = self.managed_policies[policy_name] - if not policy: - raise IAMNotFoundException("Policy {0} not found".format(policy_name)) - return policy - - def get_policies(self): - return self.managed_policies.values() - def create_role(self, role_name, assume_role_policy_document, path): role_id = random_resource_id() role = Role(role_id, role_name, assume_role_policy_document, path) @@ -596,19 +590,44 @@ class IAMBackend(BaseBackend): policy_name = policy_arn.split(':')[-1] policy_name = policy_name.split('/')[1] policy = self.get_policy(policy_name) - version = Version(policy_arn, policy_document, set_as_default) + if not policy: + raise IAMNotFoundException("Policy not found") + version = PolicyVersion(policy_arn, policy_document, set_as_default) policy.versions.append(version) if set_as_default: policy.default_version_id = version.version_id + return version + + def get_policy_version(self, policy_arn, version_id): + policy_name = policy_arn.split(':')[-1] + policy_name = policy_name.split('/')[1] + policy = self.get_policy(policy_name) + if not policy: + raise IAMNotFoundException("Policy not found") + for version in policy.versions: + if version.version_id == version_id: + return version + raise IAMNotFoundException("Policy version not found") + + def list_policy_versions(self, policy_arn): + policy_name = policy_arn.split(':')[-1] + policy_name = policy_name.split('/')[1] + policy = self.get_policy(policy_name) + if not policy: + raise IAMNotFoundException("Policy not found") + return policy.versions def delete_policy_version(self, policy_arn, version_id): policy_name = policy_arn.split(':')[-1] policy_name = policy_name.split('/')[1] policy = self.get_policy(policy_name) + if not policy: + raise IAMNotFoundException("Policy not found") for i, v in enumerate(policy.versions): if v.version_id == version_id: del policy.versions[i] return + raise IAMNotFoundException("Policy not found") def create_instance_profile(self, name, path, role_ids): instance_profile_id = random_resource_id() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 407592f8d..d82cdb189 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -98,9 +98,15 @@ class IamResponse(BaseResponse): policy_document = self._get_param('PolicyDocument') set_as_default = self._get_param('SetAsDefault') policy_version = iam_backend.create_policy_version(policy_arn, policy_document, set_as_default) + template = self.response_template(CREATE_POLICY_VERSION_TEMPLATE) + return template.render(policy_version=policy_version) - template = self.response_template(LIST_POLICY_VERSIONS_TEMPLATE) - return template.render(policy_versions=[policy_version]) + def get_policy_version(self): + policy_arn = self._get_param('PolicyArn') + version_id = self._get_param('VersionId') + policy_version = iam_backend.get_policy_version(policy_arn, version_id) + template = self.response_template(GET_POLICY_VERSION_TEMPLATE) + return template.render(policy_version=policy_version) def list_policy_versions(self): policy_arn = self._get_param('PolicyArn') @@ -624,15 +630,43 @@ LIST_ROLE_POLICIES = """ + + + {{ policy_version.document }} + {{ policy_version.version_id }} + {{ policy_version.is_default }} + 2012-05-09T15:45:35Z + + + + 20f7279f-99ee-11e1-a4c3-27EXAMPLE804 + +""" + +GET_POLICY_VERSION_TEMPLATE = """ + + + {{ policy_version.document }} + {{ policy_version.version_id }} + {{ policy_version.is_default }} + 2012-05-09T15:45:35Z + + + + 20f7279f-99ee-11e1-a4c3-27EXAMPLE804 + +""" + LIST_POLICY_VERSIONS_TEMPLATE = """ false - {% for version in policy_versions %} + {% for policy_version in policy_versions %} - {{ version.document }} - {{ version.version_id }} - {{ version.is_default_version }} + {{ policy_version.document }} + {{ policy_version.version_id }} + {{ policy_version.is_default }} 2012-05-09T15:45:35Z {% endfor %} diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index e039f8f61..9249c61a8 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -196,6 +196,83 @@ def test_update_assume_role_policy(): role.assume_role_policy_document.should.equal("my-policy") +@mock_iam +def test_create_policy_versions(): + conn = boto3.client('iam', region_name='us-east-1') + with assert_raises(ClientError): + conn.create_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestCreatePolicyVersion", + PolicyDocument='{"some":"policy"}') + conn.create_policy( + PolicyName="TestCreatePolicyVersion", + PolicyDocument='{"some":"policy"}') + version = conn.create_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestCreatePolicyVersion", + PolicyDocument='{"some":"policy"}') + version.get('PolicyVersion').get('Document').should.equal({'some': 'policy'}) + + +@mock_iam +def test_get_policy_version(): + conn = boto3.client('iam', region_name='us-east-1') + conn.create_policy( + PolicyName="TestGetPolicyVersion", + PolicyDocument='{"some":"policy"}') + version = conn.create_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestGetPolicyVersion", + PolicyDocument='{"some":"policy"}') + with assert_raises(ClientError): + conn.get_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestGetPolicyVersion", + VersionId='v2-does-not-exist') + retrieved = conn.get_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestGetPolicyVersion", + VersionId=version.get('PolicyVersion').get('VersionId')) + retrieved.get('PolicyVersion').get('Document').should.equal({'some': 'policy'}) + + +@mock_iam +def test_list_policy_versions(): + conn = boto3.client('iam', region_name='us-east-1') + with assert_raises(ClientError): + versions = conn.list_policy_versions( + PolicyArn="arn:aws:iam::aws:policy/TestListPolicyVersions") + conn.create_policy( + PolicyName="TestListPolicyVersions", + PolicyDocument='{"some":"policy"}') + conn.create_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestListPolicyVersions", + PolicyDocument='{"first":"policy"}') + conn.create_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestListPolicyVersions", + PolicyDocument='{"second":"policy"}') + versions = conn.list_policy_versions( + PolicyArn="arn:aws:iam::aws:policy/TestListPolicyVersions") + versions.get('Versions')[0].get('Document').should.equal({'first': 'policy'}) + versions.get('Versions')[1].get('Document').should.equal({'second': 'policy'}) + + +@mock_iam +def test_delete_policy_version(): + conn = boto3.client('iam', region_name='us-east-1') + conn.create_policy( + PolicyName="TestDeletePolicyVersion", + PolicyDocument='{"some":"policy"}') + conn.create_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestDeletePolicyVersion", + PolicyDocument='{"first":"policy"}') + with assert_raises(ClientError): + conn.delete_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestDeletePolicyVersion", + VersionId='v2-nope-this-does-not-exist') + conn.delete_policy_version( + PolicyArn="arn:aws:iam::aws:policy/TestDeletePolicyVersion", + VersionId='v1') + versions = conn.list_policy_versions( + PolicyArn="arn:aws:iam::aws:policy/TestDeletePolicyVersion") + len(versions.get('Versions')).should.equal(0) + + @mock_iam_deprecated() def test_create_user(): conn = boto.connect_iam() diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 0df4c2dc9..f179d9f85 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -392,7 +392,7 @@ def test_delete_message(): @mock_sqs_deprecated def test_send_batch_operation(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) # See https://github.com/boto/boto/issues/831 queue.set_message_class(RawMessage) From aad1e177870a8a418f38b85c2826b08d26fd48b7 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 11 May 2017 09:35:24 -0700 Subject: [PATCH 164/274] Shorter sleeps in SQS test One of these tests actually waited the entire 60 seconds of the visibility timeout but that value appears to have been copied from a previous test that didn't. Updating all tests with shorter timeouts so folks who copy setup code in the future don't fall into this trap --- tests/test_sqs/test_sqs.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 0df4c2dc9..c7d067bd8 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -126,7 +126,7 @@ def test_delete_queue(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", - Attributes={"VisibilityTimeout": "60"}) + Attributes={"VisibilityTimeout": "3"}) queue = sqs.Queue('test-queue') conn.list_queues()['QueueUrls'].should.have.length_of(1) @@ -143,10 +143,10 @@ def test_set_queue_attribute(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", - Attributes={"VisibilityTimeout": '60'}) + Attributes={"VisibilityTimeout": '3'}) queue = sqs.Queue("test-queue") - queue.attributes['VisibilityTimeout'].should.equal('60') + queue.attributes['VisibilityTimeout'].should.equal('3') queue.set_attributes(Attributes={"VisibilityTimeout": '45'}) queue = sqs.Queue("test-queue") @@ -176,7 +176,7 @@ def test_send_message(): @mock_sqs_deprecated def test_send_message_with_xml_characters(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = '< & >' @@ -192,7 +192,7 @@ def test_send_message_with_xml_characters(): @mock_sqs_deprecated def test_send_message_with_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body = 'this is a test message' @@ -217,13 +217,13 @@ def test_send_message_with_attributes(): @mock_sqs_deprecated def test_send_message_with_delay(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = 'this is a test message' body_two = 'this is another test message' - queue.write(queue.new_message(body_one), delay_seconds=60) + queue.write(queue.new_message(body_one), delay_seconds=3) queue.write(queue.new_message(body_two)) queue.count().should.equal(1) @@ -238,7 +238,7 @@ def test_send_message_with_delay(): @mock_sqs_deprecated def test_send_large_message_fails(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = 'test message' * 200000 @@ -271,7 +271,7 @@ def test_message_becomes_inflight_when_received(): @mock_sqs_deprecated def test_receive_message_with_explicit_visibility_timeout(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = 'this is another test message' @@ -360,7 +360,7 @@ def test_read_message_from_queue(): @mock_sqs_deprecated def test_queue_length(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) queue.write(queue.new_message('this is a test message')) @@ -371,7 +371,7 @@ def test_queue_length(): @mock_sqs_deprecated def test_delete_message(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) queue.write(queue.new_message('this is a test message')) @@ -392,7 +392,7 @@ def test_delete_message(): @mock_sqs_deprecated def test_send_batch_operation(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) # See https://github.com/boto/boto/issues/831 queue.set_message_class(RawMessage) @@ -414,7 +414,7 @@ def test_send_batch_operation(): @mock_sqs_deprecated def test_send_batch_operation_with_message_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) message_tuple = ("my_first_message", 'test message 1', 0, { @@ -431,7 +431,7 @@ def test_send_batch_operation_with_message_attributes(): @mock_sqs_deprecated def test_delete_batch_operation(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) conn.send_message_batch(queue, [ ("my_first_message", 'test message 1', 0), @@ -450,7 +450,7 @@ def test_queue_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') queue_name = 'test-queue' - visibility_timeout = 60 + visibility_timeout = 3 queue = conn.create_queue( queue_name, visibility_timeout=visibility_timeout) From 601fd8a7b4fea5db2f23741735e6e7f1332b4417 Mon Sep 17 00:00:00 2001 From: Anthony Miyaguchi Date: Tue, 16 May 2017 14:52:54 -0700 Subject: [PATCH 165/274] Fix issue #949 - Add mock as dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 1116b1794..9b23c602d 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ install_requires = [ "pyaml", "pytz", "python-dateutil", + "mock", ] extras_require = { From e344a7e95bb15a52b1049138b7253f563a47c108 Mon Sep 17 00:00:00 2001 From: Marcos Sampaio Date: Wed, 17 May 2017 14:09:43 +1000 Subject: [PATCH 166/274] Adds SSM support in the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 42408e848..f07984328 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | SQS | @mock_sqs | core endpoints done | |------------------------------------------------------------------------------| +| SSM | @mock_ssm | core endpoints done | +|------------------------------------------------------------------------------| | STS | @mock_sts | core endpoints done | |------------------------------------------------------------------------------| | SWF | @mock_sfw | basic endpoints done | From bfa8b4552c754a5b1e476d197700c3c189307c5a Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Wed, 17 May 2017 18:16:40 -0400 Subject: [PATCH 167/274] Fix CloudFormation Lambda ZipFile implementation to be plain text The AWS CloudFormation documentation[1] states the following about the ZipFile property: > For nodejs4.3, nodejs6.10, python2.7, and python3.6 runtime environments, the source code of your Lambda function. > You can't use this property with other runtime environments. [1]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-zipfile --- moto/awslambda/models.py | 16 ++++++++++++ .../test_cloudformation_stack_integration.py | 26 +++++-------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index a3b1f715f..1e651cb04 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -198,10 +198,26 @@ class LambdaFunction(BaseModel): if prop in properties: spec[prop] = properties[prop] + # when ZipFile is present in CloudFormation, per the official docs, + # the code it's a plaintext code snippet up to 4096 bytes. + # this snippet converts this plaintext code to a proper base64-encoded ZIP file. + if 'ZipFile' in properties['Code']: + spec['Code']['ZipFile'] = base64.b64encode( + cls._create_zipfile_from_plaintext_code(spec['Code']['ZipFile'])) + backend = lambda_backends[region_name] fn = backend.create_function(spec) return fn + @staticmethod + def _create_zipfile_from_plaintext_code(code): + zip_output = io.BytesIO() + zip_file = zipfile.ZipFile(zip_output, 'w', zipfile.ZIP_DEFLATED) + zip_file.writestr('lambda_function.zip', code) + zip_file.close() + zip_output.seek(0) + return zip_output.read() + class LambdaBackend(BaseBackend): diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 2480ee051..87dcfd950 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1846,29 +1846,14 @@ def test_datapipeline(): data_pipelines['pipelineIdList'][0]['id']) -def _process_lamda(pfunc): - import io - import zipfile - zip_output = io.BytesIO() - zip_file = zipfile.ZipFile(zip_output, 'w', zipfile.ZIP_DEFLATED) - zip_file.writestr('lambda_function.zip', pfunc) - zip_file.close() - zip_output.seek(0) - return zip_output.read() - - -def get_test_zip_file1(): - pfunc = """ -def lambda_handler(event, context): - return (event, context) -""" - return _process_lamda(pfunc) - - @mock_cloudformation @mock_lambda def test_lambda_function(): # switch this to python as backend lambda only supports python execution. + lambda_code = """ +def lambda_handler(event, context): + return (event, context) +""" template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { @@ -1876,7 +1861,8 @@ def test_lambda_function(): "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": base64.b64encode(get_test_zip_file1()).decode('utf-8') + # CloudFormation expects a string as ZipFile, not a ZIP file base64-encoded + "ZipFile": {"Fn::Join": ["\n", lambda_code.splitlines()]} }, "Handler": "lambda_function.handler", "Description": "Test function", From 965dc806c5577fea89f1fcf78e3cdfcbff84b65f Mon Sep 17 00:00:00 2001 From: mickeypash Date: Fri, 19 May 2017 23:30:29 +0100 Subject: [PATCH 168/274] Fix the error code for IAMNotFoundException to NoSuchEntity used by AWS. --- moto/iam/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py index b4d89c0f2..84f15f51f 100644 --- a/moto/iam/exceptions.py +++ b/moto/iam/exceptions.py @@ -7,7 +7,7 @@ class IAMNotFoundException(RESTError): def __init__(self, message): super(IAMNotFoundException, self).__init__( - "Not Found", message) + "NoSuchEntity", message) class IAMConflictException(RESTError): From 517416c4d95eb31d40ac3fa81921ac6873f3a72b Mon Sep 17 00:00:00 2001 From: Simon-Pierre Gingras Date: Fri, 19 May 2017 15:59:25 -0700 Subject: [PATCH 169/274] feat(s3) HeadObject: honor If-Modified-Since header --- moto/core/utils.py | 8 +++++++- moto/s3/responses.py | 12 +++++++++++- tests/test_s3/test_s3.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/moto/core/utils.py b/moto/core/utils.py index 7d4a9d412..9ee0c1814 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -174,11 +174,17 @@ def iso_8601_datetime_without_milliseconds(datetime): return datetime.strftime("%Y-%m-%dT%H:%M:%S") + 'Z' +RFC1123 = '%a, %d %b %Y %H:%M:%S GMT' + + def rfc_1123_datetime(datetime): - RFC1123 = '%a, %d %b %Y %H:%M:%S GMT' return datetime.strftime(RFC1123) +def str_to_rfc_1123_datetime(str): + return datetime.datetime.strptime(str, RFC1123) + + def unix_time(dt=None): dt = dt or datetime.datetime.utcnow() epoch = datetime.datetime.utcfromtimestamp(0) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index fd33c5ead..43e27a815 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import re import six +from moto.core.utils import str_to_rfc_1123_datetime from six.moves.urllib.parse import parse_qs, urlparse import xmltodict @@ -595,12 +596,21 @@ class ResponseObject(_TemplateEnvironmentMixin): def _key_response_head(self, bucket_name, query, key_name, headers): response_headers = {} version_id = query.get('versionId', [None])[0] + + if_modified_since = headers.get('if-modified-since', None) + if if_modified_since: + if_modified_since = str_to_rfc_1123_datetime(if_modified_since) + key = self.backend.get_key( bucket_name, key_name, version_id=version_id) if key: response_headers.update(key.metadata) response_headers.update(key.response_dict) - return 200, response_headers, "" + + if if_modified_since and key.last_modified < if_modified_since: + return 304, response_headers, 'Not Modified' + else: + return 200, response_headers, "" else: return 404, response_headers, "" diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index de9c6a7de..6af653f9e 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals + +import datetime from six.moves.urllib.request import urlopen from six.moves.urllib.error import HTTPError from functools import wraps @@ -10,6 +12,7 @@ import json import boto import boto3 from botocore.client import ClientError +import botocore.exceptions from boto.exception import S3CreateError, S3ResponseError from boto.s3.connection import S3Connection from boto.s3.key import Key @@ -1266,6 +1269,31 @@ def test_boto3_head_object_with_versioning(): old_head_object['ContentLength'].should.equal(len(old_content)) +@mock_s3 +def test_boto3_head_object_if_modified_since(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = "blah" + s3.create_bucket(Bucket=bucket_name) + + key = 'hello.txt' + + with freeze_time(datetime.datetime.now() - datetime.timedelta(hours=3)): + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test' + ) + + with assert_raises(botocore.exceptions.ClientError) as err: + s3.head_object( + Bucket=bucket_name, + Key=key, + IfModifiedSince=datetime.datetime.now() - datetime.timedelta(hours=2) + ) + e = err.exception + e.response['Error'].should.equal({'Code': '304', 'Message': 'Not Modified'}) + + @mock_s3 @reduced_min_part_size def test_boto3_multipart_etag(): From 15d3397a600813527e8472fca9832491c3c83137 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 18 May 2017 10:37:00 -0700 Subject: [PATCH 170/274] implementing IAM delete_role Fixes #957 --- moto/iam/models.py | 7 +++++++ moto/iam/responses.py | 6 ++++++ tests/test_iam/test_iam.py | 19 ++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index c7ee70ca2..da11d58b2 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -569,6 +569,13 @@ class IAMBackend(BaseBackend): return role raise IAMNotFoundException("Role {0} not found".format(role_name)) + def delete_role(self, role_name): + for role in self.get_roles(): + if role.name == role_name: + del self.roles[role.id] + return + raise IAMNotFoundException("Role {0} not found".format(role_name)) + def get_roles(self): return self.roles.values() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 3c40a323f..138c08d23 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -62,6 +62,12 @@ class IamResponse(BaseResponse): template = self.response_template(GET_ROLE_TEMPLATE) return template.render(role=role) + def delete_role(self): + role_name = self._get_param('RoleName') + iam_backend.delete_role(role_name) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name="DeleteRoleResponse") + def list_role_policies(self): role_name = self._get_param('RoleName') role_policies_names = iam_backend.list_role_policies(role_name) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index f2c77685f..46b727360 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -8,7 +8,7 @@ from boto.exception import BotoServerError from botocore.exceptions import ClientError from moto import mock_iam, mock_iam_deprecated from moto.iam.models import aws_managed_policies -from nose.tools import assert_raises, assert_equals, assert_not_equals +from nose.tools import assert_raises, assert_equals from nose.tools import raises from tests.helpers import requires_boto_gte @@ -114,6 +114,23 @@ def test_remove_role_from_instance_profile(): dict(profile.roles).should.be.empty +@mock_iam() +def test_delete_role(): + conn = boto3.client('iam', region_name='us-east-1') + + with assert_raises(ClientError): + conn.delete_role(RoleName="my-role") + + conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/") + role = conn.get_role(RoleName="my-role") + role.get('Role').get('Arn').should.equal('arn:aws:iam::123456789012:role/my-path/my-role') + + conn.delete_role(RoleName="my-role") + + with assert_raises(ClientError): + conn.get_role(RoleName="my-role") + + @mock_iam_deprecated() def test_list_instance_profiles(): conn = boto.connect_iam() From 588e211c7157d04d8c9ab31a8bb3abfcc0a18b6e Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 15:49:59 -0500 Subject: [PATCH 171/274] Adding ECR --- moto/__init__.py | 1 + moto/ecr/__init__.py | 7 ++ moto/ecr/models.py | 221 ++++++++++++++++++++++++++++++++++++++++++ moto/ecr/responses.py | 72 ++++++++++++++ moto/ecr/urls.py | 10 ++ 5 files changed, 311 insertions(+) create mode 100644 moto/ecr/__init__.py create mode 100644 moto/ecr/models.py create mode 100644 moto/ecr/responses.py create mode 100644 moto/ecr/urls.py diff --git a/moto/__init__.py b/moto/__init__.py index c93719cb2..d6f84db5e 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -14,6 +14,7 @@ from .datapipeline import mock_datapipeline, mock_datapipeline_deprecated # fla from .dynamodb import mock_dynamodb, mock_dynamodb_deprecated # flake8: noqa from .dynamodb2 import mock_dynamodb2, mock_dynamodb2_deprecated # flake8: noqa from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa +from .ecr import mock_ecr, mock_ecr_deprecated # flake8: noqa from .ecs import mock_ecs, mock_ecs_deprecated # flake8: noqa from .elb import mock_elb, mock_elb_deprecated # flake8: noqa from .emr import mock_emr, mock_emr_deprecated # flake8: noqa diff --git a/moto/ecr/__init__.py b/moto/ecr/__init__.py new file mode 100644 index 000000000..56b2cacbb --- /dev/null +++ b/moto/ecr/__init__.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +from .models import ecr_backends +from ..core.models import base_decorator, deprecated_base_decorator + +ecr_backend = ecr_backends['us-east-1'] +mock_ecr = base_decorator(ecr_backends) +mock_ecr_deprecated = deprecated_base_decorator(ecr_backends) diff --git a/moto/ecr/models.py b/moto/ecr/models.py new file mode 100644 index 000000000..5f8255007 --- /dev/null +++ b/moto/ecr/models.py @@ -0,0 +1,221 @@ +from __future__ import unicode_literals +# from datetime import datetime +from random import random + +from moto.core import BaseBackend, BaseModel +from moto.ec2 import ec2_backends +from copy import copy +import hashlib + + +class BaseObject(BaseModel): + + def camelCase(self, key): + words = [] + for i, word in enumerate(key.split('_')): + if i > 0: + words.append(word.title()) + else: + words.append(word) + return ''.join(words) + + def gen_response_object(self): + response_object = copy(self.__dict__) + for key, value in response_object.items(): + if '_' in key: + response_object[self.camelCase(key)] = value + del response_object[key] + return response_object + + @property + def response_object(self): + return self.gen_response_object() + + +class Repository(BaseObject): + + def __init__(self, repository_name): + self.arn = 'arn:aws:ecr:us-east-1:012345678910:repository/{0}'.format( + repository_name) + self.name = repository_name + # self.created = datetime.utcnow() + self.uri = '012345678910.dkr.ecr.us-east-1.amazonaws.com/{0}'.format( + repository_name + ) + self.registry_id = '012345678910' + self.images = [] + + @property + def physical_resource_id(self): + return self.name + + @property + def response_object(self): + response_object = self.gen_response_object() + + response_object['registryId'] = self.registry_id + response_object['repositoryArn'] = self.arn + response_object['repositoryName'] = self.name + response_object['repositoryUri'] = self.uri + # response_object['createdAt'] = self.created + del response_object['arn'], response_object['name'] + return response_object + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + ecr_backend = ecr_backends[region_name] + return ecr_backend.create_repository( + # RepositoryName is optional in CloudFormation, thus create a random + # name if necessary + repository_name=properties.get( + 'RepositoryName', 'ecrrepository{0}'.format(int(random() * 10 ** 6))), + ) + + @classmethod + def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + if original_resource.name != properties['RepositoryName']: + ecr_backend = ecr_backends[region_name] + ecr_backend.delete_cluster(original_resource.arn) + return ecr_backend.create_repository( + # RepositoryName is optional in CloudFormation, thus create a + # random name if necessary + repository_name=properties.get( + 'RepositoryName', 'RepositoryName{0}'.format(int(random() * 10 ** 6))), + ) + else: + # no-op when nothing changed between old and new resources + return original_resource + + +class Image(BaseObject): + + def __init__(self, tag, manifest, repository, registry_id="012345678910"): + self.image_tag = tag + self.image_manifest = manifest + self.image_size_in_bytes = 50 * 1024 * 1024 + self.repository = repository + self.registry_id = registry_id + self.image_digest = None + self.image_pushed_at = None + + def _create_digest(self): + image_contents = 'docker_image{0}'.format(int(random() * 10 ** 6)) + self.image_digest = "sha256:%s" % hashlib.sha256(image_contents).hexdigest() + + def get_image_digest(self): + if not self.image_digest: + self._create_digest() + return self.image_digest + + @property + def response_object(self): + response_object = self.gen_response_object() + response_object['imageId'] = {} + response_object['imageId']['imageTag'] = self.image_tag + response_object['imageId']['imageDigest'] = self.get_image_digest() + response_object['imageManifest'] = self.image_manifest + response_object['repositoryName'] = self.repository + response_object['registryId'] = self.registry_id + return response_object + + @property + def response_list_object(self): + response_object = self.gen_response_object() + response_object['imageTag'] = self.image_tag + response_object['imageDigest'] = "i don't know" + return response_object + + @property + def response_describe_object(self): + response_object = self.gen_response_object() + response_object['imageTags'] = [self.image_tag] + response_object['imageDigest'] = self.get_image_digest() + response_object['imageManifest'] = self.image_manifest + response_object['repositoryName'] = self.repository + response_object['registryId'] = self.registry_id + response_object['imageSizeInBytes'] = self.image_size_in_bytes + response_object['imagePushedAt'] = '2017-05-09' + return response_object + + +class ECRBackend(BaseBackend): + + def __init__(self): + self.repositories = {} + + def describe_repositories(self, registry_id=None, repository_names=None): + """ + maxResults and nextToken not implemented + """ + repositories = [] + for repository in self.repositories.values(): + # If a registry_id was supplied, ensure this repository matches + if registry_id: + if repository.registry_id != registry_id: + continue + # If a list of repository names was supplied, esure this repository + # is in that list + if repository_names: + if repository.name not in repository_names: + continue + repositories.append(repository.response_object) + return repositories + + def create_repository(self, repository_name): + repository = Repository(repository_name) + self.repositories[repository_name] = repository + return repository + + def delete_repository(self, respository_name, registry_id=None): + if respository_name in self.repositories: + return self.repositories.pop(respository_name) + else: + raise Exception("{0} is not a repository".format(respository_name)) + + def list_images(self, repository_name, registry_id=None): + """ + maxResults and filtering not implemented + """ + images = [] + for repository in self.repositories.values(): + if repository_name: + if repository.name != repository_name: + continue + if registry_id: + if repository.registry_id != registry_id: + continue + + for image in repository.images: + images.append(image) + return images + + def describe_images(self, repository_name, registry_id=None, image_id=None): + + if repository_name in self.repositories: + repository = self.repositories[repository_name] + else: + raise Exception("{0} is not a repository".format(repository_name)) + + response = [] + for image in repository.images: + response.append(image) + return response + + def put_image(self, repository_name, image_manifest, image_tag): + if repository_name in self.repositories: + repository = self.repositories[repository_name] + else: + raise Exception("{0} is not a repository".format(repository_name)) + + image = Image(image_tag, image_manifest, repository_name) + repository.images.append(image) + return image + + +ecr_backends = {} +for region, ec2_backend in ec2_backends.items(): + ecr_backends[region] = ECRBackend() diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py new file mode 100644 index 000000000..3a37162a0 --- /dev/null +++ b/moto/ecr/responses.py @@ -0,0 +1,72 @@ +from __future__ import unicode_literals +import json + +from moto.core.responses import BaseResponse +from .models import ecr_backends + + +class ECRResponse(BaseResponse): + + @property + def ecr_backend(self): + return ecr_backends[self.region] + + @property + def request_params(self): + try: + return json.loads(self.body) + except ValueError: + return {} + + def _get_param(self, param): + return self.request_params.get(param, None) + + def create_repository(self): + repository_name = self._get_param('repositoryName') + if repository_name is None: + repository_name = 'default' + repository = self.ecr_backend.create_repository(repository_name) + return json.dumps({ + 'repository': repository.response_object + }) + + def describe_repositories(self): + describe_repositories_name = self._get_param('repositoryNames') + repositories = self.ecr_backend.describe_repositories(describe_repositories_name) + return json.dumps({ + 'repositories': repositories, + 'failures': [] + }) + + def delete_repository(self): + repository_str = self._get_param('repositoryName') + repository = self.ecr_backend.delete_repository(repository_str) + return json.dumps({ + 'repository': repository.response_object + }) + + def put_image(self): + repository_str = self._get_param('repositoryName') + image_manifest = self._get_param('imageManifest') + image_tag = self._get_param('imageTag') + image = self.ecr_backend.put_image(repository_str, image_manifest, image_tag) + + return json.dumps({ + 'image': image.response_object + }) + + def list_images(self): + repository_str = self._get_param('repositoryName') + registry_id = self._get_param('registryId') + images = self.ecr_backend.list_images(repository_str, registry_id) + return json.dumps({ + 'imageIds': [image.response_list_object for image in images], + }) + + def describe_images(self): + repository_str = self._get_param('repositoryName') + registry_id = self._get_param('registryId') + images = self.ecr_backend.describe_images(repository_str, registry_id) + return json.dumps({ + 'imageDetails': [image.response_describe_object for image in images], + }) diff --git a/moto/ecr/urls.py b/moto/ecr/urls.py new file mode 100644 index 000000000..86b8a8dbc --- /dev/null +++ b/moto/ecr/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import ECRResponse + +url_bases = [ + "https?://ecr.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': ECRResponse.dispatch, +} From 20b30695403f79e240929a76b9f8604484c069fc Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 15:52:17 -0500 Subject: [PATCH 172/274] Add ECR tests --- tests/test_ecr/test_ecr_boto3.py | 241 +++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 tests/test_ecr/test_ecr_boto3.py diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py new file mode 100644 index 000000000..ce5a54b17 --- /dev/null +++ b/tests/test_ecr/test_ecr_boto3.py @@ -0,0 +1,241 @@ +from __future__ import unicode_literals + +# from nose.tools import assert_raises +import hashlib +import json +from random import random + +import sure # noqa + +import boto3 + +from moto import mock_ecr +import datetime + + +def _create_image_digest(contents=None): + if not contents: + contents = 'docker_image{0}'.format(int(random() * 10 ** 6)) + return "sha256:%s" % hashlib.sha256(contents).hexdigest() + + +def _create_image_manifest(): + return { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": + { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": _create_image_digest("config") + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": _create_image_digest("layer1") + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": _create_image_digest("layer2") + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": _create_image_digest("layer3") + } + ] + } + + +@mock_ecr +def test_create_repository(): + client = boto3.client('ecr', region_name='us-east-1') + response = client.create_repository( + repositoryName='test_ecr_repository' + ) + response['repository']['repositoryName'].should.equal('test_ecr_repository') + response['repository']['repositoryArn'].should.equal( + 'arn:aws:ecr:us-east-1:012345678910:repository/test_ecr_repository') + response['repository']['registryId'].should.equal('012345678910') + response['repository']['repositoryUri'].should.equal( + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_ecr_repository') + # response['repository']['createdAt'].should.equal(0) + + +@mock_ecr +def test_describe_repositories(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories() + len(response['repositories']).should.equal(2) + + respository_arns = ['arn:aws:ecr:us-east-1:012345678910:repository/test_repository1', + 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository0'] + set([response['repositories'][0]['repositoryArn'], + response['repositories'][1]['repositoryArn']]).should.equal(set(respository_arns)) + + respository_uris = ['012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository1', + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository0'] + set([response['repositories'][0]['repositoryUri'], + response['repositories'][1]['repositoryUri']]).should.equal(set(respository_uris)) + + +@mock_ecr +def test_delete_repository(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + response = client.delete_repository(repositoryName='test_repository') + response['repository']['repositoryName'].should.equal('test_repository') + response['repository']['repositoryArn'].should.equal( + 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository') + response['repository']['registryId'].should.equal('012345678910') + response['repository']['repositoryUri'].should.equal( + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository') + # response['repository']['createdAt'].should.equal(0) + + response = client.describe_repositories() + len(response['repositories']).should.equal(0) + + +@mock_ecr +def test_put_image(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + response = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + response['image']['repositoryName'].should.equal('test_repository') + response['image']['imageId']['imageTag'].should.equal('latest') + + +@mock_ecr +def test_list_images(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v1' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v2' + ) + + response = client.list_images(repositoryName='test_repository') + type(response['imageIds']).should.be(list) + len(response['imageIds']).should.be(3) + + image_tags = ['latest', 'v1', 'v2'] + set([response['imageIds'][0]['imageTag'], + response['imageIds'][1]['imageTag'], + response['imageIds'][2]['imageTag']]).should.equal(set(image_tags)) + + +@mock_ecr +def test_describe_images(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v1' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='v2' + ) + + response = client.describe_images(repositoryName='test_repository') + type(response['imageDetails']).should.be(list) + len(response['imageDetails']).should.be(3) + + response['imageDetails'][0]['imageDigest'].should.contain("sha") + response['imageDetails'][1]['imageDigest'].should.contain("sha") + response['imageDetails'][2]['imageDigest'].should.contain("sha") + + response['imageDetails'][0]['registryId'].should.equal("012345678910") + response['imageDetails'][1]['registryId'].should.equal("012345678910") + response['imageDetails'][2]['registryId'].should.equal("012345678910") + + response['imageDetails'][0]['repositoryName'].should.equal("test_repository") + response['imageDetails'][1]['repositoryName'].should.equal("test_repository") + response['imageDetails'][2]['repositoryName'].should.equal("test_repository") + + len(response['imageDetails'][0]['imageTags']).should.be(1) + len(response['imageDetails'][1]['imageTags']).should.be(1) + len(response['imageDetails'][2]['imageTags']).should.be(1) + + image_tags = ['latest', 'v1', 'v2'] + set([response['imageDetails'][0]['imageTags'][0], + response['imageDetails'][1]['imageTags'][0], + response['imageDetails'][2]['imageTags'][0]]).should.equal(set(image_tags)) + + response['imageDetails'][0]['imageSizeInBytes'].should.equal(52428800) + response['imageDetails'][1]['imageSizeInBytes'].should.equal(52428800) + response['imageDetails'][2]['imageSizeInBytes'].should.equal(52428800) + + # response['imageDetails'][0]['imagePushedAt'].should.equal('2017-05-09') + # response['imageDetails'][1]['imagePushedAt'].should.equal('2017-05-09') + # response['imageDetails'][2]['imagePushedAt'].should.equal('2017-05-09') + + ''' + image_digests = [ + "hi", "mike", "name" + ] + set([response['imageDetails'][0]['imageDigest'], + response['imageDetails'][1]['imageDigest'], + response['imageDetails'][2]['imageDigest']]).should.equal(set(image_digests)) + ''' + + +''' +'imageDetails': [ + { + 'registryId': 'string', + 'repositoryName': 'string', + 'imageDigest': 'string', + 'imageTags': [ + 'string', + ], + 'imageSizeInBytes': 123, + 'imagePushedAt': datetime(2015, 1, 1) + }, +], +''' From 5e88b5d1b49bc853f67969b2b7cc0b9bc346d9f9 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Wed, 26 Apr 2017 23:40:28 -0700 Subject: [PATCH 173/274] MD5 calculation of SQS message attributes This implements the same MD5 hashing pattern as implemented in the Ruby and Java AWS SDKs Doesn't yet handle list types but if you're reading this you might be surprised how easy that is to add. Give it a shot and if you get stuck reach out to me for help. --- moto/sqs/models.py | 55 +++++++++++++++++++++++++++++++++++--- moto/sqs/responses.py | 18 +++++-------- tests/test_sqs/test_sqs.py | 38 ++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index cedf03199..f8b7d91b1 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +import base64 import hashlib import re +import struct from xml.sax.saxutils import escape import boto.sqs @@ -17,6 +19,8 @@ from .exceptions import ( DEFAULT_ACCOUNT_ID = 123456789012 DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU" +TRANSPORT_TYPE_ENCODINGS = {'String': b'\x01', 'Binary': b'\x02', 'Number': b'\x01'} + class Message(BaseModel): @@ -33,10 +37,53 @@ class Message(BaseModel): self.delayed_until = 0 @property - def md5(self): - body_md5 = hashlib.md5() - body_md5.update(self._body.encode('utf-8')) - return body_md5.hexdigest() + def body_md5(self): + md5 = hashlib.md5() + md5.update(self._body.encode('utf-8')) + return md5.hexdigest() + + @property + def attribute_md5(self): + """ + The MD5 of all attributes is calculated by first generating a + utf-8 string from each attribute and MD5-ing the concatenation + of them all. Each attribute is encoded with some bytes that + describe the length of each part and the type of attribute. + + Not yet implemented: + List types (https://github.com/aws/aws-sdk-java/blob/7844c64cf248aed889811bf2e871ad6b276a89ca/aws-java-sdk-sqs/src/main/java/com/amazonaws/services/sqs/MessageMD5ChecksumHandler.java#L58k) + """ + md5 = hashlib.md5() + for name in sorted(self.message_attributes.keys()): + attr = self.message_attributes[name] + data_type = attr['data_type'] + + encoded = ''.encode('utf-8') + # Each part of each attribute is encoded right after it's + # own length is packed into a 4-byte integer + # 'timestamp' -> b'\x00\x00\x00\t' + encoded += struct.pack("!I", len(name.encode('utf-8'))) + name.encode('utf-8') + # The datatype is additionally given a final byte + # representing which type it is + encoded += struct.pack("!I", len(data_type)).encode('utf-8') + data_type.encode('utf-8') + encoded += TRANSPORT_TYPE_ENCODINGS[data_type] + + if data_type == 'String' or data_type == 'Number': + value = attr['string_value'] + elif data_type == 'Binary': + value = base64.b64decode(attr['binary_value']) + else: + print("Moto hasn't implemented MD5 hashing for {} attributes".format(data_type)) + # The following should be enough of a clue to users that + # they are not, in fact, looking at a correct MD5 while + # also following the character and length constraints of + # MD5 so as not to break client softwre + return('deadbeefdeadbeefdeadbeefdeadbeef') + + encoded += struct.pack("!I", len(value.encode('utf-8'))) + value.encode('utf-8') + + md5.update(encoded) + return md5.hexdigest() @property def body(self): diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 75602b1b7..53bbac6ef 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -337,11 +337,9 @@ SET_QUEUE_ATTRIBUTE_RESPONSE = """ SEND_MESSAGE_RESPONSE = """ - {{- message.md5 -}} + {{- message.body_md5 -}} - {% if message.message_attributes.items()|count > 0 %} - 324758f82d026ac6ec5b31a3b192d1e3 - {% endif %} + {{- message.attribute_md5 -}} {{- message.id -}} @@ -357,7 +355,7 @@ RECEIVE_MESSAGE_RESPONSE = """ {{ message.id }} {{ message.receipt_handle }} - {{ message.md5 }} + {{ message.body_md5 }} {{ message.body }} SenderId @@ -375,9 +373,7 @@ RECEIVE_MESSAGE_RESPONSE = """ ApproximateFirstReceiveTimestamp {{ message.approximate_first_receive_timestamp }} - {% if message.message_attributes.items()|count > 0 %} - 324758f82d026ac6ec5b31a3b192d1e3 - {% endif %} + {{- message.attribute_md5 -}} {% for name, value in message.message_attributes.items() %} {{ name }} @@ -405,10 +401,8 @@ SEND_MESSAGE_BATCH_RESPONSE = """ {{ message.user_id }} {{ message.id }} - {{ message.md5 }} - {% if message.message_attributes.items()|count > 0 %} - 324758f82d026ac6ec5b31a3b192d1e3 - {% endif %} + {{ message.body_md5 }} + {{- message.attribute_md5 -}} {% endfor %} diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index f179d9f85..987efa3d5 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -43,10 +43,44 @@ def test_get_inexistent_queue(): def test_message_send(): sqs = boto3.resource('sqs', region_name='us-east-1') queue = sqs.create_queue(QueueName="blah") - msg = queue.send_message(MessageBody="derp") - + msg = queue.send_message( + MessageBody="derp", + MessageAttributes={ + 'timestamp': { + 'StringValue': '1493147359900', + 'DataType': 'Number', + } + } + ) msg.get('MD5OfMessageBody').should.equal( '58fd9edd83341c29f1aebba81c31e257') + msg.get('MD5OfMessageAttributes').should.equal( + '235c5c510d26fb653d073faed50ae77c') + msg.get('ResponseMetadata', {}).get('RequestId').should.equal( + '27daac76-34dd-47df-bd01-1f6e873584a0') + msg.get('MessageId').should_not.contain(' \n') + + messages = queue.receive_messages() + messages.should.have.length_of(1) + + +@mock_sqs +def test_message_with_complex_attributes(): + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + msg = queue.send_message( + MessageBody="derp", + MessageAttributes={ + 'ccc': {'StringValue': 'testjunk', 'DataType': 'String'}, + 'aaa': {'BinaryValue': b'\x02\x03\x04', 'DataType': 'Binary'}, + 'zzz': {'DataType': 'Number', 'StringValue': '0230.01'}, + 'öther_encodings': {'DataType': 'String', 'StringValue': 'T\xFCst'} + } + ) + msg.get('MD5OfMessageBody').should.equal( + '58fd9edd83341c29f1aebba81c31e257') + msg.get('MD5OfMessageAttributes').should.equal( + '8ae21a7957029ef04146b42aeaa18a22') msg.get('ResponseMetadata', {}).get('RequestId').should.equal( '27daac76-34dd-47df-bd01-1f6e873584a0') msg.get('MessageId').should_not.contain(' \n') From daba69914767f0b48fbf379cea44d12d21f2e635 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 11 May 2017 07:06:42 -0700 Subject: [PATCH 174/274] binary values are sent as base64-encoded strings --- tests/test_sqs/test_sqs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 987efa3d5..0e1149200 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -7,6 +7,7 @@ import botocore.exceptions from boto.exception import SQSError from boto.sqs.message import RawMessage, Message +import base64 import requests import sure # noqa import time @@ -233,7 +234,7 @@ def test_send_message_with_attributes(): message = queue.new_message(body) message_attributes = { 'test.attribute_name': {'data_type': 'String', 'string_value': 'attribute value'}, - 'test.binary_attribute': {'data_type': 'Binary', 'binary_value': 'binary value'}, + 'test.binary_attribute': {'data_type': 'Binary', 'binary_value': base64.b64encode('binary value')}, 'test.number_attribute': {'data_type': 'Number', 'string_value': 'string value'} } message.message_attributes = message_attributes From 6679def702922d19eeea5e9e0016311a868b58de Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 11 May 2017 09:28:19 -0700 Subject: [PATCH 175/274] Python 2/3 compat for MD5 of SQS attributes --- moto/sqs/models.py | 14 ++++++++++---- tests/test_sqs/test_sqs.py | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index f8b7d91b1..d2c538ecb 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import base64 import hashlib import re +import six import struct from xml.sax.saxutils import escape @@ -53,24 +54,29 @@ class Message(BaseModel): Not yet implemented: List types (https://github.com/aws/aws-sdk-java/blob/7844c64cf248aed889811bf2e871ad6b276a89ca/aws-java-sdk-sqs/src/main/java/com/amazonaws/services/sqs/MessageMD5ChecksumHandler.java#L58k) """ + def utf8(str): + if isinstance(str, six.string_types): + return str.encode('utf-8') + return str md5 = hashlib.md5() for name in sorted(self.message_attributes.keys()): attr = self.message_attributes[name] data_type = attr['data_type'] - encoded = ''.encode('utf-8') + encoded = utf8('') # Each part of each attribute is encoded right after it's # own length is packed into a 4-byte integer # 'timestamp' -> b'\x00\x00\x00\t' - encoded += struct.pack("!I", len(name.encode('utf-8'))) + name.encode('utf-8') + encoded += struct.pack("!I", len(utf8(name))) + utf8(name) # The datatype is additionally given a final byte # representing which type it is - encoded += struct.pack("!I", len(data_type)).encode('utf-8') + data_type.encode('utf-8') + encoded += struct.pack("!I", len(data_type)) + utf8(data_type) encoded += TRANSPORT_TYPE_ENCODINGS[data_type] if data_type == 'String' or data_type == 'Number': value = attr['string_value'] elif data_type == 'Binary': + print(data_type, attr['binary_value'], type(attr['binary_value'])) value = base64.b64decode(attr['binary_value']) else: print("Moto hasn't implemented MD5 hashing for {} attributes".format(data_type)) @@ -80,7 +86,7 @@ class Message(BaseModel): # MD5 so as not to break client softwre return('deadbeefdeadbeefdeadbeefdeadbeef') - encoded += struct.pack("!I", len(value.encode('utf-8'))) + value.encode('utf-8') + encoded += struct.pack("!I", len(utf8(value))) + utf8(value) md5.update(encoded) return md5.hexdigest() diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 0e1149200..cad8ace76 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -232,9 +232,10 @@ def test_send_message_with_attributes(): body = 'this is a test message' message = queue.new_message(body) + BASE64_BINARY = base64.b64encode(b'binary value').decode('utf-8') message_attributes = { 'test.attribute_name': {'data_type': 'String', 'string_value': 'attribute value'}, - 'test.binary_attribute': {'data_type': 'Binary', 'binary_value': base64.b64encode('binary value')}, + 'test.binary_attribute': {'data_type': 'Binary', 'binary_value': BASE64_BINARY}, 'test.number_attribute': {'data_type': 'Number', 'string_value': 'string value'} } message.message_attributes = message_attributes From a21413f4eaec49b216c890619d306d9468401740 Mon Sep 17 00:00:00 2001 From: Kate Heddleston Date: Wed, 17 May 2017 13:03:33 -0700 Subject: [PATCH 176/274] NoSuchKey error in S3 is actually '404' Fixes #571 and #953 --- moto/s3/models.py | 4 ++-- moto/s3/responses.py | 4 +++- tests/test_s3/test_s3.py | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index 3cd50050d..b824c4dbf 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -11,7 +11,7 @@ import six from bisect import insort from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime -from .exceptions import BucketAlreadyExists, MissingBucket, MissingKey, InvalidPart, EntityTooSmall +from .exceptions import BucketAlreadyExists, MissingBucket, InvalidPart, EntityTooSmall from .utils import clean_key_name, _VersionedKeyStore UPLOAD_ID_BYTES = 43 @@ -473,7 +473,7 @@ class S3Backend(BaseBackend): if isinstance(key, FakeKey): return key else: - raise MissingKey(key_name=key_name) + return None def initiate_multipart(self, bucket_name, key_name, metadata): bucket = self.get_bucket(bucket_name) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index fd33c5ead..9a3c00e19 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -12,7 +12,7 @@ from moto.core.responses import _TemplateEnvironmentMixin from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_name_from_url, parse_key_name as bucketpath_parse_key_name, is_delete_keys as bucketpath_is_delete_keys -from .exceptions import BucketAlreadyExists, S3ClientError, InvalidPartOrder +from .exceptions import BucketAlreadyExists, S3ClientError, MissingKey, InvalidPartOrder from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey from .utils import bucket_name_from_url, metadata_from_headers from xml.dom import minidom @@ -508,6 +508,8 @@ class ResponseObject(_TemplateEnvironmentMixin): version_id = query.get('versionId', [None])[0] key = self.backend.get_key( bucket_name, key_name, version_id=version_id) + if key is None: + raise MissingKey(key_name) if 'acl' in query: template = self.response_template(S3_OBJECT_ACL_RESPONSE) return 200, response_headers, template.render(obj=key) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index de9c6a7de..3907cec6e 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1223,9 +1223,10 @@ def test_boto3_head_object(): s3.Object('blah', 'hello.txt').meta.client.head_object( Bucket='blah', Key='hello.txt') - with assert_raises(ClientError): + with assert_raises(ClientError) as e: s3.Object('blah', 'hello2.txt').meta.client.head_object( Bucket='blah', Key='hello_bad.txt') + e.exception.response['Error']['Code'].should.equal('404') @mock_s3 @@ -1353,7 +1354,7 @@ def test_boto3_delete_markers(): Bucket=bucket_name, Key=key ) - e.response['Error']['Code'].should.equal('NoSuchKey') + e.response['Error']['Code'].should.equal('404') s3.delete_object( Bucket=bucket_name, From 9f019792df857bd8247aedc3f2ea55f595d58b84 Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 21:57:14 -0500 Subject: [PATCH 177/274] Added tests --- moto/ecr/responses.py | 5 +- tests/test_ecr/test_ecr_boto3.py | 186 +++++++++++++++++++++++++------ 2 files changed, 159 insertions(+), 32 deletions(-) diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index 3a37162a0..a778b6bac 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -32,7 +32,10 @@ class ECRResponse(BaseResponse): def describe_repositories(self): describe_repositories_name = self._get_param('repositoryNames') - repositories = self.ecr_backend.describe_repositories(describe_repositories_name) + registry_id = self._get_param('registryId') + + repositories = self.ecr_backend.describe_repositories( + repository_names=describe_repositories_name, registry_id=registry_id) return json.dumps({ 'repositories': repositories, 'failures': [] diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index ce5a54b17..5a9f5bb61 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -# from nose.tools import assert_raises import hashlib import json from random import random @@ -10,7 +9,6 @@ import sure # noqa import boto3 from moto import mock_ecr -import datetime def _create_image_digest(contents=None): @@ -87,6 +85,73 @@ def test_describe_repositories(): response['repositories'][1]['repositoryUri']]).should.equal(set(respository_uris)) +@mock_ecr +def test_describe_repositories_1(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(registryId='012345678910') + len(response['repositories']).should.equal(2) + + respository_arns = ['arn:aws:ecr:us-east-1:012345678910:repository/test_repository1', + 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository0'] + set([response['repositories'][0]['repositoryArn'], + response['repositories'][1]['repositoryArn']]).should.equal(set(respository_arns)) + + respository_uris = ['012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository1', + '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository0'] + set([response['repositories'][0]['repositoryUri'], + response['repositories'][1]['repositoryUri']]).should.equal(set(respository_uris)) + + +@mock_ecr +def test_describe_repositories_2(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(registryId='109876543210') + len(response['repositories']).should.equal(0) + + +@mock_ecr +def test_describe_repositories_3(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(repositoryNames=['test_repository1']) + len(response['repositories']).should.equal(1) + respository_arn = 'arn:aws:ecr:us-east-1:012345678910:repository/test_repository1' + response['repositories'][0]['repositoryArn'].should.equal(respository_arn) + + respository_uri = '012345678910.dkr.ecr.us-east-1.amazonaws.com/test_repository1' + response['repositories'][0]['repositoryUri'].should.equal(respository_uri) + + +@mock_ecr +def test_describe_repositories_4(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository1' + ) + _ = client.create_repository( + repositoryName='test_repository0' + ) + response = client.describe_repositories(repositoryNames=['not_a_valid_name']) + len(response['repositories']).should.equal(0) + + @mock_ecr def test_delete_repository(): client = boto3.client('ecr', region_name='us-east-1') @@ -106,6 +171,20 @@ def test_delete_repository(): len(response['repositories']).should.equal(0) +@mock_ecr +def test_delete_repository_1(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + invalid_repository_name = 'not_a_repository' + try: + client.delete_repository(repositoryName=invalid_repository_name) + except Exception as e: + str(e).should.equal('{0} is not a repository'.format(invalid_repository_name)) + + @mock_ecr def test_put_image(): client = boto3.client('ecr', region_name='us-east-1') @@ -126,28 +205,38 @@ def test_put_image(): def test_list_images(): client = boto3.client('ecr', region_name='us-east-1') _ = client.create_repository( - repositoryName='test_repository' + repositoryName='test_repository_1' + ) + + _ = client.create_repository( + repositoryName='test_repository_2' ) _ = client.put_image( - repositoryName='test_repository', + repositoryName='test_repository_1', imageManifest=json.dumps(_create_image_manifest()), imageTag='latest' ) _ = client.put_image( - repositoryName='test_repository', + repositoryName='test_repository_1', imageManifest=json.dumps(_create_image_manifest()), imageTag='v1' ) _ = client.put_image( - repositoryName='test_repository', + repositoryName='test_repository_1', imageManifest=json.dumps(_create_image_manifest()), imageTag='v2' ) - response = client.list_images(repositoryName='test_repository') + _ = client.put_image( + repositoryName='test_repository_2', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='oldest' + ) + + response = client.list_images(repositoryName='test_repository_1') type(response['imageIds']).should.be(list) len(response['imageIds']).should.be(3) @@ -156,6 +245,15 @@ def test_list_images(): response['imageIds'][1]['imageTag'], response['imageIds'][2]['imageTag']]).should.equal(set(image_tags)) + response = client.list_images(repositoryName='test_repository_2') + type(response['imageIds']).should.be(list) + len(response['imageIds']).should.be(1) + response['imageIds'][0]['imageTag'].should.equal('oldest') + + response = client.list_images(repositoryName='test_repository_2', registryId='109876543210') + type(response['imageIds']).should.be(list) + len(response['imageIds']).should.be(0) + @mock_ecr def test_describe_images(): @@ -211,31 +309,57 @@ def test_describe_images(): response['imageDetails'][1]['imageSizeInBytes'].should.equal(52428800) response['imageDetails'][2]['imageSizeInBytes'].should.equal(52428800) - # response['imageDetails'][0]['imagePushedAt'].should.equal('2017-05-09') - # response['imageDetails'][1]['imagePushedAt'].should.equal('2017-05-09') - # response['imageDetails'][2]['imagePushedAt'].should.equal('2017-05-09') + invalid_repository_name = 'not_a_valid_repository' + try: + client.describe_images(repositoryName=invalid_repository_name) + except Exception as e: + str(e).should.equal('{0} is not a repository'.format(invalid_repository_name)) - ''' - image_digests = [ - "hi", "mike", "name" - ] - set([response['imageDetails'][0]['imageDigest'], - response['imageDetails'][1]['imageDigest'], - response['imageDetails'][2]['imageDigest']]).should.equal(set(image_digests)) - ''' + +@mock_ecr +def test_put_image(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + response = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + response['image']['imageId']['imageTag'].should.equal('latest') + response['image']['imageId']['imageDigest'].should.contain("sha") + response['image']['repositoryName'].should.equal('test_repository') + response['image']['registryId'].should.equal('012345678910') + + invalid_repository_name = 'not_a_valid_repository' + + try: + client.put_image( + repositoryName=invalid_repository_name, + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest') + except Exception as e: + str(e).should.equal('{0} is not a repository'.format(invalid_repository_name)) ''' -'imageDetails': [ - { - 'registryId': 'string', - 'repositoryName': 'string', - 'imageDigest': 'string', - 'imageTags': [ - 'string', - ], - 'imageSizeInBytes': 123, - 'imagePushedAt': datetime(2015, 1, 1) - }, -], -''' +obj = { + "image": { + "repository": "test_repository", + "imageManifest": "{\"layers\": [{\"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\", \"digest\": \"sha256:77ea7eee3d80b1a38f83906dd3048e2689457eb90e18a7d12f839c5ae37106a2\", \"size\": 32654}, {\"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\", \"digest\": \"sha256:95cf1a2e1698fe3ca1fcc3f653119146b271d0b62e487ec264441e886a11bd06\", \"size\": 16724}, {\"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\", \"digest\": \"sha256:a0e70458d19e37e14d6388030a017c587283e2fb6ef10c0744cad0294c47e8f8\", \"size\": 73109}], \"schemaVersion\": 2, \"config\": {\"mediaType\": \"application/vnd.docker.container.image.v1+json\", \"digest\": \"sha256:b79606fb3afea5bd1609ed40b622142f1c98125abcfe89a76a661b0e8e343910\", \"size\": 7023}, \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\"}", + "imageId": { + "imageTag": "latest", + "imageDigest": "sha256:c639b9999fadc04554ed2ef5cec140d35136d23f0ee15ad71f0708e334fc21ba" + }, + "imageSizeInBytes": 52428800, + "imageDigest": null, + "imageTag": "latest", + "registryId": "012345678910", + "repositoryName": "test_repository", + "imagePushedAt": null + } +} +''' \ No newline at end of file From d6873c3dcb916c59f2dfc39d21960e778cdd1f09 Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 22:04:33 -0500 Subject: [PATCH 178/274] Adding ECR to moto/backends.py --- moto/backends.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/moto/backends.py b/moto/backends.py index eae94db75..0af4ae2e2 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -10,6 +10,7 @@ from moto.datapipeline import datapipeline_backends from moto.dynamodb import dynamodb_backends from moto.dynamodb2 import dynamodb_backends2 from moto.ec2 import ec2_backends +from moto.ecr import ecr_backends from moto.ecs import ecs_backends from moto.elb import elb_backends from moto.emr import emr_backends @@ -39,6 +40,7 @@ BACKENDS = { 'dynamodb': dynamodb_backends, 'dynamodb2': dynamodb_backends2, 'ec2': ec2_backends, + 'ecr': ecr_backends, 'ecs': ecs_backends, 'elb': elb_backends, 'events': events_backends, From c7a166f68e3c84a44744e5129782c8338e5eca47 Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 22:29:49 -0500 Subject: [PATCH 179/274] Remove tests that expect exceptions. --- tests/test_ecr/test_ecr_boto3.py | 54 ++------------------------------ 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 5a9f5bb61..f466823d4 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -169,21 +169,7 @@ def test_delete_repository(): response = client.describe_repositories() len(response['repositories']).should.equal(0) - - -@mock_ecr -def test_delete_repository_1(): - client = boto3.client('ecr', region_name='us-east-1') - _ = client.create_repository( - repositoryName='test_repository' - ) - - invalid_repository_name = 'not_a_repository' - try: - client.delete_repository(repositoryName=invalid_repository_name) - except Exception as e: - str(e).should.equal('{0} is not a repository'.format(invalid_repository_name)) - + @mock_ecr def test_put_image(): @@ -309,12 +295,6 @@ def test_describe_images(): response['imageDetails'][1]['imageSizeInBytes'].should.equal(52428800) response['imageDetails'][2]['imageSizeInBytes'].should.equal(52428800) - invalid_repository_name = 'not_a_valid_repository' - try: - client.describe_images(repositoryName=invalid_repository_name) - except Exception as e: - str(e).should.equal('{0} is not a repository'.format(invalid_repository_name)) - @mock_ecr def test_put_image(): @@ -332,34 +312,4 @@ def test_put_image(): response['image']['imageId']['imageTag'].should.equal('latest') response['image']['imageId']['imageDigest'].should.contain("sha") response['image']['repositoryName'].should.equal('test_repository') - response['image']['registryId'].should.equal('012345678910') - - invalid_repository_name = 'not_a_valid_repository' - - try: - client.put_image( - repositoryName=invalid_repository_name, - imageManifest=json.dumps(_create_image_manifest()), - imageTag='latest') - except Exception as e: - str(e).should.equal('{0} is not a repository'.format(invalid_repository_name)) - - -''' -obj = { - "image": { - "repository": "test_repository", - "imageManifest": "{\"layers\": [{\"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\", \"digest\": \"sha256:77ea7eee3d80b1a38f83906dd3048e2689457eb90e18a7d12f839c5ae37106a2\", \"size\": 32654}, {\"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\", \"digest\": \"sha256:95cf1a2e1698fe3ca1fcc3f653119146b271d0b62e487ec264441e886a11bd06\", \"size\": 16724}, {\"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\", \"digest\": \"sha256:a0e70458d19e37e14d6388030a017c587283e2fb6ef10c0744cad0294c47e8f8\", \"size\": 73109}], \"schemaVersion\": 2, \"config\": {\"mediaType\": \"application/vnd.docker.container.image.v1+json\", \"digest\": \"sha256:b79606fb3afea5bd1609ed40b622142f1c98125abcfe89a76a661b0e8e343910\", \"size\": 7023}, \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\"}", - "imageId": { - "imageTag": "latest", - "imageDigest": "sha256:c639b9999fadc04554ed2ef5cec140d35136d23f0ee15ad71f0708e334fc21ba" - }, - "imageSizeInBytes": 52428800, - "imageDigest": null, - "imageTag": "latest", - "registryId": "012345678910", - "repositoryName": "test_repository", - "imagePushedAt": null - } -} -''' \ No newline at end of file + response['image']['registryId'].should.equal('012345678910') \ No newline at end of file From 35692b5c9ac5e00a81d4d11278298c796f4c7cf7 Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 22:49:59 -0500 Subject: [PATCH 180/274] Stub out all remaining ECR methods with NotImplementedError. --- moto/ecr/responses.py | 75 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index a778b6bac..f8b1606cc 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -73,3 +73,78 @@ class ECRResponse(BaseResponse): return json.dumps({ 'imageDetails': [image.response_describe_object for image in images], }) + + def batch_check_layer_availability(self): + if self.is_not_dryrun('BatchCheckLayerAvailability'): + raise NotImplementedError( + 'ECR.batch_check_layer_availability is not yet implemented') + + def batch_delete_image(self): + if self.is_not_dryrun('BatchDeleteImage'): + raise NotImplementedError( + 'ECR.batch_delete_image is not yet implemented') + + def batch_get_image(self): + if self.is_not_dryrun('BatchGetImage'): + raise NotImplementedError( + 'ECR.batch_get_image is not yet implemented') + + def can_paginate(self): + if self.is_not_dryrun('CanPaginate'): + raise NotImplementedError( + 'ECR.can_paginate is not yet implemented') + + def complete_layer_upload(self): + if self.is_not_dryrun('CompleteLayerUpload'): + raise NotImplementedError( + 'ECR.complete_layer_upload is not yet implemented') + + def delete_repository_policy(self): + if self.is_not_dryrun('DeleteRepositoryPolicy'): + raise NotImplementedError( + 'ECR.delete_repository_policy is not yet implemented') + + def generate_presigned_url(self): + if self.is_not_dryrun('GeneratePresignedUrl'): + raise NotImplementedError( + 'ECR.generate_presigned_url is not yet implemented') + + def get_authorization_token(self): + if self.is_not_dryrun('GetAuthorizationToken'): + raise NotImplementedError( + 'ECR.get_authorization_token is not yet implemented') + + def get_download_url_for_layer(self): + if self.is_not_dryrun('GetDownloadUrlForLayer'): + raise NotImplementedError( + 'ECR.get_download_url_for_layer is not yet implemented') + + def get_paginator(self): + if self.is_not_dryrun('GetPaginator'): + raise NotImplementedError( + 'ECR.get_paginator is not yet implemented') + + def get_repository_policy(self): + if self.is_not_dryrun('GetRepositoryPolicy'): + raise NotImplementedError( + 'ECR.get_repository_policy is not yet implemented') + + def get_waiter(self): + if self.is_not_dryrun('GetWaiter'): + raise NotImplementedError( + 'ECR.get_waiter is not yet implemented') + + def initiate_layer_upload(self): + if self.is_not_dryrun('InitiateLayerUpload'): + raise NotImplementedError( + 'ECR.initiate_layer_upload is not yet implemented') + + def set_repository_policy(self): + if self.is_not_dryrun('SetRepositoryPolicy'): + raise NotImplementedError( + 'ECR.set_repository_policy is not yet implemented') + + def upload_layer_part(self): + if self.is_not_dryrun('UploadLayerPart'): + raise NotImplementedError( + 'ECR.upload_layer_part is not yet implemented') From 91d99e56951b818462a07f036858577367f1000b Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 22:50:39 -0500 Subject: [PATCH 181/274] Fix python 3 error with generate sha --- tests/test_ecr/test_ecr_boto3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index f466823d4..1191c42d2 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -14,7 +14,7 @@ from moto import mock_ecr def _create_image_digest(contents=None): if not contents: contents = 'docker_image{0}'.format(int(random() * 10 ** 6)) - return "sha256:%s" % hashlib.sha256(contents).hexdigest() + return "sha256:%s" % hashlib.sha256(contents.encode('utf-8')).hexdigest() def _create_image_manifest(): @@ -169,7 +169,7 @@ def test_delete_repository(): response = client.describe_repositories() len(response['repositories']).should.equal(0) - + @mock_ecr def test_put_image(): From 25ae4d42a2d3c50007d369c3288c0482037d95e0 Mon Sep 17 00:00:00 2001 From: Mike Bjerkness Date: Mon, 22 May 2017 23:04:36 -0500 Subject: [PATCH 182/274] Fix encoding error in ecr/models.py --- moto/ecr/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ecr/models.py b/moto/ecr/models.py index 5f8255007..82ce2ebd6 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -104,7 +104,7 @@ class Image(BaseObject): def _create_digest(self): image_contents = 'docker_image{0}'.format(int(random() * 10 ** 6)) - self.image_digest = "sha256:%s" % hashlib.sha256(image_contents).hexdigest() + self.image_digest = "sha256:%s" % hashlib.sha256(image_contents.encode('utf-8')).hexdigest() def get_image_digest(self): if not self.image_digest: From a2a651493628397dc32005320dd4203ebb83993a Mon Sep 17 00:00:00 2001 From: Simon-Pierre Gingras Date: Tue, 23 May 2017 11:29:01 -0700 Subject: [PATCH 183/274] attempt at fixing tests --- moto/s3/responses.py | 4 ++-- tests/test_s3/test_s3.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 43e27a815..dbc6bf28f 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -484,7 +484,7 @@ class ResponseObject(_TemplateEnvironmentMixin): elif method == 'PUT': return self._key_response_put(request, body, bucket_name, query, key_name, headers) elif method == 'HEAD': - return self._key_response_head(bucket_name, query, key_name, headers) + return self._key_response_head(bucket_name, query, key_name, headers=request.headers) elif method == 'DELETE': return self._key_response_delete(bucket_name, query, key_name, headers) elif method == 'POST': @@ -597,7 +597,7 @@ class ResponseObject(_TemplateEnvironmentMixin): response_headers = {} version_id = query.get('versionId', [None])[0] - if_modified_since = headers.get('if-modified-since', None) + if_modified_since = headers.get('If-Modified-Since', None) if if_modified_since: if_modified_since = str_to_rfc_1123_datetime(if_modified_since) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 6af653f9e..cd1c2e43e 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1277,18 +1277,17 @@ def test_boto3_head_object_if_modified_since(): key = 'hello.txt' - with freeze_time(datetime.datetime.now() - datetime.timedelta(hours=3)): - s3.put_object( - Bucket=bucket_name, - Key=key, - Body='test' - ) + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test' + ) with assert_raises(botocore.exceptions.ClientError) as err: s3.head_object( Bucket=bucket_name, Key=key, - IfModifiedSince=datetime.datetime.now() - datetime.timedelta(hours=2) + IfModifiedSince=datetime.datetime.utcnow() + datetime.timedelta(hours=1) ) e = err.exception e.response['Error'].should.equal({'Code': '304', 'Message': 'Not Modified'}) From 4e2f775c1f4167f91d08e9df997e790df847e549 Mon Sep 17 00:00:00 2001 From: Jeff Hardy Date: Fri, 26 May 2017 12:37:33 -0700 Subject: [PATCH 184/274] Use region list from Boto. Boto can be configured with extra regions, but moto will fail to import if they are not in the hardcoded list in ec2/models.py. Instead, use the region list from boto to build the ec2_backends dict to ensure all regions are available. --- moto/ec2/models.py | 21 +++------------------ tests/test_ec2/test_regions.py | 8 +++++++- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 7fa7e1009..7e3df9880 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -5,6 +5,8 @@ import itertools import re import six +import boto.ec2 + from collections import defaultdict from datetime import datetime from boto.ec2.instance import Instance as BotoInstance, Reservation @@ -1143,24 +1145,7 @@ class Zone(object): class RegionsAndZonesBackend(object): - regions = [ - Region("ap-northeast-1", "ec2.ap-northeast-1.amazonaws.com"), - Region("ap-northeast-2", "ec2.ap-northeast-2.amazonaws.com"), - Region("ap-south-1", "ec2.ap-south-1.amazonaws.com"), - Region("ap-southeast-1", "ec2.ap-southeast-1.amazonaws.com"), - Region("ap-southeast-2", "ec2.ap-southeast-2.amazonaws.com"), - Region("ca-central-1", "ec2.ca-central-1.amazonaws.com.cn"), - Region("cn-north-1", "ec2.cn-north-1.amazonaws.com.cn"), - Region("eu-central-1", "ec2.eu-central-1.amazonaws.com"), - Region("eu-west-1", "ec2.eu-west-1.amazonaws.com"), - Region("eu-west-2", "ec2.eu-west-2.amazonaws.com"), - Region("sa-east-1", "ec2.sa-east-1.amazonaws.com"), - Region("us-east-1", "ec2.us-east-1.amazonaws.com"), - Region("us-east-2", "ec2.us-east-2.amazonaws.com"), - Region("us-gov-west-1", "ec2.us-gov-west-1.amazonaws.com"), - Region("us-west-1", "ec2.us-west-1.amazonaws.com"), - Region("us-west-2", "ec2.us-west-2.amazonaws.com"), - ] + regions = [Region(ri.name, ri.endpoint) for ri in boto.ec2.regions()] zones = dict( (region, [Zone(region + c, region) for c in 'abc']) diff --git a/tests/test_ec2/test_regions.py b/tests/test_ec2/test_regions.py index 4beca7c67..1e87b253c 100644 --- a/tests/test_ec2/test_regions.py +++ b/tests/test_ec2/test_regions.py @@ -5,13 +5,19 @@ import boto.ec2.elb import sure from moto import mock_ec2_deprecated, mock_autoscaling_deprecated, mock_elb_deprecated +from moto.ec2 import ec2_backends + +def test_use_boto_regions(): + boto_regions = {r.name for r in boto.ec2.regions()} + moto_regions = set(ec2_backends) + + moto_regions.should.equal(boto_regions) def add_servers_to_region(ami_id, count, region): conn = boto.ec2.connect_to_region(region) for index in range(count): conn.run_instances(ami_id) - @mock_ec2_deprecated def test_add_servers_to_a_single_region(): region = 'ap-northeast-1' From 98264148e1041d08d80effdc9e7574dc8cf1b93f Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Wed, 31 May 2017 15:11:42 -0700 Subject: [PATCH 185/274] ELB connection draining timeout defaults to 300 seconds --- moto/elb/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/elb/responses.py b/moto/elb/responses.py index ed8d6d03a..2bc76385f 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -159,7 +159,7 @@ class ELBResponse(BaseResponse): if connection_draining: attribute = ConnectionDrainingAttribute() attribute.enabled = connection_draining["enabled"] == "true" - attribute.timeout = connection_draining["timeout"] + attribute.timeout = connection_draining.get("timeout") self.elb_backend.set_connection_draining_attribute( load_balancer_name, attribute) From b0c83c4e70999c9230b2d1fc922e6aae4ea5ebc8 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Wed, 31 May 2017 15:53:31 -0700 Subject: [PATCH 186/274] Testing ELB connection draining timeouts --- moto/elb/responses.py | 17 ++++++++++------- tests/test_elb/test_elb.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/moto/elb/responses.py b/moto/elb/responses.py index 2bc76385f..ec20486f0 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -159,9 +159,8 @@ class ELBResponse(BaseResponse): if connection_draining: attribute = ConnectionDrainingAttribute() attribute.enabled = connection_draining["enabled"] == "true" - attribute.timeout = connection_draining.get("timeout") - self.elb_backend.set_connection_draining_attribute( - load_balancer_name, attribute) + attribute.timeout = connection_draining.get("timeout", 300) + self.elb_backend.set_connection_draining_attribute(load_balancer_name, attribute) connection_settings = self._get_dict_param( "LoadBalancerAttributes.ConnectionSettings.") @@ -172,7 +171,7 @@ class ELBResponse(BaseResponse): load_balancer_name, attribute) template = self.response_template(MODIFY_ATTRIBUTES_TEMPLATE) - return template.render(attributes=load_balancer.attributes) + return template.render(load_balancer=load_balancer, attributes=load_balancer.attributes) def create_load_balancer_policy(self): load_balancer_name = self._get_param('LoadBalancerName') @@ -592,9 +591,11 @@ DESCRIBE_ATTRIBUTES_TEMPLATE = """{{ attributes.cross_zone_load_balancing.enabled }} - {{ attributes.connection_draining.enabled }} {% if attributes.connection_draining.enabled %} + true {{ attributes.connection_draining.timeout }} + {% else %} + false {% endif %} @@ -607,7 +608,7 @@ DESCRIBE_ATTRIBUTES_TEMPLATE = """ - my-loadbalancer + {{ load_balancer.name }} {{ attributes.access_log.enabled }} @@ -624,9 +625,11 @@ MODIFY_ATTRIBUTES_TEMPLATE = """ Date: Wed, 24 May 2017 09:54:00 -0300 Subject: [PATCH 188/274] extended CloudFormation models for Lambda and DynamoDB --- moto/awslambda/models.py | 79 ++++++++++++++++--- moto/cloudformation/parsing.py | 8 +- moto/cloudwatch/models.py | 21 +++++ moto/dynamodb/models.py | 22 ++++++ .../test_cloudformation_stack_crud.py | 78 ++++++++++++++++++ 5 files changed, 195 insertions(+), 13 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 1e651cb04..13d4726ac 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -4,6 +4,7 @@ import base64 import datetime import hashlib import io +import os import json import sys import zipfile @@ -16,12 +17,12 @@ except: import boto.awslambda from moto.core import BaseBackend, BaseModel from moto.s3.models import s3_backend -from moto.s3.exceptions import MissingBucket +from moto.s3.exceptions import MissingBucket, MissingKey class LambdaFunction(BaseModel): - def __init__(self, spec): + def __init__(self, spec, validate_s3=True): # required self.code = spec['Code'] self.function_name = spec['FunctionName'] @@ -58,24 +59,25 @@ class LambdaFunction(BaseModel): self.code_size = len(to_unzip_code) self.code_sha_256 = hashlib.sha256(to_unzip_code).hexdigest() else: - # validate s3 bucket + # validate s3 bucket and key + key = None try: # FIXME: does not validate bucket region key = s3_backend.get_key( self.code['S3Bucket'], self.code['S3Key']) except MissingBucket: - raise ValueError( - "InvalidParameterValueException", - "Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist") - else: - # validate s3 key - if key is None: + if do_validate_s3(): + raise ValueError( + "InvalidParameterValueException", + "Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist") + except MissingKey: + if do_validate_s3(): raise ValueError( "InvalidParameterValueException", "Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.") - else: - self.code_size = key.size - self.code_sha_256 = hashlib.sha256(key.value).hexdigest() + if key: + self.code_size = key.size + self.code_sha_256 = hashlib.sha256(key.value).hexdigest() self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format( self.function_name) @@ -209,6 +211,13 @@ class LambdaFunction(BaseModel): fn = backend.create_function(spec) return fn + def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException + if attribute_name == 'Arn': + region = 'us-east-1' + return 'arn:aws:lambda:{0}:123456789012:function:{1}'.format(region, self.function_name) + raise UnformattedGetAttTemplateException() + @staticmethod def _create_zipfile_from_plaintext_code(code): zip_output = io.BytesIO() @@ -219,6 +228,48 @@ class LambdaFunction(BaseModel): return zip_output.read() +class EventSourceMapping(BaseModel): + + def __init__(self, spec): + # required + self.function_name = spec['FunctionName'] + self.event_source_arn = spec['EventSourceArn'] + self.starting_position = spec['StartingPosition'] + + # optional + self.batch_size = spec.get('BatchSize', 100) + self.enabled = spec.get('Enabled', True) + self.starting_position_timestamp = spec.get('StartingPositionTimestamp', None) + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + spec = { + 'FunctionName': properties['FunctionName'], + 'EventSourceArn': properties['EventSourceArn'], + 'StartingPosition': properties['StartingPosition'] + } + optional_properties = 'BatchSize Enabled StartingPositionTimestamp'.split() + for prop in optional_properties: + if prop in properties: + spec[prop] = properties[prop] + return EventSourceMapping(spec) + + +class LambdaVersion(BaseModel): + + def __init__(self, spec): + self.version = spec['Version'] + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + spec = { + 'Version': properties.get('Version') + } + return LambdaVersion(spec) + + class LambdaBackend(BaseBackend): def __init__(self): @@ -242,6 +293,10 @@ class LambdaBackend(BaseBackend): return self._functions.values() +def do_validate_s3(): + return os.environ.get('VALIDATE_LAMBDA_S3', '') in ['', '1', 'true'] + + lambda_backends = {} for region in boto.awslambda.regions(): lambda_backends[region.name] = LambdaBackend() diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 6d38289c7..1908a2a71 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -7,7 +7,9 @@ import warnings from moto.autoscaling import models as autoscaling_models from moto.awslambda import models as lambda_models +from moto.cloudwatch import models as cloudwatch_models from moto.datapipeline import models as datapipeline_models +from moto.dynamodb import models as dynamodb_models from moto.ec2 import models as ec2_models from moto.ecs import models as ecs_models from moto.elb import models as elb_models @@ -27,7 +29,10 @@ from boto.cloudformation.stack import Output MODEL_MAP = { "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, "AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration, + "AWS::DynamoDB::Table": dynamodb_models.Table, + "AWS::Lambda::EventSourceMapping": lambda_models.EventSourceMapping, "AWS::Lambda::Function": lambda_models.LambdaFunction, + "AWS::Lambda::Version": lambda_models.LambdaVersion, "AWS::EC2::EIP": ec2_models.ElasticAddress, "AWS::EC2::Instance": ec2_models.Instance, "AWS::EC2::InternetGateway": ec2_models.InternetGateway, @@ -53,6 +58,7 @@ MODEL_MAP = { "AWS::IAM::InstanceProfile": iam_models.InstanceProfile, "AWS::IAM::Role": iam_models.Role, "AWS::KMS::Key": kms_models.Key, + "AWS::Logs::LogGroup": cloudwatch_models.LogGroup, "AWS::RDS::DBInstance": rds_models.Database, "AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup, "AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup, @@ -133,7 +139,7 @@ def clean_json(resource_json, resources_map): try: return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1]) except NotImplementedError as n: - logger.warning(n.message.format( + logger.warning(str(n).format( resource_json['Fn::GetAtt'][0])) except UnformattedGetAttTemplateException: raise ValidationError( diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index dd97ddcbb..ed0086d93 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -111,6 +111,27 @@ class CloudWatchBackend(BaseBackend): return self.metric_data +class LogGroup(BaseModel): + + def __init__(self, spec): + # required + self.name = spec['LogGroupName'] + # optional + self.tags = spec.get('Tags', []) + + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + spec = { + 'LogGroupName': properties['LogGroupName'] + } + optional_properties = 'Tags'.split() + for prop in optional_properties: + if prop in properties: + spec[prop] = properties[prop] + return LogGroup(spec) + + cloudwatch_backends = {} for region in boto.ec2.cloudwatch.regions(): cloudwatch_backends[region.name] = CloudWatchBackend() diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index 39bf15fca..300189a0e 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -137,6 +137,20 @@ class Table(BaseModel): } return results + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + key_attr = [i['AttributeName'] for i in properties['KeySchema'] if i['KeyType'] == 'HASH'][0] + key_type = [i['AttributeType'] for i in properties['AttributeDefinitions'] if i['AttributeName'] == key_attr][0] + spec = { + 'name': properties['TableName'], + 'hash_key_attr': key_attr, + 'hash_key_type': key_type + } + # TODO: optional properties still missing: + # range_key_attr, range_key_type, read_capacity, write_capacity + return Table(**spec) + def __len__(self): count = 0 for key, value in self.items.items(): @@ -245,6 +259,14 @@ class Table(BaseModel): except KeyError: return None + def get_cfn_attribute(self, attribute_name): + from moto.cloudformation.exceptions import UnformattedGetAttTemplateException + if attribute_name == 'StreamArn': + region = 'us-east-1' + time = '2000-01-01T00:00:00.000' + return 'arn:aws:dynamodb:{0}:123456789012:table/{1}/stream/{2}'.format(region, self.name, time) + raise UnformattedGetAttTemplateException() + class DynamoDBBackend(BaseBackend): diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index eb3798f82..0e3634756 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import os import json import boto @@ -565,3 +566,80 @@ def test_describe_stack_events_shows_create_update_and_delete(): assert False, "Too many stack events" list(stack_events_to_look_for).should.be.empty + + +@mock_cloudformation_deprecated +@mock_route53_deprecated +def test_create_stack_lambda_and_dynamodb(): + conn = boto.connect_cloudformation() + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack Lambda Test 1", + "Parameters": { + }, + "Resources": { + "func1": { + "Type" : "AWS::Lambda::Function", + "Properties" : { + "Code": { + "S3Bucket": "bucket_123", + "S3Key": "key_123" + }, + "FunctionName": "func1", + "Handler": "handler.handler", + "Role": "role1", + "Runtime": "python2.7", + "Description": "descr", + "MemorySize": 12345, + } + }, + "func1version": { + "Type": "AWS::Lambda::LambdaVersion", + "Properties" : { + "Version": "v1.2.3" + } + }, + "tab1": { + "Type" : "AWS::DynamoDB::Table", + "Properties" : { + "TableName": "tab1", + "KeySchema": [{ + "AttributeName": "attr1", + "KeyType": "HASH" + }], + "AttributeDefinitions": [{ + "AttributeName": "attr1", + "AttributeType": "string" + }], + "ProvisionedThroughput": { + "ReadCapacityUnits": 10, + "WriteCapacityUnits": 10 + } + } + }, + "func1mapping": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties" : { + "FunctionName": "v1.2.3", + "EventSourceArn": "arn:aws:dynamodb:region:XXXXXX:table/tab1/stream/2000T00:00:00.000", + "StartingPosition": "0", + "BatchSize": 100, + "Enabled": True + } + } + }, + } + validate_s3_before = os.environ.get('VALIDATE_LAMBDA_S3', '') + try: + os.environ['VALIDATE_LAMBDA_S3'] = 'false' + conn.create_stack( + "test_stack_lambda_1", + template_body=json.dumps(dummy_template), + parameters={}.items() + ) + finally: + os.environ['VALIDATE_LAMBDA_S3'] = validate_s3_before + + stack = conn.describe_stacks()[0] + resources = stack.list_resources() + assert len(resources) == 4 From a0651ccde556a441281b1a902ec994b1faf8f1bd Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Fri, 2 Jun 2017 16:18:52 -0400 Subject: [PATCH 189/274] Add exports to CloudFormationBackend --- moto/cloudformation/models.py | 10 +++++++++- moto/cloudformation/parsing.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 0dc262b2d..4a033b6d2 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -42,7 +42,7 @@ class FakeStack(BaseModel): return resource_map def _create_output_map(self): - output_map = OutputMap(self.resource_map, self.template_dict) + output_map = OutputMap(self.resource_map, self.template_dict, self.stack_id) output_map.create() return output_map @@ -90,6 +90,10 @@ class FakeStack(BaseModel): def stack_outputs(self): return self.output_map.values() + @property + def exports(self): + return self.output_map.exports + def update(self, template, role_arn=None, parameters=None, tags=None): self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") self.template = template @@ -131,6 +135,7 @@ class CloudFormationBackend(BaseBackend): def __init__(self): self.stacks = OrderedDict() self.deleted_stacks = {} + self.exports = OrderedDict() def create_stack(self, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None): stack_id = generate_stack_id(name) @@ -145,6 +150,8 @@ class CloudFormationBackend(BaseBackend): role_arn=role_arn, ) self.stacks[stack_id] = new_stack + for export in new_stack.exports: + self.exports[export.name] = export return new_stack def describe_stacks(self, name_or_stack_id): @@ -191,6 +198,7 @@ class CloudFormationBackend(BaseBackend): stack = self.stacks.pop(name_or_stack_id, None) stack.delete() self.deleted_stacks[stack.stack_id] = stack + [self.exports.pop(export.name) for export in stack.exports] return self.stacks.pop(name_or_stack_id, None) else: # Delete by stack name diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 6d38289c7..248ecc57a 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -454,8 +454,9 @@ class ResourceMap(collections.Mapping): class OutputMap(collections.Mapping): - def __init__(self, resources, template): + def __init__(self, resources, template, stack_id): self._template = template + self._stack_id = stack_id self._output_json_map = template.get('Outputs') # Create the default resources @@ -484,6 +485,35 @@ class OutputMap(collections.Mapping): def outputs(self): return self._output_json_map.keys() if self._output_json_map else [] + @property + def exports(self): + exports = [] + if self.outputs: + for key, value in self._output_json_map.iteritems(): + if value.get('Export'): + exports.append(Export(self._stack_id, value['Export'].get('Name'), value.get('Value'))) + return exports + def create(self): for output in self.outputs: self[output] + + +class Export(object): + + def __init__(self, exporting_stack_id, name, value): + self._exporting_stack_id = exporting_stack_id + self._name = name + self._value = value + + @property + def exporting_stack_id(self): + return self._exporting_stack_id + + @property + def name(self): + return self._name + + @property + def value(self): + return self._value From 5eb866146a71455ac94bfe7750a1d88503620790 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Fri, 2 Jun 2017 13:19:45 -0700 Subject: [PATCH 190/274] add assert to catch odd numbers in operator/value parsing --- moto/dynamodb2/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 4bca83582..1a609bebb 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -112,6 +112,7 @@ class Item(object): def update(self, update_expression, expression_attribute_names, expression_attribute_values): parts = [p for p in re.split(r'\b(SET|REMOVE|ADD|DELETE)\b', update_expression) if p] + assert len(parts) % 2 == 0, "Mismatched operators and values in update expression: '{}'".format(update_expression) for action, valstr in zip(parts[:-1:2], parts[1::2]): values = valstr.split(',') for value in values: From de9ea10eb1faabe1e724b86af719ca6ddef7bfa5 Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Fri, 2 Jun 2017 16:22:48 -0400 Subject: [PATCH 191/274] Add list_exports to CloudFormationResponse --- moto/cloudformation/models.py | 11 +++ moto/cloudformation/responses.py | 26 +++++++ .../test_cloudformation_stack_crud_boto3.py | 76 +++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 4a033b6d2..2b3dfee47 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -206,6 +206,17 @@ class CloudFormationBackend(BaseBackend): if stack.name == name_or_stack_id: self.delete_stack(stack.stack_id) + def list_exports(self, token): + all_exports = [x for x in self.exports.values()] + if token is None: + exports = all_exports[0:100] + next_token = '100' if len(all_exports) > 100 else None + else: + token = int(token) + exports = all_exports[token:token + 100] + next_token = str(token + 100) if len(all_exports) > token + 100 else None + return exports, next_token + cloudformation_backends = {} for region in boto.cloudformation.regions(): diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 60f647efa..d66a172a8 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -210,6 +210,12 @@ class CloudFormationResponse(BaseResponse): template = self.response_template(DELETE_STACK_RESPONSE_TEMPLATE) return template.render() + def list_exports(self): + token = self._get_param('NextToken') + exports, next_token = self.cloudformation_backend.list_exports(token=token) + template = self.response_template(LIST_EXPORTS_RESPONSE) + return template.render(exports=exports, next_token=next_token) + CREATE_STACK_RESPONSE_TEMPLATE = """ @@ -410,3 +416,23 @@ DELETE_STACK_RESPONSE_TEMPLATE = """ """ + +LIST_EXPORTS_RESPONSE = """ + + + {% for export in exports %} + + {{ export.exporting_stack_id }} + {{ export.name }} + {{ export.value }} + + {% endfor %} + + {% if next_token %} + {{ next_token }} + {% endif %} + + + 5ccc7dcd-744c-11e5-be70-example + +""" diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 85815e9f8..8b4d72ad3 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -57,8 +57,31 @@ dummy_update_template = { } } +dummy_output_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Resources": { + "Instance": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": "ami-08111162" + } + } + }, + "Outputs" : { + "StackVPC" : { + "Description" : "The ID of the VPC", + "Value" : "VPCID", + "Export" : { + "Name" : "My VPC ID" + } + } + } +} + dummy_template_json = json.dumps(dummy_template) dummy_update_template_json = json.dumps(dummy_template) +dummy_output_template_json = json.dumps(dummy_output_template) @mock_cloudformation @@ -408,3 +431,56 @@ def test_stack_events(): assert False, "Too many stack events" list(stack_events_to_look_for).should.be.empty + + +@mock_cloudformation +def test_list_exports(): + cf_client = boto3.client('cloudformation', region_name='us-east-1') + cf_resource = boto3.resource('cloudformation', region_name='us-east-1') + stack = cf_resource.create_stack( + StackName="test_stack", + TemplateBody=dummy_output_template_json, + ) + output_value = 'VPCID' + exports = cf_client.list_exports()['Exports'] + + stack.outputs.should.have.length_of(1) + stack.outputs[0]['OutputValue'].should.equal(output_value) + + exports.should.have.length_of(1) + exports[0]['ExportingStackId'].should.equal(stack.stack_id) + exports[0]['Name'].should.equal('My VPC ID') + exports[0]['Value'].should.equal(output_value) + + +@mock_cloudformation +def test_list_exports_with_token(): + cf = boto3.client('cloudformation', region_name='us-east-1') + for i in range(101): + cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_output_template_json, + ) + exports = cf.list_exports() + exports['Exports'].should.have.length_of(100) + exports.get('NextToken').should_not.be.none + + more_exports = cf.list_exports(NextToken=exports['NextToken']) + more_exports['Exports'].should.have.length_of(1) + more_exports.get('NextToken').should.be.none + + +@mock_cloudformation +def test_delete_stack_with_export(): + cf = boto3.client('cloudformation', region_name='us-east-1') + stack = cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_output_template_json, + ) + + stack_id = stack['StackId'] + exports = cf.list_exports()['Exports'] + exports.should.have.length_of(1) + + cf.delete_stack(StackName=stack_id) + cf.list_exports()['Exports'].should.have.length_of(0) From c6603c6248d6690e080b6b076e330c6d818d48a9 Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Fri, 2 Jun 2017 16:23:42 -0400 Subject: [PATCH 192/274] Validate export names are unique --- moto/cloudformation/models.py | 7 +++++++ .../test_cloudformation_stack_crud_boto3.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 2b3dfee47..00cbf781d 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -150,6 +150,7 @@ class CloudFormationBackend(BaseBackend): role_arn=role_arn, ) self.stacks[stack_id] = new_stack + self._validate_export_uniqueness(new_stack) for export in new_stack.exports: self.exports[export.name] = export return new_stack @@ -217,6 +218,12 @@ class CloudFormationBackend(BaseBackend): next_token = str(token + 100) if len(all_exports) > token + 100 else None return exports, next_token + def _validate_export_uniqueness(self, stack): + new_stack_export_names = [x.name for x in stack.exports] + export_names = self.exports.keys() + if not set(export_names).isdisjoint(new_stack_export_names): + raise ValidationError(stack.stack_id, message='Export names must be unique across a given region') + cloudformation_backends = {} for region in boto.cloudformation.regions(): diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 8b4d72ad3..ba324985f 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -12,6 +12,7 @@ import sure # noqa # Ensure 'assert_raises' context manager support for Python 2.6 import tests.backport_assert_raises # noqa from nose.tools import assert_raises +import random dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -457,9 +458,11 @@ def test_list_exports(): def test_list_exports_with_token(): cf = boto3.client('cloudformation', region_name='us-east-1') for i in range(101): + # Add index to ensure name is unique + dummy_output_template['Outputs']['StackVPC']['Export']['Name'] += str(i) cf.create_stack( StackName="test_stack", - TemplateBody=dummy_output_template_json, + TemplateBody=json.dumps(dummy_output_template), ) exports = cf.list_exports() exports['Exports'].should.have.length_of(100) @@ -484,3 +487,17 @@ def test_delete_stack_with_export(): cf.delete_stack(StackName=stack_id) cf.list_exports()['Exports'].should.have.length_of(0) + + +@mock_cloudformation +def test_export_names_must_be_unique(): + cf = boto3.resource('cloudformation', region_name='us-east-1') + first_stack = cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_output_template_json, + ) + with assert_raises(ClientError): + cf.create_stack( + StackName="test_stack", + TemplateBody=dummy_output_template_json, + ) From b713eef491b84f440a85de25b10d6304874f817f Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Fri, 2 Jun 2017 13:41:33 -0700 Subject: [PATCH 193/274] cleanup after merge --- moto/dynamodb2/responses.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 39b67240c..d3fa68b7b 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -316,16 +316,18 @@ class DynamoHandler(BaseResponse): else: index = table.schema - reverse_attribute_lookup = dict((v, k) for k, v in - six.iteritems(self.body['ExpressionAttributeNames'])) + reverse_attribute_lookup = dict((v, k) for k, v in + six.iteritems(self.body['ExpressionAttributeNames'])) if " AND " in key_condition_expression: expressions = key_condition_expression.split(" AND ", 1) index_hash_key = [key for key in index if key['KeyType'] == 'HASH'][0] - hash_key_var = reverse_attribute_lookup.get(index_hash_key['AttributeName'], index_hash_key['AttributeName']) + hash_key_var = reverse_attribute_lookup.get(index_hash_key['AttributeName'], + index_hash_key['AttributeName']) hash_key_regex = r'(^|[\s(]){0}\b'.format(hash_key_var) - i, hash_key_expression = next((i, e) for i, e in enumerate(expressions) if re.search(hash_key_regex, e)) + i, hash_key_expression = next((i, e) for i, e in enumerate(expressions) + if re.search(hash_key_regex, e)) hash_key_expression = hash_key_expression.strip('()') expressions.pop(i) From 87752457a38cbfa78fd85c526dded8460f96d309 Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Fri, 2 Jun 2017 16:57:16 -0400 Subject: [PATCH 194/274] Remove useless list comprehension --- moto/cloudformation/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 00cbf781d..6557af9dc 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -208,7 +208,7 @@ class CloudFormationBackend(BaseBackend): self.delete_stack(stack.stack_id) def list_exports(self, token): - all_exports = [x for x in self.exports.values()] + all_exports = self.exports.values() if token is None: exports = all_exports[0:100] next_token = '100' if len(all_exports) > 100 else None From c0afcfade5cf3d578a598b279339f8cbc4c53c3d Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Fri, 2 Jun 2017 17:03:16 -0400 Subject: [PATCH 195/274] Use .items() not .iteritems() --- moto/cloudformation/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 248ecc57a..2a00984e4 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -489,7 +489,7 @@ class OutputMap(collections.Mapping): def exports(self): exports = [] if self.outputs: - for key, value in self._output_json_map.iteritems(): + for key, value in self._output_json_map.items(): if value.get('Export'): exports.append(Export(self._stack_id, value['Export'].get('Name'), value.get('Value'))) return exports From 9d37992c64efb56b09c413edbc990825713fa09c Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Fri, 2 Jun 2017 17:16:25 -0400 Subject: [PATCH 196/274] Make all_exports subscriptable --- moto/cloudformation/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 6557af9dc..c25103a4c 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -208,7 +208,7 @@ class CloudFormationBackend(BaseBackend): self.delete_stack(stack.stack_id) def list_exports(self, token): - all_exports = self.exports.values() + all_exports = list(self.exports.values()) if token is None: exports = all_exports[0:100] next_token = '100' if len(all_exports) > 100 else None From 49c947ece753e6e501f48d83b196f2aa9cd0db5f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 3 Jun 2017 19:06:49 -0400 Subject: [PATCH 197/274] Stop autodecoding content so we can mimic requests. Closes #963. --- moto/packages/responses/responses.py | 2 ++ tests/test_s3/test_s3.py | 29 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/moto/packages/responses/responses.py b/moto/packages/responses/responses.py index 1f5892b25..0226a7fb1 100644 --- a/moto/packages/responses/responses.py +++ b/moto/packages/responses/responses.py @@ -270,6 +270,8 @@ class RequestsMock(object): body=body, headers=headers, preload_content=False, + # Need to not decode_content to mimic requests + decode_content=False, ) response = adapter.build_response(request, response) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index a4b8719f6..5c830a905 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -6,7 +6,9 @@ import datetime from six.moves.urllib.request import urlopen from six.moves.urllib.error import HTTPError from functools import wraps +from gzip import GzipFile from io import BytesIO +import zlib import json import boto @@ -1405,6 +1407,33 @@ def test_boto3_delete_markers(): ) +@mock_s3 +def test_get_stream_gzipped(): + payload = "this is some stuff here" + + s3_client = boto3.client("s3", region_name='us-east-1') + s3_client.create_bucket(Bucket='moto-tests') + buffer_ = BytesIO() + with GzipFile(fileobj=buffer_, mode='w') as f: + f.write(payload) + payload_gz = buffer_.getvalue() + + s3_client.put_object( + Bucket='moto-tests', + Key='keyname', + Body=payload_gz, + ContentEncoding='gzip', + ) + + obj = s3_client.get_object( + Bucket='moto-tests', + Key='keyname', + ) + res = zlib.decompress(obj['Body'].read(), 16+zlib.MAX_WBITS) + assert res == payload + + + TEST_XML = """\ From 113bfcb4eacaa3346e28f1e5103928aaa4c47c83 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 3 Jun 2017 19:29:59 -0400 Subject: [PATCH 198/274] Fix duplicate bucket creation with LocationConstraint. Closes #970. --- moto/s3/responses.py | 6 ++++++ tests/test_s3/test_s3.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 115fe98ae..3b349d864 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -336,6 +336,12 @@ class ResponseObject(_TemplateEnvironmentMixin): self.backend.set_bucket_website_configuration(bucket_name, body) return "" else: + if body: + try: + region_name = xmltodict.parse(body)['CreateBucketConfiguration']['LocationConstraint'] + except KeyError: + pass + try: new_bucket = self.backend.create_bucket( bucket_name, region_name) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 5c830a905..8841f9f71 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1207,6 +1207,22 @@ def test_boto3_bucket_create(): "utf-8").should.equal("some text") +@mock_s3 +def test_bucket_create_duplicate(): + s3 = boto3.resource('s3', region_name='us-west-2') + s3.create_bucket(Bucket="blah", CreateBucketConfiguration={ + 'LocationConstraint': 'us-west-2', + }) + with assert_raises(ClientError) as exc: + s3.create_bucket( + Bucket="blah", + CreateBucketConfiguration={ + 'LocationConstraint': 'us-west-2', + } + ) + exc.exception.response['Error']['Code'].should.equal('BucketAlreadyExists') + + @mock_s3 def test_boto3_bucket_create_eu_central(): s3 = boto3.resource('s3', region_name='eu-central-1') From a956c3a85cea2c2c890ae89b95eb4e1b76b9d16a Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 3 Jun 2017 19:35:23 -0400 Subject: [PATCH 199/274] Fix tests for py3. --- tests/test_s3/test_s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 8841f9f71..1cb00d4be 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1425,7 +1425,7 @@ def test_boto3_delete_markers(): @mock_s3 def test_get_stream_gzipped(): - payload = "this is some stuff here" + payload = b"this is some stuff here" s3_client = boto3.client("s3", region_name='us-east-1') s3_client.create_bucket(Bucket='moto-tests') From 94ec799d8ac2c068af061143afa28c462fc55beb Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 3 Jun 2017 20:12:47 -0400 Subject: [PATCH 200/274] Update changelog. --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb13a0a04..e0ec033f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Moto Changelog Latest ------ +1.0.1 +----- + + * Add Cloudformation exports + * Add ECR + * IAM policy versions + 1.0.0 ----- From 856de724d0678e04861a9fae186782f5ee2f2e75 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 3 Jun 2017 20:13:03 -0400 Subject: [PATCH 201/274] 1.0.1 --- moto/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/__init__.py b/moto/__init__.py index d6f84db5e..304e25cc5 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -3,7 +3,7 @@ import logging # logging.getLogger('boto').setLevel(logging.CRITICAL) __title__ = 'moto' -__version__ = '1.0.0' +__version__ = '1.0.1' from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa from .autoscaling import mock_autoscaling, mock_autoscaling_deprecated # flake8: noqa diff --git a/setup.py b/setup.py index 9b23c602d..289c1684c 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ extras_require = { setup( name='moto', - version='1.0.0', + version='1.0.1', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', From 1abd880ab8d8db4eb04f12242e98cb4dbd863454 Mon Sep 17 00:00:00 2001 From: Giacomo Tagliabue Date: Tue, 6 Jun 2017 22:26:18 -0400 Subject: [PATCH 202/274] add pass_through option to responses --- moto/packages/responses/responses.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/moto/packages/responses/responses.py b/moto/packages/responses/responses.py index 0226a7fb1..3bc437f0b 100644 --- a/moto/packages/responses/responses.py +++ b/moto/packages/responses/responses.py @@ -10,6 +10,7 @@ import six from collections import namedtuple, Sequence, Sized from functools import update_wrapper from cookies import Cookies +from requests.adapters import HTTPAdapter from requests.utils import cookiejar_from_dict from requests.exceptions import ConnectionError from requests.sessions import REDIRECT_STATI @@ -120,10 +121,12 @@ class RequestsMock(object): POST = 'POST' PUT = 'PUT' - def __init__(self, assert_all_requests_are_fired=True): + def __init__(self, assert_all_requests_are_fired=True, pass_through=True): self._calls = CallList() self.reset() self.assert_all_requests_are_fired = assert_all_requests_are_fired + self.pass_through = pass_through + self.original_send = HTTPAdapter.send def reset(self): self._urls = [] @@ -235,6 +238,9 @@ class RequestsMock(object): match = self._find_match(request) # TODO(dcramer): find the correct class for this if match is None: + if self.pass_through: + return self.original_send(adapter, request, **kwargs) + error_msg = 'Connection refused: {0} {1}'.format(request.method, request.url) response = ConnectionError(error_msg) @@ -317,7 +323,7 @@ class RequestsMock(object): # expose default mock namespace -mock = _default_mock = RequestsMock(assert_all_requests_are_fired=False) +mock = _default_mock = RequestsMock(assert_all_requests_are_fired=False, pass_through=False) __all__ = [] for __attr in (a for a in dir(_default_mock) if not a.startswith('_')): __all__.append(__attr) From a1549b04b43be71c07b3b391ac5b391b40163d8c Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Thu, 8 Jun 2017 11:38:29 -0400 Subject: [PATCH 203/274] Add Fn::Split and Fn::Select support --- moto/cloudformation/parsing.py | 9 ++++++ .../test_cloudformation/test_stack_parsing.py | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index eee6aa8e7..09b4530af 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -162,6 +162,15 @@ def clean_json(resource_json, resources_map): if cleaned_val else '{0}'.format(val)) return resource_json['Fn::Join'][0].join(join_list) + if 'Fn::Split' in resource_json: + to_split = clean_json(resource_json['Fn::Split'][1], resources_map) + return to_split.split(resource_json['Fn::Split'][0]) + + if 'Fn::Select' in resource_json: + select_index = int(resource_json['Fn::Select'][0]) + select_list = clean_json(resource_json['Fn::Select'][1], resources_map) + return select_list[select_index] + cleaned_json = {} for key, value in resource_json.items(): cleaned_val = clean_json(value, resources_map) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 610b02325..7b582b9b5 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -72,6 +72,19 @@ get_attribute_output = { } } +split_select_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "Queue": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": {"Fn::Select": [ "1", {"Fn::Split": [ "-", "123-myqueue" ] } ] }, + "VisibilityTimeout": 60, + } + } + } +} + outputs_template = dict(list(dummy_template.items()) + list(output_dict.items())) bad_outputs_template = dict( @@ -85,6 +98,7 @@ output_type_template_json = json.dumps(outputs_template) bad_output_template_json = json.dumps(bad_outputs_template) get_attribute_outputs_template_json = json.dumps( get_attribute_outputs_template) +split_select_template_json = json.dumps(split_select_template) def test_parse_stack_resources(): @@ -266,3 +280,17 @@ def test_reference_other_conditions(): resources_map={}, condition_map={"OtherCondition": True}, ).should.equal(False) + + +def test_parse_split_and_select(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=split_select_template_json, + parameters={}, + region_name='us-west-1') + + stack.resource_map.should.have.length_of(1) + queue = stack.resource_map['Queue'] + queue.name.should.equal("myqueue") + From 711dbaf4fdc34017d5147988bece77ac8b2e68ec Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Thu, 8 Jun 2017 13:30:17 -0400 Subject: [PATCH 204/274] Simplify Fn::Join parsing --- moto/cloudformation/parsing.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 09b4530af..71a60371a 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -155,12 +155,8 @@ def clean_json(resource_json, resources_map): return clean_json(false_value, resources_map) if 'Fn::Join' in resource_json: - join_list = [] - for val in resource_json['Fn::Join'][1]: - cleaned_val = clean_json(val, resources_map) - join_list.append('{0}'.format(cleaned_val) - if cleaned_val else '{0}'.format(val)) - return resource_json['Fn::Join'][0].join(join_list) + join_list = clean_json(resource_json['Fn::Join'][1], resources_map) + return resource_json['Fn::Join'][0].join([str(x) for x in join_list]) if 'Fn::Split' in resource_json: to_split = clean_json(resource_json['Fn::Split'][1], resources_map) From d3faaad46b655ce50048d0d0819d618886804a43 Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Thu, 8 Jun 2017 15:21:32 -0400 Subject: [PATCH 205/274] Add Fn::Sub support --- moto/cloudformation/parsing.py | 20 +++++++++++ .../test_cloudformation/test_stack_parsing.py | 33 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 71a60371a..744b1d08e 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -4,6 +4,7 @@ import functools import logging import copy import warnings +import re from moto.autoscaling import models as autoscaling_models from moto.awslambda import models as lambda_models @@ -167,6 +168,25 @@ def clean_json(resource_json, resources_map): select_list = clean_json(resource_json['Fn::Select'][1], resources_map) return select_list[select_index] + if 'Fn::Sub' in resource_json: + if isinstance(resource_json['Fn::Sub'], list): + warnings.warn( + "Tried to parse Fn::Sub with variable mapping but it's not supported by moto's CloudFormation implementation") + else: + fn_sub_value = clean_json(resource_json['Fn::Sub'], resources_map) + to_sub = re.findall('(?=\${)[^!^"]*?}', fn_sub_value) + literals = re.findall('(?=\${!)[^"]*?}', fn_sub_value) + for sub in to_sub: + if '.' in sub: + cleaned_ref = clean_json({'Fn::GetAtt': re.findall('(?<=\${)[^"]*?(?=})', sub)[0].split('.')}, resources_map) + else: + cleaned_ref = clean_json({'Ref': re.findall('(?<=\${)[^"]*?(?=})', sub)[0]}, resources_map) + fn_sub_value = fn_sub_value.replace(sub, cleaned_ref) + for literal in literals: + fn_sub_value = fn_sub_value.replace(literal, literal.replace('!', '')) + return fn_sub_value + pass + cleaned_json = {} for key, value in resource_json.items(): cleaned_val = clean_json(value, resources_map) diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 7b582b9b5..594515468 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -85,6 +85,26 @@ split_select_template = { } } +sub_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "Queue1": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": {"Fn::Sub": '${AWS::StackName}-queue-${!Literal}'}, + "VisibilityTimeout": 60, + } + }, + "Queue2": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": {"Fn::Sub": '${Queue1.QueueName}'}, + "VisibilityTimeout": 60, + } + }, + } +} + outputs_template = dict(list(dummy_template.items()) + list(output_dict.items())) bad_outputs_template = dict( @@ -99,6 +119,7 @@ bad_output_template_json = json.dumps(bad_outputs_template) get_attribute_outputs_template_json = json.dumps( get_attribute_outputs_template) split_select_template_json = json.dumps(split_select_template) +sub_template_json = json.dumps(sub_template) def test_parse_stack_resources(): @@ -294,3 +315,15 @@ def test_parse_split_and_select(): queue = stack.resource_map['Queue'] queue.name.should.equal("myqueue") + +def test_sub(): + stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=sub_template_json, + parameters={}, + region_name='us-west-1') + + queue1 = stack.resource_map['Queue1'] + queue2 = stack.resource_map['Queue2'] + queue2.name.should.equal(queue1.name) From 8e4c79625c4008ee5382d506ad80248ee647f4df Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Thu, 8 Jun 2017 15:33:14 -0400 Subject: [PATCH 206/274] Clean Export name and value before appending to exports --- moto/cloudformation/parsing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 744b1d08e..8877b90c7 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -522,7 +522,9 @@ class OutputMap(collections.Mapping): if self.outputs: for key, value in self._output_json_map.items(): if value.get('Export'): - exports.append(Export(self._stack_id, value['Export'].get('Name'), value.get('Value'))) + cleaned_name = clean_json(value['Export'].get('Name'), self._resource_map) + cleaned_value = clean_json(value.get('Value'), self._resource_map) + exports.append(Export(self._stack_id, cleaned_name, cleaned_value)) return exports def create(self): From f5106f2cc811efdb4464e1de6ff4dacae7427839 Mon Sep 17 00:00:00 2001 From: Jessie Nadler Date: Thu, 8 Jun 2017 15:33:28 -0400 Subject: [PATCH 207/274] Add Fn::ImportValue support --- moto/cloudformation/models.py | 6 +- moto/cloudformation/parsing.py | 9 ++- .../test_cloudformation_stack_crud_boto3.py | 36 +++++++++++- .../test_cloudformation/test_stack_parsing.py | 55 ++++++++++++++++++- 4 files changed, 101 insertions(+), 5 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index c25103a4c..ec922d8f5 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -15,7 +15,7 @@ from .exceptions import ValidationError class FakeStack(BaseModel): - def __init__(self, stack_id, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None): + def __init__(self, stack_id, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None): self.stack_id = stack_id self.name = name self.template = template @@ -30,6 +30,7 @@ class FakeStack(BaseModel): resource_status_reason="User Initiated") self.description = self.template_dict.get('Description') + self.cross_stack_resources = cross_stack_resources or [] self.resource_map = self._create_resource_map() self.output_map = self._create_output_map() self._add_stack_event("CREATE_COMPLETE") @@ -37,7 +38,7 @@ class FakeStack(BaseModel): def _create_resource_map(self): resource_map = ResourceMap( - self.stack_id, self.name, self.parameters, self.tags, self.region_name, self.template_dict) + self.stack_id, self.name, self.parameters, self.tags, self.region_name, self.template_dict, self.cross_stack_resources) resource_map.create() return resource_map @@ -148,6 +149,7 @@ class CloudFormationBackend(BaseBackend): notification_arns=notification_arns, tags=tags, role_arn=role_arn, + cross_stack_resources=self.exports, ) self.stacks[stack_id] = new_stack self._validate_export_uniqueness(new_stack) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 8877b90c7..928cd68e0 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -187,6 +187,12 @@ def clean_json(resource_json, resources_map): return fn_sub_value pass + if 'Fn::ImportValue' in resource_json: + cleaned_val = clean_json(resource_json['Fn::ImportValue'], resources_map) + values = [x.value for x in resources_map.cross_stack_resources.values() if x.name == cleaned_val] + if any(values): + return values[0] + cleaned_json = {} for key, value in resource_json.items(): cleaned_val = clean_json(value, resources_map) @@ -326,13 +332,14 @@ class ResourceMap(collections.Mapping): each resources is passed this lazy map that it can grab dependencies from. """ - def __init__(self, stack_id, stack_name, parameters, tags, region_name, template): + def __init__(self, stack_id, stack_name, parameters, tags, region_name, template, cross_stack_resources): self._template = template self._resource_json_map = template['Resources'] self._region_name = region_name self.input_parameters = parameters self.tags = copy.deepcopy(tags) self.resolved_parameters = {} + self.cross_stack_resources = cross_stack_resources # Create the default resources self._parsed_resources = { diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index ba324985f..e428d1f63 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -5,7 +5,7 @@ import boto import boto.s3 import boto.s3.key from botocore.exceptions import ClientError -from moto import mock_cloudformation, mock_s3 +from moto import mock_cloudformation, mock_s3, mock_sqs import json import sure # noqa @@ -80,9 +80,23 @@ dummy_output_template = { } } +dummy_import_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "Queue": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": {"Fn::ImportValue": 'My VPC ID'}, + "VisibilityTimeout": 60, + } + } + } +} + dummy_template_json = json.dumps(dummy_template) dummy_update_template_json = json.dumps(dummy_template) dummy_output_template_json = json.dumps(dummy_output_template) +dummy_import_template_json = json.dumps(dummy_import_template) @mock_cloudformation @@ -501,3 +515,23 @@ def test_export_names_must_be_unique(): StackName="test_stack", TemplateBody=dummy_output_template_json, ) + +@mock_sqs +@mock_cloudformation +def test_stack_with_imports(): + cf = boto3.resource('cloudformation', region_name='us-east-1') + ec2_resource = boto3.resource('sqs', region_name='us-east-1') + + output_stack = cf.create_stack( + StackName="test_stack1", + TemplateBody=dummy_output_template_json, + ) + import_stack = cf.create_stack( + StackName="test_stack2", + TemplateBody=dummy_import_template_json + ) + + output_stack.outputs.should.have.length_of(1) + output = output_stack.outputs[0]['OutputValue'] + queue = ec2_resource.get_queue_by_name(QueueName=output) + queue.should_not.be.none diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py index 594515468..ee53e9a68 100644 --- a/tests/test_cloudformation/test_stack_parsing.py +++ b/tests/test_cloudformation/test_stack_parsing.py @@ -7,7 +7,7 @@ import sure # noqa from moto.cloudformation.exceptions import ValidationError from moto.cloudformation.models import FakeStack -from moto.cloudformation.parsing import resource_class_from_type, parse_condition +from moto.cloudformation.parsing import resource_class_from_type, parse_condition, Export from moto.sqs.models import Queue from moto.s3.models import FakeBucket from boto.cloudformation.stack import Output @@ -105,6 +105,38 @@ sub_template = { } } +export_value_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "Queue": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": {"Fn::Sub": '${AWS::StackName}-queue'}, + "VisibilityTimeout": 60, + } + } + }, + "Outputs": { + "Output1": { + "Value": "value", + "Export": {"Name": 'queue-us-west-1'} + } + } +} + +import_value_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "Queue": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": {"Fn::ImportValue": 'queue-us-west-1'}, + "VisibilityTimeout": 60, + } + } + } +} + outputs_template = dict(list(dummy_template.items()) + list(output_dict.items())) bad_outputs_template = dict( @@ -120,6 +152,8 @@ get_attribute_outputs_template_json = json.dumps( get_attribute_outputs_template) split_select_template_json = json.dumps(split_select_template) sub_template_json = json.dumps(sub_template) +export_value_template_json = json.dumps(export_value_template) +import_value_template_json = json.dumps(import_value_template) def test_parse_stack_resources(): @@ -327,3 +361,22 @@ def test_sub(): queue1 = stack.resource_map['Queue1'] queue2 = stack.resource_map['Queue2'] queue2.name.should.equal(queue1.name) + + +def test_import(): + export_stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=export_value_template_json, + parameters={}, + region_name='us-west-1') + import_stack = FakeStack( + stack_id="test_id", + name="test_stack", + template=import_value_template_json, + parameters={}, + region_name='us-west-1', + cross_stack_resources={export_stack.exports[0].value: export_stack.exports[0]}) + + queue = import_stack.resource_map['Queue'] + queue.name.should.equal("value") From d94d7f696218aca14571e289113b8bf1e7040ba1 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Fri, 9 Jun 2017 12:27:49 -0700 Subject: [PATCH 208/274] Add propagated tags and ASG name tag to asg instances --- moto/autoscaling/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index ec46d1182..18ec7e35a 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -9,6 +9,7 @@ from moto.elb.exceptions import LoadBalancerNotFoundError # http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown DEFAULT_COOLDOWN = 300 +ASG_NAME_TAG = "aws:autoscaling:groupName" class InstanceState(object): @@ -169,8 +170,8 @@ class FakeAutoScalingGroup(BaseModel): self.termination_policies = termination_policies self.instance_states = [] - self.set_desired_capacity(desired_capacity) self.tags = tags if tags else [] + self.set_desired_capacity(desired_capacity) @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): @@ -261,12 +262,17 @@ class FakeAutoScalingGroup(BaseModel): # Need more instances count_needed = int(self.desired_capacity) - \ int(curr_instance_count) + + propagated_tags = {t['key']: t['value'] for t in self.tags + if t['propagate_at_launch'] == 'true'} + propagated_tags[ASG_NAME_TAG] = self.name reservation = self.autoscaling_backend.ec2_backend.add_instances( self.launch_config.image_id, count_needed, self.launch_config.user_data, self.launch_config.security_groups, instance_type=self.launch_config.instance_type, + tags={'instance': propagated_tags} ) for instance in reservation.instances: instance.autoscaling_group = self From dc0edb9b8cd0bf7e924f560940f1ad2a2862a4d5 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Fri, 9 Jun 2017 13:10:00 -0700 Subject: [PATCH 209/274] Add test for asg tags --- tests/test_autoscaling/test_autoscaling.py | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 8487ecb49..5cc697785 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -8,7 +8,7 @@ from boto.ec2.autoscale import Tag import boto.ec2.elb import sure # noqa -from moto import mock_autoscaling, mock_ec2_deprecated, mock_elb_deprecated, mock_autoscaling_deprecated +from moto import mock_autoscaling, mock_ec2_deprecated, mock_elb_deprecated, mock_autoscaling_deprecated, mock_ec2 from tests.helpers import requires_boto_gte @@ -138,6 +138,30 @@ def test_list_many_autoscaling_groups(): groups.should.have.length_of(51) assert 'NextToken' not in response2.keys() +@mock_autoscaling +@mock_ec2 +def test_list_many_autoscaling_groups(): + conn = boto3.client('autoscaling', region_name='us-east-1') + conn.create_launch_configuration(LaunchConfigurationName='TestLC') + + conn.create_auto_scaling_group(AutoScalingGroupName='TestGroup1', + MinSize=1, + MaxSize=2, + LaunchConfigurationName='TestLC', + Tags=[{ + "ResourceId": 'TestGroup1', + "ResourceType": "auto-scaling-group", + "PropagateAtLaunch": True, + "Key": 'TestTagKey1', + "Value": 'TestTagValue1' + }]) + + ec2 = boto3.client('ec2', region_name='us-east-1') + instances = ec2.describe_instances() + + tags = instances['Reservations'][0]['Instances'][0]['Tags'] + tags.should.contain({u'Value': 'TestTagValue1', u'Key': 'TestTagKey1'}) + tags.should.contain({u'Value': 'TestGroup1', u'Key': 'aws:autoscaling:groupName'}) @mock_autoscaling_deprecated def test_autoscaling_group_describe_filter(): From 5429f3590ec6e5fcd8b25adbcfa8534f343ae338 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Fri, 9 Jun 2017 15:22:39 -0700 Subject: [PATCH 210/274] Fix linting problem --- moto/autoscaling/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 18ec7e35a..a2fcb2a63 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -11,6 +11,7 @@ DEFAULT_COOLDOWN = 300 ASG_NAME_TAG = "aws:autoscaling:groupName" + class InstanceState(object): def __init__(self, instance, lifecycle_state="InService"): From be07fbda523592c4d29b30453e1f182a9dce630a Mon Sep 17 00:00:00 2001 From: Greg Sterin Date: Fri, 9 Jun 2017 17:32:19 -0700 Subject: [PATCH 211/274] Support Expected in dynamoDB updateItem --- moto/dynamodb2/models.py | 31 ++++++++++- moto/dynamodb2/responses.py | 38 ++++++++++++- .../test_dynamodb_table_without_range_key.py | 55 +++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index d632119d9..7525a43a9 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -634,7 +634,8 @@ class DynamoDBBackend(BaseBackend): return table.scan(scan_filters, limit, exclusive_start_key) - def update_item(self, table_name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values): + def update_item(self, table_name, key, update_expression, attribute_updates, expression_attribute_names, + expression_attribute_values, expected=None): table = self.get_table(table_name) if all([table.hash_key_attr in key, table.range_key_attr in key]): @@ -652,6 +653,34 @@ class DynamoDBBackend(BaseBackend): range_value = None item = table.get_item(hash_value, range_value) + + if item is None: + item_attr = {} + elif hasattr(item, 'attrs'): + item_attr = item.attrs + else: + item_attr = item + + if not expected: + expected = {} + + for key, val in expected.items(): + if 'Exists' in val and val['Exists'] is False: + if key in item_attr: + raise ValueError("The conditional request failed") + elif key not in item_attr: + raise ValueError("The conditional request failed") + elif 'Value' in val and DynamoType(val['Value']).value != item_attr[key].value: + raise ValueError("The conditional request failed") + elif 'ComparisonOperator' in val: + comparison_func = get_comparison_func( + val['ComparisonOperator']) + dynamo_types = [DynamoType(ele) for ele in val[ + "AttributeValueList"]] + for t in dynamo_types: + if not comparison_func(item_attr[key].value, t.value): + raise ValueError('The conditional request failed') + # Update does not fail on new items, so create one if item is None: data = { diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index aa5561f58..1d9b70043 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -207,7 +207,7 @@ class DynamoHandler(BaseResponse): try: result = dynamodb_backend2.put_item( name, item, expected, overwrite) - except Exception: + except ValueError: er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' return self.error(er) @@ -474,14 +474,46 @@ class DynamoHandler(BaseResponse): 'ExpressionAttributeValues', {}) existing_item = dynamodb_backend2.get_item(name, key) + if 'Expected' in self.body: + expected = self.body['Expected'] + else: + expected = None + + # Attempt to parse simple ConditionExpressions into an Expected + # expression + if not expected: + condition_expression = self.body.get('ConditionExpression') + if condition_expression and 'OR' not in condition_expression: + cond_items = [c.strip() + for c in condition_expression.split('AND')] + + if cond_items: + expected = {} + exists_re = re.compile('^attribute_exists\((.*)\)$') + not_exists_re = re.compile( + '^attribute_not_exists\((.*)\)$') + + for cond in cond_items: + exists_m = exists_re.match(cond) + not_exists_m = not_exists_re.match(cond) + if exists_m: + expected[exists_m.group(1)] = {'Exists': True} + elif not_exists_m: + expected[not_exists_m.group(1)] = {'Exists': False} + # Support spaces between operators in an update expression # E.g. `a = b + c` -> `a=b+c` if update_expression: update_expression = re.sub( '\s*([=\+-])\s*', '\\1', update_expression) - item = dynamodb_backend2.update_item( - name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values) + try: + item = dynamodb_backend2.update_item( + name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values, + expected) + except ValueError: + er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' + return self.error(er) item_dict = item.to_json() item_dict['ConsumedCapacityUnits'] = 0.5 diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 4f08c5094..0e1099559 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -608,6 +608,61 @@ def test_boto3_put_item_conditions_fails(): } }).should.throw(botocore.client.ClientError) +@mock_dynamodb2 +def test_boto3_update_item_conditions_fails(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'baz'}) + table.update_item.when.called_with( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=bar', + Expected={ + 'foo': { + 'Value': 'bar', + } + }).should.throw(botocore.client.ClientError) + +@mock_dynamodb2 +def test_boto3_update_item_conditions_fails_because_expect_not_exists(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'baz'}) + table.update_item.when.called_with( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=bar', + Expected={ + 'foo': { + 'Exists': False + } + }).should.throw(botocore.client.ClientError) + +@mock_dynamodb2 +def test_boto3_update_item_conditions_pass(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'bar'}) + table.update_item( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=baz', + Expected={ + 'foo': { + 'Value': 'bar', + } + }) + returned_item = table.get_item(Key={'username': 'johndoe'}) + assert dict(returned_item)['Item']['foo'].should.equal("baz") + +@mock_dynamodb2 +def test_boto3_update_item_conditions_pass_because_expext_not_exists(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'bar'}) + table.update_item( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=baz', + Expected={ + 'whatever': { + 'Exists': False, + } + }) + returned_item = table.get_item(Key={'username': 'johndoe'}) + assert dict(returned_item)['Item']['foo'].should.equal("baz") @mock_dynamodb2 def test_boto3_put_item_conditions_pass(): From c8794e842d89991224d92a11de9d364948a5685f Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Mon, 12 Jun 2017 16:42:42 -0700 Subject: [PATCH 212/274] create_load_balancer requires port definitions Throw the appropriate error when defining a loadbalancer with no ports --- moto/elb/exceptions.py | 8 ++++++++ moto/elb/models.py | 7 +++++-- tests/test_elb/test_elb.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/moto/elb/exceptions.py b/moto/elb/exceptions.py index 897bd6dd1..071181a6c 100644 --- a/moto/elb/exceptions.py +++ b/moto/elb/exceptions.py @@ -47,3 +47,11 @@ class DuplicateLoadBalancerName(ELBClientError): "DuplicateLoadBalancerName", "The specified load balancer name already exists for this account: {0}" .format(name)) + + +class EmptyListenersError(ELBClientError): + + def __init__(self): + super(EmptyListenersError, self).__init__( + "ValidationError", + "Listeners cannot be empty") diff --git a/moto/elb/models.py b/moto/elb/models.py index 9ca6bdb4d..5b6a58bb9 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -16,10 +16,11 @@ from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from moto.ec2.models import ec2_backends from .exceptions import ( - LoadBalancerNotFoundError, - TooManyTagsError, BadHealthCheckDefinition, DuplicateLoadBalancerName, + EmptyListenersError, + LoadBalancerNotFoundError, + TooManyTagsError, ) @@ -239,6 +240,8 @@ class ELBBackend(BaseBackend): vpc_id = subnet.vpc_id if name in self.load_balancers: raise DuplicateLoadBalancerName(name) + if not ports: + raise EmptyListenersError() new_load_balancer = FakeLoadBalancer( name=name, zones=zones, ports=ports, scheme=scheme, subnets=subnets, vpc_id=vpc_id) self.load_balancers[name] = new_load_balancer diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index f413e4731..36f96c0e2 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -15,7 +15,9 @@ from boto.ec2.elb.policies import ( LBCookieStickinessPolicy, OtherPolicy, ) +from botocore.exceptions import ClientError from boto.exception import BotoServerError +from nose.tools import assert_raises import sure # noqa from moto import mock_elb, mock_ec2, mock_elb_deprecated, mock_ec2_deprecated @@ -109,6 +111,18 @@ def test_create_and_delete_boto3_support(): 'LoadBalancerDescriptions']).should.have.length_of(0) +@mock_elb +def test_create_load_balancer_with_no_listeners_defined(): + client = boto3.client('elb', region_name='us-east-1') + + with assert_raises(ClientError): + client.create_load_balancer( + LoadBalancerName='my-lb', + Listeners=[], + AvailabilityZones=['us-east-1a', 'us-east-1b'] + ) + + @mock_elb def test_describe_paginated_balancers(): client = boto3.client('elb', region_name='us-east-1') From 559a863d7f382d7d85e7ec213df90d60fa006ab0 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 13 Jun 2017 17:09:09 -0700 Subject: [PATCH 213/274] Include db_name when describing RDS instances --- moto/rds2/models.py | 1 + tests/test_rds2/test_rds2.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index eda181f40..4036cdcd1 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -131,6 +131,7 @@ class Database(BaseModel): template = Template(""" {{ database.backup_retention_period }} {{ database.status }} + {% if database.db_name %}{{ database.db_name }}{% endif %} {{ database.multi_az }} {{ database.db_instance_identifier }} diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 7eadf2d36..81c0deee6 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -14,12 +14,14 @@ def test_create_database(): database = conn.create_db_instance(DBInstanceIdentifier='db-master-1', AllocatedStorage=10, Engine='postgres', + DBName='staging-postgres', DBInstanceClass='db.m1.small', MasterUsername='root', MasterUserPassword='hunter2', Port=1234, DBSecurityGroups=["my_sg"]) database['DBInstance']['DBInstanceStatus'].should.equal('available') + database['DBInstance']['DBName'].should.equal('staging-postgres') database['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") database['DBInstance']['AllocatedStorage'].should.equal(10) database['DBInstance']['DBInstanceClass'].should.equal("db.m1.small") From a0471b04072d9538aa885c97964f22617e9ac879 Mon Sep 17 00:00:00 2001 From: Peter Gorniak Date: Thu, 15 Jun 2017 15:34:58 -0700 Subject: [PATCH 214/274] add comment about splitting update expression by operator keywords --- moto/dynamodb2/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 32dbfadbd..e6f050781 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -116,7 +116,10 @@ class Item(BaseModel): } def update(self, update_expression, expression_attribute_names, expression_attribute_values): + # Update subexpressions are identifiable by the operator keyword, so split on that and + # get rid of the empty leading string. parts = [p for p in re.split(r'\b(SET|REMOVE|ADD|DELETE)\b', update_expression) if p] + # make sure that we correctly found only operator/value pairs assert len(parts) % 2 == 0, "Mismatched operators and values in update expression: '{}'".format(update_expression) for action, valstr in zip(parts[:-1:2], parts[1::2]): values = valstr.split(',') From c118d12e6fa5e8791133c8135c73906e1164a8b4 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Mon, 19 Jun 2017 18:22:33 -0700 Subject: [PATCH 215/274] Add describe_parameters support --- moto/ssm/models.py | 6 ++++++ moto/ssm/responses.py | 14 ++++++++++++++ setup.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 3344623dd..cb4d5946e 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -46,6 +46,12 @@ class SimpleSystemManagerBackend(BaseBackend): except KeyError: pass + def get_all_parameters(self): + result = [] + for k, _ in self._parameters.iteritems(): + result.append(self._parameters[k]) + return result + def get_parameters(self, names, with_decryption): result = [] for name in names: diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index ee21d7380..6c53bf039 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -43,6 +43,20 @@ class SimpleSystemManagerResponse(BaseResponse): return json.dumps(response) + def describe_parameters(self): + # filters = self._get_param('Filters') + result = self.ssm_backend.get_all_parameters() + + response = { + 'Parameters': [], + } + + for parameter in result: + param_data = parameter.response_object(False) + response['Parameters'].append(param_data) + + return json.dumps(response) + def put_parameter(self): name = self._get_param('Name') description = self._get_param('Description') diff --git a/setup.py b/setup.py index 289c1684c..2da16557c 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ extras_require = { setup( name='moto', - version='1.0.1', + version='1.0.1.1', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', From db20dfcd82034ca49d98ed3d112bc454bbacf1d4 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Tue, 20 Jun 2017 11:47:53 -0700 Subject: [PATCH 216/274] Added filtering --- moto/ssm/models.py | 5 +- moto/ssm/responses.py | 51 +++++++++++- setup.py | 2 +- tests/test_ssm/test_ssm_boto3.py | 137 +++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 5 deletions(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index cb4d5946e..4efa22817 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -28,11 +28,14 @@ class Parameter(BaseModel): return value[len(prefix):] def response_object(self, decrypt=False): - return { + r = { 'Name': self.name, 'Type': self.type, 'Value': self.decrypt(self.value) if decrypt else self.value } + if self.keyid: + r['KeyId'] = self.keyid + return r class SimpleSystemManagerBackend(BaseBackend): diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index 6c53bf039..f4ed9561d 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -44,16 +44,61 @@ class SimpleSystemManagerResponse(BaseResponse): return json.dumps(response) def describe_parameters(self): - # filters = self._get_param('Filters') + page_size = 10 + filters = self._get_param('Filters') + token = self._get_param('NextToken') + if hasattr(token, 'strip'): + token = token.strip() + if not token: + token = '0' + + token = int(token) + + result = self.ssm_backend.get_all_parameters() response = { 'Parameters': [], } - for parameter in result: + end = token + page_size + for parameter in result[token:]: param_data = parameter.response_object(False) - response['Parameters'].append(param_data) + add = False + + if filters: + for filter in filters: + if filter['Key'] == 'Name': + k = param_data['Name'] + for v in filter['Values']: + if k.startswith(v): + add = True + break + elif filter['Key'] == 'Type': + k = param_data['Type'] + for v in filter['Values']: + if k == v: + add = True + break + elif filter['Key'] == 'KeyId': + k = param_data.get('KeyId') + if k: + for v in filter['Values']: + if k == v: + add = True + break + else: + add = True + + if add: + response['Parameters'].append(param_data) + + token = token + 1 + if len(response['Parameters']) == page_size: + response['NextToken'] = str(end) + break + + return json.dumps(response) diff --git a/setup.py b/setup.py index 2da16557c..b00567895 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ extras_require = { setup( name='moto', - version='1.0.1.1', + version='1.0.1.2', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 6b8a1a369..8b5d1f200 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -47,6 +47,143 @@ def test_put_parameter(): response['Parameters'][0]['Type'].should.equal('String') +@mock_ssm +def test_describe_parameters(): + client = boto3.client('ssm', region_name='us-east-1') + + client.put_parameter( + Name='test', + Description='A test parameter', + Value='value', + Type='String') + + response = client.describe_parameters() + + len(response['Parameters']).should.equal(1) + response['Parameters'][0]['Name'].should.equal('test') + response['Parameters'][0]['Type'].should.equal('String') + + +@mock_ssm +def test_describe_parameters_paging(): + client = boto3.client('ssm', region_name='us-east-1') + + for i in range(50): + client.put_parameter( + Name="param-%d" % i, + Value="value-%d" % i, + Type="String" + ) + + response = client.describe_parameters() + len(response['Parameters']).should.equal(10) + response['NextToken'].should.equal('10') + + response = client.describe_parameters(NextToken=response['NextToken']) + len(response['Parameters']).should.equal(10) + response['NextToken'].should.equal('20') + + response = client.describe_parameters(NextToken=response['NextToken']) + len(response['Parameters']).should.equal(10) + response['NextToken'].should.equal('30') + + response = client.describe_parameters(NextToken=response['NextToken']) + len(response['Parameters']).should.equal(10) + response['NextToken'].should.equal('40') + + response = client.describe_parameters(NextToken=response['NextToken']) + len(response['Parameters']).should.equal(10) + response['NextToken'].should.equal('50') + + response = client.describe_parameters(NextToken=response['NextToken']) + len(response['Parameters']).should.equal(0) + ''.should.equal(response.get('NextToken', '')) + +@mock_ssm +def test_describe_parameters_filter_names(): + client = boto3.client('ssm', region_name='us-east-1') + + for i in range(50): + p = { + 'Name': "param-%d" % i, + 'Value': "value-%d" % i, + 'Type': "String" + } + if i % 5 == 0: + p['Type'] = 'SecureString' + p['KeyId'] = 'a key' + client.put_parameter(**p) + + + response = client.describe_parameters(Filters=[ + { + 'Key': 'Name', + 'Values': ['param-45', 'param-22'] + }, + ]) + len(response['Parameters']).should.equal(2) + response['Parameters'][0]['Name'].should.equal('param-22') + response['Parameters'][0]['Type'].should.equal('String') + response['Parameters'][1]['Name'].should.equal('param-45') + response['Parameters'][1]['Type'].should.equal('SecureString') + ''.should.equal(response.get('NextToken', '')) + +@mock_ssm +def test_describe_parameters_filter_type(): + client = boto3.client('ssm', region_name='us-east-1') + + for i in range(50): + p = { + 'Name': "param-%d" % i, + 'Value': "value-%d" % i, + 'Type': "String" + } + if i % 5 == 0: + p['Type'] = 'SecureString' + p['KeyId'] = 'a key' + client.put_parameter(**p) + + + response = client.describe_parameters(Filters=[ + { + 'Key': 'Type', + 'Values': ['SecureString'] + }, + ]) + len(response['Parameters']).should.equal(10) + response['Parameters'][0]['Name'].should.equal('param-35') + response['Parameters'][0]['Type'].should.equal('SecureString') + '10'.should.equal(response.get('NextToken', '')) + +@mock_ssm +def test_describe_parameters_filter_keyid(): + client = boto3.client('ssm', region_name='us-east-1') + + for i in range(50): + p = { + 'Name': "param-%d" % i, + 'Value': "value-%d" % i, + 'Type': "String" + } + if i % 5 == 0: + p['Type'] = 'SecureString' + p['KeyId'] = "key:%d" % i + client.put_parameter(**p) + + + response = client.describe_parameters(Filters=[ + { + 'Key': 'KeyId', + 'Values': ['key:5','key:10'] + }, + ]) + len(response['Parameters']).should.equal(2) + response['Parameters'][0]['Name'].should.equal('param-10') + response['Parameters'][0]['Type'].should.equal('SecureString') + response['Parameters'][1]['Name'].should.equal('param-5') + response['Parameters'][1]['Type'].should.equal('SecureString') + ''.should.equal(response.get('NextToken', '')) + @mock_ssm def test_put_parameter_secure_default_kms(): client = boto3.client('ssm', region_name='us-east-1') From 05ddcef2a023a88018e42249a7364a7410479dc4 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 20 Jun 2017 11:58:18 -0700 Subject: [PATCH 217/274] Re-enabling tests on Python3 --- tests/test_rds/test_rds.py | 10 ------ tests/test_rds2/test_rds2.py | 63 +----------------------------------- 2 files changed, 1 insertion(+), 72 deletions(-) diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py index 0a474ee26..5bf733dc6 100644 --- a/tests/test_rds/test_rds.py +++ b/tests/test_rds/test_rds.py @@ -10,7 +10,6 @@ from moto import mock_ec2_deprecated, mock_rds_deprecated, mock_rds from tests.helpers import disable_on_py3 -@disable_on_py3() @mock_rds_deprecated def test_create_database(): conn = boto.rds.connect_to_region("us-west-2") @@ -28,7 +27,6 @@ def test_create_database(): database.security_groups[0].name.should.equal('my_sg') -@disable_on_py3() @mock_rds_deprecated def test_get_databases(): conn = boto.rds.connect_to_region("us-west-2") @@ -46,7 +44,6 @@ def test_get_databases(): databases[0].id.should.equal("db-master-1") -@disable_on_py3() @mock_rds def test_get_databases_paginated(): conn = boto3.client('rds', region_name="us-west-2") @@ -73,7 +70,6 @@ def test_describe_non_existant_database(): "not-a-db").should.throw(BotoServerError) -@disable_on_py3() @mock_rds_deprecated def test_delete_database(): conn = boto.rds.connect_to_region("us-west-2") @@ -158,7 +154,6 @@ def test_security_group_authorize(): security_group.ip_ranges[0].cidr_ip.should.equal('10.3.2.45/32') -@disable_on_py3() @mock_rds_deprecated def test_add_security_group_to_database(): conn = boto.rds.connect_to_region("us-west-2") @@ -227,7 +222,6 @@ def test_delete_database_subnet_group(): "db_subnet1").should.throw(BotoServerError) -@disable_on_py3() @mock_ec2_deprecated @mock_rds_deprecated def test_create_database_in_subnet_group(): @@ -245,7 +239,6 @@ def test_create_database_in_subnet_group(): database.subnet_group.name.should.equal("db_subnet1") -@disable_on_py3() @mock_rds_deprecated def test_create_database_replica(): conn = boto.rds.connect_to_region("us-west-2") @@ -271,7 +264,6 @@ def test_create_database_replica(): list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) -@disable_on_py3() @mock_rds_deprecated def test_create_cross_region_database_replica(): west_1_conn = boto.rds.connect_to_region("us-west-1") @@ -299,7 +291,6 @@ def test_create_cross_region_database_replica(): list(primary.read_replica_dbinstance_identifiers).should.have.length_of(0) -@disable_on_py3() @mock_rds_deprecated def test_connecting_to_us_east_1(): # boto does not use us-east-1 in the URL for RDS, @@ -320,7 +311,6 @@ def test_connecting_to_us_east_1(): database.security_groups[0].name.should.equal('my_sg') -@disable_on_py3() @mock_rds_deprecated def test_create_database_with_iops(): conn = boto.rds.connect_to_region("us-west-2") diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 81c0deee6..148b00aa1 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -4,10 +4,8 @@ from botocore.exceptions import ClientError, ParamValidationError import boto3 import sure # noqa from moto import mock_ec2, mock_kms, mock_rds2 -from tests.helpers import disable_on_py3 -@disable_on_py3() @mock_rds2 def test_create_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -32,7 +30,6 @@ def test_create_database(): 'arn:aws:rds:us-west-2:1234567890:db:db-master-1') -@disable_on_py3() @mock_rds2 def test_get_databases(): conn = boto3.client('rds', region_name='us-west-2') @@ -67,7 +64,6 @@ def test_get_databases(): 'arn:aws:rds:us-west-2:1234567890:db:db-master-1') -@disable_on_py3() @mock_rds2 def test_get_databases_paginated(): conn = boto3.client('rds', region_name="us-west-2") @@ -86,7 +82,7 @@ def test_get_databases_paginated(): resp2 = conn.describe_db_instances(Marker=resp["Marker"]) resp2["DBInstances"].should.have.length_of(1) -@disable_on_py3() + @mock_rds2 def test_describe_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -94,7 +90,6 @@ def test_describe_non_existant_database(): DBInstanceIdentifier="not-a-db").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_modify_db_instance(): conn = boto3.client('rds', region_name='us-west-2') @@ -115,7 +110,6 @@ def test_modify_db_instance(): instances['DBInstances'][0]['AllocatedStorage'].should.equal(20) -@disable_on_py3() @mock_rds2 def test_modify_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -124,7 +118,6 @@ def test_modify_non_existant_database(): ApplyImmediately=True).should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_reboot_db_instance(): conn = boto3.client('rds', region_name='us-west-2') @@ -140,7 +133,6 @@ def test_reboot_db_instance(): database['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") -@disable_on_py3() @mock_rds2 def test_reboot_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -148,7 +140,6 @@ def test_reboot_non_existant_database(): DBInstanceIdentifier="not-a-db").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_delete_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -170,7 +161,6 @@ def test_delete_database(): list(instances['DBInstances']).should.have.length_of(0) -@disable_on_py3() @mock_rds2 def test_delete_non_existant_database(): conn = boto3.client('rds2', region_name="us-west-2") @@ -178,7 +168,6 @@ def test_delete_non_existant_database(): DBInstanceIdentifier="not-a-db").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_create_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -193,7 +182,6 @@ def test_create_option_group(): option_group['OptionGroup']['MajorEngineVersion'].should.equal('5.6') -@disable_on_py3() @mock_rds2 def test_create_option_group_bad_engine_name(): conn = boto3.client('rds', region_name='us-west-2') @@ -203,7 +191,6 @@ def test_create_option_group_bad_engine_name(): OptionGroupDescription='test invalid engine').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_create_option_group_bad_engine_major_version(): conn = boto3.client('rds', region_name='us-west-2') @@ -213,7 +200,6 @@ def test_create_option_group_bad_engine_major_version(): OptionGroupDescription='test invalid engine version').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_create_option_group_empty_description(): conn = boto3.client('rds', region_name='us-west-2') @@ -223,7 +209,6 @@ def test_create_option_group_empty_description(): OptionGroupDescription='').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_create_option_group_duplicate(): conn = boto3.client('rds', region_name='us-west-2') @@ -237,7 +222,6 @@ def test_create_option_group_duplicate(): OptionGroupDescription='test option group').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_describe_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -250,7 +234,6 @@ def test_describe_option_group(): 'OptionGroupName'].should.equal('test') -@disable_on_py3() @mock_rds2 def test_describe_non_existant_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -258,7 +241,6 @@ def test_describe_non_existant_option_group(): OptionGroupName="not-a-option-group").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_delete_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -274,7 +256,6 @@ def test_delete_option_group(): OptionGroupName='test').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_delete_non_existant_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -282,7 +263,6 @@ def test_delete_non_existant_option_group(): OptionGroupName='non-existant').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_describe_option_group_options(): conn = boto3.client('rds', region_name='us-west-2') @@ -301,7 +281,6 @@ def test_describe_option_group_options(): EngineName='mysql', MajorEngineVersion='non-existent').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_modify_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -317,7 +296,6 @@ def test_modify_option_group(): result['OptionGroup']['OptionGroupName'].should.equal('test') -@disable_on_py3() @mock_rds2 def test_modify_option_group_no_options(): conn = boto3.client('rds', region_name='us-west-2') @@ -327,7 +305,6 @@ def test_modify_option_group_no_options(): OptionGroupName='test').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_modify_non_existant_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -335,7 +312,6 @@ def test_modify_non_existant_option_group(): 'OptionName', 'Port', 'DBSecurityGroupMemberships', 'VpcSecurityGroupMemberships', 'OptionSettings')]).should.throw(ParamValidationError) -@disable_on_py3() @mock_rds2 def test_delete_non_existant_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -343,7 +319,6 @@ def test_delete_non_existant_database(): DBInstanceIdentifier="not-a-db").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_list_tags_invalid_arn(): conn = boto3.client('rds', region_name='us-west-2') @@ -351,7 +326,6 @@ def test_list_tags_invalid_arn(): ResourceName='arn:aws:rds:bad-arn').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_list_tags_db(): conn = boto3.client('rds', region_name='us-west-2') @@ -385,7 +359,6 @@ def test_list_tags_db(): 'Key': 'foo1'}]) -@disable_on_py3() @mock_rds2 def test_add_tags_db(): conn = boto3.client('rds', region_name='us-west-2') @@ -426,7 +399,6 @@ def test_add_tags_db(): list(result['TagList']).should.have.length_of(3) -@disable_on_py3() @mock_rds2 def test_remove_tags_db(): conn = boto3.client('rds', region_name='us-west-2') @@ -458,7 +430,6 @@ def test_remove_tags_db(): len(result['TagList']).should.equal(1) -@disable_on_py3() @mock_rds2 def test_add_tags_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -484,7 +455,6 @@ def test_add_tags_option_group(): list(result['TagList']).should.have.length_of(2) -@disable_on_py3() @mock_rds2 def test_remove_tags_option_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -514,7 +484,6 @@ def test_remove_tags_option_group(): list(result['TagList']).should.have.length_of(1) -@disable_on_py3() @mock_rds2 def test_create_database_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -527,7 +496,6 @@ def test_create_database_security_group(): result['DBSecurityGroup']['IPRanges'].should.equal([]) -@disable_on_py3() @mock_rds2 def test_get_security_groups(): conn = boto3.client('rds', region_name='us-west-2') @@ -548,7 +516,6 @@ def test_get_security_groups(): result['DBSecurityGroups'][0]['DBSecurityGroupName'].should.equal("db_sg1") -@disable_on_py3() @mock_rds2 def test_get_non_existant_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -556,7 +523,6 @@ def test_get_non_existant_security_group(): DBSecurityGroupName="not-a-sg").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_delete_database_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -571,7 +537,6 @@ def test_delete_database_security_group(): result['DBSecurityGroups'].should.have.length_of(0) -@disable_on_py3() @mock_rds2 def test_delete_non_existant_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -579,7 +544,6 @@ def test_delete_non_existant_security_group(): DBSecurityGroupName="not-a-db").should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_security_group_authorize(): conn = boto3.client('rds', region_name='us-west-2') @@ -605,7 +569,6 @@ def test_security_group_authorize(): ]) -@disable_on_py3() @mock_rds2 def test_add_security_group_to_database(): conn = boto3.client('rds', region_name='us-west-2') @@ -629,7 +592,6 @@ def test_add_security_group_to_database(): 'DBSecurityGroupName'].should.equal('db_sg') -@disable_on_py3() @mock_rds2 def test_list_tags_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -651,7 +613,6 @@ def test_list_tags_security_group(): 'Key': 'foo1'}]) -@disable_on_py3() @mock_rds2 def test_add_tags_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -676,7 +637,6 @@ def test_add_tags_security_group(): 'Key': 'foo1'}]) -@disable_on_py3() @mock_rds2 def test_remove_tags_security_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -698,7 +658,6 @@ def test_remove_tags_security_group(): result['TagList'].should.equal([{'Value': 'bar1', 'Key': 'foo1'}]) -@disable_on_py3() @mock_ec2 @mock_rds2 def test_create_database_subnet_group(): @@ -723,7 +682,6 @@ def test_create_database_subnet_group(): list(subnet_group_ids).should.equal(subnet_ids) -@disable_on_py3() @mock_ec2 @mock_rds2 def test_create_database_in_subnet_group(): @@ -749,7 +707,6 @@ def test_create_database_in_subnet_group(): 'DBSubnetGroupName'].should.equal('db_subnet1') -@disable_on_py3() @mock_ec2 @mock_rds2 def test_describe_database_subnet_group(): @@ -779,7 +736,6 @@ def test_describe_database_subnet_group(): DBSubnetGroupName="not-a-subnet").should.throw(ClientError) -@disable_on_py3() @mock_ec2 @mock_rds2 def test_delete_database_subnet_group(): @@ -806,7 +762,6 @@ def test_delete_database_subnet_group(): DBSubnetGroupName="db_subnet1").should.throw(ClientError) -@disable_on_py3() @mock_ec2 @mock_rds2 def test_list_tags_database_subnet_group(): @@ -834,7 +789,6 @@ def test_list_tags_database_subnet_group(): 'Key': 'foo1'}]) -@disable_on_py3() @mock_ec2 @mock_rds2 def test_add_tags_database_subnet_group(): @@ -866,7 +820,6 @@ def test_add_tags_database_subnet_group(): 'Key': 'foo1'}]) -@disable_on_py3() @mock_ec2 @mock_rds2 def test_remove_tags_database_subnet_group(): @@ -894,7 +847,6 @@ def test_remove_tags_database_subnet_group(): result['TagList'].should.equal([{'Value': 'bar1', 'Key': 'foo1'}]) -@disable_on_py3() @mock_rds2 def test_create_database_replica(): conn = boto3.client('rds', region_name='us-west-2') @@ -928,7 +880,6 @@ def test_create_database_replica(): 'ReadReplicaDBInstanceIdentifiers'].should.equal([]) -@disable_on_py3() @mock_rds2 @mock_kms def test_create_database_with_encrypted_storage(): @@ -954,7 +905,6 @@ def test_create_database_with_encrypted_storage(): key['KeyMetadata']['KeyId']) -@disable_on_py3() @mock_rds2 def test_create_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -970,7 +920,6 @@ def test_create_db_parameter_group(): 'Description'].should.equal('test parameter group') -@disable_on_py3() @mock_rds2 def test_create_db_instance_with_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -994,7 +943,6 @@ def test_create_db_instance_with_parameter_group(): 'ParameterApplyStatus'].should.equal('in-sync') -@disable_on_py3() @mock_rds2 def test_create_database_with_default_port(): conn = boto3.client('rds', region_name='us-west-2') @@ -1008,7 +956,6 @@ def test_create_database_with_default_port(): database['DBInstance']['Endpoint']['Port'].should.equal(5432) -@disable_on_py3() @mock_rds2 def test_modify_db_instance_with_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -1042,7 +989,6 @@ def test_modify_db_instance_with_parameter_group(): 'ParameterApplyStatus'].should.equal('in-sync') -@disable_on_py3() @mock_rds2 def test_create_db_parameter_group_empty_description(): conn = boto3.client('rds', region_name='us-west-2') @@ -1051,7 +997,6 @@ def test_create_db_parameter_group_empty_description(): Description='').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_create_db_parameter_group_duplicate(): conn = boto3.client('rds', region_name='us-west-2') @@ -1063,7 +1008,6 @@ def test_create_db_parameter_group_duplicate(): Description='test parameter group').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_describe_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -1076,7 +1020,6 @@ def test_describe_db_parameter_group(): 'DBParameterGroupName'].should.equal('test') -@disable_on_py3() @mock_rds2 def test_describe_non_existant_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -1085,7 +1028,6 @@ def test_describe_non_existant_db_parameter_group(): len(db_parameter_groups['DBParameterGroups']).should.equal(0) -@disable_on_py3() @mock_rds2 def test_delete_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -1102,7 +1044,6 @@ def test_delete_db_parameter_group(): len(db_parameter_groups['DBParameterGroups']).should.equal(0) -@disable_on_py3() @mock_rds2 def test_modify_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -1128,7 +1069,6 @@ def test_modify_db_parameter_group(): db_parameters['Parameters'][0]['ApplyMethod'].should.equal('immediate') -@disable_on_py3() @mock_rds2 def test_delete_non_existant_db_parameter_group(): conn = boto3.client('rds', region_name='us-west-2') @@ -1136,7 +1076,6 @@ def test_delete_non_existant_db_parameter_group(): DBParameterGroupName='non-existant').should.throw(ClientError) -@disable_on_py3() @mock_rds2 def test_create_parameter_group_with_tags(): conn = boto3.client('rds', region_name='us-west-2') From f2d64e86395864a355d660ba023a1a7f27916d00 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Tue, 20 Jun 2017 12:03:50 -0700 Subject: [PATCH 218/274] Revert version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b00567895..289c1684c 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ extras_require = { setup( name='moto', - version='1.0.1.2', + version='1.0.1', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', From 5a4b2139501b1ec695afac6970615efd61686010 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Tue, 20 Jun 2017 12:38:52 -0700 Subject: [PATCH 219/274] Remove blank lines --- moto/ssm/responses.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index f4ed9561d..09fe6d0c2 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -51,12 +51,9 @@ class SimpleSystemManagerResponse(BaseResponse): token = token.strip() if not token: token = '0' - token = int(token) - result = self.ssm_backend.get_all_parameters() - response = { 'Parameters': [], } @@ -98,8 +95,6 @@ class SimpleSystemManagerResponse(BaseResponse): response['NextToken'] = str(end) break - - return json.dumps(response) def put_parameter(self): From b67e10d5c9e89f6e9531057ffdd847ba56dec4ba Mon Sep 17 00:00:00 2001 From: William Richard Date: Tue, 20 Jun 2017 15:32:32 -0400 Subject: [PATCH 220/274] Make sure the repository response_object is json serializable with images If images had been pushed to a repository, they would be included in the response object, and the json encoder could not serialize the Image class. Since they are not included in the boto response, I just deleted the images field from the response object for Repositories. I also found a duplicate test in the ecr class, so I removed one of them. --- moto/ecr/models.py | 2 +- tests/test_ecr/test_ecr_boto3.py | 41 ++++++++++++++++---------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/moto/ecr/models.py b/moto/ecr/models.py index 82ce2ebd6..cbe8b2565 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -58,7 +58,7 @@ class Repository(BaseObject): response_object['repositoryName'] = self.name response_object['repositoryUri'] = self.uri # response_object['createdAt'] = self.created - del response_object['arn'], response_object['name'] + del response_object['arn'], response_object['name'], response_object['images'] return response_object @classmethod diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 1191c42d2..3a32c1515 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -152,6 +152,23 @@ def test_describe_repositories_4(): len(response['repositories']).should.equal(0) +@mock_ecr +def test_describe_repositories_with_image(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + _ = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag='latest' + ) + + response = client.describe_repositories(repositoryNames=['test_repository']) + len(response['repositories']).should.equal(1) + + @mock_ecr def test_delete_repository(): client = boto3.client('ecr', region_name='us-east-1') @@ -177,14 +194,17 @@ def test_put_image(): _ = client.create_repository( repositoryName='test_repository' ) + response = client.put_image( repositoryName='test_repository', imageManifest=json.dumps(_create_image_manifest()), imageTag='latest' ) - response['image']['repositoryName'].should.equal('test_repository') response['image']['imageId']['imageTag'].should.equal('latest') + response['image']['imageId']['imageDigest'].should.contain("sha") + response['image']['repositoryName'].should.equal('test_repository') + response['image']['registryId'].should.equal('012345678910') @mock_ecr @@ -294,22 +314,3 @@ def test_describe_images(): response['imageDetails'][0]['imageSizeInBytes'].should.equal(52428800) response['imageDetails'][1]['imageSizeInBytes'].should.equal(52428800) response['imageDetails'][2]['imageSizeInBytes'].should.equal(52428800) - - -@mock_ecr -def test_put_image(): - client = boto3.client('ecr', region_name='us-east-1') - _ = client.create_repository( - repositoryName='test_repository' - ) - - response = client.put_image( - repositoryName='test_repository', - imageManifest=json.dumps(_create_image_manifest()), - imageTag='latest' - ) - - response['image']['imageId']['imageTag'].should.equal('latest') - response['image']['imageId']['imageDigest'].should.contain("sha") - response['image']['repositoryName'].should.equal('test_repository') - response['image']['registryId'].should.equal('012345678910') \ No newline at end of file From f0fae81af1f522e7344708e5117b70d2ae7957c1 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Tue, 20 Jun 2017 12:55:01 -0700 Subject: [PATCH 221/274] Fix iteritems --- moto/ssm/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 4efa22817..f1aac336b 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -51,7 +51,7 @@ class SimpleSystemManagerBackend(BaseBackend): def get_all_parameters(self): result = [] - for k, _ in self._parameters.iteritems(): + for k, _ in self._parameters.items(): result.append(self._parameters[k]) return result From 3f20ad2c13acbf1dce8f7decacb3a7a6d30e9db6 Mon Sep 17 00:00:00 2001 From: William Richard Date: Tue, 20 Jun 2017 16:22:34 -0400 Subject: [PATCH 222/274] Support filtering by image id or image tag when describing ecr images --- moto/ecr/models.py | 19 ++++++++--- moto/ecr/responses.py | 3 +- tests/test_ecr/test_ecr_boto3.py | 54 ++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/moto/ecr/models.py b/moto/ecr/models.py index cbe8b2565..b90700ff4 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -193,16 +193,27 @@ class ECRBackend(BaseBackend): images.append(image) return images - def describe_images(self, repository_name, registry_id=None, image_id=None): + def describe_images(self, repository_name, registry_id=None, image_ids=None): if repository_name in self.repositories: repository = self.repositories[repository_name] else: raise Exception("{0} is not a repository".format(repository_name)) - response = [] - for image in repository.images: - response.append(image) + if image_ids: + response = set() + for image_id in image_ids: + if 'imageDigest' in image_id: + desired_digest = image_id['imageDigest'] + response.update([i for i in repository.images if i.get_image_digest() == desired_digest]) + if 'imageTag' in image_id: + desired_tag = image_id['imageTag'] + response.update([i for i in repository.images if i.image_tag == desired_tag]) + else: + response = [] + for image in repository.images: + response.append(image) + return response def put_image(self, repository_name, image_manifest, image_tag): diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index f8b1606cc..40d8cfb66 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -69,7 +69,8 @@ class ECRResponse(BaseResponse): def describe_images(self): repository_str = self._get_param('repositoryName') registry_id = self._get_param('registryId') - images = self.ecr_backend.describe_images(repository_str, registry_id) + image_ids = self._get_param('imageIds') + images = self.ecr_backend.describe_images(repository_str, registry_id, image_ids) return json.dumps({ 'imageDetails': [image.response_describe_object for image in images], }) diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 3a32c1515..647015446 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -314,3 +314,57 @@ def test_describe_images(): response['imageDetails'][0]['imageSizeInBytes'].should.equal(52428800) response['imageDetails'][1]['imageSizeInBytes'].should.equal(52428800) response['imageDetails'][2]['imageSizeInBytes'].should.equal(52428800) + + +@mock_ecr +def test_describe_images_by_tag(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + tag_map = {} + for tag in ['latest', 'v1', 'v2']: + put_response = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag=tag + ) + tag_map[tag] = put_response['image'] + + for tag, put_response in tag_map.items(): + response = client.describe_images(repositoryName='test_repository', imageIds=[{'imageTag': tag}]) + len(response['imageDetails']).should.be(1) + image_detail = response['imageDetails'][0] + image_detail['registryId'].should.equal("012345678910") + image_detail['repositoryName'].should.equal("test_repository") + image_detail['imageTags'].should.equal([put_response['imageId']['imageTag']]) + image_detail['imageDigest'].should.equal(put_response['imageId']['imageDigest']) + + +@mock_ecr +def test_describe_images_by_digest(): + client = boto3.client('ecr', region_name='us-east-1') + _ = client.create_repository( + repositoryName='test_repository' + ) + + tags = ['latest', 'v1', 'v2'] + digest_map = {} + for tag in tags: + put_response = client.put_image( + repositoryName='test_repository', + imageManifest=json.dumps(_create_image_manifest()), + imageTag=tag + ) + digest_map[put_response['image']['imageId']['imageDigest']] = put_response['image'] + + for digest, put_response in digest_map.items(): + response = client.describe_images(repositoryName='test_repository', + imageIds=[{'imageDigest': digest}]) + len(response['imageDetails']).should.be(1) + image_detail = response['imageDetails'][0] + image_detail['registryId'].should.equal("012345678910") + image_detail['repositoryName'].should.equal("test_repository") + image_detail['imageTags'].should.equal([put_response['imageId']['imageTag']]) + image_detail['imageDigest'].should.equal(digest) From 63f01039c3f321c6f726c620eba6ec66e98f55ec Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 20 Jun 2017 13:51:25 -0700 Subject: [PATCH 223/274] Implementing RDS Snapshots --- moto/rds2/exceptions.py | 8 ++++ moto/rds2/models.py | 81 +++++++++++++++++++++++++++++++++++- moto/rds2/responses.py | 59 +++++++++++++++++++++++++- tests/test_rds2/test_rds2.py | 75 +++++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+), 2 deletions(-) diff --git a/moto/rds2/exceptions.py b/moto/rds2/exceptions.py index 29e92941d..057a13ba2 100644 --- a/moto/rds2/exceptions.py +++ b/moto/rds2/exceptions.py @@ -28,6 +28,14 @@ class DBInstanceNotFoundError(RDSClientError): "Database {0} not found.".format(database_identifier)) +class DBSnapshotNotFoundError(RDSClientError): + + def __init__(self): + super(DBSnapshotNotFoundError, self).__init__( + 'DBSnapshotNotFound', + "DBSnapshotIdentifier does not refer to an existing DB snapshot.") + + class DBSecurityGroupNotFoundError(RDSClientError): def __init__(self, security_group_name): diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 4036cdcd1..ae97ba1f2 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import copy +import datetime from collections import defaultdict import boto.rds2 @@ -10,9 +11,11 @@ from moto.cloudformation.exceptions import UnformattedGetAttTemplateException from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel from moto.core.utils import get_random_hex +from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.ec2.models import ec2_backends from .exceptions import (RDSClientError, DBInstanceNotFoundError, + DBSnapshotNotFoundError, DBSecurityGroupNotFoundError, DBSubnetGroupNotFoundError, DBParameterGroupNotFoundError) @@ -205,7 +208,7 @@ class Database(BaseModel): {% endif %} {% if database.iops %} {{ database.iops }} - io1 + standard {% else %} {{ database.storage_type }} {% endif %} @@ -399,6 +402,53 @@ class Database(BaseModel): backend.delete_database(self.db_instance_identifier) +class Snapshot(BaseModel): + def __init__(self, database, snapshot_id, tags): + self.database = database + self.snapshot_id = snapshot_id + self.tags = tags + self.created_at = iso_8601_datetime_with_milliseconds(datetime.datetime.now()) + + @property + def snapshot_arn(self): + return "arn:aws:rds:{0}:1234567890:snapshot:{1}".format(self.database.region, self.snapshot_id) + + def to_xml(self): + template = Template(""" + {{ snapshot.snapshot_id }} + {{ database.db_instance_identifier }} + {{ snapshot.created_at }} + {{ database.engine }} + {{ database.allocated_storage }} + available + {{ database.port }} + {{ database.availability_zone }} + {{ database.db_subnet_group.vpc_id }} + {{ snapshot.created_at }} + {{ database.master_username }} + {{ database.engine_version }} + general-public-license + manual + {% if database.iops %} + {{ database.iops }} + io1 + {% else %} + {{ database.storage_type }} + {% endif %} + {{ database.option_group_name }} + {{ 100 }} + {{ database.region }} + + + {{ database.storage_encrypted }} + {{ database.kms_key_id }} + {{ snapshot.snapshot_arn }} + + false + """) + return template.render(snapshot=self, database=self.database) + + class SecurityGroup(BaseModel): def __init__(self, group_name, description, tags): @@ -607,6 +657,7 @@ class RDS2Backend(BaseBackend): self.arn_regex = re_compile( r'^arn:aws:rds:.*:[0-9]*:(db|es|og|pg|ri|secgrp|snapshot|subgrp):.*$') self.databases = OrderedDict() + self.snapshots = OrderedDict() self.db_parameter_groups = {} self.option_groups = {} self.security_groups = {} @@ -624,6 +675,20 @@ class RDS2Backend(BaseBackend): self.databases[database_id] = database return database + def create_snapshot(self, db_instance_identifier, db_snapshot_identifier, tags): + database = self.databases.get(db_instance_identifier) + if not database: + raise DBInstanceNotFoundError(db_instance_identifier) + snapshot = Snapshot(database, db_snapshot_identifier, tags) + self.snapshots[db_snapshot_identifier] = snapshot + return snapshot + + def delete_snapshot(self, db_snapshot_identifier): + if db_snapshot_identifier not in self.snapshots: + raise DBSnapshotNotFoundError() + + return self.snapshots.pop(db_snapshot_identifier) + def create_database_replica(self, db_kwargs): database_id = db_kwargs['db_instance_identifier'] source_database_id = db_kwargs['source_db_identifier'] @@ -646,6 +711,20 @@ class RDS2Backend(BaseBackend): raise DBInstanceNotFoundError(db_instance_identifier) return self.databases.values() + def describe_snapshots(self, db_instance_identifier, db_snapshot_identifier): + if db_instance_identifier: + for snapshot in self.snapshots.values(): + if snapshot.database.db_instance_identifier == db_instance_identifier: + return [snapshot] + raise DBSnapshotNotFoundError() + + if db_snapshot_identifier: + if db_snapshot_identifier in self.snapshots: + return [self.snapshots[db_snapshot_identifier]] + raise DBSnapshotNotFoundError() + + return self.snapshots.values() + def modify_database(self, db_instance_identifier, db_kwargs): database = self.describe_databases(db_instance_identifier)[0] database.update(db_kwargs) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index f8f33f2b9..cdadd3424 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -39,7 +39,7 @@ class RDS2Response(BaseResponse): "region": self.region, "security_groups": self._get_multi_param('DBSecurityGroups.DBSecurityGroupName'), "storage_encrypted": self._get_param("StorageEncrypted"), - "storage_type": self._get_param("StorageType"), + "storage_type": self._get_param("StorageType", 'standard'), # VpcSecurityGroupIds.member.N "tags": list(), } @@ -150,6 +150,27 @@ class RDS2Response(BaseResponse): template = self.response_template(REBOOT_DATABASE_TEMPLATE) return template.render(database=database) + def create_db_snapshot(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + db_snapshot_identifier = self._get_param('DBSnapshotIdentifier') + tags = self._get_param('Tags', []) + snapshot = self.backend.create_snapshot(db_instance_identifier, db_snapshot_identifier, tags) + template = self.response_template(CREATE_SNAPSHOT_TEMPLATE) + return template.render(snapshot=snapshot) + + def describe_db_snapshots(self): + db_instance_identifier = self._get_param('DBInstanceIdentifier') + db_snapshot_identifier = self._get_param('DBSnapshotIdentifier') + snapshots = self.backend.describe_snapshots(db_instance_identifier, db_snapshot_identifier) + template = self.response_template(DESCRIBE_SNAPSHOTS_TEMPLATE) + return template.render(snapshots=snapshots) + + def delete_db_snapshot(self): + db_snapshot_identifier = self._get_param('DBSnapshotIdentifier') + snapshot = self.backend.delete_snapshot(db_snapshot_identifier) + template = self.response_template(DELETE_SNAPSHOT_TEMPLATE) + return template.render(snapshot=snapshot) + def list_tags_for_resource(self): arn = self._get_param('ResourceName') template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE) @@ -397,6 +418,42 @@ DELETE_DATABASE_TEMPLATE = """ + + {{ snapshot.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + + +""" + +DESCRIBE_SNAPSHOTS_TEMPLATE = """ + + + {%- for snapshot in snapshots -%} + {{ snapshot.to_xml() }} + {%- endfor -%} + + {% if marker %} + {{ marker }} + {% endif %} + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + +""" + +DELETE_SNAPSHOT_TEMPLATE = """ + + {{ snapshot.to_xml() }} + + + 523e3218-afc7-11c3-90f5-f90431260ab4 + + +""" + CREATE_SECURITY_GROUP_TEMPLATE = """ {{ security_group.to_xml() }} diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 148b00aa1..7a801257c 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -168,6 +168,81 @@ def test_delete_non_existant_database(): DBInstanceIdentifier="not-a-db").should.throw(ClientError) +@mock_rds2 +def test_create_db_snapshots(): + conn = boto3.client('rds', region_name='us-west-2') + conn.create_db_snapshot.when.called_with( + DBInstanceIdentifier='db-primary-1', + DBSnapshotIdentifier='snapshot-1').should.throw(ClientError) + + conn.create_db_instance(DBInstanceIdentifier='db-primary-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + + snapshot = conn.create_db_snapshot(DBInstanceIdentifier='db-primary-1', + DBSnapshotIdentifier='g-1').get('DBSnapshot') + + snapshot.get('Engine').should.equal('postgres') + snapshot.get('DBInstanceIdentifier').should.equal('db-primary-1') + snapshot.get('DBSnapshotIdentifier').should.equal('g-1') + + +@mock_rds2 +def test_describe_db_snapshots(): + conn = boto3.client('rds', region_name='us-west-2') + conn.create_db_instance(DBInstanceIdentifier='db-primary-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + conn.describe_db_snapshots.when.called_with( + DBInstanceIdentifier="db-primary-1").should.throw(ClientError) + + created = conn.create_db_snapshot(DBInstanceIdentifier='db-primary-1', + DBSnapshotIdentifier='snapshot-1').get('DBSnapshot') + + created.get('Engine').should.equal('postgres') + + by_database_id = conn.describe_db_snapshots(DBInstanceIdentifier='db-primary-1').get('DBSnapshots') + by_snapshot_id = conn.describe_db_snapshots(DBSnapshotIdentifier='snapshot-1').get('DBSnapshots') + by_snapshot_id.should.equal(by_database_id) + + snapshot = by_snapshot_id[0] + snapshot.should.equal(created) + snapshot.get('Engine').should.equal('postgres') + + +@mock_rds2 +def test_delete_db_snapshot(): + conn = boto3.client('rds', region_name='us-west-2') + conn.create_db_instance(DBInstanceIdentifier='db-primary-1', + AllocatedStorage=10, + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small', + MasterUsername='root', + MasterUserPassword='hunter2', + Port=1234, + DBSecurityGroups=["my_sg"]) + conn.create_db_snapshot(DBInstanceIdentifier='db-primary-1', + DBSnapshotIdentifier='snapshot-1') + + conn.describe_db_snapshots(DBSnapshotIdentifier='snapshot-1').get('DBSnapshots')[0] + conn.delete_db_snapshot(DBSnapshotIdentifier='snapshot-1') + conn.describe_db_snapshots.when.called_with( + DBSnapshotIdentifier='snapshot-1').should.throw(ClientError) + + @mock_rds2 def test_create_option_group(): conn = boto3.client('rds', region_name='us-west-2') From ccb4ffde7c4f5bede92a4601f2832316a3335d56 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 20 Jun 2017 13:53:22 -0700 Subject: [PATCH 224/274] Supporting io1 type --- moto/rds2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index ae97ba1f2..2d9a66401 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -208,7 +208,7 @@ class Database(BaseModel): {% endif %} {% if database.iops %} {{ database.iops }} - standard + io1 {% else %} {{ database.storage_type }} {% endif %} From fb2efb1c6dca45b6781fa4e2d977735aeedc2d9a Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 20 Jun 2017 14:00:56 -0700 Subject: [PATCH 225/274] Implementing snapshots on rds delete --- moto/rds/responses.py | 3 ++- moto/rds2/models.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/moto/rds/responses.py b/moto/rds/responses.py index 0895a8bf2..cdcbe3603 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -114,7 +114,8 @@ class RDSResponse(BaseResponse): def delete_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') - database = self.backend.delete_database(db_instance_identifier) + db_snapshot_name = self._get_param('FinalDBSnapshotIdentifier') + database = self.backend.delete_database(db_instance_identifier, db_snapshot_name) template = self.response_template(DELETE_DATABASE_TEMPLATE) return template.render(database=database) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 2d9a66401..549f6e247 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -403,10 +403,10 @@ class Database(BaseModel): class Snapshot(BaseModel): - def __init__(self, database, snapshot_id, tags): + def __init__(self, database, snapshot_id, tags=None): self.database = database self.snapshot_id = snapshot_id - self.tags = tags + self.tags = tags or [] self.created_at = iso_8601_datetime_with_milliseconds(datetime.datetime.now()) @property @@ -746,13 +746,15 @@ class RDS2Backend(BaseBackend): return backend.describe_databases(db_name)[0] - def delete_database(self, db_instance_identifier): + def delete_database(self, db_instance_identifier, db_snapshot_name): if db_instance_identifier in self.databases: database = self.databases.pop(db_instance_identifier) if database.is_replica: primary = self.find_db_from_id(database.source_db_identifier) primary.remove_replica(database) database.status = 'deleting' + if db_snapshot_name: + self.snapshots[db_snapshot_name] = Snapshot(database, db_snapshot_name) return database else: raise DBInstanceNotFoundError(db_instance_identifier) From 8df7169915cf4e894133c7640ea8758e2eddda6a Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 20 Jun 2017 14:01:28 -0700 Subject: [PATCH 226/274] Snapshots are optional --- moto/rds2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 549f6e247..86f7bae9a 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -746,7 +746,7 @@ class RDS2Backend(BaseBackend): return backend.describe_databases(db_name)[0] - def delete_database(self, db_instance_identifier, db_snapshot_name): + def delete_database(self, db_instance_identifier, db_snapshot_name=None): if db_instance_identifier in self.databases: database = self.databases.pop(db_instance_identifier) if database.is_replica: From e57798cb96966b83e42fb9ab3a9cc74f32879084 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 20 Jun 2017 14:46:13 -0700 Subject: [PATCH 227/274] Implementing snapshots on rds instance deletion --- moto/rds/responses.py | 3 +-- moto/rds2/responses.py | 3 ++- tests/test_rds2/test_rds2.py | 14 ++++++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/moto/rds/responses.py b/moto/rds/responses.py index cdcbe3603..0895a8bf2 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -114,8 +114,7 @@ class RDSResponse(BaseResponse): def delete_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') - db_snapshot_name = self._get_param('FinalDBSnapshotIdentifier') - database = self.backend.delete_database(db_instance_identifier, db_snapshot_name) + database = self.backend.delete_database(db_instance_identifier) template = self.response_template(DELETE_DATABASE_TEMPLATE) return template.render(database=database) diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index cdadd3424..b26f2e347 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -140,7 +140,8 @@ class RDS2Response(BaseResponse): def delete_db_instance(self): db_instance_identifier = self._get_param('DBInstanceIdentifier') - database = self.backend.delete_database(db_instance_identifier) + db_snapshot_name = self._get_param('FinalDBSnapshotIdentifier') + database = self.backend.delete_database(db_instance_identifier, db_snapshot_name) template = self.response_template(DELETE_DATABASE_TEMPLATE) return template.render(database=database) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index 7a801257c..f869dc1ce 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -145,10 +145,10 @@ def test_delete_database(): conn = boto3.client('rds', region_name='us-west-2') instances = conn.describe_db_instances() list(instances['DBInstances']).should.have.length_of(0) - conn.create_db_instance(DBInstanceIdentifier='db-master-1', + conn.create_db_instance(DBInstanceIdentifier='db-primary-1', AllocatedStorage=10, - DBInstanceClass='postgres', - Engine='db.m1.small', + Engine='postgres', + DBInstanceClass='db.m1.small', MasterUsername='root', MasterUserPassword='hunter2', Port=1234, @@ -156,10 +156,16 @@ def test_delete_database(): instances = conn.describe_db_instances() list(instances['DBInstances']).should.have.length_of(1) - conn.delete_db_instance(DBInstanceIdentifier="db-master-1") + conn.delete_db_instance(DBInstanceIdentifier="db-primary-1", + FinalDBSnapshotIdentifier='primary-1-snapshot') + instances = conn.describe_db_instances() list(instances['DBInstances']).should.have.length_of(0) + # Saved the snapshot + snapshots = conn.describe_db_snapshots(DBInstanceIdentifier="db-primary-1").get('DBSnapshots') + snapshots[0].get('Engine').should.equal('postgres') + @mock_rds2 def test_delete_non_existant_database(): From c5ce2848befef6b8475da22d6ab3804493e19f6a Mon Sep 17 00:00:00 2001 From: William Richard Date: Wed, 21 Jun 2017 12:58:01 -0400 Subject: [PATCH 228/274] Boto3 and cloudformation have different keys for auto scaling tags - handle that gracefully --- moto/autoscaling/models.py | 19 +-- tests/test_autoscaling/test_autoscaling.py | 33 +++-- .../test_cloudformation_stack_integration.py | 119 ++++++++++++------ 3 files changed, 115 insertions(+), 56 deletions(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index a2fcb2a63..9df9fea12 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -13,14 +13,12 @@ ASG_NAME_TAG = "aws:autoscaling:groupName" class InstanceState(object): - def __init__(self, instance, lifecycle_state="InService"): self.instance = instance self.lifecycle_state = lifecycle_state class FakeScalingPolicy(BaseModel): - def __init__(self, name, policy_type, adjustment_type, as_name, scaling_adjustment, cooldown, autoscaling_backend): self.name = name @@ -47,7 +45,6 @@ class FakeScalingPolicy(BaseModel): class FakeLaunchConfiguration(BaseModel): - def __init__(self, name, image_id, key_name, ramdisk_id, kernel_id, security_groups, user_data, instance_type, instance_monitoring, instance_profile_name, spot_price, ebs_optimized, associate_public_ip_address, block_device_mapping_dict): @@ -146,7 +143,6 @@ class FakeLaunchConfiguration(BaseModel): class FakeAutoScalingGroup(BaseModel): - def __init__(self, name, availability_zones, desired_capacity, max_size, min_size, launch_config_name, vpc_zone_identifier, default_cooldown, health_check_period, health_check_type, @@ -261,11 +257,17 @@ class FakeAutoScalingGroup(BaseModel): if self.desired_capacity > curr_instance_count: # Need more instances - count_needed = int(self.desired_capacity) - \ - int(curr_instance_count) + count_needed = int(self.desired_capacity) - int(curr_instance_count) + + propagated_tags = {} + for tag in self.tags: + # boto uses 'propagate_at_launch + # boto3 and cloudformation use PropagateAtLaunch + if 'propagate_at_launch' in tag and tag['propagate_at_launch'] == 'true': + propagated_tags[tag['key']] = tag['value'] + if 'PropagateAtLaunch' in tag and tag['PropagateAtLaunch']: + propagated_tags[tag['Key']] = tag['Value'] - propagated_tags = {t['key']: t['value'] for t in self.tags - if t['propagate_at_launch'] == 'true'} propagated_tags[ASG_NAME_TAG] = self.name reservation = self.autoscaling_backend.ec2_backend.add_instances( self.launch_config.image_id, @@ -290,7 +292,6 @@ class FakeAutoScalingGroup(BaseModel): class AutoScalingBackend(BaseBackend): - def __init__(self, ec2_backend, elb_backend): self.autoscaling_groups = OrderedDict() self.launch_configurations = OrderedDict() diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 5cc697785..b919eb71c 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -138,6 +138,7 @@ def test_list_many_autoscaling_groups(): groups.should.have.length_of(51) assert 'NextToken' not in response2.keys() + @mock_autoscaling @mock_ec2 def test_list_many_autoscaling_groups(): @@ -163,6 +164,7 @@ def test_list_many_autoscaling_groups(): tags.should.contain({u'Value': 'TestTagValue1', u'Key': 'TestTagKey1'}) tags.should.contain({u'Value': 'TestGroup1', u'Key': 'aws:autoscaling:groupName'}) + @mock_autoscaling_deprecated def test_autoscaling_group_describe_filter(): conn = boto.connect_autoscale() @@ -493,7 +495,20 @@ def test_create_autoscaling_group_boto3(): LaunchConfigurationName='test_launch_configuration', MinSize=0, MaxSize=20, - DesiredCapacity=5 + DesiredCapacity=5, + Tags=[ + {'ResourceId': 'test_asg', + 'ResourceType': 'auto-scaling-group', + 'Key': 'propogated-tag-key', + 'Value': 'propogate-tag-value', + 'PropagateAtLaunch': True + }, + {'ResourceId': 'test_asg', + 'ResourceType': 'auto-scaling-group', + 'Key': 'not-propogated-tag-key', + 'Value': 'not-propogate-tag-value', + 'PropagateAtLaunch': False + }] ) response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) @@ -556,12 +571,14 @@ def test_autoscaling_taqs_update_boto3(): MinSize=0, MaxSize=20, DesiredCapacity=5, - Tags=[{ - "ResourceId": 'test_asg', - "Key": 'test_key', - "Value": 'test_value', - "PropagateAtLaunch": True - }] + Tags=[ + { + "ResourceId": 'test_asg', + "Key": 'test_key', + "Value": 'test_value', + "PropagateAtLaunch": True + }, + ] ) client.create_or_update_tags(Tags=[{ @@ -573,7 +590,7 @@ def test_autoscaling_taqs_update_boto3(): "ResourceId": 'test_asg', "Key": 'test_key2', "Value": 'test_value2', - "PropagateAtLaunch": True + "PropagateAtLaunch": False }]) response = client.describe_auto_scaling_groups( diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 87dcfd950..df696d879 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -382,7 +382,7 @@ def test_stack_elb_integration_with_update(): "Protocol": "HTTP", } ], - "Policies": {"Ref" : "AWS::NoValue"}, + "Policies": {"Ref": "AWS::NoValue"}, } }, }, @@ -536,8 +536,8 @@ def test_stack_security_groups(): @mock_autoscaling_deprecated() @mock_elb_deprecated() @mock_cloudformation_deprecated() +@mock_ec2_deprecated() def test_autoscaling_group_with_elb(): - web_setup_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -550,7 +550,17 @@ def test_autoscaling_group_with_elb(): "MinSize": "2", "MaxSize": "2", "DesiredCapacity": "2", - "LoadBalancerNames": [{"Ref": "my-elb"}] + "LoadBalancerNames": [{"Ref": "my-elb"}], + "Tags": [ + { + "Key": "propagated-test-tag", "Value": "propagated-test-tag-value", + "PropagateAtLaunch": True}, + { + "Key": "not-propagated-test-tag", + "Value": "not-propagated-test-tag-value", + "PropagateAtLaunch": False + } + ] }, }, @@ -611,7 +621,8 @@ def test_autoscaling_group_with_elb(): as_group_resource.physical_resource_id.should.contain("my-as-group") launch_config_resource = [ - resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::LaunchConfiguration'][0] + resource for resource in resources if + resource.resource_type == 'AWS::AutoScaling::LaunchConfiguration'][0] launch_config_resource.physical_resource_id.should.contain( "my-launch-config") @@ -619,9 +630,20 @@ def test_autoscaling_group_with_elb(): 'AWS::ElasticLoadBalancing::LoadBalancer'][0] elb_resource.physical_resource_id.should.contain("my-elb") + # confirm the instances were created with the right tags + ec2_conn = boto.ec2.connect_to_region('us-west-1') + reservations = ec2_conn.get_all_reservations() + len(reservations).should.equal(1) + reservation = reservations[0] + len(reservation.instances).should.equal(2) + for instance in reservation.instances: + instance.tags['propagated-test-tag'].should.equal('propagated-test-tag-value') + instance.tags.keys().should_not.contain('not-propagated-test-tag') + @mock_autoscaling_deprecated() @mock_cloudformation_deprecated() +@mock_ec2_deprecated() def test_autoscaling_group_update(): asg_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -661,6 +683,16 @@ def test_autoscaling_group_update(): asg.desired_capacity.should.equal(2) asg_template['Resources']['my-as-group']['Properties']['MaxSize'] = 3 + asg_template['Resources']['my-as-group']['Properties']['Tags'] = [ + { + "Key": "propagated-test-tag", "Value": "propagated-test-tag-value", + "PropagateAtLaunch": True}, + { + "Key": "not-propagated-test-tag", + "Value": "not-propagated-test-tag-value", + "PropagateAtLaunch": False + } + ] asg_template_json = json.dumps(asg_template) conn.update_stack( "asg_stack", @@ -671,11 +703,22 @@ def test_autoscaling_group_update(): asg.max_size.should.equal(3) asg.desired_capacity.should.equal(2) + # confirm the instances were created with the right tags + ec2_conn = boto.ec2.connect_to_region('us-west-1') + reservations = ec2_conn.get_all_reservations() + running_instance_count = 0 + for res in reservations: + for instance in res.instances: + if instance.state == 'running': + running_instance_count += 1 + instance.tags['propagated-test-tag'].should.equal('propagated-test-tag-value') + instance.tags.keys().should_not.contain('not-propagated-test-tag') + running_instance_count.should.equal(2) + @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_vpc_single_instance_in_subnet(): - template_json = json.dumps(vpc_single_instance_in_subnet.template) conn = boto.cloudformation.connect_to_region("us-west-1") conn.create_stack( @@ -738,16 +781,16 @@ def test_rds_db_parameter_groups(): TemplateBody=template_json, Parameters=[{'ParameterKey': key, 'ParameterValue': value} for key, value in [ - ("DBInstanceIdentifier", "master_db"), - ("DBName", "my_db"), - ("DBUser", "my_user"), - ("DBPassword", "my_password"), - ("DBAllocatedStorage", "20"), - ("DBInstanceClass", "db.m1.medium"), - ("EC2SecurityGroup", "application"), - ("MultiAZ", "true"), - ] - ], + ("DBInstanceIdentifier", "master_db"), + ("DBName", "my_db"), + ("DBUser", "my_user"), + ("DBPassword", "my_password"), + ("DBAllocatedStorage", "20"), + ("DBInstanceClass", "db.m1.medium"), + ("EC2SecurityGroup", "application"), + ("MultiAZ", "true"), + ] + ], ) rds_conn = boto3.client('rds', region_name="us-west-1") @@ -758,8 +801,10 @@ def test_rds_db_parameter_groups(): 'DBParameterGroups'][0]['DBParameterGroupName'] found_cloudformation_set_parameter = False - for db_parameter in rds_conn.describe_db_parameters(DBParameterGroupName=db_parameter_group_name)['Parameters']: - if db_parameter['ParameterName'] == 'BACKLOG_QUEUE_LIMIT' and db_parameter['ParameterValue'] == '2048': + for db_parameter in rds_conn.describe_db_parameters(DBParameterGroupName=db_parameter_group_name)[ + 'Parameters']: + if db_parameter['ParameterName'] == 'BACKLOG_QUEUE_LIMIT' and db_parameter[ + 'ParameterValue'] == '2048': found_cloudformation_set_parameter = True found_cloudformation_set_parameter.should.equal(True) @@ -965,7 +1010,6 @@ def test_iam_roles(): @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_single_instance_with_ebs_volume(): - template_json = json.dumps(single_instance_with_ebs_volume.template) conn = boto.cloudformation.connect_to_region("us-west-1") conn.create_stack( @@ -1005,7 +1049,6 @@ def test_create_template_without_required_param(): @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_classic_eip(): - template_json = json.dumps(ec2_classic_eip.template) conn = boto.cloudformation.connect_to_region("us-west-1") conn.create_stack("test_stack", template_body=template_json) @@ -1022,7 +1065,6 @@ def test_classic_eip(): @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_vpc_eip(): - template_json = json.dumps(vpc_eip.template) conn = boto.cloudformation.connect_to_region("us-west-1") conn.create_stack("test_stack", template_body=template_json) @@ -1039,7 +1081,6 @@ def test_vpc_eip(): @mock_ec2_deprecated() @mock_cloudformation_deprecated() def test_fn_join(): - template_json = json.dumps(fn_join.template) conn = boto.cloudformation.connect_to_region("us-west-1") conn.create_stack("test_stack", template_body=template_json) @@ -2009,25 +2050,25 @@ def test_stack_spot_fleet(): "TargetCapacity": 6, "AllocationStrategy": "diversified", "LaunchSpecifications": [ - { - "EbsOptimized": "false", - "InstanceType": 't2.small', - "ImageId": "ami-1234", - "SubnetId": subnet_id, - "WeightedCapacity": "2", - "SpotPrice": "0.13", - }, { - "EbsOptimized": "true", - "InstanceType": 't2.large', - "ImageId": "ami-1234", - "Monitoring": {"Enabled": "true"}, - "SecurityGroups": [{"GroupId": "sg-123"}], - "SubnetId": subnet_id, - "IamInstanceProfile": {"Arn": "arn:aws:iam::123456789012:role/fleet"}, - "WeightedCapacity": "4", - "SpotPrice": "10.00", - } + "EbsOptimized": "false", + "InstanceType": 't2.small', + "ImageId": "ami-1234", + "SubnetId": subnet_id, + "WeightedCapacity": "2", + "SpotPrice": "0.13", + }, + { + "EbsOptimized": "true", + "InstanceType": 't2.large', + "ImageId": "ami-1234", + "Monitoring": {"Enabled": "true"}, + "SecurityGroups": [{"GroupId": "sg-123"}], + "SubnetId": subnet_id, + "IamInstanceProfile": {"Arn": "arn:aws:iam::123456789012:role/fleet"}, + "WeightedCapacity": "4", + "SpotPrice": "10.00", + } ] } } From 8ca27e184aa98b62fd6841862499f66084b71bf1 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Mon, 26 Jun 2017 11:17:36 -0700 Subject: [PATCH 229/274] Simplify tests --- tests/test_ssm/test_ssm_boto3.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 8b5d1f200..a62536a23 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -118,14 +118,12 @@ def test_describe_parameters_filter_names(): response = client.describe_parameters(Filters=[ { 'Key': 'Name', - 'Values': ['param-45', 'param-22'] + 'Values': ['param-22'] }, ]) - len(response['Parameters']).should.equal(2) + len(response['Parameters']).should.equal(1) response['Parameters'][0]['Name'].should.equal('param-22') response['Parameters'][0]['Type'].should.equal('String') - response['Parameters'][1]['Name'].should.equal('param-45') - response['Parameters'][1]['Type'].should.equal('SecureString') ''.should.equal(response.get('NextToken', '')) @mock_ssm @@ -174,14 +172,12 @@ def test_describe_parameters_filter_keyid(): response = client.describe_parameters(Filters=[ { 'Key': 'KeyId', - 'Values': ['key:5','key:10'] + 'Values': ['key:10'] }, ]) - len(response['Parameters']).should.equal(2) + len(response['Parameters']).should.equal(1) response['Parameters'][0]['Name'].should.equal('param-10') response['Parameters'][0]['Type'].should.equal('SecureString') - response['Parameters'][1]['Name'].should.equal('param-5') - response['Parameters'][1]['Type'].should.equal('SecureString') ''.should.equal(response.get('NextToken', '')) @mock_ssm From 27f1248788b71943e3c04c9b7e173ac70e22dd81 Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Mon, 26 Jun 2017 11:20:56 -0700 Subject: [PATCH 230/274] Fix spacing --- tests/test_ssm/test_ssm_boto3.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index a62536a23..de0793e82 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -99,6 +99,7 @@ def test_describe_parameters_paging(): len(response['Parameters']).should.equal(0) ''.should.equal(response.get('NextToken', '')) + @mock_ssm def test_describe_parameters_filter_names(): client = boto3.client('ssm', region_name='us-east-1') @@ -114,7 +115,6 @@ def test_describe_parameters_filter_names(): p['KeyId'] = 'a key' client.put_parameter(**p) - response = client.describe_parameters(Filters=[ { 'Key': 'Name', @@ -126,6 +126,7 @@ def test_describe_parameters_filter_names(): response['Parameters'][0]['Type'].should.equal('String') ''.should.equal(response.get('NextToken', '')) + @mock_ssm def test_describe_parameters_filter_type(): client = boto3.client('ssm', region_name='us-east-1') @@ -153,6 +154,7 @@ def test_describe_parameters_filter_type(): response['Parameters'][0]['Type'].should.equal('SecureString') '10'.should.equal(response.get('NextToken', '')) + @mock_ssm def test_describe_parameters_filter_keyid(): client = boto3.client('ssm', region_name='us-east-1') @@ -180,6 +182,7 @@ def test_describe_parameters_filter_keyid(): response['Parameters'][0]['Type'].should.equal('SecureString') ''.should.equal(response.get('NextToken', '')) + @mock_ssm def test_put_parameter_secure_default_kms(): client = boto3.client('ssm', region_name='us-east-1') From 7bf5211bef53c3b821bb60435ddab9db9f96da9e Mon Sep 17 00:00:00 2001 From: Declan Shanaghy Date: Mon, 26 Jun 2017 12:07:44 -0700 Subject: [PATCH 231/274] Simplify test 2 --- tests/test_ssm/test_ssm_boto3.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index de0793e82..60a027933 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -150,7 +150,6 @@ def test_describe_parameters_filter_type(): }, ]) len(response['Parameters']).should.equal(10) - response['Parameters'][0]['Name'].should.equal('param-35') response['Parameters'][0]['Type'].should.equal('SecureString') '10'.should.equal(response.get('NextToken', '')) From 8921920ae6671cd251ffdb18ed452d6932bdb203 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Tue, 27 Jun 2017 17:18:21 +1000 Subject: [PATCH 232/274] add flag to enable SSL for moto_server --- moto/server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/moto/server.py b/moto/server.py index e5426bc7a..be41f1ed0 100644 --- a/moto/server.py +++ b/moto/server.py @@ -171,6 +171,12 @@ def main(argv=sys.argv[1:]): help='Reload server on a file change', default=False ) + parser.add_argument( + '-s', '--ssl', + action='store_true', + help='Enable SSL encrypted connection (use https://... URL)', + default=False + ) args = parser.parse_args(argv) @@ -180,7 +186,8 @@ def main(argv=sys.argv[1:]): main_app.debug = True run_simple(args.host, args.port, main_app, - threaded=True, use_reloader=args.reload) + threaded=True, use_reloader=args.reload, + ssl_context='adhoc' if args.ssl else None) if __name__ == '__main__': From c4b9088bfcee05f39ccec8989eafa15b0fc69ddf Mon Sep 17 00:00:00 2001 From: Steven Cipriano Date: Tue, 27 Jun 2017 11:31:43 -0700 Subject: [PATCH 233/274] Add support for recursive emr settings - Updates _RecursiveDictRef to not implement __getitem__, avoiding errors when using recursive settings for an emr job flow --- moto/core/responses.py | 3 +++ tests/test_emr/test_emr_boto3.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/moto/core/responses.py b/moto/core/responses.py index adad5d1de..82e9d4cad 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -414,6 +414,9 @@ class _RecursiveDictRef(object): def __getattr__(self, key): return self.dic.__getattr__(key) + def __getitem__(self, key): + return self.dic.__getitem__(key) + def set_reference(self, key, dic): """Set the RecursiveDictRef object to keep reference to dict object (dic) at the key. diff --git a/tests/test_emr/test_emr_boto3.py b/tests/test_emr/test_emr_boto3.py index 830abdb85..237ff8bba 100644 --- a/tests/test_emr/test_emr_boto3.py +++ b/tests/test_emr/test_emr_boto3.py @@ -64,7 +64,18 @@ def test_describe_cluster(): args['Configurations'] = [ {'Classification': 'yarn-site', 'Properties': {'someproperty': 'somevalue', - 'someotherproperty': 'someothervalue'}}] + 'someotherproperty': 'someothervalue'}}, + {'Classification': 'nested-configs', + 'Properties': {}, + 'Configurations': [ + { + 'Classification': 'nested-config', + 'Properties': { + 'nested-property': 'nested-value' + } + } + ]} + ] args['Instances']['AdditionalMasterSecurityGroups'] = ['additional-master'] args['Instances']['AdditionalSlaveSecurityGroups'] = ['additional-slave'] args['Instances']['Ec2KeyName'] = 'mykey' @@ -87,6 +98,10 @@ def test_describe_cluster(): config['Classification'].should.equal('yarn-site') config['Properties'].should.equal(args['Configurations'][0]['Properties']) + nested_config = cl['Configurations'][1] + nested_config['Classification'].should.equal('nested-configs') + nested_config['Properties'].should.equal(args['Configurations'][1]['Properties']) + attrs = cl['Ec2InstanceAttributes'] attrs['AdditionalMasterSecurityGroups'].should.equal( args['Instances']['AdditionalMasterSecurityGroups']) From 898031b40c9bd4a4396f5972e0916081c90f7575 Mon Sep 17 00:00:00 2001 From: Luis Jimenez Date: Thu, 29 Jun 2017 09:24:09 -0400 Subject: [PATCH 234/274] SQSResponse: include MD5OfMessageAttributes parameter only when there are message attributes --- moto/sqs/responses.py | 6 +++ tests/test_sqs/test_sqs.py | 76 +++++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 53bbac6ef..ba4a56b8f 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -339,7 +339,9 @@ SEND_MESSAGE_RESPONSE = """ {{- message.body_md5 -}} + {% if message.message_attributes.items()|count > 0 %} {{- message.attribute_md5 -}} + {% endif %} {{- message.id -}} @@ -373,7 +375,9 @@ RECEIVE_MESSAGE_RESPONSE = """ ApproximateFirstReceiveTimestamp {{ message.approximate_first_receive_timestamp }} + {% if message.message_attributes.items()|count > 0 %} {{- message.attribute_md5 -}} + {% endif %} {% for name, value in message.message_attributes.items() %} {{ name }} @@ -402,7 +406,9 @@ SEND_MESSAGE_BATCH_RESPONSE = """ {{ message.user_id }} {{ message.id }} {{ message.body_md5 }} + {% if message.message_attributes.items()|count > 0 %} {{- message.attribute_md5 -}} + {% endif %} {% endfor %} diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index b01a55406..db351f5ab 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -39,9 +39,25 @@ def test_get_inexistent_queue(): sqs.get_queue_by_name.when.called_with( QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) +@mock_sqs +def test_message_send_without_attributes(): + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + msg = queue.send_message( + MessageBody="derp" + ) + msg.get('MD5OfMessageBody').should.equal( + '58fd9edd83341c29f1aebba81c31e257') + msg.shouldnt.have.key('MD5OfMessageAttributes') + msg.get('ResponseMetadata', {}).get('RequestId').should.equal( + '27daac76-34dd-47df-bd01-1f6e873584a0') + msg.get('MessageId').should_not.contain(' \n') + + messages = queue.receive_messages() + messages.should.have.length_of(1) @mock_sqs -def test_message_send(): +def test_message_send_with_attributes(): sqs = boto3.resource('sqs', region_name='us-east-1') queue = sqs.create_queue(QueueName="blah") msg = queue.send_message( @@ -189,7 +205,7 @@ def test_set_queue_attribute(): @mock_sqs -def test_send_message(): +def test_send_receive_message_without_attributes(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue") @@ -198,14 +214,62 @@ def test_send_message(): body_one = 'this is a test message' body_two = 'this is another test message' - response = queue.send_message(MessageBody=body_one) - response = queue.send_message(MessageBody=body_two) + queue.send_message(MessageBody=body_one) + queue.send_message(MessageBody=body_two) messages = conn.receive_message( QueueUrl=queue.url, MaxNumberOfMessages=2)['Messages'] - messages[0]['Body'].should.equal(body_one) - messages[1]['Body'].should.equal(body_two) + message1 = messages[0] + message2 = messages[1] + + message1['Body'].should.equal(body_one) + message2['Body'].should.equal(body_two) + + message1.shouldnt.have.key('MD5OfMessageAttributes') + message2.shouldnt.have.key('MD5OfMessageAttributes') + +@mock_sqs +def test_send_receive_message_with_attributes(): + sqs = boto3.resource('sqs', region_name='us-east-1') + conn = boto3.client("sqs", region_name='us-east-1') + conn.create_queue(QueueName="test-queue") + queue = sqs.Queue("test-queue") + + body_one = 'this is a test message' + body_two = 'this is another test message' + + queue.send_message( + MessageBody=body_one, + MessageAttributes={ + 'timestamp': { + 'StringValue': '1493147359900', + 'DataType': 'Number', + } + } + ) + + queue.send_message( + MessageBody=body_two, + MessageAttributes={ + 'timestamp': { + 'StringValue': '1493147359901', + 'DataType': 'Number', + } + } + ) + + messages = conn.receive_message( + QueueUrl=queue.url, MaxNumberOfMessages=2)['Messages'] + + message1 = messages[0] + message2 = messages[1] + + message1.get('Body').should.equal(body_one) + message2.get('Body').should.equal(body_two) + + message1.get('MD5OfMessageAttributes').should.equal('235c5c510d26fb653d073faed50ae77c') + message2.get('MD5OfMessageAttributes').should.equal('994258b45346a2cc3f9cbb611aa7af30') @mock_sqs From e4f42d58807c9ca73a9d8d8ebaa71bf364827dd3 Mon Sep 17 00:00:00 2001 From: Ferran Puig Date: Mon, 3 Jul 2017 16:17:01 +0200 Subject: [PATCH 235/274] Don't use exponential notation for SQS message timestamps --- moto/sqs/models.py | 4 ++-- tests/test_sqs/test_sqs.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 43a633c42..f6657269c 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -96,7 +96,7 @@ class Message(BaseModel): return escape(self._body) def mark_sent(self, delay_seconds=None): - self.sent_timestamp = unix_time_millis() + self.sent_timestamp = int(unix_time_millis()) if delay_seconds: self.delay(delay_seconds=delay_seconds) @@ -111,7 +111,7 @@ class Message(BaseModel): visibility_timeout = 0 if not self.approximate_first_receive_timestamp: - self.approximate_first_receive_timestamp = unix_time_millis() + self.approximate_first_receive_timestamp = int(unix_time_millis()) self.approximate_receive_count += 1 diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index db351f5ab..3eb8e2213 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -272,6 +272,25 @@ def test_send_receive_message_with_attributes(): message2.get('MD5OfMessageAttributes').should.equal('994258b45346a2cc3f9cbb611aa7af30') +@mock_sqs +def test_send_receive_message_timestamps(): + sqs = boto3.resource('sqs', region_name='us-east-1') + conn = boto3.client("sqs", region_name='us-east-1') + conn.create_queue(QueueName="test-queue") + queue = sqs.Queue("test-queue") + + queue.send_message(MessageBody="derp") + messages = conn.receive_message( + QueueUrl=queue.url, MaxNumberOfMessages=1)['Messages'] + + message = messages[0] + sent_timestamp = message.get('Attributes').get('SentTimestamp') + approximate_first_receive_timestamp = message.get('Attributes').get('ApproximateFirstReceiveTimestamp') + + int.when.called_with(sent_timestamp).shouldnt.throw(ValueError) + int.when.called_with(approximate_first_receive_timestamp).shouldnt.throw(ValueError) + + @mock_sqs def test_receive_messages_with_wait_seconds_timeout_of_zero(): """ From c3d9f4e056013b8844f64789c9268b89a332848f Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Wed, 5 Jul 2017 16:02:45 -0700 Subject: [PATCH 236/274] Persisting selected LicenseModel in RDS instances --- moto/rds/models.py | 2 +- moto/rds2/models.py | 7 +++---- moto/rds2/responses.py | 1 + tests/test_rds2/test_rds2.py | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/moto/rds/models.py b/moto/rds/models.py index a499b134d..77deff09d 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -182,7 +182,7 @@ class Database(BaseModel): {{ database.source_db_identifier }} {% endif %} {{ database.engine }} - general-public-license + {{ database.license_model }} {{ database.engine_version }} diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 86f7bae9a..5abd2ed1b 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -89,8 +89,7 @@ class Database(BaseModel): self.preferred_backup_window = kwargs.get( 'preferred_backup_window', '13:14-13:44') - self.license_model = kwargs.get( - 'license_model', 'general-public-license') + self.license_model = kwargs.get('license_model', 'general-public-license') self.option_group_name = kwargs.get('option_group_name', None) self.default_option_groups = {"MySQL": "default.mysql5.6", "mysql": "default.mysql5.6", @@ -159,7 +158,7 @@ class Database(BaseModel): {{ database.source_db_identifier }} {% endif %} {{ database.engine }} - general-public-license + {{ database.license_model }} {{ database.engine_version }} @@ -427,7 +426,7 @@ class Snapshot(BaseModel): {{ snapshot.created_at }} {{ database.master_username }} {{ database.engine_version }} - general-public-license + {{ database.license_model }} manual {% if database.iops %} {{ database.iops }} diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index b26f2e347..ef02bfbf1 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -26,6 +26,7 @@ class RDS2Response(BaseResponse): "db_subnet_group_name": self._get_param("DBSubnetGroupName"), "engine": self._get_param("Engine"), "engine_version": self._get_param("EngineVersion"), + "license_model": self._get_param("LicenseModel"), "iops": self._get_int_param("Iops"), "kms_key_id": self._get_param("KmsKeyId"), "master_user_password": self._get_param('MasterUserPassword'), diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index f869dc1ce..a50f99868 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -14,6 +14,7 @@ def test_create_database(): Engine='postgres', DBName='staging-postgres', DBInstanceClass='db.m1.small', + LicenseModel='license-included', MasterUsername='root', MasterUserPassword='hunter2', Port=1234, @@ -23,6 +24,7 @@ def test_create_database(): database['DBInstance']['DBInstanceIdentifier'].should.equal("db-master-1") database['DBInstance']['AllocatedStorage'].should.equal(10) database['DBInstance']['DBInstanceClass'].should.equal("db.m1.small") + database['DBInstance']['LicenseModel'].should.equal("license-included") database['DBInstance']['MasterUsername'].should.equal("root") database['DBInstance']['DBSecurityGroups'][0][ 'DBSecurityGroupName'].should.equal('my_sg') From dbbbc01f886fd1c9d264687c25f36e73feab3966 Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Thu, 6 Jul 2017 21:29:18 -0700 Subject: [PATCH 237/274] Test boto3 elb listener deletion --- tests/test_elb/test_elb.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 36f96c0e2..78c6e0ad0 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -214,6 +214,13 @@ def test_create_and_delete_listener_boto3_support(): balancer['ListenerDescriptions'][1]['Listener'][ 'InstancePort'].should.equal(8443) + client.delete_load_balancer_listeners( + LoadBalancerName='my-lb', + LoadBalancerPorts=[443]) + + balancer = client.describe_load_balancers()['LoadBalancerDescriptions'][0] + list(balancer['ListenerDescriptions']).should.have.length_of(1) + @mock_elb_deprecated def test_set_sslcertificate(): From 98342bfcc3c66d63c4e691c492e19840149cfdbb Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Thu, 6 Jul 2017 21:52:01 -0700 Subject: [PATCH 238/274] Raise error on duplicate elbv1 listener AWS returns an error condition when a listener is defined that interferes with an existing listener on the same load balancer port. --- moto/elb/exceptions.py | 9 +++++++++ moto/elb/models.py | 7 +++++++ tests/test_elb/test_elb.py | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/moto/elb/exceptions.py b/moto/elb/exceptions.py index 071181a6c..6c316ef47 100644 --- a/moto/elb/exceptions.py +++ b/moto/elb/exceptions.py @@ -40,6 +40,15 @@ class BadHealthCheckDefinition(ELBClientError): "HealthCheck Target must begin with one of HTTP, TCP, HTTPS, SSL") +class DuplicateListenerError(ELBClientError): + + def __init__(self, name, port): + super(DuplicateListenerError, self).__init__( + "DuplicateListener", + "A listener already exists for {0} with LoadBalancerPort {1}, but with a different InstancePort, Protocol, or SSLCertificateId" + .format(name, port)) + + class DuplicateLoadBalancerName(ELBClientError): def __init__(self, name): diff --git a/moto/elb/models.py b/moto/elb/models.py index 5b6a58bb9..d09548340 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -18,6 +18,7 @@ from moto.ec2.models import ec2_backends from .exceptions import ( BadHealthCheckDefinition, DuplicateLoadBalancerName, + DuplicateListenerError, EmptyListenersError, LoadBalancerNotFoundError, TooManyTagsError, @@ -257,6 +258,12 @@ class ELBBackend(BaseBackend): ssl_certificate_id = port.get('sslcertificate_id') for listener in balancer.listeners: if lb_port == listener.load_balancer_port: + if protocol != listener.protocol: + raise DuplicateListenerError(name, lb_port) + if instance_port != listener.instance_port: + raise DuplicateListenerError(name, lb_port) + if ssl_certificate_id != listener.ssl_certificate_id: + raise DuplicateListenerError(name, lb_port) break else: balancer.listeners.append(FakeListener( diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 78c6e0ad0..f9019eed2 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -214,6 +214,14 @@ def test_create_and_delete_listener_boto3_support(): balancer['ListenerDescriptions'][1]['Listener'][ 'InstancePort'].should.equal(8443) + # Creating this listener with an conflicting definition throws error + with assert_raises(ClientError): + client.create_load_balancer_listeners( + LoadBalancerName='my-lb', + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 443, 'InstancePort': 1234}] + ) + client.delete_load_balancer_listeners( LoadBalancerName='my-lb', LoadBalancerPorts=[443]) From 2a65f40a194854aef6337357cc29ba5d55b4c2ef Mon Sep 17 00:00:00 2001 From: fdfk Date: Tue, 11 Jul 2017 08:02:31 +0000 Subject: [PATCH 239/274] Adding list_verified_email_addresses and testing --- moto/ses/models.py | 7 +++++++ moto/ses/responses.py | 31 +++++++++++++++++++++++++++++++ tests/test_ses/test_ses_boto3.py | 7 +++++++ 3 files changed, 45 insertions(+) diff --git a/moto/ses/models.py b/moto/ses/models.py index 2f51d1473..179f4d8e0 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -36,6 +36,7 @@ class SESBackend(BaseBackend): def __init__(self): self.addresses = [] + self.email_addresses = [] self.domains = [] self.sent_messages = [] self.sent_message_count = 0 @@ -49,12 +50,18 @@ class SESBackend(BaseBackend): def verify_email_identity(self, address): self.addresses.append(address) + def verify_email_address(self, address): + self.email_addresses.append(address) + def verify_domain(self, domain): self.domains.append(domain) def list_identities(self): return self.domains + self.addresses + def list_verified_email_addresses(self): + return self.email_addresses + def delete_identity(self, identity): if '@' in identity: self.addresses.remove(identity) diff --git a/moto/ses/responses.py b/moto/ses/responses.py index d7bfe0787..6cd018aa6 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -15,11 +15,22 @@ class EmailResponse(BaseResponse): template = self.response_template(VERIFY_EMAIL_IDENTITY) return template.render() + def verify_email_address(self): + address = self.querystring.get('EmailAddress')[0] + ses_backend.verify_email_address(address) + template = self.response_template(VERIFY_EMAIL_ADDRESS) + return template.render() + def list_identities(self): identities = ses_backend.list_identities() template = self.response_template(LIST_IDENTITIES_RESPONSE) return template.render(identities=identities) + def list_verified_email_addresses(self): + email_addresses = ses_backend.list_verified_email_addresses() + template = self.response_template(LIST_VERIFIED_EMAIL_RESPONSE) + return template.render(email_addresses=email_addresses) + def verify_domain_dkim(self): domain = self.querystring.get('Domain')[0] ses_backend.verify_domain(domain) @@ -95,6 +106,13 @@ VERIFY_EMAIL_IDENTITY = """ + + + 47e0ef1a-9bf2-11e1-9279-0100e8cf109a + +""" + LIST_IDENTITIES_RESPONSE = """ @@ -108,6 +126,19 @@ LIST_IDENTITIES_RESPONSE = """ + + + {% for email in email_addresses %} + {{ email }} + {% endfor %} + + + + cacecf23-9bf1-11e1-9279-0100e8cf109a + +""" + VERIFY_DOMAIN_DKIM_RESPONSE = """ diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py index 224ebb626..5d39f61d4 100644 --- a/tests/test_ses/test_ses_boto3.py +++ b/tests/test_ses/test_ses_boto3.py @@ -19,6 +19,13 @@ def test_verify_email_identity(): address = identities['Identities'][0] address.should.equal('test@example.com') +@mock_ses +def test_verify_email_address(): + conn = boto3.client('ses', region_name='us-east-1') + conn.verify_email_address(EmailAddress="test@example.com") + email_addresses = conn.list_verified_email_addresses() + email = email_addresses['VerifiedEmailAddresses'][0] + email.should.equal('test@example.com') @mock_ses def test_domain_verify(): From 5e5333c24351cb00f21cabab5f5666b284d196d8 Mon Sep 17 00:00:00 2001 From: gilgamezh Date: Fri, 14 Jul 2017 19:29:20 -0300 Subject: [PATCH 240/274] Avoid to override SocketType when disabling the mock and bad_socket_shadow is True --- moto/packages/httpretty/core.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/moto/packages/httpretty/core.py b/moto/packages/httpretty/core.py index 0974f38dd..5a8d01798 100644 --- a/moto/packages/httpretty/core.py +++ b/moto/packages/httpretty/core.py @@ -72,6 +72,10 @@ from datetime import datetime from datetime import timedelta from errno import EAGAIN +# Some versions of python internally shadowed the +# SocketType variable incorrectly https://bugs.python.org/issue20386 +BAD_SOCKET_SHADOW = socket.socket != socket.SocketType + old_socket = socket.socket old_create_connection = socket.create_connection old_gethostbyname = socket.gethostbyname @@ -976,7 +980,8 @@ class httpretty(HttpBaseClass): def disable(cls): cls._is_enabled = False socket.socket = old_socket - socket.SocketType = old_socket + if not BAD_SOCKET_SHADOW: + socket.SocketType = old_socket socket._socketobject = old_socket socket.create_connection = old_create_connection @@ -986,7 +991,8 @@ class httpretty(HttpBaseClass): socket.__dict__['socket'] = old_socket socket.__dict__['_socketobject'] = old_socket - socket.__dict__['SocketType'] = old_socket + if not BAD_SOCKET_SHADOW: + socket.__dict__['SocketType'] = old_socket socket.__dict__['create_connection'] = old_create_connection socket.__dict__['gethostname'] = old_gethostname @@ -1014,13 +1020,10 @@ class httpretty(HttpBaseClass): @classmethod def enable(cls): cls._is_enabled = True - # Some versions of python internally shadowed the - # SocketType variable incorrectly https://bugs.python.org/issue20386 - bad_socket_shadow = (socket.socket != socket.SocketType) socket.socket = fakesock.socket socket._socketobject = fakesock.socket - if not bad_socket_shadow: + if not BAD_SOCKET_SHADOW: socket.SocketType = fakesock.socket socket.create_connection = create_fake_connection @@ -1030,7 +1033,7 @@ class httpretty(HttpBaseClass): socket.__dict__['socket'] = fakesock.socket socket.__dict__['_socketobject'] = fakesock.socket - if not bad_socket_shadow: + if not BAD_SOCKET_SHADOW: socket.__dict__['SocketType'] = fakesock.socket socket.__dict__['create_connection'] = create_fake_connection From abf3078c28f59813315ad753b7332c6369b4ccbd Mon Sep 17 00:00:00 2001 From: eric-weaver Date: Sat, 15 Jul 2017 22:36:12 -0400 Subject: [PATCH 241/274] implement s3 object tagging --- moto/s3/models.py | 36 ++++++++++++++- moto/s3/responses.py | 49 ++++++++++++++++++++- tests/test_s3/__init__.py | 0 tests/test_s3/test_s3.py | 92 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 tests/test_s3/__init__.py diff --git a/moto/s3/models.py b/moto/s3/models.py index b824c4dbf..c1a4fb04d 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -11,7 +11,7 @@ import six from bisect import insort from moto.core import BaseBackend, BaseModel from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime -from .exceptions import BucketAlreadyExists, MissingBucket, InvalidPart, EntityTooSmall +from .exceptions import BucketAlreadyExists, MissingBucket, InvalidPart, EntityTooSmall, MissingKey from .utils import clean_key_name, _VersionedKeyStore UPLOAD_ID_BYTES = 43 @@ -43,6 +43,7 @@ class FakeKey(BaseModel): self._etag = etag self._version_id = version_id self._is_versioned = is_versioned + self._tagging = FakeTagging() @property def version_id(self): @@ -59,6 +60,9 @@ class FakeKey(BaseModel): self._metadata = {} self._metadata.update(metadata) + def set_tagging(self, tagging): + self._tagging = tagging + def set_storage_class(self, storage_class): self._storage_class = storage_class @@ -103,6 +107,10 @@ class FakeKey(BaseModel): def metadata(self): return self._metadata + @property + def tagging(self): + return self._tagging + @property def response_dict(self): res = { @@ -253,6 +261,25 @@ def get_canned_acl(acl): return FakeAcl(grants=grants) +class FakeTagging(BaseModel): + + def __init__(self, tag_set=None): + self.tag_set = tag_set or FakeTagSet() + + +class FakeTagSet(BaseModel): + + def __init__(self, tags=None): + self.tags = tags or [] + + +class FakeTag(BaseModel): + + def __init__(self, key, value=None): + self.key = key + self.value = value + + class LifecycleRule(BaseModel): def __init__(self, id=None, prefix=None, status=None, expiration_days=None, @@ -475,6 +502,13 @@ class S3Backend(BaseBackend): else: return None + def set_key_tagging(self, bucket_name, key_name, tagging): + key = self.get_key(bucket_name, key_name) + if key is None: + raise MissingKey(key_name) + key.set_tagging(tagging) + return key + def initiate_multipart(self, bucket_name, key_name, metadata): bucket = self.get_bucket(bucket_name) new_multipart = FakeMultipart(key_name, metadata) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 3b349d864..a1d5757c8 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -14,7 +14,7 @@ from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_n from .exceptions import BucketAlreadyExists, S3ClientError, MissingKey, InvalidPartOrder -from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey +from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey, FakeTagging, FakeTagSet, FakeTag from .utils import bucket_name_from_url, metadata_from_headers from xml.dom import minidom @@ -520,6 +520,9 @@ class ResponseObject(_TemplateEnvironmentMixin): if 'acl' in query: template = self.response_template(S3_OBJECT_ACL_RESPONSE) return 200, response_headers, template.render(obj=key) + if 'tagging' in query: + template = self.response_template(S3_OBJECT_TAGGING_RESPONSE) + return 200, response_headers, template.render(obj=key) response_headers.update(key.metadata) response_headers.update(key.response_dict) @@ -556,6 +559,7 @@ class ResponseObject(_TemplateEnvironmentMixin): storage_class = request.headers.get('x-amz-storage-class', 'STANDARD') acl = self._acl_from_headers(request.headers) + tagging = self._tagging_from_headers(request.headers) if 'acl' in query: key = self.backend.get_key(bucket_name, key_name) @@ -563,6 +567,11 @@ class ResponseObject(_TemplateEnvironmentMixin): key.set_acl(acl) return 200, response_headers, "" + if 'tagging' in query: + tagging = self._tagging_from_xml(body) + self.backend.set_key_tagging(bucket_name, key_name, tagging) + return 200, response_headers, "" + if 'x-amz-copy-source' in request.headers: # Copy key src_key_parsed = urlparse(request.headers.get("x-amz-copy-source")) @@ -596,6 +605,7 @@ class ResponseObject(_TemplateEnvironmentMixin): new_key.set_metadata(metadata) new_key.set_acl(acl) new_key.website_redirect_location = request.headers.get('x-amz-website-redirect-location') + new_key.set_tagging(tagging) template = self.response_template(S3_OBJECT_RESPONSE) response_headers.update(new_key.response_dict) @@ -655,6 +665,30 @@ class ResponseObject(_TemplateEnvironmentMixin): else: return None + def _tagging_from_headers(self, headers): + if headers.get('x-amz-tagging'): + parsed_header = parse_qs(headers['x-amz-tagging'], keep_blank_values=True) + tags = [] + for tag in parsed_header.items(): + tags.append(FakeTag(tag[0], tag[1][0])) + + tag_set = FakeTagSet(tags) + tagging = FakeTagging(tag_set) + return tagging + else: + return FakeTagging() + + def _tagging_from_xml(self, xml): + parsed_xml = xmltodict.parse(xml) + + tags = [] + for tag in parsed_xml['Tagging']['TagSet']['Tag']: + tags.append(FakeTag(tag['Key'], tag['Value'])) + + tag_set = FakeTagSet(tags) + tagging = FakeTagging(tag_set) + return tagging + def _key_response_delete(self, bucket_name, query, key_name, headers): if query.get('uploadId'): upload_id = query['uploadId'][0] @@ -968,6 +1002,19 @@ S3_OBJECT_ACL_RESPONSE = """ """ +S3_OBJECT_TAGGING_RESPONSE = """\ + + + + {% for tag in obj.tagging.tag_set.tags %} + + {{ tag.key }} + {{ tag.value }} + + {% endfor %} + +""" + S3_OBJECT_COPY_RESPONSE = """\ {{ key.etag }} diff --git a/tests/test_s3/__init__.py b/tests/test_s3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 1cb00d4be..6e6b999ce 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1340,6 +1340,98 @@ def test_boto3_multipart_etag(): resp['ETag'].should.equal(EXPECTED_ETAG) +@mock_s3 +def test_boto3_put_object_with_tagging(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-tags' + s3.create_bucket(Bucket=bucket_name) + + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test', + Tagging='foo=bar', + ) + + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key) + + resp['TagSet'].should.contain({'Key': 'foo', 'Value': 'bar'}) + + +@mock_s3 +def test_boto3_put_object_tagging(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-tags' + s3.create_bucket(Bucket=bucket_name) + + with assert_raises(ClientError) as err: + s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]} + ) + + e = err.exception + e.response['Error'].should.equal({ + 'Code': 'NoSuchKey', + 'Message': 'The specified key does not exist.', + 'RequestID': '7a62c49f-347e-4fc4-9331-6e8eEXAMPLE', + }) + + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test' + ) + + resp = s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]} + ) + + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + + +@mock_s3 +def test_boto3_get_object_tagging(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-tags' + s3.create_bucket(Bucket=bucket_name) + + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test' + ) + + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key) + resp['TagSet'].should.have.length_of(0) + + resp = s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]} + ) + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key) + + resp['TagSet'].should.have.length_of(2) + resp['TagSet'].should.contain({'Key': 'item1', 'Value': 'foo'}) + resp['TagSet'].should.contain({'Key': 'item2', 'Value': 'bar'}) + + @mock_s3 def test_boto3_list_object_versions(): s3 = boto3.client('s3', region_name='us-east-1') From 63b09eae133df001af4391dcdb2c18cfab744a0f Mon Sep 17 00:00:00 2001 From: Christian Hellman Date: Mon, 17 Jul 2017 23:33:40 +0000 Subject: [PATCH 242/274] Added DescribeAccountAttributes --- moto/ec2/responses/__init__.py | 2 + moto/ec2/responses/account_attributes.py | 69 +++++++++++++++++++++++ tests/test_ec2/test_account_attributes.py | 44 +++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 moto/ec2/responses/account_attributes.py create mode 100644 tests/test_ec2/test_account_attributes.py diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py index 449d25a45..1222a7ef8 100644 --- a/moto/ec2/responses/__init__.py +++ b/moto/ec2/responses/__init__.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from .account_attributes import AccountAttributes from .amazon_dev_pay import AmazonDevPay from .amis import AmisResponse from .availability_zones_and_regions import AvailabilityZonesAndRegions @@ -34,6 +35,7 @@ from .nat_gateways import NatGateways class EC2Response( + AccountAttributes, AmazonDevPay, AmisResponse, AvailabilityZonesAndRegions, diff --git a/moto/ec2/responses/account_attributes.py b/moto/ec2/responses/account_attributes.py new file mode 100644 index 000000000..8a5b9a4b0 --- /dev/null +++ b/moto/ec2/responses/account_attributes.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals +from moto.core.responses import BaseResponse + + +class AccountAttributes(BaseResponse): + + def describe_account_attributes(self): + template = self.response_template(DESCRIBE_ACCOUNT_ATTRIBUTES_RESULT) + return template.render() + + +DESCRIBE_ACCOUNT_ATTRIBUTES_RESULT = u""" + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + + vpc-max-security-groups-per-interface + + + 5 + + + + + max-instances + + + 20 + + + + + supported-platforms + + + EC2 + + + VPC + + + + + default-vpc + + + none + + + + + max-elastic-ips + + + 5 + + + + + vpc-max-elastic-ips + + + 5 + + + + + +""" diff --git a/tests/test_ec2/test_account_attributes.py b/tests/test_ec2/test_account_attributes.py new file mode 100644 index 000000000..30309bec8 --- /dev/null +++ b/tests/test_ec2/test_account_attributes.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals +import boto3 +from moto import mock_ec2 +import sure # noqa + + +@mock_ec2 +def test_describe_account_attributes(): + conn = boto3.client('ec2', region_name='us-east-1') + response = conn.describe_account_attributes() + expected_attribute_values = [{ + 'AttributeValues': [{ + 'AttributeValue': '5' + }], + 'AttributeName': 'vpc-max-security-groups-per-interface' + }, { + 'AttributeValues': [{ + 'AttributeValue': '20' + }], + 'AttributeName': 'max-instances' + }, { + 'AttributeValues': [{ + 'AttributeValue': 'EC2' + }, { + 'AttributeValue': 'VPC' + }], + 'AttributeName': 'supported-platforms' + }, { + 'AttributeValues': [{ + 'AttributeValue': 'none' + }], + 'AttributeName': 'default-vpc' + }, { + 'AttributeValues': [{ + 'AttributeValue': '5' + }], + 'AttributeName': 'max-elastic-ips' + }, { + 'AttributeValues': [{ + 'AttributeValue': '5' + }], + 'AttributeName': 'vpc-max-elastic-ips' + }] + response['AccountAttributes'].should.equal(expected_attribute_values) From 73ede75c392e43de47ba804ea165256c5ab39bd3 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 19 Jul 2017 12:20:01 -0700 Subject: [PATCH 243/274] Adding test for ELBv1 security groups --- tests/test_elb/test_elb.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index f9019eed2..681ffb830 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -143,6 +143,28 @@ def test_describe_paginated_balancers(): assert 'NextToken' not in resp2.keys() +@mock_elb +@mock_ec2 +def test_add_and_remove_security_groups(): + client = boto3.client('elb', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-west-1') + + vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') + security_group = ec2.create_security_group( + GroupName='sg01', Description='Test security group sg01', VpcId=vpc.id) + + client.create_load_balancer( + LoadBalancerName='my-lb', + Listeners=[ + {'Protocol': 'tcp', 'LoadBalancerPort': 80, 'InstancePort': 8080}], + AvailabilityZones=['us-east-1a', 'us-east-1b'] + ) + + response = client.apply_security_groups_to_load_balancer( + LoadBalancerName='my-lb', + SecurityGroups=[security_group.id]) + assert response['SecurityGroups'] == [security_group.id] + @mock_elb_deprecated def test_add_listener(): From 7d0a575ab10ee8b7975076238d2aafc2472c2a3a Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 19 Jul 2017 12:20:58 -0700 Subject: [PATCH 244/274] Removing unused import --- tests/test_elb/test_elb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 681ffb830..1feed433e 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -11,7 +11,6 @@ from boto.ec2.elb.attributes import ( ) from boto.ec2.elb.policies import ( Policies, - AppCookieStickinessPolicy, LBCookieStickinessPolicy, OtherPolicy, ) From b512316c828f8c484f7fd781dde031975ca61205 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 19 Jul 2017 12:36:04 -0700 Subject: [PATCH 245/274] removing further unused imports --- tests/test_elb/test_elb.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 1feed433e..35dddc39e 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -9,11 +9,6 @@ from boto.ec2.elb.attributes import ( ConnectionDrainingAttribute, AccessLogAttribute, ) -from boto.ec2.elb.policies import ( - Policies, - LBCookieStickinessPolicy, - OtherPolicy, -) from botocore.exceptions import ClientError from boto.exception import BotoServerError from nose.tools import assert_raises From 6ed8d12317f2f223767320930d8f886472796d05 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 19 Jul 2017 15:58:49 -0700 Subject: [PATCH 246/274] Enforcing ELB security groups must be real --- moto/ec2/models.py | 2 +- moto/elb/exceptions.py | 8 ++++++++ moto/elb/models.py | 27 ++++++++++++++++++++++++--- moto/elb/responses.py | 34 ++++++++++++++++++++++++++++------ tests/test_elb/test_elb.py | 21 ++++++++++++++++++--- 5 files changed, 79 insertions(+), 13 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 7e3df9880..6c35093e2 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -3553,8 +3553,8 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, DHCPOptionsSetBackend, NetworkAclBackend, VpnGatewayBackend, CustomerGatewayBackend, NatGatewayBackend): def __init__(self, region_name): - super(EC2Backend, self).__init__() self.region_name = region_name + super(EC2Backend, self).__init__() # Default VPC exists by default, which is the current behavior # of EC2-VPC. See for detail: diff --git a/moto/elb/exceptions.py b/moto/elb/exceptions.py index 6c316ef47..3ea6a1642 100644 --- a/moto/elb/exceptions.py +++ b/moto/elb/exceptions.py @@ -64,3 +64,11 @@ class EmptyListenersError(ELBClientError): super(EmptyListenersError, self).__init__( "ValidationError", "Listeners cannot be empty") + + +class InvalidSecurityGroupError(ELBClientError): + + def __init__(self): + super(InvalidSecurityGroupError, self).__init__( + "ValidationError", + "One or more of the specified security groups do not exist.") diff --git a/moto/elb/models.py b/moto/elb/models.py index d09548340..504c68908 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -20,6 +20,7 @@ from .exceptions import ( DuplicateLoadBalancerName, DuplicateListenerError, EmptyListenersError, + InvalidSecurityGroupError, LoadBalancerNotFoundError, TooManyTagsError, ) @@ -63,7 +64,7 @@ class FakeBackend(BaseModel): class FakeLoadBalancer(BaseModel): - def __init__(self, name, zones, ports, scheme='internet-facing', vpc_id=None, subnets=None): + def __init__(self, name, zones, ports, scheme='internet-facing', vpc_id=None, subnets=None, security_groups=None): self.name = name self.health_check = None self.instance_ids = [] @@ -77,6 +78,7 @@ class FakeLoadBalancer(BaseModel): self.policies.other_policies = [] self.policies.app_cookie_stickiness_policies = [] self.policies.lb_cookie_stickiness_policies = [] + self.security_groups = security_groups or [] self.subnets = subnets or [] self.vpc_id = vpc_id or 'vpc-56e10e3d' self.tags = {} @@ -233,7 +235,7 @@ class ELBBackend(BaseBackend): self.__dict__ = {} self.__init__(region_name) - def create_load_balancer(self, name, zones, ports, scheme='internet-facing', subnets=None): + def create_load_balancer(self, name, zones, ports, scheme='internet-facing', subnets=None, security_groups=None): vpc_id = None ec2_backend = ec2_backends[self.region_name] if subnets: @@ -243,8 +245,19 @@ class ELBBackend(BaseBackend): raise DuplicateLoadBalancerName(name) if not ports: raise EmptyListenersError() + if not security_groups: + security_groups = [] + for security_group in security_groups: + if ec2_backend.get_security_group_from_id(security_group) is None: + raise InvalidSecurityGroupError() new_load_balancer = FakeLoadBalancer( - name=name, zones=zones, ports=ports, scheme=scheme, subnets=subnets, vpc_id=vpc_id) + name=name, + zones=zones, + ports=ports, + scheme=scheme, + subnets=subnets, + security_groups=security_groups, + vpc_id=vpc_id) self.load_balancers[name] = new_load_balancer return new_load_balancer @@ -302,6 +315,14 @@ class ELBBackend(BaseBackend): def get_load_balancer(self, load_balancer_name): return self.load_balancers.get(load_balancer_name) + def apply_security_groups_to_load_balancer(self, load_balancer_name, security_group_ids): + load_balancer = self.load_balancers.get(load_balancer_name) + ec2_backend = ec2_backends[self.region_name] + for security_group_id in security_group_ids: + if ec2_backend.get_security_group_from_id(security_group_id) is None: + raise InvalidSecurityGroupError() + load_balancer.security_groups = security_group_ids + def configure_health_check(self, load_balancer_name, timeout, healthy_threshold, unhealthy_threshold, interval, target): diff --git a/moto/elb/responses.py b/moto/elb/responses.py index ec20486f0..659c454b1 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -27,6 +27,7 @@ class ELBResponse(BaseResponse): ports = self._get_list_prefix("Listeners.member") scheme = self._get_param('Scheme') subnets = self._get_multi_param("Subnets.member") + security_groups = self._get_multi_param("SecurityGroups.member") load_balancer = self.elb_backend.create_load_balancer( name=load_balancer_name, @@ -34,6 +35,7 @@ class ELBResponse(BaseResponse): ports=ports, scheme=scheme, subnets=subnets, + security_groups=security_groups, ) self._add_tags(load_balancer) template = self.response_template(CREATE_LOAD_BALANCER_TEMPLATE) @@ -84,6 +86,13 @@ class ELBResponse(BaseResponse): template = self.response_template(DELETE_LOAD_BALANCER_TEMPLATE) return template.render() + def apply_security_groups_to_load_balancer(self): + load_balancer_name = self._get_param('LoadBalancerName') + security_group_ids = self._get_multi_param("SecurityGroups.member") + self.elb_backend.apply_security_groups_to_load_balancer(load_balancer_name, security_group_ids) + template = self.response_template(APPLY_SECURITY_GROUPS_TEMPLATE) + return template.render(security_group_ids=security_group_ids) + def configure_health_check(self): check = self.elb_backend.configure_health_check( load_balancer_name=self._get_param('LoadBalancerName'), @@ -99,8 +108,7 @@ class ELBResponse(BaseResponse): def register_instances_with_load_balancer(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [value[0] for key, value in self.querystring.items( - ) if "Instances.member" in key] + instance_ids = [param.values()[0] for param in self._get_list_prefix('Instances.member')] template = self.response_template(REGISTER_INSTANCES_TEMPLATE) load_balancer = self.elb_backend.register_instances( load_balancer_name, instance_ids) @@ -119,8 +127,7 @@ class ELBResponse(BaseResponse): def deregister_instances_from_load_balancer(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [value[0] for key, value in self.querystring.items( - ) if "Instances.member" in key] + instance_ids = [param.values()[0] for param in self._get_list_prefix('Instances.member')] template = self.response_template(DEREGISTER_INSTANCES_TEMPLATE) load_balancer = self.elb_backend.deregister_instances( load_balancer_name, instance_ids) @@ -252,8 +259,7 @@ class ELBResponse(BaseResponse): def describe_instance_health(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [value[0] for key, value in self.querystring.items( - ) if "Instances.member" in key] + instance_ids = [param.values()[0] for param in self._get_list_prefix('Instances.member')] if len(instance_ids) == 0: instance_ids = self.elb_backend.get_load_balancer( load_balancer_name).instance_ids @@ -400,6 +406,9 @@ DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ + + + {% for security_group_id in security_group_ids %} + {{ security_group_id }} + {% endfor %} + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + CONFIGURE_HEALTH_CHECK_TEMPLATE = """ diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 35dddc39e..98ec7d8e6 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -18,17 +18,22 @@ from moto import mock_elb, mock_ec2, mock_elb_deprecated, mock_ec2_deprecated @mock_elb_deprecated +@mock_ec2_deprecated def test_create_load_balancer(): conn = boto.connect_elb() + ec2 = boto.connect_ec2('the_key', 'the_secret') + + security_group = ec2.create_security_group('sg-abc987', 'description') zones = ['us-east-1a', 'us-east-1b'] ports = [(80, 8080, 'http'), (443, 8443, 'tcp')] - conn.create_load_balancer('my-lb', zones, ports, scheme='internal') + conn.create_load_balancer('my-lb', zones, ports, scheme='internal', security_groups=[security_group.id]) balancers = conn.get_all_load_balancers() balancer = balancers[0] balancer.name.should.equal("my-lb") balancer.scheme.should.equal("internal") + list(balancer.security_groups).should.equal([security_group.id]) set(balancer.availability_zones).should.equal( set(['us-east-1a', 'us-east-1b'])) listener1 = balancer.listeners[0] @@ -139,9 +144,9 @@ def test_describe_paginated_balancers(): @mock_elb @mock_ec2 -def test_add_and_remove_security_groups(): +def test_apply_security_groups_to_load_balancer(): client = boto3.client('elb', region_name='us-east-1') - ec2 = boto3.resource('ec2', region_name='us-west-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') security_group = ec2.create_security_group( @@ -157,7 +162,17 @@ def test_add_and_remove_security_groups(): response = client.apply_security_groups_to_load_balancer( LoadBalancerName='my-lb', SecurityGroups=[security_group.id]) + assert response['SecurityGroups'] == [security_group.id] + balancer = client.describe_load_balancers()['LoadBalancerDescriptions'][0] + assert balancer['SecurityGroups'] == [security_group.id] + + # Usign a not-real security group raises an error + with assert_raises(ClientError) as error: + response = client.apply_security_groups_to_load_balancer( + LoadBalancerName='my-lb', + SecurityGroups=['not-really-a-security-group']) + assert "One or more of the specified security groups do not exist." in str(error.exception) @mock_elb_deprecated From 45d723044099d19ced81118c2ebc7c87b79d65ed Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 19 Jul 2017 16:01:00 -0700 Subject: [PATCH 247/274] fixing typo --- tests/test_elb/test_elb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elb/test_elb.py b/tests/test_elb/test_elb.py index 98ec7d8e6..5827e70c7 100644 --- a/tests/test_elb/test_elb.py +++ b/tests/test_elb/test_elb.py @@ -167,7 +167,7 @@ def test_apply_security_groups_to_load_balancer(): balancer = client.describe_load_balancers()['LoadBalancerDescriptions'][0] assert balancer['SecurityGroups'] == [security_group.id] - # Usign a not-real security group raises an error + # Using a not-real security group raises an error with assert_raises(ClientError) as error: response = client.apply_security_groups_to_load_balancer( LoadBalancerName='my-lb', From 7d00f6e92c1434d55be4d4da6c2b8bc2ad091433 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 19 Jul 2017 16:33:24 -0700 Subject: [PATCH 248/274] python 2 support for dict_values --- moto/elb/responses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/elb/responses.py b/moto/elb/responses.py index 659c454b1..b1980c9b2 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -108,7 +108,7 @@ class ELBResponse(BaseResponse): def register_instances_with_load_balancer(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [param.values()[0] for param in self._get_list_prefix('Instances.member')] + instance_ids = [list(param.values())[0] for param in self._get_list_prefix('Instances.member')] template = self.response_template(REGISTER_INSTANCES_TEMPLATE) load_balancer = self.elb_backend.register_instances( load_balancer_name, instance_ids) @@ -127,7 +127,7 @@ class ELBResponse(BaseResponse): def deregister_instances_from_load_balancer(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [param.values()[0] for param in self._get_list_prefix('Instances.member')] + instance_ids = [list(param.values())[0] for param in self._get_list_prefix('Instances.member')] template = self.response_template(DEREGISTER_INSTANCES_TEMPLATE) load_balancer = self.elb_backend.deregister_instances( load_balancer_name, instance_ids) @@ -259,7 +259,7 @@ class ELBResponse(BaseResponse): def describe_instance_health(self): load_balancer_name = self._get_param('LoadBalancerName') - instance_ids = [param.values()[0] for param in self._get_list_prefix('Instances.member')] + instance_ids = [list(param.values())[0] for param in self._get_list_prefix('Instances.member')] if len(instance_ids) == 0: instance_ids = self.elb_backend.get_load_balancer( load_balancer_name).instance_ids From 115b9cee3e823208fa8ff389f6fd955b849ae673 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Thu, 20 Jul 2017 14:25:46 +1000 Subject: [PATCH 249/274] add CloudFormation model for Kinesis streams --- moto/cloudformation/parsing.py | 2 ++ moto/kinesis/models.py | 7 +++++ .../test_cloudformation_stack_crud.py | 29 ++++++++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 928cd68e0..923ada058 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -15,6 +15,7 @@ from moto.ec2 import models as ec2_models from moto.ecs import models as ecs_models from moto.elb import models as elb_models from moto.iam import models as iam_models +from moto.kinesis import models as kinesis_models from moto.kms import models as kms_models from moto.rds import models as rds_models from moto.rds2 import models as rds2_models @@ -31,6 +32,7 @@ MODEL_MAP = { "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, "AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration, "AWS::DynamoDB::Table": dynamodb_models.Table, + "AWS::Kinesis::Stream": kinesis_models.Stream, "AWS::Lambda::EventSourceMapping": lambda_models.EventSourceMapping, "AWS::Lambda::Function": lambda_models.LambdaFunction, "AWS::Lambda::Version": lambda_models.LambdaVersion, diff --git a/moto/kinesis/models.py b/moto/kinesis/models.py index 13900e6a6..aae94bbbd 100644 --- a/moto/kinesis/models.py +++ b/moto/kinesis/models.py @@ -172,6 +172,13 @@ class Stream(BaseModel): } } + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + region = properties.get('Region', 'us-east-1') + shard_count = properties.get('ShardCount', 1) + return Stream(properties['Name'], shard_count, region) + class FirehoseRecord(BaseModel): diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 0e3634756..801faf8a1 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -569,7 +569,6 @@ def test_describe_stack_events_shows_create_update_and_delete(): @mock_cloudformation_deprecated -@mock_route53_deprecated def test_create_stack_lambda_and_dynamodb(): conn = boto.connect_cloudformation() dummy_template = { @@ -643,3 +642,31 @@ def test_create_stack_lambda_and_dynamodb(): stack = conn.describe_stacks()[0] resources = stack.list_resources() assert len(resources) == 4 + + +@mock_cloudformation_deprecated +def test_create_stack_kinesis(): + conn = boto.connect_cloudformation() + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack Kinesis Test 1", + "Parameters": {}, + "Resources": { + "stream1": { + "Type" : "AWS::Kinesis::Stream", + "Properties" : { + "Name": "stream1", + "ShardCount": 2 + } + } + } + } + conn.create_stack( + "test_stack_kinesis_1", + template_body=json.dumps(dummy_template), + parameters={}.items() + ) + + stack = conn.describe_stacks()[0] + resources = stack.list_resources() + assert len(resources) == 1 From 38fa6809c086b0aae80fa9c6746671627b63e239 Mon Sep 17 00:00:00 2001 From: Taro Sato Date: Wed, 19 Jul 2017 17:18:31 -0700 Subject: [PATCH 250/274] Make HEAD bucket throw ClientError instead of NoSuchBucket on boto3 --- moto/s3/responses.py | 11 +++++++++-- tests/test_s3/test_s3.py | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index a1d5757c8..ec1361cb8 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -13,7 +13,7 @@ from moto.core.responses import _TemplateEnvironmentMixin from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_name_from_url, parse_key_name as bucketpath_parse_key_name, is_delete_keys as bucketpath_is_delete_keys -from .exceptions import BucketAlreadyExists, S3ClientError, MissingKey, InvalidPartOrder +from .exceptions import BucketAlreadyExists, S3ClientError, MissingBucket, MissingKey, InvalidPartOrder from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey, FakeTagging, FakeTagSet, FakeTag from .utils import bucket_name_from_url, metadata_from_headers from xml.dom import minidom @@ -155,7 +155,14 @@ class ResponseObject(_TemplateEnvironmentMixin): "Method {0} has not been impelemented in the S3 backend yet".format(method)) def _bucket_response_head(self, bucket_name, headers): - self.backend.get_bucket(bucket_name) + try: + self.backend.get_bucket(bucket_name) + except MissingBucket: + # Unless we do this, boto3 does not raise ClientError on + # HEAD (which the real API responds with), and instead + # raises NoSuchBucket, leading to inconsistency in + # error response between real and mocked responses. + return 404, {}, "Not Found" return 200, {}, "" def _bucket_response_get(self, bucket_name, querystring, headers): diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 6e6b999ce..26b25dd9a 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1250,6 +1250,31 @@ def test_boto3_head_object(): e.exception.response['Error']['Code'].should.equal('404') +@mock_s3 +def test_boto3_bucket_deletion(): + cli = boto3.client('s3', region_name='us-east-1') + cli.create_bucket(Bucket="foobar") + + cli.put_object(Bucket="foobar", Key="the-key", Body="some value") + + # Try to delete a bucket that still has keys + cli.delete_bucket.when.called_with(Bucket="foobar").should.throw( + cli.exceptions.ClientError, + ('An error occurred (BucketNotEmpty) when calling the DeleteBucket operation: ' + 'The bucket you tried to delete is not empty')) + + cli.delete_object(Bucket="foobar", Key="the-key") + cli.delete_bucket(Bucket="foobar") + + # Get non-existing bucket + cli.head_bucket.when.called_with(Bucket="foobar").should.throw( + cli.exceptions.ClientError, + "An error occurred (404) when calling the HeadBucket operation: Not Found") + + # Delete non-existing bucket + cli.delete_bucket.when.called_with(Bucket="foobar").should.throw(cli.exceptions.NoSuchBucket) + + @mock_s3 def test_boto3_get_object(): s3 = boto3.resource('s3', region_name='us-east-1') @@ -1560,4 +1585,3 @@ TEST_XML = """\ """ - From 025e975e446f0945c53182dbe6aebfe43c07273f Mon Sep 17 00:00:00 2001 From: William Richard Date: Tue, 25 Jul 2017 17:54:05 -0400 Subject: [PATCH 251/274] Add ecr get_authorization_token response and tests --- moto/ecr/responses.py | 17 ++++++++++---- tests/test_ecr/test_ecr_boto3.py | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index 40d8cfb66..6c12a186d 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -1,12 +1,13 @@ from __future__ import unicode_literals import json +from datetime import datetime +import time from moto.core.responses import BaseResponse from .models import ecr_backends class ECRResponse(BaseResponse): - @property def ecr_backend(self): return ecr_backends[self.region] @@ -111,9 +112,17 @@ class ECRResponse(BaseResponse): 'ECR.generate_presigned_url is not yet implemented') def get_authorization_token(self): - if self.is_not_dryrun('GetAuthorizationToken'): - raise NotImplementedError( - 'ECR.get_authorization_token is not yet implemented') + registry_ids = self._get_param('registryIds') + if not registry_ids: + registry_ids = [self.region] + auth_data = [] + for registry_id in registry_ids: + auth_data.append({ + 'authorizationToken': '{}-auth-token'.format(registry_id), + 'expiresAt': time.mktime(datetime(2015, 1, 1).timetuple()), + 'proxyEndpoint': 'https://012345678910.dkr.ecr.{}.amazonaws.com'.format(registry_id) + }) + return json.dumps({'authorizationData': auth_data}) def get_download_url_for_layer(self): if self.is_not_dryrun('GetDownloadUrlForLayer'): diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 647015446..5a10fb778 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -2,11 +2,13 @@ from __future__ import unicode_literals import hashlib import json +from datetime import datetime from random import random import sure # noqa import boto3 +from dateutil.tz import tzlocal from moto import mock_ecr @@ -368,3 +370,39 @@ def test_describe_images_by_digest(): image_detail['repositoryName'].should.equal("test_repository") image_detail['imageTags'].should.equal([put_response['imageId']['imageTag']]) image_detail['imageDigest'].should.equal(digest) + + +@mock_ecr +def test_get_authorization_token_assume_region(): + client = boto3.client('ecr', region_name='us-east-1') + auth_token_response = client.get_authorization_token() + + list(auth_token_response.keys()).should.equal(['authorizationData', 'ResponseMetadata']) + auth_token_response['authorizationData'].should.equal([ + { + 'authorizationToken': 'us-east-1-auth-token', + 'proxyEndpoint': 'https://012345678910.dkr.ecr.us-east-1.amazonaws.com', + 'expiresAt': datetime(2015, 1, 1, tzinfo=tzlocal()) + }, + ]) + + +@mock_ecr +def test_get_authorization_token_explicit_regions(): + client = boto3.client('ecr', region_name='us-east-1') + auth_token_response = client.get_authorization_token(registryIds=['us-east-1', 'us-west-1']) + + list(auth_token_response.keys()).should.equal(['authorizationData', 'ResponseMetadata']) + auth_token_response['authorizationData'].should.equal([ + { + 'authorizationToken': 'us-east-1-auth-token', + 'proxyEndpoint': 'https://012345678910.dkr.ecr.us-east-1.amazonaws.com', + 'expiresAt': datetime(2015, 1, 1, tzinfo=tzlocal()), + }, + { + 'authorizationToken': 'us-west-1-auth-token', + 'proxyEndpoint': 'https://012345678910.dkr.ecr.us-west-1.amazonaws.com', + 'expiresAt': datetime(2015, 1, 1, tzinfo=tzlocal()) + + } + ]) From a5089c3d690ad835d3b6034cd7948662e1d753fa Mon Sep 17 00:00:00 2001 From: James Brennan Date: Wed, 26 Jul 2017 11:38:12 +0000 Subject: [PATCH 252/274] Add add, remove, list endpoints for SSM tags --- moto/ssm/models.py | 16 ++++++++++++++++ moto/ssm/responses.py | 25 +++++++++++++++++++++++++ tests/test_ssm/test_ssm_boto3.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index f1aac336b..63cb3c8ba 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from collections import defaultdict + from moto.core import BaseBackend, BaseModel from moto.ec2 import ec2_backends @@ -42,6 +44,7 @@ class SimpleSystemManagerBackend(BaseBackend): def __init__(self): self._parameters = {} + self._resource_tags = defaultdict(lambda: defaultdict(dict)) def delete_parameter(self, name): try: @@ -68,6 +71,19 @@ class SimpleSystemManagerBackend(BaseBackend): self._parameters[name] = Parameter( name, value, type, description, keyid) + def add_tags_to_resource(self, resource_type, resource_id, tags): + for key, value in tags.items(): + self._resource_tags[resource_type][resource_id][key] = value + + def remove_tags_from_resource(self, resource_type, resource_id, keys): + tags = self._resource_tags[resource_type][resource_id] + for key in keys: + if key in tags: + del tags[key] + + def list_tags_for_resource(self, resource_type, resource_id): + return self._resource_tags[resource_type][resource_id] + ssm_backends = {} for region, ec2_backend in ec2_backends.items(): diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index 09fe6d0c2..1fa1a81b2 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -108,3 +108,28 @@ class SimpleSystemManagerResponse(BaseResponse): self.ssm_backend.put_parameter( name, description, value, type_, keyid, overwrite) return json.dumps({}) + + def add_tags_to_resource(self): + resource_id = self._get_param('ResourceId') + resource_type = self._get_param('ResourceType') + tags = {t['Key']: t['Value'] for t in self._get_param('Tags')} + self.ssm_backend.add_tags_to_resource( + resource_id, resource_type, tags) + return json.dumps({}) + + def remove_tags_from_resource(self): + resource_id = self._get_param('ResourceId') + resource_type = self._get_param('ResourceType') + keys = self._get_param('TagKeys') + self.ssm_backend.remove_tags_from_resource( + resource_id, resource_type, keys) + return json.dumps({}) + + def list_tags_for_resource(self): + resource_id = self._get_param('ResourceId') + resource_type = self._get_param('ResourceType') + tags = self.ssm_backend.list_tags_for_resource( + resource_id, resource_type) + tag_list = [{'Key': k, 'Value': v} for (k, v) in tags.items()] + response = {'TagList': tag_list} + return json.dumps(response) diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 60a027933..418c58708 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -247,3 +247,33 @@ def test_put_parameter_secure_custom_kms(): response['Parameters'][0]['Name'].should.equal('test') response['Parameters'][0]['Value'].should.equal('value') response['Parameters'][0]['Type'].should.equal('SecureString') + +@mock_ssm +def test_add_remove_list_tags_for_resource(): + client = boto3.client('ssm', region_name='us-east-1') + + client.add_tags_to_resource( + ResourceId='test', + ResourceType='Parameter', + Tags=[{'Key': 'test-key', 'Value': 'test-value'}] + ) + + response = client.list_tags_for_resource( + ResourceId='test', + ResourceType='Parameter' + ) + len(response['TagList']).should.equal(1) + response['TagList'][0]['Key'].should.equal('test-key') + response['TagList'][0]['Value'].should.equal('test-value') + + client.remove_tags_from_resource( + ResourceId='test', + ResourceType='Parameter', + TagKeys=['test-key'] + ) + + response = client.list_tags_for_resource( + ResourceId='test', + ResourceType='Parameter' + ) + len(response['TagList']).should.equal(0) From aeefc8056d34dc8830342bfe70326700a9df1b67 Mon Sep 17 00:00:00 2001 From: William Richard Date: Wed, 26 Jul 2017 12:03:20 -0400 Subject: [PATCH 253/274] Boto actually returns a base64 encoded string of : Fix the mock to do the same thing --- moto/ecr/responses.py | 5 ++++- tests/test_ecr/test_ecr_boto3.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py index 6c12a186d..4fa0946b8 100644 --- a/moto/ecr/responses.py +++ b/moto/ecr/responses.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import json +from base64 import b64encode from datetime import datetime import time @@ -117,8 +118,10 @@ class ECRResponse(BaseResponse): registry_ids = [self.region] auth_data = [] for registry_id in registry_ids: + password = '{}-auth-token'.format(registry_id) + auth_token = b64encode("AWS:{}".format(password).encode('ascii')).decode() auth_data.append({ - 'authorizationToken': '{}-auth-token'.format(registry_id), + 'authorizationToken': auth_token, 'expiresAt': time.mktime(datetime(2015, 1, 1).timetuple()), 'proxyEndpoint': 'https://012345678910.dkr.ecr.{}.amazonaws.com'.format(registry_id) }) diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py index 5a10fb778..581906321 100644 --- a/tests/test_ecr/test_ecr_boto3.py +++ b/tests/test_ecr/test_ecr_boto3.py @@ -380,7 +380,7 @@ def test_get_authorization_token_assume_region(): list(auth_token_response.keys()).should.equal(['authorizationData', 'ResponseMetadata']) auth_token_response['authorizationData'].should.equal([ { - 'authorizationToken': 'us-east-1-auth-token', + 'authorizationToken': 'QVdTOnVzLWVhc3QtMS1hdXRoLXRva2Vu', 'proxyEndpoint': 'https://012345678910.dkr.ecr.us-east-1.amazonaws.com', 'expiresAt': datetime(2015, 1, 1, tzinfo=tzlocal()) }, @@ -395,12 +395,12 @@ def test_get_authorization_token_explicit_regions(): list(auth_token_response.keys()).should.equal(['authorizationData', 'ResponseMetadata']) auth_token_response['authorizationData'].should.equal([ { - 'authorizationToken': 'us-east-1-auth-token', + 'authorizationToken': 'QVdTOnVzLWVhc3QtMS1hdXRoLXRva2Vu', 'proxyEndpoint': 'https://012345678910.dkr.ecr.us-east-1.amazonaws.com', 'expiresAt': datetime(2015, 1, 1, tzinfo=tzlocal()), }, { - 'authorizationToken': 'us-west-1-auth-token', + 'authorizationToken': 'QVdTOnVzLXdlc3QtMS1hdXRoLXRva2Vu', 'proxyEndpoint': 'https://012345678910.dkr.ecr.us-west-1.amazonaws.com', 'expiresAt': datetime(2015, 1, 1, tzinfo=tzlocal()) From 3eef3c23b113493027bf1c4de65107719e3e8a74 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 26 Jul 2017 22:57:55 -0700 Subject: [PATCH 254/274] Updating examples in README to latest API --- README.md | 42 +++++++++++++++++++++------------------- tests/test_s3/test_s3.py | 3 +-- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f07984328..369d430f5 100644 --- a/README.md +++ b/README.md @@ -123,28 +123,29 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L Imagine you have a function that you use to launch new ec2 instances: ```python -import boto +import boto3 + def add_servers(ami_id, count): - conn = boto.connect_ec2('the_key', 'the_secret') - for index in range(count): - conn.run_instances(ami_id) + client = boto3.client('ec2', region_name='us-west-1') + client.run_instances(ImageId=ami_id, MinCount=count, MaxCount=count) ``` To test it: ```python from . import add_servers +from moto import mock_ec2 @mock_ec2 def test_add_servers(): add_servers('ami-1234abcd', 2) - conn = boto.connect_ec2('the_key', 'the_secret') - reservations = conn.get_all_instances() - assert len(reservations) == 2 - instance1 = reservations[0].instances[0] - assert instance1.image_id == 'ami-1234abcd' + client = boto3.client('ec2', region_name='us-west-1') + instances = client.describe_instances()['Reservations'][0]['Instances'] + assert len(instances) == 2 + instance1 = instances[0] + assert instance1['ImageId'] == 'ami-1234abcd' ``` ## Usage @@ -156,13 +157,14 @@ All of the services can be used as a decorator, context manager, or in a raw for ```python @mock_s3 def test_my_model_save(): - conn = boto.connect_s3() - conn.create_bucket('mybucket') - + # Create Bucket so that test can run + conn = boto3.resource('s3', region_name='us-east-1') + conn.create_bucket(Bucket='mybucket') model_instance = MyModel('steve', 'is awesome') model_instance.save() + body = conn.Object('mybucket', 'steve').get()['Body'].read().decode() - assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + assert body == 'is awesome' ``` ### Context Manager @@ -170,13 +172,13 @@ def test_my_model_save(): ```python def test_my_model_save(): with mock_s3(): - conn = boto.connect_s3() - conn.create_bucket('mybucket') - + conn = boto3.resource('s3', region_name='us-east-1') + conn.create_bucket(Bucket='mybucket') model_instance = MyModel('steve', 'is awesome') model_instance.save() + body = conn.Object('mybucket', 'steve').get()['Body'].read().decode() - assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + assert body == 'is awesome' ``` @@ -187,13 +189,13 @@ def test_my_model_save(): mock = mock_s3() mock.start() - conn = boto.connect_s3() - conn.create_bucket('mybucket') + conn = boto3.resource('s3', region_name='us-east-1') + conn.create_bucket(Bucket='mybucket') model_instance = MyModel('steve', 'is awesome') model_instance.save() - assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' + assert conn.Object('mybucket', 'steve').get()['Body'].read().decode() == 'is awesome' mock.stop() ``` diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 26b25dd9a..619a60302 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -74,8 +74,7 @@ def test_my_model_save(): model_instance = MyModel('steve', 'is awesome') model_instance.save() - body = conn.Object('mybucket', 'steve').get()[ - 'Body'].read().decode("utf-8") + body = conn.Object('mybucket', 'steve').get()['Body'].read().decode() assert body == 'is awesome' From e445c81e83b2b9602fea4956b9f40e4b339c113b Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Sun, 23 Jul 2017 22:31:58 -0700 Subject: [PATCH 255/274] Implement IAM {update,get}_login_profile --- moto/iam/models.py | 19 ++++++++++++++ moto/iam/responses.py | 51 +++++++++++++++++++++++++++++++++----- tests/test_iam/test_iam.py | 23 +++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index da11d58b2..1e4b58578 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -256,6 +256,7 @@ class User(BaseModel): self.policies = {} self.access_keys = [] self.password = None + self.password_reset_required = False @property def arn(self): @@ -772,6 +773,24 @@ class IAMBackend(BaseBackend): raise IAMConflictException( "User {0} already has password".format(user_name)) user.password = password + return user + + def get_login_profile(self, user_name): + user = self.get_user(user_name) + if not user.password: + raise IAMNotFoundException( + "Login profile for {0} not found".format(user_name)) + return user + + def update_login_profile(self, user_name, password, password_reset_required): + # This does not currently deal with PasswordPolicyViolation. + user = self.get_user(user_name) + if not user.password: + raise IAMNotFoundException( + "Login profile for {0} not found".format(user_name)) + user.password = password + user.password_reset_required = password_reset_required + return user def delete_login_profile(self, user_name): user = self.get_user(user_name) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 138c08d23..a5e5081c3 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -290,10 +290,27 @@ class IamResponse(BaseResponse): def create_login_profile(self): user_name = self._get_param('UserName') password = self._get_param('Password') - iam_backend.create_login_profile(user_name, password) + password = self._get_param('Password') + user = iam_backend.create_login_profile(user_name, password) template = self.response_template(CREATE_LOGIN_PROFILE_TEMPLATE) - return template.render(user_name=user_name) + return template.render(user=user) + + def get_login_profile(self): + user_name = self._get_param('UserName') + user = iam_backend.get_login_profile(user_name) + + template = self.response_template(GET_LOGIN_PROFILE_TEMPLATE) + return template.render(user=user) + + def update_login_profile(self): + user_name = self._get_param('UserName') + password = self._get_param('Password') + password_reset_required = self._get_param('PasswordResetRequired') + user = iam_backend.update_login_profile(user_name, password, password_reset_required) + + template = self.response_template(UPDATE_LOGIN_PROFILE_TEMPLATE) + return template.render(user=user) def add_user_to_group(self): group_name = self._get_param('GroupName') @@ -918,12 +935,11 @@ LIST_USERS_TEMPLATE = """<{{ action }}UsersResponse> """ -CREATE_LOGIN_PROFILE_TEMPLATE = """ - +CREATE_LOGIN_PROFILE_TEMPLATE = """ - {{ user_name }} - 2011-09-19T23:00:56Z + {{ user.name }} + {{ user.created_iso_8601 }} @@ -932,6 +948,29 @@ CREATE_LOGIN_PROFILE_TEMPLATE = """ """ +GET_LOGIN_PROFILE_TEMPLATE = """ + + + {{ user.name }} + {{ user.created_iso_8601 }} + {% if user.password_reset_required %} + true + {% endif %} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + +""" + +UPDATE_LOGIN_PROFILE_TEMPLATE = """ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + +""" + GET_USER_POLICY_TEMPLATE = """ {{ user_name }} diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 46b727360..b5968f722 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -114,6 +114,29 @@ def test_remove_role_from_instance_profile(): dict(profile.roles).should.be.empty +@mock_iam() +def test_get_login_profile(): + conn = boto3.client('iam', region_name='us-east-1') + conn.create_user(UserName='my-user') + conn.create_login_profile(UserName='my-user', Password='my-pass') + + response = conn.get_login_profile(UserName='my-user') + response['LoginProfile']['UserName'].should.equal('my-user') + + +@mock_iam() +def test_update_login_profile(): + conn = boto3.client('iam', region_name='us-east-1') + conn.create_user(UserName='my-user') + conn.create_login_profile(UserName='my-user', Password='my-pass') + response = conn.get_login_profile(UserName='my-user') + response['LoginProfile'].get('PasswordResetRequired').should.equal(None) + + conn.update_login_profile(UserName='my-user', Password='new-pass', PasswordResetRequired=True) + response = conn.get_login_profile(UserName='my-user') + response['LoginProfile'].get('PasswordResetRequired').should.equal(True) + + @mock_iam() def test_delete_role(): conn = boto3.client('iam', region_name='us-east-1') From 92eedcf291999629d1b8333cb2c7e15bc08e4b26 Mon Sep 17 00:00:00 2001 From: sodastsai Date: Sun, 30 Jul 2017 20:44:06 +0800 Subject: [PATCH 256/274] Send JSON message to HTTP endpoint of SNS By the documentation from AWS - http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html , SNS would send messages to HTTP/HTTPS endpoint in JSON format. But current implementation of `moto` sends messages in form-data format. --- moto/sns/models.py | 2 +- tests/test_sns/test_publishing_boto3.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 5289c8bcd..a6b6c3a52 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -84,7 +84,7 @@ class Subscription(BaseModel): sqs_backends[region].send_message(queue_name, message) elif self.protocol in ['http', 'https']: post_data = self.get_post_data(message, message_id) - requests.post(self.endpoint, data=post_data) + requests.post(self.endpoint, json=post_data) def get_post_data(self, message, message_id): return { diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index cda9fed60..00c9ac7e2 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -1,4 +1,7 @@ from __future__ import unicode_literals + +import json + from six.moves.urllib.parse import parse_qs import boto3 @@ -56,9 +59,15 @@ def test_publish_to_sqs_in_different_region(): @freeze_time("2013-01-01") @mock_sns def test_publish_to_http(): - responses.add( + def callback(request): + request.headers["Content-Type"].should.equal("application/json") + json.loads.when.called_with(request.body).should_not.throw(Exception) + return 200, {}, "" + + responses.add_callback( method="POST", url="http://example.com/foobar", + callback=callback, ) conn = boto3.client('sns', region_name='us-east-1') From d76559ee7c8724d86f975a7eb2fb02fb01686dbb Mon Sep 17 00:00:00 2001 From: Peter Us Date: Mon, 31 Jul 2017 13:37:29 +0200 Subject: [PATCH 257/274] SNS delete_topic should also delete subscriptions. --- moto/sns/models.py | 9 ++++++- tests/test_sns/test_subscriptions.py | 31 ++++++++++++++++++++++ tests/test_sns/test_subscriptions_boto3.py | 30 +++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 5289c8bcd..6d0833476 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -193,10 +193,17 @@ class SNSBackend(BaseBackend): next_token = None return values, next_token + def _get_topic_subscriptions(self, topic): + return [sub for sub in self.subscriptions.values() if sub.topic == topic] + def list_topics(self, next_token=None): return self._get_values_nexttoken(self.topics, next_token) def delete_topic(self, arn): + topic = self.get_topic(arn) + subscriptions = self._get_topic_subscriptions(topic) + for sub in subscriptions: + self.unsubscribe(sub.arn) self.topics.pop(arn) def get_topic(self, arn): @@ -222,7 +229,7 @@ class SNSBackend(BaseBackend): if topic_arn: topic = self.get_topic(topic_arn) filtered = OrderedDict( - [(k, sub) for k, sub in self.subscriptions.items() if sub.topic == topic]) + [(sub.arn, sub) for sub in self._get_topic_subscriptions(topic)]) return self._get_values_nexttoken(filtered, next_token) else: return self._get_values_nexttoken(self.subscriptions, next_token) diff --git a/tests/test_sns/test_subscriptions.py b/tests/test_sns/test_subscriptions.py index c521bb428..292fd83c0 100644 --- a/tests/test_sns/test_subscriptions.py +++ b/tests/test_sns/test_subscriptions.py @@ -34,6 +34,37 @@ def test_creating_subscription(): "ListSubscriptionsResult"]["Subscriptions"] subscriptions.should.have.length_of(0) +@mock_sns_deprecated +def test_deleting_subscriptions_by_deleting_topic(): + conn = boto.connect_sns() + conn.create_topic("some-topic") + topics_json = conn.get_all_topics() + topic_arn = topics_json["ListTopicsResponse"][ + "ListTopicsResult"]["Topics"][0]['TopicArn'] + + conn.subscribe(topic_arn, "http", "http://example.com/") + + subscriptions = conn.get_all_subscriptions()["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["Subscriptions"] + subscriptions.should.have.length_of(1) + subscription = subscriptions[0] + subscription["TopicArn"].should.equal(topic_arn) + subscription["Protocol"].should.equal("http") + subscription["SubscriptionArn"].should.contain(topic_arn) + subscription["Endpoint"].should.equal("http://example.com/") + + # Now delete the topic + conn.delete_topic(topic_arn) + + # And there should now be 0 topics + topics_json = conn.get_all_topics() + topics = topics_json["ListTopicsResponse"]["ListTopicsResult"]["Topics"] + topics.should.have.length_of(0) + + # And there should be zero subscriptions left + subscriptions = conn.get_all_subscriptions()["ListSubscriptionsResponse"][ + "ListSubscriptionsResult"]["Subscriptions"] + subscriptions.should.have.length_of(0) @mock_sns_deprecated def test_getting_subscriptions_by_topic(): diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py index 906c483f7..ac325ed20 100644 --- a/tests/test_sns/test_subscriptions_boto3.py +++ b/tests/test_sns/test_subscriptions_boto3.py @@ -33,6 +33,36 @@ def test_creating_subscription(): subscriptions = conn.list_subscriptions()["Subscriptions"] subscriptions.should.have.length_of(0) +@mock_sns +def test_deleting_subscriptions_by_deleting_topic(): + conn = boto3.client('sns', region_name='us-east-1') + conn.create_topic(Name="some-topic") + response = conn.list_topics() + topic_arn = response["Topics"][0]['TopicArn'] + + conn.subscribe(TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/") + + subscriptions = conn.list_subscriptions()["Subscriptions"] + subscriptions.should.have.length_of(1) + subscription = subscriptions[0] + subscription["TopicArn"].should.equal(topic_arn) + subscription["Protocol"].should.equal("http") + subscription["SubscriptionArn"].should.contain(topic_arn) + subscription["Endpoint"].should.equal("http://example.com/") + + # Now delete the topic + conn.delete_topic(TopicArn=topic_arn) + + # And there should now be 0 topics + topics_json = conn.list_topics() + topics = topics_json["Topics"] + topics.should.have.length_of(0) + + # And there should be zero subscriptions left + subscriptions = conn.list_subscriptions()["Subscriptions"] + subscriptions.should.have.length_of(0) @mock_sns def test_getting_subscriptions_by_topic(): From 5011cd28b65a3edf145374fb6a9bec1a5c92c257 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 11:45:27 -0700 Subject: [PATCH 258/274] Allow boto3 redshift cluster subnet group creation Boto3 deviates from the AWS docs in the way subnets are described when creating a Redshift cluster subnet group. This entry in botocore nests the SubnetIds under SubnetIdentifier tags: https://github.com/boto/botocore/blob/develop/botocore/data/redshift/2012-12-01/service-2.json#L5423-L5429 referenced here: https://github.com/boto/botocore/blob/develop/botocore/data/redshift/2012-12-01/service-2.json#L2296 And the AWS docs do not nest them that way: https://docs.aws.amazon.com/redshift/latest/APIReference/API_CreateClusterSubnetGroup.html Fixes #1029 --- moto/redshift/responses.py | 4 ++++ tests/test_redshift/test_redshift.py | 31 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index ba28b1343..48f113cf2 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -122,6 +122,10 @@ class RedshiftResponse(BaseResponse): cluster_subnet_group_name = self._get_param('ClusterSubnetGroupName') description = self._get_param('Description') subnet_ids = self._get_multi_param('SubnetIds.member') + # There's a bug in boto3 where the subnet ids are not passed + # according to the AWS documentation + if not subnet_ids: + subnet_ids = self._get_multi_param('SubnetIds.SubnetIdentifier') subnet_group = self.redshift_backend.create_cluster_subnet_group( cluster_subnet_group_name=cluster_subnet_group_name, diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index 045e30246..aff3e8bed 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -11,7 +11,10 @@ from boto.redshift.exceptions import ( ) import sure # noqa -from moto import mock_ec2_deprecated, mock_redshift_deprecated, mock_redshift +from moto import mock_ec2 +from moto import mock_ec2_deprecated +from moto import mock_redshift +from moto import mock_redshift_deprecated @mock_redshift @@ -153,6 +156,32 @@ def test_create_cluster_in_subnet_group(): cluster['ClusterSubnetGroupName'].should.equal('my_subnet_group') +@mock_redshift +@mock_ec2 +def test_create_cluster_in_subnet_group_boto3(): + ec2 = boto3.resource('ec2', region_name='us-east-1') + vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16') + subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock='10.0.0.0/24') + client = boto3.client('redshift', region_name='us-east-1') + client.create_cluster_subnet_group( + ClusterSubnetGroupName='my_subnet_group', + Description='This is my subnet group', + SubnetIds=[subnet.id] + ) + + client.create_cluster( + ClusterIdentifier="my_cluster", + NodeType="dw.hs1.xlarge", + MasterUsername="username", + MasterUserPassword="password", + ClusterSubnetGroupName='my_subnet_group', + ) + + cluster_response = client.describe_clusters(ClusterIdentifier="my_cluster") + cluster = cluster_response['Clusters'][0] + cluster['ClusterSubnetGroupName'].should.equal('my_subnet_group') + + @mock_redshift_deprecated def test_create_cluster_with_security_group(): conn = boto.redshift.connect_to_region("us-east-1") From 04e623ea144e9759ba7f33574381eff913444dfc Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Thu, 20 Jul 2017 15:00:30 -0700 Subject: [PATCH 259/274] Implemented core endpoints of ELBv2 --- docs/index.rst | 1 + moto/__init__.py | 1 + moto/elbv2/__init__.py | 6 + moto/elbv2/exceptions.py | 103 +++++ moto/elbv2/models.py | 312 +++++++++++++++ moto/elbv2/responses.py | 649 ++++++++++++++++++++++++++++++++ moto/elbv2/urls.py | 10 + tests/test_elbv2/test_elbv2.py | 447 ++++++++++++++++++++++ tests/test_elbv2/test_server.py | 17 + 9 files changed, 1546 insertions(+) create mode 100644 moto/elbv2/__init__.py create mode 100644 moto/elbv2/exceptions.py create mode 100644 moto/elbv2/models.py create mode 100644 moto/elbv2/responses.py create mode 100644 moto/elbv2/urls.py create mode 100644 tests/test_elbv2/test_elbv2.py create mode 100644 tests/test_elbv2/test_server.py diff --git a/docs/index.rst b/docs/index.rst index 2ce31febd..9a9fa5261 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ Currently implemented Services: | ECS | @mock_ecs | basic endpoints done | +-----------------------+---------------------+-----------------------------------+ | ELB | @mock_elb | core endpoints done | +| | @mock_elbv2 | core endpoints done | +-----------------------+---------------------+-----------------------------------+ | EMR | @mock_emr | core endpoints done | +-----------------------+---------------------+-----------------------------------+ diff --git a/moto/__init__.py b/moto/__init__.py index 304e25cc5..728d8db71 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -17,6 +17,7 @@ from .ec2 import mock_ec2, mock_ec2_deprecated # flake8: noqa from .ecr import mock_ecr, mock_ecr_deprecated # flake8: noqa from .ecs import mock_ecs, mock_ecs_deprecated # flake8: noqa from .elb import mock_elb, mock_elb_deprecated # flake8: noqa +from .elbv2 import mock_elbv2 # flake8: noqa from .emr import mock_emr, mock_emr_deprecated # flake8: noqa from .events import mock_events # flake8: noqa from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa diff --git a/moto/elbv2/__init__.py b/moto/elbv2/__init__.py new file mode 100644 index 000000000..21a6d06c6 --- /dev/null +++ b/moto/elbv2/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import elbv2_backends +from ..core.models import base_decorator + +elb_backend = elbv2_backends['us-east-1'] +mock_elbv2 = base_decorator(elbv2_backends) diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py new file mode 100644 index 000000000..397aa115b --- /dev/null +++ b/moto/elbv2/exceptions.py @@ -0,0 +1,103 @@ +from __future__ import unicode_literals +from moto.core.exceptions import RESTError + + +class ELBClientError(RESTError): + code = 400 + + +class DuplicateTagKeysError(ELBClientError): + + def __init__(self, cidr): + super(DuplicateTagKeysError, self).__init__( + "DuplicateTagKeys", + "Tag key was specified more than once: {0}" + .format(cidr)) + + +class LoadBalancerNotFoundError(ELBClientError): + + def __init__(self): + super(LoadBalancerNotFoundError, self).__init__( + "LoadBalancerNotFound", + "The specified load balancer does not exist.") + + +class ListenerNotFoundError(ELBClientError): + + def __init__(self): + super(ListenerNotFoundError, self).__init__( + "ListenerNotFound", + "The specified listener does not exist.") + + +class SubnetNotFoundError(ELBClientError): + + def __init__(self): + super(SubnetNotFoundError, self).__init__( + "SubnetNotFound", + "The specified subnet does not exist.") + + +class TargetGroupNotFoundError(ELBClientError): + + def __init__(self): + super(TooManyTagsError, self).__init__( + "TargetGroupNotFound", + "The specified target group does not exist.") + + +class TooManyTagsError(ELBClientError): + + def __init__(self): + super(TooManyTagsError, self).__init__( + "TooManyTagsError", + "The quota for the number of tags that can be assigned to a load balancer has been reached") + + +class BadHealthCheckDefinition(ELBClientError): + + def __init__(self): + super(BadHealthCheckDefinition, self).__init__( + "ValidationError", + "HealthCheck Target must begin with one of HTTP, TCP, HTTPS, SSL") + + +class DuplicateListenerError(ELBClientError): + + def __init__(self): + super(DuplicateListenerError, self).__init__( + "DuplicateListener", + "A listener with the specified port already exists.") + + +class DuplicateLoadBalancerName(ELBClientError): + + def __init__(self): + super(DuplicateLoadBalancerName, self).__init__( + "DuplicateLoadBalancerName", + "A load balancer with the specified name already exists.") + + +class DuplicateTargetGroupName(ELBClientError): + + def __init__(self): + super(DuplicateTargetGroupName, self).__init__( + "DuplicateTargetGroupName", + "A target group with the specified name already exists.") + + +class InvalidTargetError(ELBClientError): + + def __init__(self): + super(InvalidTargetError, self).__init__( + "InvalidTarget", + "The specified target does not exist or is not in the same VPC as the target group.") + + +class EmptyListenersError(ELBClientError): + + def __init__(self): + super(EmptyListenersError, self).__init__( + "ValidationError", + "Listeners cannot be empty") diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py new file mode 100644 index 000000000..7682ae097 --- /dev/null +++ b/moto/elbv2/models.py @@ -0,0 +1,312 @@ +from __future__ import unicode_literals + +import datetime +from moto.compat import OrderedDict +from moto.core import BaseBackend, BaseModel +from moto.ec2.models import ec2_backends +from .exceptions import ( + DuplicateLoadBalancerName, + DuplicateListenerError, + DuplicateTargetGroupName, + InvalidTargetError, + ListenerNotFoundError, + LoadBalancerNotFoundError, + SubnetNotFoundError, + TargetGroupNotFoundError, + TooManyTagsError, +) + + +class FakeHealthStatus(BaseModel): + + def __init__(self, instance_id, port, health_port, status, reason=None): + self.instance_id = instance_id + self.port = port + self.health_port = health_port + self.status = status + self.reason = reason + + +class FakeTargetGroup(BaseModel): + def __init__(self, + name, + arn, + vpc_id, + protocol, + port, + healthcheck_protocol, + healthcheck_port, + healthcheck_path, + healthcheck_interval_seconds, + healthcheck_timeout_seconds, + healthy_threshold_count, + unhealthy_threshold_count): + self.name = name + self.arn = arn + self.vpc_id = vpc_id + self.protocol = protocol + self.port = port + self.healthcheck_protocol = healthcheck_protocol + self.healthcheck_port = healthcheck_port + self.healthcheck_path = healthcheck_path + self.healthcheck_interval_seconds = healthcheck_interval_seconds + self.healthcheck_timeout_seconds = healthcheck_timeout_seconds + self.healthy_threshold_count = healthy_threshold_count + self.unhealthy_threshold_count = unhealthy_threshold_count + self.load_balancer_arns = [] + + self.targets = OrderedDict() + + def register(self, targets): + for target in targets: + self.targets[target['id']] = { + 'id': target['id'], + 'port': target.get('port', self.port), + } + + def deregister(self, targets): + for target in targets: + t = self.targets.pop(target['id']) + if not t: + raise InvalidTargetError() + + def health_for(self, target): + t = self.targets.get(target['id']) + if t is None: + raise InvalidTargetError() + return FakeHealthStatus(t['id'], t['port'], self.healthcheck_port, 'healthy') + + +class FakeListener(BaseModel): + + def __init__(self, load_balancer_arn, arn, protocol, port, ssl_policy, certificate, default_actions): + self.load_balancer_arn = load_balancer_arn + self.arn = arn + self.protocol = protocol.upper() + self.port = port + self.ssl_policy = ssl_policy + self.certificate = certificate + self.default_actions = default_actions + + +class FakeBackend(BaseModel): + + def __init__(self, instance_port): + self.instance_port = instance_port + self.policy_names = [] + + def __repr__(self): + return "FakeBackend(inp: %s, policies: %s)" % (self.instance_port, self.policy_names) + + +class FakeLoadBalancer(BaseModel): + + def __init__(self, name, security_groups, subnets, vpc_id, arn, dns_name, scheme='internet-facing'): + self.name = name + self.created_time = datetime.datetime.now() + self.scheme = scheme + self.security_groups = security_groups + self.subnets = subnets or [] + self.vpc_id = vpc_id + self.listeners = OrderedDict() + self.tags = {} + self.arn = arn + self.dns_name = dns_name + + @property + def physical_resource_id(self): + return self.name + + def add_tag(self, key, value): + if len(self.tags) >= 10 and key not in self.tags: + raise TooManyTagsError() + self.tags[key] = value + + def list_tags(self): + return self.tags + + def remove_tag(self, key): + if key in self.tags: + del self.tags[key] + + def delete(self, region): + ''' Not exposed as part of the ELB API - used for CloudFormation. ''' + elbv2_backends[region].delete_load_balancer(self.arn) + + +class ELBv2Backend(BaseBackend): + + def __init__(self, region_name=None): + self.region_name = region_name + self.target_groups = OrderedDict() + self.load_balancers = OrderedDict() + + def reset(self): + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def create_load_balancer(self, name, security_groups, subnet_ids, scheme='internet-facing'): + vpc_id = None + ec2_backend = ec2_backends[self.region_name] + subnets = [] + if not subnet_ids: + raise SubnetNotFoundError() + for subnet_id in subnet_ids: + subnet = ec2_backend.get_subnet(subnet_id) + if subnet is None: + raise SubnetNotFoundError() + subnets.append(subnet) + + vpc_id = subnets[0].vpc_id + arn = "arn:aws:elasticloadbalancing:%s:1:loadbalancer/%s/50dc6c495c0c9188" % (self.region_name, name) + dns_name = "%s-1.%s.elb.amazonaws.com" % (name, self.region_name) + + if arn in self.load_balancers: + raise DuplicateLoadBalancerName() + + new_load_balancer = FakeLoadBalancer( + name=name, + security_groups=security_groups, + arn=arn, + scheme=scheme, + subnets=subnets, + vpc_id=vpc_id, + dns_name=dns_name) + self.load_balancers[arn] = new_load_balancer + return new_load_balancer + + def create_target_group(self, name, **kwargs): + for target_group in self.target_groups.values(): + if target_group.name == name: + raise DuplicateTargetGroupName() + + arn = "arn:aws:elasticloadbalancing:%s:1:targetgroup/%s/50dc6c495c0c9188" % (self.region_name, name) + target_group = FakeTargetGroup(name, arn, **kwargs) + self.target_groups[target_group.arn] = target_group + return target_group + + def create_listener(self, load_balancer_arn, protocol, port, ssl_policy, certificate, default_actions): + balancer = self.load_balancers.get(load_balancer_arn) + if balancer is None: + raise LoadBalancerNotFoundError() + if port in balancer.listeners: + raise DuplicateListenerError() + + arn = load_balancer_arn.replace(':loadbalancer/', ':listener/') + "/%s%s" % (port, id(self)) + listener = FakeListener(load_balancer_arn, arn, protocol, port, ssl_policy, certificate, default_actions) + balancer.listeners[listener.arn] = listener + return listener + + def describe_load_balancers(self, arns, names): + balancers = self.load_balancers.values() + arns = arns or [] + names = names or [] + if not arns and not names: + return balancers + + matched_balancers = [] + matched_balancer = None + + for arn in arns: + for balancer in balancers: + if balancer.arn == arn: + matched_balancer = balancer + if matched_balancer is None: + raise LoadBalancerNotFoundError() + elif matched_balancer not in matched_balancers: + matched_balancers.append(matched_balancer) + + for name in names: + for balancer in balancers: + if balancer.name == name: + matched_balancer = balancer + if matched_balancer is None: + raise LoadBalancerNotFoundError() + elif matched_balancer not in matched_balancers: + matched_balancers.append(matched_balancer) + + return matched_balancers + + def describe_target_groups(self, load_balancer_arn, target_group_arns, names): + if load_balancer_arn: + if load_balancer_arn not in self.load_balancers: + raise LoadBalancerNotFoundError() + return [tg for tg in self.target_groups.values() + if load_balancer_arn in tg.load_balancer_arns] + + if target_group_arns: + try: + return [self.target_groups[arn] for arn in target_group_arns] + except KeyError: + raise TargetGroupNotFoundError() + if names: + matched = [] + for name in names: + found = None + for target_group in self.target_groups: + if target_group.name == name: + found = target_group + if not found: + raise TargetGroupNotFoundError() + matched.append(found) + return matched + + return self.target_groups.values() + + def describe_listeners(self, load_balancer_arn, listener_arns): + if load_balancer_arn: + if load_balancer_arn not in self.load_balancers: + raise LoadBalancerNotFoundError() + return self.load_balancers.get(load_balancer_arn).listeners.values() + + matched = [] + for load_balancer in self.load_balancers.values(): + for listener_arn in listener_arns: + listener = load_balancer.listeners.get(listener_arn) + if not listener: + raise ListenerNotFoundError() + matched.append(listener) + return matched + + def delete_load_balancer(self, arn): + self.load_balancers.pop(arn, None) + + def delete_target_group(self, target_group_arn): + target_group = self.target_groups.pop(target_group_arn) + if target_group: + return target_group + raise TargetGroupNotFoundError() + + def delete_listener(self, listener_arn): + for load_balancer in self.load_balancers.values(): + listener = load_balancer.listeners.pop(listener_arn) + if listener: + return listener + raise ListenerNotFoundError() + + def register_targets(self, target_group_arn, instances): + target_group = self.target_groups.get(target_group_arn) + if target_group is None: + raise TargetGroupNotFoundError() + target_group.register(instances) + + def deregister_targets(self, target_group_arn, instances): + target_group = self.target_groups.get(target_group_arn) + if target_group is None: + raise TargetGroupNotFoundError() + target_group.deregister(instances) + + def describe_target_health(self, target_group_arn, targets): + target_group = self.target_groups.get(target_group_arn) + if target_group is None: + raise TargetGroupNotFoundError() + + if not targets: + targets = target_group.targets.values() + return [target_group.health_for(target) for target in targets] + + +elbv2_backends = {} +for region in ec2_backends.keys(): + elbv2_backends[region] = ELBv2Backend(region) diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py new file mode 100644 index 000000000..585a413d4 --- /dev/null +++ b/moto/elbv2/responses.py @@ -0,0 +1,649 @@ +from __future__ import unicode_literals +from moto.core.responses import BaseResponse +from .models import elbv2_backends +from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError + + +class ELBResponse(BaseResponse): + + @property + def elb_backend(self): + return elbv2_backends[self.region] + + def create_load_balancer(self): + load_balancer_name = self._get_param('Name') + subnet_ids = self._get_multi_param("Subnets.member") + security_groups = self._get_multi_param("SecurityGroups.member") + scheme = self._get_param('Scheme') + + load_balancer = self.elb_backend.create_load_balancer( + name=load_balancer_name, + security_groups=security_groups, + subnet_ids=subnet_ids, + scheme=scheme, + ) + self._add_tags(load_balancer) + template = self.response_template(CREATE_LOAD_BALANCER_TEMPLATE) + return template.render(load_balancer=load_balancer) + + def create_target_group(self): + name = self._get_param('Name') + vpc_id = self._get_param('VpcId') + protocol = self._get_param('Protocol') + port = self._get_param('Port') + healthcheck_protocol = self._get_param('HealthCheckProtocol', 'HTTP') + healthcheck_port = self._get_param('HealthCheckPort', 'traffic-port') + healthcheck_path = self._get_param('HealthCheckPath', '/') + healthcheck_interval_seconds = self._get_param('HealthCheckIntervalSeconds', '30') + healthcheck_timeout_seconds = self._get_param('HealthCheckTimeoutSeconds', '5') + healthy_threshold_count = self._get_param('HealthyThresholdCount', '5') + unhealthy_threshold_count = self._get_param('UnhealthyThresholdCount', '2') + + target_group = self.elb_backend.create_target_group( + name, + vpc_id=vpc_id, + protocol=protocol, + port=port, + healthcheck_protocol=healthcheck_protocol, + healthcheck_port=healthcheck_port, + healthcheck_path=healthcheck_path, + healthcheck_interval_seconds=healthcheck_interval_seconds, + healthcheck_timeout_seconds=healthcheck_timeout_seconds, + healthy_threshold_count=healthy_threshold_count, + unhealthy_threshold_count=unhealthy_threshold_count, + ) + + template = self.response_template(CREATE_TARGET_GROUP_TEMPLATE) + return template.render(target_group=target_group) + + def create_listener(self): + load_balancer_arn = self._get_param('LoadBalancerArn') + protocol = self._get_param('Protocol') + port = self._get_param('Port') + ssl_policy = self._get_param('SslPolicy', 'ELBSecurityPolicy-2016-08') + certificates = self._get_list_prefix('Certificates.member') + if certificates: + certificate = certificates[0].get('certificate_arn') + else: + certificate = None + default_actions = self._get_list_prefix('DefaultActions.member') + + listener = self.elb_backend.create_listener( + load_balancer_arn=load_balancer_arn, + protocol=protocol, + port=port, + ssl_policy=ssl_policy, + certificate=certificate, + default_actions=default_actions) + + template = self.response_template(CREATE_LISTENER_TEMPLATE) + return template.render(listener=listener) + + def describe_load_balancers(self): + arns = self._get_multi_param("LoadBalancerArns.member") + names = self._get_multi_param("Names.member") + all_load_balancers = list(self.elb_backend.describe_load_balancers(arns, names)) + marker = self._get_param('Marker') + all_names = [balancer.name for balancer in all_load_balancers] + if marker: + start = all_names.index(marker) + 1 + else: + start = 0 + page_size = self._get_param('PageSize', 50) # the default is 400, but using 50 to make testing easier + load_balancers_resp = all_load_balancers[start:start + page_size] + next_marker = None + if len(all_load_balancers) > start + page_size: + next_marker = load_balancers_resp[-1].name + + template = self.response_template(DESCRIBE_LOAD_BALANCERS_TEMPLATE) + return template.render(load_balancers=load_balancers_resp, marker=next_marker) + + def describe_target_groups(self): + load_balancer_arn = self._get_param('LoadBalancerArn') + target_group_arns = self._get_multi_param('TargetGroupArns.member') + names = self._get_multi_param('Names.member') + + target_groups = self.elb_backend.describe_target_groups(load_balancer_arn, target_group_arns, names) + template = self.response_template(DESCRIBE_TARGET_GROUPS_TEMPLATE) + return template.render(target_groups=target_groups) + + def describe_listeners(self): + load_balancer_arn = self._get_param('LoadBalancerArn') + listener_arns = self._get_multi_param('ListenerArns.member') + if not load_balancer_arn and not listener_arns: + raise LoadBalancerNotFoundError() + + listeners = self.elb_backend.describe_listeners(load_balancer_arn, listener_arns) + template = self.response_template(DESCRIBE_LISTENERS_TEMPLATE) + return template.render(listeners=listeners) + + def delete_load_balancer(self): + arn = self._get_param('LoadBalancerArn') + self.elb_backend.delete_load_balancer(arn) + template = self.response_template(DELETE_LOAD_BALANCER_TEMPLATE) + return template.render() + + def delete_target_group(self): + arn = self._get_param('TargetGroupArn') + self.elb_backend.delete_target_group(arn) + template = self.response_template(DELETE_TARGET_GROUP_TEMPLATE) + return template.render() + + def delete_listener(self): + arn = self._get_param('ListenerArn') + self.elb_backend.delete_listener(arn) + template = self.response_template(DELETE_LISTENER_TEMPLATE) + return template.render() + + def register_targets(self): + target_group_arn = self._get_param('TargetGroupArn') + targets = self._get_list_prefix('Targets.member') + self.elb_backend.register_targets(target_group_arn, targets) + + template = self.response_template(REGISTER_TARGETS_TEMPLATE) + return template.render() + + def deregister_targets(self): + target_group_arn = self._get_param('TargetGroupArn') + targets = self._get_list_prefix('Targets.member') + self.elb_backend.deregister_targets(target_group_arn, targets) + + template = self.response_template(DEREGISTER_TARGETS_TEMPLATE) + return template.render() + + def describe_target_health(self): + target_group_arn = self._get_param('TargetGroupArn') + targets = self._get_list_prefix('Targets.member') + target_health_descriptions = self.elb_backend.describe_target_health(target_group_arn, targets) + + template = self.response_template(DESCRIBE_TARGET_HEALTH_TEMPLATE) + return template.render(target_health_descriptions=target_health_descriptions) + + def add_tags(self): + resource_arns = self._get_multi_param('ResourceArns.member') + + for arn in resource_arns: + load_balancer = self.elb_backend.load_balancers.get(arn) + if not load_balancer: + raise LoadBalancerNotFoundError() + self._add_tags(load_balancer) + + template = self.response_template(ADD_TAGS_TEMPLATE) + return template.render() + + def remove_tags(self): + resource_arns = self._get_multi_param('ResourceArns.member') + tag_keys = self._get_multi_param('TagKeys.member') + + for arn in resource_arns: + load_balancer = self.elb_backend.load_balancers.get(arn) + if not load_balancer: + raise LoadBalancerNotFoundError() + [load_balancer.remove_tag(key) for key in tag_keys] + + template = self.response_template(REMOVE_TAGS_TEMPLATE) + return template.render() + + def describe_tags(self): + elbs = [] + for key, value in self.querystring.items(): + if "ResourceArns.member" in key: + number = key.split('.')[2] + load_balancer_arn = self._get_param( + 'ResourceArns.member.{0}'.format(number)) + elb = self.elb_backend.load_balancers.get(load_balancer_arn) + if not elb: + raise LoadBalancerNotFoundError() + elbs.append(elb) + + template = self.response_template(DESCRIBE_TAGS_TEMPLATE) + return template.render(load_balancers=elbs) + + def _add_tags(self, elb): + tag_values = [] + tag_keys = [] + + for t_key, t_val in sorted(self.querystring.items()): + if t_key.startswith('Tags.member.'): + if t_key.split('.')[3] == 'Key': + tag_keys.extend(t_val) + elif t_key.split('.')[3] == 'Value': + tag_values.extend(t_val) + + counts = {} + for i in tag_keys: + counts[i] = tag_keys.count(i) + + counts = sorted(counts.items(), key=lambda i: i[1], reverse=True) + + if counts and counts[0][1] > 1: + # We have dupes... + raise DuplicateTagKeysError(counts[0]) + + for tag_key, tag_value in zip(tag_keys, tag_values): + elb.add_tag(tag_key, tag_value) + + +ADD_TAGS_TEMPLATE = """ + + + 360e81f7-1100-11e4-b6ed-0f30EXAMPLE + +""" + +REMOVE_TAGS_TEMPLATE = """ + + + 360e81f7-1100-11e4-b6ed-0f30EXAMPLE + +""" + +DESCRIBE_TAGS_TEMPLATE = """ + + + {% for load_balancer in load_balancers %} + + {{ load_balancer.arn }} + + {% for key, value in load_balancer.tags.items() %} + + {{ value }} + {{ key }} + + {% endfor %} + + + {% endfor %} + + + + 360e81f7-1100-11e4-b6ed-0f30EXAMPLE + +""" + + +CREATE_LOAD_BALANCER_TEMPLATE = """ + + + + {{ load_balancer.arn }} + {{ load_balancer.scheme }} + {{ load_balancer.name }} + {{ load_balancer.vpc_id }} + Z2P70J7EXAMPLE + {{ load_balancer.created_time }} + + {% for subnet in load_balancer.subnets %} + + {{ subnet.id }} + {{ subnet.availability_zone }} + + {% endfor %} + + + {% for security_group in load_balancer.security_groups %} + {{ security_group }} + {% endfor %} + + {{ load_balancer.dns_name }} + + provisioning + + application + + + + + 32d531b2-f2d0-11e5-9192-3fff33344cfa + +""" + +CREATE_TARGET_GROUP_TEMPLATE = """ + + + + {{ target_group.arn }} + {{ target_group.name }} + {{ target_group.protocol }} + {{ target_group.port }} + {{ target_group.vpc_id }} + {{ target_group.health_check_protocol }} + {{ target_group.healthcheck_port }} + {{ target_group.healthcheck_path }} + {{ target_group.healthcheck_interval_seconds }} + {{ target_group.healthcheck_timeout_seconds }} + {{ target_group.healthy_threshold_count }} + {{ target_group.unhealthy_threshold_count }} + + 200 + + + + + + b83fe90e-f2d5-11e5-b95d-3b2c1831fc26 + +""" + +CREATE_LISTENER_TEMPLATE = """ + + + + {{ listener.load_balancer_arn }} + {{ listener.protocol }} + {% if listener.certificate %} + + + {{ listener.certificate }} + + + {% endif %} + {{ listener.port }} + {{ listener.ssl_policy }} + {{ listener.arn }} + + {% for action in listener.default_actions %} + + {{ action.type }} + {{ action.target_group_arn }} + + {% endfor %} + + + + + + 97f1bb38-f390-11e5-b95d-3b2c1831fc26 + +""" + +DELETE_LOAD_BALANCER_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +DELETE_TARGET_GROUP_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +DELETE_LISTENER_TEMPLATE = """ + + + 1549581b-12b7-11e3-895e-1334aEXAMPLE + +""" + +DESCRIBE_LOAD_BALANCERS_TEMPLATE = """ + + + {% for load_balancer in load_balancers %} + + {{ load_balancer.arn }} + {{ load_balancer.scheme }} + {{ load_balancer.name }} + {{ load_balancer.vpc_id }} + Z2P70J7EXAMPLE + {{ load_balancer.created_time }} + + {% for subnet in load_balancer.subnets %} + + {{ subnet.id }} + {{ subnet.availability_zone }} + + {% endfor %} + + + {% for security_group in load_balancer.security_groups %} + {{ security_group }} + {% endfor %} + + {{ load_balancer.dns_name }} + + provisioning + + application + + {% endfor %} + + {% if marker %} + {{ marker }} + {% endif %} + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + + +DESCRIBE_TARGET_GROUPS_TEMPLATE = """ + + + {% for target_group in target_groups %} + + {{ target_group.arn }} + {{ target_group.name }} + {{ target_group.protocol }} + {{ target_group.port }} + {{ target_group.vpc_id }} + {{ target_group.health_check_protocol }} + {{ target_group.healthcheck_port }} + {{ target_group.healthcheck_path }} + {{ target_group.healthcheck_interval_seconds }} + {{ target_group.healthcheck_timeout_seconds }} + {{ target_group.healthy_threshold_count }} + {{ target_group.unhealthy_threshold_count }} + + 200 + + + {% for load_balancer_arn in target_group.load_balancer_arns %} + {{ load_balancer_arn }} + {% endfor %} + + + {% endfor %} + + + + 70092c0e-f3a9-11e5-ae48-cff02092876b + +""" + + +DESCRIBE_LISTENERS_TEMPLATE = """ + + + {% for listener in listeners %} + + {{ listener.load_balancer_arn }} + {{ listener.protocol }} + {% if listener.certificate %} + + + {{ listener.certificate }} + + + {% endif %} + {{ listener.port }} + {{ listener.ssl_policy }} + {{ listener.arn }} + + {% for action in listener.default_actions %} + + {{ action.type }} + {{ action.target_group_arn }} + + {% endfor %} + + + {% endfor %} + + + + 65a3a7ea-f39c-11e5-b543-9f2c3fbb9bee + +""" + +CONFIGURE_HEALTH_CHECK_TEMPLATE = """ + + + {{ check.interval }} + {{ check.target }} + {{ check.healthy_threshold }} + {{ check.timeout }} + {{ check.unhealthy_threshold }} + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + +REGISTER_TARGETS_TEMPLATE = """ + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + +DEREGISTER_TARGETS_TEMPLATE = """ + + + + f9880f01-7852-629d-a6c3-3ae2-666a409287e6dc0c + +""" + +SET_LOAD_BALANCER_SSL_CERTIFICATE = """ + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + +""" + + +DELETE_LOAD_BALANCER_LISTENERS = """ + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + +""" + +DESCRIBE_ATTRIBUTES_TEMPLATE = """ + + + + {{ attributes.access_log.enabled }} + {% if attributes.access_log.enabled %} + {{ attributes.access_log.s3_bucket_name }} + {{ attributes.access_log.s3_bucket_prefix }} + {{ attributes.access_log.emit_interval }} + {% endif %} + + + {{ attributes.connecting_settings.idle_timeout }} + + + {{ attributes.cross_zone_load_balancing.enabled }} + + + {% if attributes.connection_draining.enabled %} + true + {{ attributes.connection_draining.timeout }} + {% else %} + false + {% endif %} + + + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + + +""" + +MODIFY_ATTRIBUTES_TEMPLATE = """ + + {{ load_balancer.name }} + + + {{ attributes.access_log.enabled }} + {% if attributes.access_log.enabled %} + {{ attributes.access_log.s3_bucket_name }} + {{ attributes.access_log.s3_bucket_prefix }} + {{ attributes.access_log.emit_interval }} + {% endif %} + + + {{ attributes.connecting_settings.idle_timeout }} + + + {{ attributes.cross_zone_load_balancing.enabled }} + + + {% if attributes.connection_draining.enabled %} + true + {{ attributes.connection_draining.timeout }} + {% else %} + false + {% endif %} + + + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + + +""" + +CREATE_LOAD_BALANCER_POLICY_TEMPLATE = """ + + + 83c88b9d-12b7-11e3-8b82-87b12EXAMPLE + + +""" + +SET_LOAD_BALANCER_POLICIES_OF_LISTENER_TEMPLATE = """ + + + 07b1ecbc-1100-11e3-acaf-dd7edEXAMPLE + + +""" + +SET_LOAD_BALANCER_POLICIES_FOR_BACKEND_SERVER_TEMPLATE = """ + + + 0eb9b381-dde0-11e2-8d78-6ddbaEXAMPLE + + +""" + +DESCRIBE_TARGET_HEALTH_TEMPLATE = """ + + + {% for target_health in target_health_descriptions %} + + {{ target_health.health_port }} + + {{ target_health.status }} + + + {{ target_health.port }} + {{ target_health.instance_id }} + + + {% endfor %} + + + + c534f810-f389-11e5-9192-3fff33344cfa + +""" diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py new file mode 100644 index 000000000..48fcb37ab --- /dev/null +++ b/moto/elbv2/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import ELBResponse + +url_bases = [ + "https?://elasticloadbalancing.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': ELBResponse.dispatch, +} diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py new file mode 100644 index 000000000..c9eb9ea43 --- /dev/null +++ b/tests/test_elbv2/test_elbv2.py @@ -0,0 +1,447 @@ +from __future__ import unicode_literals +import boto3 +import botocore +from botocore.exceptions import ClientError +from nose.tools import assert_raises +import sure # noqa + +from moto import mock_elbv2, mock_ec2 + + +@mock_elbv2 +@mock_ec2 +def test_create_load_balancer(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + lb = response.get('LoadBalancers')[0] + + lb.get('DNSName').should.equal("my-lb-1.us-east-1.elb.amazonaws.com") + lb.get('LoadBalancerArn').should.equal('arn:aws:elasticloadbalancing:us-east-1:1:loadbalancer/my-lb/50dc6c495c0c9188') + lb.get('SecurityGroups').should.equal([security_group.id]) + lb.get('AvailabilityZones').should.equal([ + {'SubnetId': subnet1.id, 'ZoneName': 'us-east-1a'}, + {'SubnetId': subnet2.id, 'ZoneName': 'us-east-1b'}]) + + # Ensure the tags persisted + response = conn.describe_tags(ResourceArns=[lb.get('LoadBalancerArn')]) + tags = {d['Key']: d['Value'] for d in response['TagDescriptions'][0]['Tags']} + tags.should.equal({'key_name': 'a_value'}) + + +@mock_elbv2 +@mock_ec2 +def test_describe_load_balancers(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response = conn.describe_load_balancers() + + response.get('LoadBalancers').should.have.length_of(1) + lb = response.get('LoadBalancers')[0] + lb.get('LoadBalancerName').should.equal('my-lb') + + response = conn.describe_load_balancers(LoadBalancerArns=[lb.get('LoadBalancerArn')]) + response.get('LoadBalancers')[0].get('LoadBalancerName').should.equal('my-lb') + + response = conn.describe_load_balancers(Names=['my-lb']) + response.get('LoadBalancers')[0].get('LoadBalancerName').should.equal('my-lb') + + with assert_raises(ClientError): + conn.describe_load_balancers(LoadBalancerArns=['not-a/real/arn']) + with assert_raises(ClientError): + conn.describe_load_balancers(Names=['nope']) + + +@mock_elbv2 +@mock_ec2 +def test_add_remove_tags(): + conn = boto3.client('elbv2', region_name='us-east-1') + + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + lbs = conn.describe_load_balancers()['LoadBalancers'] + lbs.should.have.length_of(1) + lb = lbs[0] + + with assert_raises(ClientError): + conn.add_tags(ResourceArns=['missing-arn'], + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }]) + + conn.add_tags(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }]) + + tags = {d['Key']: d['Value'] for d in conn.describe_tags( + ResourceArns=[lb.get('LoadBalancerArn')])['TagDescriptions'][0]['Tags']} + tags.should.have.key('a').which.should.equal('b') + + conn.add_tags(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'a', + 'Value': 'b' + }, { + 'Key': 'b', + 'Value': 'b' + }, { + 'Key': 'c', + 'Value': 'b' + }, { + 'Key': 'd', + 'Value': 'b' + }, { + 'Key': 'e', + 'Value': 'b' + }, { + 'Key': 'f', + 'Value': 'b' + }, { + 'Key': 'g', + 'Value': 'b' + }, { + 'Key': 'h', + 'Value': 'b' + }, { + 'Key': 'j', + 'Value': 'b' + }]) + + conn.add_tags.when.called_with(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'k', + 'Value': 'b' + }]).should.throw(botocore.exceptions.ClientError) + + conn.add_tags(ResourceArns=[lb.get('LoadBalancerArn')], + Tags=[{ + 'Key': 'j', + 'Value': 'c' + }]) + + tags = {d['Key']: d['Value'] for d in conn.describe_tags( + ResourceArns=[lb.get('LoadBalancerArn')])['TagDescriptions'][0]['Tags']} + + tags.should.have.key('a').which.should.equal('b') + tags.should.have.key('b').which.should.equal('b') + tags.should.have.key('c').which.should.equal('b') + tags.should.have.key('d').which.should.equal('b') + tags.should.have.key('e').which.should.equal('b') + tags.should.have.key('f').which.should.equal('b') + tags.should.have.key('g').which.should.equal('b') + tags.should.have.key('h').which.should.equal('b') + tags.should.have.key('j').which.should.equal('c') + tags.shouldnt.have.key('k') + + conn.remove_tags(ResourceArns=[lb.get('LoadBalancerArn')], + TagKeys=['a']) + + tags = {d['Key']: d['Value'] for d in conn.describe_tags( + ResourceArns=[lb.get('LoadBalancerArn')])['TagDescriptions'][0]['Tags']} + + tags.shouldnt.have.key('a') + tags.should.have.key('b').which.should.equal('b') + tags.should.have.key('c').which.should.equal('b') + tags.should.have.key('d').which.should.equal('b') + tags.should.have.key('e').which.should.equal('b') + tags.should.have.key('f').which.should.equal('b') + tags.should.have.key('g').which.should.equal('b') + tags.should.have.key('h').which.should.equal('b') + tags.should.have.key('j').which.should.equal('c') + + +@mock_elbv2 +@mock_ec2 +def test_create_elb_in_multiple_region(): + for region in ['us-west-1', 'us-west-2']: + conn = boto3.client('elbv2', region_name=region) + ec2 = boto3.resource('ec2', region_name=region) + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone=region + 'a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone=region + 'b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + list( + boto3.client('elbv2', region_name='us-west-1').describe_load_balancers().get('LoadBalancers') + ).should.have.length_of(1) + list( + boto3.client('elbv2', region_name='us-west-2').describe_load_balancers().get('LoadBalancers') + ).should.have.length_of(1) + + +@mock_elbv2 +@mock_ec2 +def test_create_target_group_and_listeners(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + load_balancer_arn = response.get('LoadBalancers')[0].get('LoadBalancerArn') + + response = conn.create_target_group( + Name='a-target', + Protocol='HTTP', + Port=8080, + VpcId=vpc.id, + HealthCheckProtocol='HTTP', + HealthCheckPort='8080', + HealthCheckPath='/', + HealthCheckIntervalSeconds=5, + HealthCheckTimeoutSeconds=5, + HealthyThresholdCount=5, + UnhealthyThresholdCount=2, + Matcher={'HttpCode': '200'}) + target_group = response.get('TargetGroups')[0] + + # Check it's in the describe_target_groups response + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(1) + + # Plain HTTP listener + response = conn.create_listener( + LoadBalancerArn=load_balancer_arn, + Protocol='HTTP', + Port=80, + DefaultActions=[{'Type': 'forward', 'TargetGroupArn': target_group.get('TargetGroupArn')}]) + listener = response.get('Listeners')[0] + listener.get('Port').should.equal(80) + listener.get('Protocol').should.equal('HTTP') + listener.get('DefaultActions').should.equal([{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward'}]) + http_listener_arn = listener.get('ListenerArn') + + # And another with SSL + response = conn.create_listener( + LoadBalancerArn=load_balancer_arn, + Protocol='HTTPS', + Port=443, + Certificates=[{'CertificateArn': 'arn:aws:iam:123456789012:server-certificate/test-cert'}], + DefaultActions=[{'Type': 'forward', 'TargetGroupArn': target_group.get('TargetGroupArn')}]) + listener = response.get('Listeners')[0] + listener.get('Port').should.equal(443) + listener.get('Protocol').should.equal('HTTPS') + listener.get('Certificates').should.equal([{ + 'CertificateArn': 'arn:aws:iam:123456789012:server-certificate/test-cert', + }]) + listener.get('DefaultActions').should.equal([{ + 'TargetGroupArn': target_group.get('TargetGroupArn'), + 'Type': 'forward'}]) + + https_listener_arn = listener.get('ListenerArn') + + response = conn.describe_listeners(LoadBalancerArn=load_balancer_arn) + response.get('Listeners').should.have.length_of(2) + response = conn.describe_listeners(ListenerArns=[https_listener_arn]) + response.get('Listeners').should.have.length_of(1) + listener = response.get('Listeners')[0] + listener.get('Port').should.equal(443) + listener.get('Protocol').should.equal('HTTPS') + + response = conn.describe_listeners(ListenerArns=[http_listener_arn, https_listener_arn]) + response.get('Listeners').should.have.length_of(2) + + # Delete one listener + response = conn.describe_listeners(LoadBalancerArn=load_balancer_arn) + response.get('Listeners').should.have.length_of(2) + conn.delete_listener(ListenerArn=http_listener_arn) + response = conn.describe_listeners(LoadBalancerArn=load_balancer_arn) + response.get('Listeners').should.have.length_of(1) + + # Then delete the load balancer + conn.delete_load_balancer(LoadBalancerArn=load_balancer_arn) + + # It's gone + response = conn.describe_load_balancers() + response.get('LoadBalancers').should.have.length_of(0) + + # And it deleted the remaining listener + response = conn.describe_listeners(ListenerArns=[http_listener_arn, https_listener_arn]) + response.get('Listeners').should.have.length_of(0) + + # But not the target groups + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(1) + + # Which we'll now delete + conn.delete_target_group(TargetGroupArn=target_group.get('TargetGroupArn')) + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(0) + + +@mock_elbv2 +@mock_ec2 +def test_describe_paginated_balancers(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + for i in range(51): + conn.create_load_balancer( + Name='my-lb%d' % i, + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + resp = conn.describe_load_balancers() + resp['LoadBalancers'].should.have.length_of(50) + resp['NextMarker'].should.equal(resp['LoadBalancers'][-1]['LoadBalancerName']) + resp2 = conn.describe_load_balancers(Marker=resp['NextMarker']) + resp2['LoadBalancers'].should.have.length_of(1) + assert 'NextToken' not in resp2.keys() + + +@mock_elbv2 +@mock_ec2 +def test_delete_load_balancer(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response.get('LoadBalancers').should.have.length_of(1) + lb = response.get('LoadBalancers')[0] + + conn.delete_load_balancer(LoadBalancerArn=lb.get('LoadBalancerArn')) + balancers = conn.describe_load_balancers().get('LoadBalancers') + balancers.should.have.length_of(0) + + +@mock_ec2 +@mock_elbv2 +def test_register_targets(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response = conn.create_target_group( + Name='a-target', + Protocol='HTTP', + Port=8080, + VpcId=vpc.id, + HealthCheckProtocol='HTTP', + HealthCheckPort='8080', + HealthCheckPath='/', + HealthCheckIntervalSeconds=5, + HealthCheckTimeoutSeconds=5, + HealthyThresholdCount=5, + UnhealthyThresholdCount=2, + Matcher={'HttpCode': '200'}) + target_group = response.get('TargetGroups')[0] + + # No targets registered yet + response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) + response.get('TargetHealthDescriptions').should.have.length_of(0) + + response = ec2.create_instances( + ImageId='ami-1234abcd', MinCount=2, MaxCount=2) + instance_id1 = response[0].id + instance_id2 = response[1].id + + response = conn.register_targets( + TargetGroupArn=target_group.get('TargetGroupArn'), + Targets=[ + { + 'Id': instance_id1, + 'Port': 5060, + }, + { + 'Id': instance_id2, + 'Port': 4030, + }, + ]) + + response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) + response.get('TargetHealthDescriptions').should.have.length_of(2) + + response = conn.deregister_targets( + TargetGroupArn=target_group.get('TargetGroupArn'), + Targets=[{'Id': instance_id2}]) + + response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) + response.get('TargetHealthDescriptions').should.have.length_of(1) diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py new file mode 100644 index 000000000..6dc271920 --- /dev/null +++ b/tests/test_elbv2/test_server.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import sure # noqa + +import moto.server as server + +''' +Test the different server responses +''' + + +def test_elb_describe_instances(): + backend = server.create_backend_app("elbv2") + test_client = backend.test_client() + + res = test_client.get('/?Action=DescribeLoadBalancers') + + res.data.should.contain(b'DescribeLoadBalancersResponse') From ee6d2537004a2591ec3fd5466a41aa1ff30eb737 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Fri, 21 Jul 2017 16:28:56 -0700 Subject: [PATCH 260/274] updating reference in server test --- tests/test_elbv2/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py index 6dc271920..05786104d 100644 --- a/tests/test_elbv2/test_server.py +++ b/tests/test_elbv2/test_server.py @@ -8,7 +8,7 @@ Test the different server responses ''' -def test_elb_describe_instances(): +def test_elbv2_describe_instances(): backend = server.create_backend_app("elbv2") test_client = backend.test_client() From 5cd1e2450d50917202c6648fb5befb34ef47a944 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Sun, 23 Jul 2017 22:06:55 -0700 Subject: [PATCH 261/274] adding elbv2 backend --- moto/backends.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/moto/backends.py b/moto/backends.py index 0af4ae2e2..b452b45fd 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -13,6 +13,7 @@ from moto.ec2 import ec2_backends from moto.ecr import ecr_backends from moto.ecs import ecs_backends from moto.elb import elb_backends +from moto.elbv2 import elbv2_backends from moto.emr import emr_backends from moto.events import events_backends from moto.glacier import glacier_backends @@ -43,6 +44,7 @@ BACKENDS = { 'ecr': ecr_backends, 'ecs': ecs_backends, 'elb': elb_backends, + 'elbv2': elbv2_backends, 'events': events_backends, 'emr': emr_backends, 'glacier': glacier_backends, From ce392fab79ba8a160f34c9ad90025ab3ccaefe98 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Tue, 1 Aug 2017 18:12:36 -0700 Subject: [PATCH 262/274] Properly dispatch by api version in server mode I'm not happy with this solution. Please think of a fix if you're reading this. --- moto/elb/urls.py | 30 +++++++++++++++++++++++++++++- moto/elbv2/responses.py | 34 +++++++++++++++++----------------- moto/elbv2/urls.py | 10 +++------- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/moto/elb/urls.py b/moto/elb/urls.py index 48fcb37ab..a81ebc3e0 100644 --- a/moto/elb/urls.py +++ b/moto/elb/urls.py @@ -1,10 +1,38 @@ from __future__ import unicode_literals from .responses import ELBResponse +from moto.elbv2.responses import ELBV2Response + + +def api_version_elb_backend(*args, **kwargs): + """ + ELB and ELBV2 (Classic and Application load balancers) use the same + hostname and url space. To differentiate them we must read the + `Version` parameter out of the url-encoded request body. TODO: There + has _got_ to be a better way to do this. Please help us think of + one. + """ + request = args[0] + + if hasattr(request, 'values'): + # boto3 + version = request.values.get('Version') + else: + # boto + request.parse_request() + version = request.querystring.get('Version')[0] + + if '2012-06-01' == version: + return ELBResponse.dispatch(*args, **kwargs) + elif '2015-12-01' == version: + return ELBV2Response.dispatch(*args, **kwargs) + else: + raise Exception("Unknown ELB API version: {}".format(version)) + url_bases = [ "https?://elasticloadbalancing.(.+).amazonaws.com", ] url_paths = { - '{0}/$': ELBResponse.dispatch, + '{0}/$': api_version_elb_backend, } diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index 585a413d4..c000dd0c9 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -4,10 +4,10 @@ from .models import elbv2_backends from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError -class ELBResponse(BaseResponse): +class ELBV2Response(BaseResponse): @property - def elb_backend(self): + def elbv2_backend(self): return elbv2_backends[self.region] def create_load_balancer(self): @@ -16,7 +16,7 @@ class ELBResponse(BaseResponse): security_groups = self._get_multi_param("SecurityGroups.member") scheme = self._get_param('Scheme') - load_balancer = self.elb_backend.create_load_balancer( + load_balancer = self.elbv2_backend.create_load_balancer( name=load_balancer_name, security_groups=security_groups, subnet_ids=subnet_ids, @@ -39,7 +39,7 @@ class ELBResponse(BaseResponse): healthy_threshold_count = self._get_param('HealthyThresholdCount', '5') unhealthy_threshold_count = self._get_param('UnhealthyThresholdCount', '2') - target_group = self.elb_backend.create_target_group( + target_group = self.elbv2_backend.create_target_group( name, vpc_id=vpc_id, protocol=protocol, @@ -68,7 +68,7 @@ class ELBResponse(BaseResponse): certificate = None default_actions = self._get_list_prefix('DefaultActions.member') - listener = self.elb_backend.create_listener( + listener = self.elbv2_backend.create_listener( load_balancer_arn=load_balancer_arn, protocol=protocol, port=port, @@ -82,7 +82,7 @@ class ELBResponse(BaseResponse): def describe_load_balancers(self): arns = self._get_multi_param("LoadBalancerArns.member") names = self._get_multi_param("Names.member") - all_load_balancers = list(self.elb_backend.describe_load_balancers(arns, names)) + all_load_balancers = list(self.elbv2_backend.describe_load_balancers(arns, names)) marker = self._get_param('Marker') all_names = [balancer.name for balancer in all_load_balancers] if marker: @@ -103,7 +103,7 @@ class ELBResponse(BaseResponse): target_group_arns = self._get_multi_param('TargetGroupArns.member') names = self._get_multi_param('Names.member') - target_groups = self.elb_backend.describe_target_groups(load_balancer_arn, target_group_arns, names) + target_groups = self.elbv2_backend.describe_target_groups(load_balancer_arn, target_group_arns, names) template = self.response_template(DESCRIBE_TARGET_GROUPS_TEMPLATE) return template.render(target_groups=target_groups) @@ -113,32 +113,32 @@ class ELBResponse(BaseResponse): if not load_balancer_arn and not listener_arns: raise LoadBalancerNotFoundError() - listeners = self.elb_backend.describe_listeners(load_balancer_arn, listener_arns) + listeners = self.elbv2_backend.describe_listeners(load_balancer_arn, listener_arns) template = self.response_template(DESCRIBE_LISTENERS_TEMPLATE) return template.render(listeners=listeners) def delete_load_balancer(self): arn = self._get_param('LoadBalancerArn') - self.elb_backend.delete_load_balancer(arn) + self.elbv2_backend.delete_load_balancer(arn) template = self.response_template(DELETE_LOAD_BALANCER_TEMPLATE) return template.render() def delete_target_group(self): arn = self._get_param('TargetGroupArn') - self.elb_backend.delete_target_group(arn) + self.elbv2_backend.delete_target_group(arn) template = self.response_template(DELETE_TARGET_GROUP_TEMPLATE) return template.render() def delete_listener(self): arn = self._get_param('ListenerArn') - self.elb_backend.delete_listener(arn) + self.elbv2_backend.delete_listener(arn) template = self.response_template(DELETE_LISTENER_TEMPLATE) return template.render() def register_targets(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') - self.elb_backend.register_targets(target_group_arn, targets) + self.elbv2_backend.register_targets(target_group_arn, targets) template = self.response_template(REGISTER_TARGETS_TEMPLATE) return template.render() @@ -146,7 +146,7 @@ class ELBResponse(BaseResponse): def deregister_targets(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') - self.elb_backend.deregister_targets(target_group_arn, targets) + self.elbv2_backend.deregister_targets(target_group_arn, targets) template = self.response_template(DEREGISTER_TARGETS_TEMPLATE) return template.render() @@ -154,7 +154,7 @@ class ELBResponse(BaseResponse): def describe_target_health(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') - target_health_descriptions = self.elb_backend.describe_target_health(target_group_arn, targets) + target_health_descriptions = self.elbv2_backend.describe_target_health(target_group_arn, targets) template = self.response_template(DESCRIBE_TARGET_HEALTH_TEMPLATE) return template.render(target_health_descriptions=target_health_descriptions) @@ -163,7 +163,7 @@ class ELBResponse(BaseResponse): resource_arns = self._get_multi_param('ResourceArns.member') for arn in resource_arns: - load_balancer = self.elb_backend.load_balancers.get(arn) + load_balancer = self.elbv2_backend.load_balancers.get(arn) if not load_balancer: raise LoadBalancerNotFoundError() self._add_tags(load_balancer) @@ -176,7 +176,7 @@ class ELBResponse(BaseResponse): tag_keys = self._get_multi_param('TagKeys.member') for arn in resource_arns: - load_balancer = self.elb_backend.load_balancers.get(arn) + load_balancer = self.elbv2_backend.load_balancers.get(arn) if not load_balancer: raise LoadBalancerNotFoundError() [load_balancer.remove_tag(key) for key in tag_keys] @@ -191,7 +191,7 @@ class ELBResponse(BaseResponse): number = key.split('.')[2] load_balancer_arn = self._get_param( 'ResourceArns.member.{0}'.format(number)) - elb = self.elb_backend.load_balancers.get(load_balancer_arn) + elb = self.elbv2_backend.load_balancers.get(load_balancer_arn) if not elb: raise LoadBalancerNotFoundError() elbs.append(elb) diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py index 48fcb37ab..ff72e3605 100644 --- a/moto/elbv2/urls.py +++ b/moto/elbv2/urls.py @@ -1,10 +1,6 @@ from __future__ import unicode_literals -from .responses import ELBResponse +from .responses import ELBV2Response -url_bases = [ - "https?://elasticloadbalancing.(.+).amazonaws.com", -] +url_bases = [] -url_paths = { - '{0}/$': ELBResponse.dispatch, -} +url_paths = {} From d56c30932f8e2c3a50b4a01f9381887f727fc9c8 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Tue, 1 Aug 2017 18:19:26 -0700 Subject: [PATCH 263/274] this is handled in moto/elb/urls.py --- moto/elbv2/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py index ff72e3605..13e04a224 100644 --- a/moto/elbv2/urls.py +++ b/moto/elbv2/urls.py @@ -1,5 +1,4 @@ from __future__ import unicode_literals -from .responses import ELBV2Response url_bases = [] From 8188fea0ced1e75610b835745d78c6a18532fd02 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Tue, 1 Aug 2017 18:26:38 -0700 Subject: [PATCH 264/274] This is required for the server test to work --- moto/elbv2/urls.py | 9 +++++++-- tests/test_elbv2/test_server.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/moto/elbv2/urls.py b/moto/elbv2/urls.py index 13e04a224..13a8e056f 100644 --- a/moto/elbv2/urls.py +++ b/moto/elbv2/urls.py @@ -1,5 +1,10 @@ from __future__ import unicode_literals +from .responses import ELBV2Response -url_bases = [] +url_bases = [ + "https?://elasticloadbalancing.(.+).amazonaws.com", +] -url_paths = {} +url_paths = { + '{0}/$': ELBV2Response.dispatch, +} diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py index 05786104d..5acad4051 100644 --- a/tests/test_elbv2/test_server.py +++ b/tests/test_elbv2/test_server.py @@ -8,7 +8,7 @@ Test the different server responses ''' -def test_elbv2_describe_instances(): +def test_elbv2_describe_load_balancers(): backend = server.create_backend_app("elbv2") test_client = backend.test_client() From 2f05f6c9eaec6eeae5b6d47c5ede92a7c70f8723 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 13:29:14 -0700 Subject: [PATCH 265/274] Adding version string to server tests --- tests/test_elb/test_server.py | 2 +- tests/test_elbv2/test_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_elb/test_server.py b/tests/test_elb/test_server.py index 04b12524e..0033284d7 100644 --- a/tests/test_elb/test_server.py +++ b/tests/test_elb/test_server.py @@ -12,6 +12,6 @@ def test_elb_describe_instances(): backend = server.create_backend_app("elb") test_client = backend.test_client() - res = test_client.get('/?Action=DescribeLoadBalancers') + res = test_client.get('/?Action=DescribeLoadBalancers&Version=2015-12-01') res.data.should.contain(b'DescribeLoadBalancersResponse') diff --git a/tests/test_elbv2/test_server.py b/tests/test_elbv2/test_server.py index 5acad4051..ddd40a02d 100644 --- a/tests/test_elbv2/test_server.py +++ b/tests/test_elbv2/test_server.py @@ -12,6 +12,6 @@ def test_elbv2_describe_load_balancers(): backend = server.create_backend_app("elbv2") test_client = backend.test_client() - res = test_client.get('/?Action=DescribeLoadBalancers') + res = test_client.get('/?Action=DescribeLoadBalancers&Version=2015-12-01') res.data.should.contain(b'DescribeLoadBalancersResponse') From 08a932f5f104a9174d2118fafc0c95e812161936 Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 13:29:26 -0700 Subject: [PATCH 266/274] handling AWSPreparedRequest instances in dispatch --- moto/elb/urls.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/moto/elb/urls.py b/moto/elb/urls.py index a81ebc3e0..6b754fcce 100644 --- a/moto/elb/urls.py +++ b/moto/elb/urls.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals -from .responses import ELBResponse +from six.moves.urllib.parse import parse_qs +from botocore.awsrequest import AWSPreparedRequest + +from moto.elb.responses import ELBResponse from moto.elbv2.responses import ELBV2Response @@ -16,6 +19,9 @@ def api_version_elb_backend(*args, **kwargs): if hasattr(request, 'values'): # boto3 version = request.values.get('Version') + elif isinstance(request, AWSPreparedRequest): + # botocore + version = parse_qs(request.body).get('Version')[0] else: # boto request.parse_request() From 161a187ee595ff8cf2061b02c4e9951a9301f23d Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 13:30:07 -0700 Subject: [PATCH 267/274] updating explanation of boto client usage --- moto/elb/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/elb/urls.py b/moto/elb/urls.py index 6b754fcce..3d96e1892 100644 --- a/moto/elb/urls.py +++ b/moto/elb/urls.py @@ -20,10 +20,10 @@ def api_version_elb_backend(*args, **kwargs): # boto3 version = request.values.get('Version') elif isinstance(request, AWSPreparedRequest): - # botocore + # boto in-memory version = parse_qs(request.body).get('Version')[0] else: - # boto + # boto in server mode request.parse_request() version = request.querystring.get('Version')[0] From 543e5fb07730c8116cab33da75ef3d492a21c48e Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 15:57:15 -0700 Subject: [PATCH 268/274] Implementing ELBV2 target group attributes --- moto/elbv2/models.py | 5 +++ moto/elbv2/responses.py | 58 ++++++++++++++++++++++++++- tests/test_elbv2/test_elbv2.py | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 7682ae097..10d9ad220 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -55,6 +55,11 @@ class FakeTargetGroup(BaseModel): self.unhealthy_threshold_count = unhealthy_threshold_count self.load_balancer_arns = [] + self.attributes = { + 'deregistration_delay.timeout_seconds': 300, + 'stickiness.enabled': 'false', + } + self.targets = OrderedDict() def register(self, targets): diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index c000dd0c9..751652901 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals from moto.core.responses import BaseResponse from .models import elbv2_backends -from .exceptions import DuplicateTagKeysError, LoadBalancerNotFoundError +from .exceptions import DuplicateTagKeysError +from .exceptions import LoadBalancerNotFoundError +from .exceptions import TargetGroupNotFoundError class ELBV2Response(BaseResponse): @@ -107,6 +109,14 @@ class ELBV2Response(BaseResponse): template = self.response_template(DESCRIBE_TARGET_GROUPS_TEMPLATE) return template.render(target_groups=target_groups) + def describe_target_group_attributes(self): + target_group_arn = self._get_param('TargetGroupArn') + target_group = self.elbv2_backend.target_groups.get(target_group_arn) + if not target_group: + raise TargetGroupNotFoundError() + template = self.response_template(DESCRIBE_TARGET_GROUP_ATTRIBUTES_TEMPLATE) + return template.render(attributes=target_group.attributes) + def describe_listeners(self): load_balancer_arn = self._get_param('LoadBalancerArn') listener_arns = self._get_multi_param('ListenerArns.member') @@ -135,6 +145,19 @@ class ELBV2Response(BaseResponse): template = self.response_template(DELETE_LISTENER_TEMPLATE) return template.render() + def modify_target_group_attributes(self): + target_group_arn = self._get_param('TargetGroupArn') + target_group = self.elbv2_backend.target_groups.get(target_group_arn) + attributes = { + attr['key']: attr['value'] + for attr in self._get_list_prefix('Attributes.member') + } + target_group.attributes.update(attributes) + if not target_group: + raise TargetGroupNotFoundError() + template = self.response_template(MODIFY_TARGET_GROUP_ATTRIBUTES_TEMPLATE) + return template.render(attributes=attributes) + def register_targets(self): target_group_arn = self._get_param('TargetGroupArn') targets = self._get_list_prefix('Targets.member') @@ -455,6 +478,23 @@ DESCRIBE_TARGET_GROUPS_TEMPLATE = """ + + + {% for key, value in attributes.items() %} + + {{ key }} + {{ value }} + + {% endfor %} + + + + 70092c0e-f3a9-11e5-ae48-cff02092876b + +""" + + DESCRIBE_LISTENERS_TEMPLATE = """ @@ -504,6 +544,22 @@ CONFIGURE_HEALTH_CHECK_TEMPLATE = """ + + + {% for key, value in attributes.items() %} + + {{ key }} + {{ value }} + + {% endfor %} + + + + 70092c0e-f3a9-11e5-ae48-cff02092876b + +""" + REGISTER_TARGETS_TEMPLATE = """ diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index c9eb9ea43..6bfe2ca4f 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -445,3 +445,76 @@ def test_register_targets(): response = conn.describe_target_health(TargetGroupArn=target_group.get('TargetGroupArn')) response.get('TargetHealthDescriptions').should.have.length_of(1) + + +@mock_ec2 +@mock_elbv2 +def test_target_group_attributes(): + conn = boto3.client('elbv2', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + security_group = ec2.create_security_group(GroupName='a-security-group', Description='First One') + vpc = ec2.create_vpc(CidrBlock='172.28.7.0/24', InstanceTenancy='default') + subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1a') + subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock='172.28.7.192/26', AvailabilityZone='us-east-1b') + + response = conn.create_load_balancer( + Name='my-lb', + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme='internal', + Tags=[{'Key': 'key_name', 'Value': 'a_value'}]) + + response = conn.create_target_group( + Name='a-target', + Protocol='HTTP', + Port=8080, + VpcId=vpc.id, + HealthCheckProtocol='HTTP', + HealthCheckPort='8080', + HealthCheckPath='/', + HealthCheckIntervalSeconds=5, + HealthCheckTimeoutSeconds=5, + HealthyThresholdCount=5, + UnhealthyThresholdCount=2, + Matcher={'HttpCode': '200'}) + target_group = response.get('TargetGroups')[0] + + # Check it's in the describe_target_groups response + response = conn.describe_target_groups() + response.get('TargetGroups').should.have.length_of(1) + target_group_arn = target_group['TargetGroupArn'] + + # The attributes should start with the two defaults + response = conn.describe_target_group_attributes(TargetGroupArn=target_group_arn) + response['Attributes'].should.have.length_of(2) + attributes = {attr['Key']: attr['Value'] for attr in response['Attributes']} + attributes['deregistration_delay.timeout_seconds'].should.equal('300') + attributes['stickiness.enabled'].should.equal('false') + + # add cookie stickiness + response = conn.modify_target_group_attributes( + TargetGroupArn=target_group_arn, + Attributes=[ + { + 'Key': 'stickiness.enabled', + 'Value': 'true', + }, + { + 'Key': 'stickiness.type', + 'Value': 'lb_cookie', + }, + ]) + + # the response should have only the keys updated + response['Attributes'].should.have.length_of(2) + attributes = {attr['Key']: attr['Value'] for attr in response['Attributes']} + attributes['stickiness.type'].should.equal('lb_cookie') + attributes['stickiness.enabled'].should.equal('true') + + # These new values should be in the full attribute list + response = conn.describe_target_group_attributes(TargetGroupArn=target_group_arn) + response['Attributes'].should.have.length_of(3) + attributes = {attr['Key']: attr['Value'] for attr in response['Attributes']} + attributes['stickiness.type'].should.equal('lb_cookie') + attributes['stickiness.enabled'].should.equal('true') From 7cff4067789e59fa68372a6ef0bbdb06415c435c Mon Sep 17 00:00:00 2001 From: Jack Danger Date: Wed, 2 Aug 2017 15:58:32 -0700 Subject: [PATCH 269/274] fixing case of comments --- tests/test_elbv2/test_elbv2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 6bfe2ca4f..ece17571d 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -492,7 +492,7 @@ def test_target_group_attributes(): attributes['deregistration_delay.timeout_seconds'].should.equal('300') attributes['stickiness.enabled'].should.equal('false') - # add cookie stickiness + # Add cookie stickiness response = conn.modify_target_group_attributes( TargetGroupArn=target_group_arn, Attributes=[ @@ -506,7 +506,7 @@ def test_target_group_attributes(): }, ]) - # the response should have only the keys updated + # The response should have only the keys updated response['Attributes'].should.have.length_of(2) attributes = {attr['Key']: attr['Value'] for attr in response['Attributes']} attributes['stickiness.type'].should.equal('lb_cookie') From 0bceaabc40ba779846b3c8e45976aebff7116bde Mon Sep 17 00:00:00 2001 From: Andrew Hill Date: Fri, 4 Aug 2017 11:57:48 +1000 Subject: [PATCH 270/274] Fix SWF name in docs --- README.md | 2 +- docs/_build/html/_sources/index.rst.txt | 2 +- docs/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 369d430f5..5c4cdc259 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | STS | @mock_sts | core endpoints done | |------------------------------------------------------------------------------| -| SWF | @mock_sfw | basic endpoints done | +| SWF | @mock_swf | basic endpoints done | |------------------------------------------------------------------------------| ``` diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt index 2ce31febd..0c4133048 100644 --- a/docs/_build/html/_sources/index.rst.txt +++ b/docs/_build/html/_sources/index.rst.txt @@ -74,7 +74,7 @@ Currently implemented Services: +-----------------------+---------------------+-----------------------------------+ | STS | @mock_sts | core endpoints done | +-----------------------+---------------------+-----------------------------------+ -| SWF | @mock_sfw | basic endpoints done | +| SWF | @mock_swf | basic endpoints done | +-----------------------+---------------------+-----------------------------------+ diff --git a/docs/index.rst b/docs/index.rst index 9a9fa5261..321342401 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -75,7 +75,7 @@ Currently implemented Services: +-----------------------+---------------------+-----------------------------------+ | STS | @mock_sts | core endpoints done | +-----------------------+---------------------+-----------------------------------+ -| SWF | @mock_sfw | basic endpoints done | +| SWF | @mock_swf | basic endpoints done | +-----------------------+---------------------+-----------------------------------+ From ce2f3e6e2b30d5bd412064922acca468e71a6aea Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 5 Aug 2017 15:47:40 +1000 Subject: [PATCH 271/274] fix receiving of messages from queues with a dot character in their name --- moto/sqs/urls.py | 2 +- tests/test_sqs/test_server.py | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/moto/sqs/urls.py b/moto/sqs/urls.py index 0780615ab..9ec014a80 100644 --- a/moto/sqs/urls.py +++ b/moto/sqs/urls.py @@ -9,5 +9,5 @@ dispatch = SQSResponse().dispatch url_paths = { '{0}/$': dispatch, - '{0}/(?P\d+)/(?P[a-zA-Z0-9\-_]+)': dispatch, + '{0}/(?P\d+)/(?P[a-zA-Z0-9\-_\.]+)': dispatch, } diff --git a/tests/test_sqs/test_server.py b/tests/test_sqs/test_server.py index b7a43ab90..e7f745fd2 100644 --- a/tests/test_sqs/test_server.py +++ b/tests/test_sqs/test_server.py @@ -19,22 +19,29 @@ def test_sqs_list_identities(): res = test_client.get('/?Action=ListQueues') res.data.should.contain(b"ListQueuesResponse") - res = test_client.put('/?Action=CreateQueue&QueueName=testqueue') - res = test_client.put('/?Action=CreateQueue&QueueName=otherqueue') + # Make sure that we can receive messages from queues whose name contains dots (".") + # The AWS API mandates that the names of FIFO queues use the suffix ".fifo" + # See: https://github.com/spulec/moto/issues/866 + + for queue_name in ('testqueue', 'otherqueue.fifo'): + + res = test_client.put('/?Action=CreateQueue&QueueName=%s' % queue_name) + + + res = test_client.put( + '/123/%s?MessageBody=test-message&Action=SendMessage' % queue_name) + + res = test_client.get( + '/123/%s?Action=ReceiveMessage&MaxNumberOfMessages=1' % queue_name) + + message = re.search("(.*?)", + res.data.decode('utf-8')).groups()[0] + message.should.equal('test-message') res = test_client.get('/?Action=ListQueues&QueueNamePrefix=other') + res.data.should.contain(b'otherqueue.fifo') res.data.should_not.contain(b'testqueue') - res = test_client.put( - '/123/testqueue?MessageBody=test-message&Action=SendMessage') - - res = test_client.get( - '/123/testqueue?Action=ReceiveMessage&MaxNumberOfMessages=1') - - message = re.search("(.*?)", - res.data.decode('utf-8')).groups()[0] - message.should.equal('test-message') - def test_messages_polling(): backend = server.create_backend_app("sqs") From 24d1562d2fff60963b3497792634e33c08acf1d8 Mon Sep 17 00:00:00 2001 From: Waldemar Hummer Date: Sat, 5 Aug 2017 20:29:40 +1000 Subject: [PATCH 272/274] allow non-ascii characters in request URLs --- moto/server.py | 8 ++++++++ tests/test_s3/test_server.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/moto/server.py b/moto/server.py index be41f1ed0..8d0103cc2 100644 --- a/moto/server.py +++ b/moto/server.py @@ -3,6 +3,7 @@ import json import re import sys import argparse +import six from six.moves.urllib.parse import urlencode @@ -47,6 +48,13 @@ class DomainDispatcherApplication(object): def get_application(self, environ): path_info = environ.get('PATH_INFO', '') + + # The URL path might contain non-ASCII text, for instance unicode S3 bucket names + if six.PY2 and isinstance(path_info, str): + path_info = six.u(path_info) + if six.PY3 and isinstance(path_info, six.binary_type): + path_info = path_info.decode('utf-8') + if path_info.startswith("/moto-api") or path_info == "/favicon.ico": host = "moto_api" elif path_info.startswith("/latest/meta-data/"): diff --git a/tests/test_s3/test_server.py b/tests/test_s3/test_server.py index 5353ec209..c3ca3c3ff 100644 --- a/tests/test_s3/test_server.py +++ b/tests/test_s3/test_server.py @@ -1,3 +1,5 @@ +# coding=utf-8 + from __future__ import unicode_literals import sure # noqa @@ -78,3 +80,18 @@ def test_s3_server_post_without_content_length(): res = test_client.post('/', "https://tester.localhost:5000/", environ_overrides={'CONTENT_LENGTH': ''}) res.status_code.should.equal(411) + + +def test_s3_server_post_unicode_bucket_key(): + # Make sure that we can deal with non-ascii characters in request URLs (e.g., S3 object names) + dispatcher = server.DomainDispatcherApplication(server.create_backend_app) + backend_app = dispatcher.get_application({ + 'HTTP_HOST': 's3.amazonaws.com', + 'PATH_INFO': '/test-bucket/test-object-てすと' + }) + assert backend_app + backend_app = dispatcher.get_application({ + 'HTTP_HOST': 's3.amazonaws.com', + 'PATH_INFO': '/test-bucket/test-object-てすと'.encode('utf-8') + }) + assert backend_app From 5ed546d59cd66251d3c25b1759a1eb59132d47a5 Mon Sep 17 00:00:00 2001 From: ygrosu Date: Wed, 9 Aug 2017 10:56:15 +0300 Subject: [PATCH 273/274] updating documentation to describe support for boto2 --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 5c4cdc259..cabf6b45f 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,28 @@ def test_add_servers(): assert instance1['ImageId'] == 'ami-1234abcd' ``` +#### Using moto 1.0.X with boto2 +moto 1.0.X mock docorators are defined for boto3 and do not work with boto2. Use the @mock_AWSSVC_deprecated to work with boto2. + +Using moto with boto2 +```python +from moto import mock_ec2_deprecated +import boto + +@mock_ec2_deprecated +def test_something_with_ec2(): + ec2_conn = boto.ec2.connect_to_region('us-east-1') + ec2_conn.get_only_instances(instance_ids='i-123456') + +``` + +When using both boto2 and boto3, one can do this to avoid confusion: +```python +from moto import mock_ec2_deprecated as mock_ec2_b2 +from moto import mock_ec2 + +``` + ## Usage All of the services can be used as a decorator, context manager, or in a raw form. From 0a03a7237e50c9867bbe2c7f7bc19565ec75065d Mon Sep 17 00:00:00 2001 From: Brian Pandola Date: Wed, 9 Aug 2017 18:43:21 -0700 Subject: [PATCH 274/274] Redshift Updates - Implement create_cluster_snapshot endpoint - Implement describe_cluster_snapshots endpoint - Implement delete_cluster_snapshot endpoint - Implement restore_from_cluster_snapshot endpoint - Implement limited support for describe_tags endpoint - Correctly serialize errors to json (for boto) or xml (for boto3) - Simulate cluster spin up by returning initial status as 'creating' and subsequent statuses as 'available' - Fix issue with modify_cluster endpoint where cluster values get set to None when omitted from request - Add 'Endpoint' key to describe_clusters response syntax --- moto/redshift/exceptions.py | 15 +++ moto/redshift/models.py | 102 ++++++++++++++- moto/redshift/responses.py | 180 +++++++++++++++++++++++++- tests/test_redshift/test_redshift.py | 185 ++++++++++++++++++++++++++- 4 files changed, 474 insertions(+), 8 deletions(-) diff --git a/moto/redshift/exceptions.py b/moto/redshift/exceptions.py index 8bcca807e..877e850e4 100644 --- a/moto/redshift/exceptions.py +++ b/moto/redshift/exceptions.py @@ -56,3 +56,18 @@ class InvalidSubnetError(RedshiftClientError): super(InvalidSubnetError, self).__init__( 'InvalidSubnet', "Subnet {0} not found.".format(subnet_identifier)) + + +class ClusterSnapshotNotFoundError(RedshiftClientError): + def __init__(self, snapshot_identifier): + super(ClusterSnapshotNotFoundError, self).__init__( + 'ClusterSnapshotNotFound', + "Snapshot {0} not found.".format(snapshot_identifier)) + + +class ClusterSnapshotAlreadyExistsError(RedshiftClientError): + def __init__(self, snapshot_identifier): + super(ClusterSnapshotAlreadyExistsError, self).__init__( + 'ClusterSnapshotAlreadyExists', + "Cannot create the snapshot because a snapshot with the " + "identifier {0} already exists".format(snapshot_identifier)) diff --git a/moto/redshift/models.py b/moto/redshift/models.py index 5e64f7a16..29c802fb0 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -1,12 +1,19 @@ from __future__ import unicode_literals +import copy +import datetime + import boto.redshift +from moto.compat import OrderedDict from moto.core import BaseBackend, BaseModel +from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.ec2 import ec2_backends from .exceptions import ( ClusterNotFoundError, ClusterParameterGroupNotFoundError, ClusterSecurityGroupNotFoundError, + ClusterSnapshotAlreadyExistsError, + ClusterSnapshotNotFoundError, ClusterSubnetGroupNotFoundError, InvalidSubnetError, ) @@ -23,6 +30,7 @@ class Cluster(BaseModel): encrypted, region): self.redshift_backend = redshift_backend self.cluster_identifier = cluster_identifier + self.status = 'available' self.node_type = node_type self.master_username = master_username self.master_user_password = master_user_password @@ -152,7 +160,7 @@ class Cluster(BaseModel): } for group in self.vpc_security_groups], "ClusterSubnetGroupName": self.cluster_subnet_group_name, "AvailabilityZone": self.availability_zone, - "ClusterStatus": "creating", + "ClusterStatus": self.status, "NumberOfNodes": self.number_of_nodes, "AutomatedSnapshotRetentionPeriod": self.automated_snapshot_retention_period, "PubliclyAccessible": self.publicly_accessible, @@ -171,6 +179,13 @@ class Cluster(BaseModel): "NodeType": self.node_type, "ClusterIdentifier": self.cluster_identifier, "AllowVersionUpgrade": self.allow_version_upgrade, + "Endpoint": { + "Address": '{}.{}.redshift.amazonaws.com'.format( + self.cluster_identifier, + self.region), + "Port": self.port + }, + "PendingModifiedValues": [] } @@ -262,6 +277,42 @@ class ParameterGroup(BaseModel): } +class Snapshot(BaseModel): + + def __init__(self, cluster, snapshot_identifier, tags=None): + self.cluster = copy.copy(cluster) + self.snapshot_identifier = snapshot_identifier + self.snapshot_type = 'manual' + self.status = 'available' + self.tags = tags or [] + self.create_time = iso_8601_datetime_with_milliseconds( + datetime.datetime.now()) + + @property + def arn(self): + return "arn:aws:redshift:{0}:1234567890:snapshot:{1}/{2}".format( + self.cluster.region, + self.cluster.cluster_identifier, + self.snapshot_identifier) + + def to_json(self): + return { + 'SnapshotIdentifier': self.snapshot_identifier, + 'ClusterIdentifier': self.cluster.cluster_identifier, + 'SnapshotCreateTime': self.create_time, + 'Status': self.status, + 'Port': self.cluster.port, + 'AvailabilityZone': self.cluster.availability_zone, + 'MasterUsername': self.cluster.master_username, + 'ClusterVersion': self.cluster.cluster_version, + 'SnapshotType': self.snapshot_type, + 'NodeType': self.cluster.node_type, + 'NumberOfNodes': self.cluster.number_of_nodes, + 'DBName': self.cluster.db_name, + 'Tags': self.tags + } + + class RedshiftBackend(BaseBackend): def __init__(self, ec2_backend): @@ -278,6 +329,7 @@ class RedshiftBackend(BaseBackend): ) } self.ec2_backend = ec2_backend + self.snapshots = OrderedDict() def reset(self): ec2_backend = self.ec2_backend @@ -383,6 +435,54 @@ class RedshiftBackend(BaseBackend): return self.parameter_groups.pop(parameter_group_name) raise ClusterParameterGroupNotFoundError(parameter_group_name) + def create_snapshot(self, cluster_identifier, snapshot_identifier, tags): + cluster = self.clusters.get(cluster_identifier) + if not cluster: + raise ClusterNotFoundError(cluster_identifier) + if self.snapshots.get(snapshot_identifier) is not None: + raise ClusterSnapshotAlreadyExistsError(snapshot_identifier) + snapshot = Snapshot(cluster, snapshot_identifier, tags) + self.snapshots[snapshot_identifier] = snapshot + return snapshot + + def describe_snapshots(self, cluster_identifier, snapshot_identifier): + if cluster_identifier: + for snapshot in self.snapshots.values(): + if snapshot.cluster.cluster_identifier == cluster_identifier: + return [snapshot] + raise ClusterNotFoundError(cluster_identifier) + + if snapshot_identifier: + if snapshot_identifier in self.snapshots: + return [self.snapshots[snapshot_identifier]] + raise ClusterSnapshotNotFoundError(snapshot_identifier) + + return self.snapshots.values() + + def delete_snapshot(self, snapshot_identifier): + if snapshot_identifier not in self.snapshots: + raise ClusterSnapshotNotFoundError(snapshot_identifier) + + deleted_snapshot = self.snapshots.pop(snapshot_identifier) + deleted_snapshot.status = 'deleted' + return deleted_snapshot + + def describe_tags_for_resource_type(self, resource_type): + tagged_resources = [] + if resource_type == 'Snapshot': + for snapshot in self.snapshots.values(): + for tag in snapshot.tags: + data = { + 'ResourceName': snapshot.arn, + 'ResourceType': 'snapshot', + 'Tag': { + 'Key': tag['Key'], + 'Value': tag['Value'] + } + } + tagged_resources.append(data) + return tagged_resources + redshift_backends = {} for region in boto.redshift.regions(): diff --git a/moto/redshift/responses.py b/moto/redshift/responses.py index 48f113cf2..411569d01 100644 --- a/moto/redshift/responses.py +++ b/moto/redshift/responses.py @@ -1,12 +1,31 @@ from __future__ import unicode_literals import json + import dicttoxml +from jinja2 import Template +from six import iteritems from moto.core.responses import BaseResponse from .models import redshift_backends +def convert_json_error_to_xml(json_error): + error = json.loads(json_error) + code = error['Error']['Code'] + message = error['Error']['Message'] + template = Template(""" + + + {{ code }} + {{ message }} + Sender + + 6876f774-7273-11e4-85dc-39e55ca848d1 + """) + return template.render(code=code, message=message) + + class RedshiftResponse(BaseResponse): @property @@ -20,6 +39,24 @@ class RedshiftResponse(BaseResponse): xml = dicttoxml.dicttoxml(response, attr_type=False, root=False) return xml.decode("utf-8") + def call_action(self): + status, headers, body = super(RedshiftResponse, self).call_action() + if status >= 400 and not self.request_json: + body = convert_json_error_to_xml(body) + return status, headers, body + + def unpack_complex_list_params(self, label, names): + unpacked_list = list() + count = 1 + while self._get_param('{0}.{1}.{2}'.format(label, count, names[0])): + param = dict() + for i in range(len(names)): + param[names[i]] = self._get_param( + '{0}.{1}.{2}'.format(label, count, names[i])) + unpacked_list.append(param) + count += 1 + return unpacked_list + def create_cluster(self): cluster_kwargs = { "cluster_identifier": self._get_param('ClusterIdentifier'), @@ -43,12 +80,66 @@ class RedshiftResponse(BaseResponse): "encrypted": self._get_param("Encrypted"), "region": self.region, } - cluster = self.redshift_backend.create_cluster(**cluster_kwargs) - + cluster = self.redshift_backend.create_cluster(**cluster_kwargs).to_json() + cluster['ClusterStatus'] = 'creating' return self.get_response({ "CreateClusterResponse": { "CreateClusterResult": { - "Cluster": cluster.to_json(), + "Cluster": cluster, + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def restore_from_cluster_snapshot(self): + snapshot_identifier = self._get_param('SnapshotIdentifier') + snapshots = self.redshift_backend.describe_snapshots( + None, + snapshot_identifier) + snapshot = snapshots[0] + kwargs_from_snapshot = { + "node_type": snapshot.cluster.node_type, + "master_username": snapshot.cluster.master_username, + "master_user_password": snapshot.cluster.master_user_password, + "db_name": snapshot.cluster.db_name, + "cluster_type": 'multi-node' if snapshot.cluster.number_of_nodes > 1 else 'single-node', + "availability_zone": snapshot.cluster.availability_zone, + "port": snapshot.cluster.port, + "cluster_version": snapshot.cluster.cluster_version, + "number_of_nodes": snapshot.cluster.number_of_nodes, + } + kwargs_from_request = { + "cluster_identifier": self._get_param('ClusterIdentifier'), + "port": self._get_int_param('Port'), + "availability_zone": self._get_param('AvailabilityZone'), + "allow_version_upgrade": self._get_bool_param( + 'AllowVersionUpgrade'), + "cluster_subnet_group_name": self._get_param( + 'ClusterSubnetGroupName'), + "publicly_accessible": self._get_param("PubliclyAccessible"), + "cluster_parameter_group_name": self._get_param( + 'ClusterParameterGroupName'), + "cluster_security_groups": self._get_multi_param( + 'ClusterSecurityGroups.member'), + "vpc_security_group_ids": self._get_multi_param( + 'VpcSecurityGroupIds.member'), + "preferred_maintenance_window": self._get_param( + 'PreferredMaintenanceWindow'), + "automated_snapshot_retention_period": self._get_int_param( + 'AutomatedSnapshotRetentionPeriod'), + "region": self.region, + "encrypted": False, + } + kwargs_from_snapshot.update(kwargs_from_request) + cluster_kwargs = kwargs_from_snapshot + cluster = self.redshift_backend.create_cluster(**cluster_kwargs).to_json() + cluster['ClusterStatus'] = 'creating' + return self.get_response({ + "RestoreFromClusterSnapshotResponse": { + "RestoreFromClusterSnapshotResult": { + "Cluster": cluster, }, "ResponseMetadata": { "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", @@ -72,7 +163,7 @@ class RedshiftResponse(BaseResponse): }) def modify_cluster(self): - cluster_kwargs = { + request_kwargs = { "cluster_identifier": self._get_param('ClusterIdentifier'), "new_cluster_identifier": self._get_param('NewClusterIdentifier'), "node_type": self._get_param('NodeType'), @@ -90,6 +181,19 @@ class RedshiftResponse(BaseResponse): "publicly_accessible": self._get_param("PubliclyAccessible"), "encrypted": self._get_param("Encrypted"), } + # There's a bug in boto3 where the security group ids are not passed + # according to the AWS documentation + if not request_kwargs['vpc_security_group_ids']: + request_kwargs['vpc_security_group_ids'] = self._get_multi_param( + 'VpcSecurityGroupIds.VpcSecurityGroupId') + + cluster_kwargs = {} + # We only want parameters that were actually passed in, otherwise + # we'll stomp all over our cluster metadata with None values. + for (key, value) in iteritems(request_kwargs): + if value is not None and value != []: + cluster_kwargs[key] = value + cluster = self.redshift_backend.modify_cluster(**cluster_kwargs) return self.get_response({ @@ -273,3 +377,71 @@ class RedshiftResponse(BaseResponse): } } }) + + def create_cluster_snapshot(self): + cluster_identifier = self._get_param('ClusterIdentifier') + snapshot_identifier = self._get_param('SnapshotIdentifier') + tags = self.unpack_complex_list_params( + 'Tags.Tag', ('Key', 'Value')) + snapshot = self.redshift_backend.create_snapshot(cluster_identifier, + snapshot_identifier, + tags) + return self.get_response({ + 'CreateClusterSnapshotResponse': { + "CreateClusterSnapshotResult": { + "Snapshot": snapshot.to_json(), + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def describe_cluster_snapshots(self): + cluster_identifier = self._get_param('ClusterIdentifier') + snapshot_identifier = self._get_param('DBSnapshotIdentifier') + snapshots = self.redshift_backend.describe_snapshots(cluster_identifier, + snapshot_identifier) + return self.get_response({ + "DescribeClusterSnapshotsResponse": { + "DescribeClusterSnapshotsResult": { + "Snapshots": [snapshot.to_json() for snapshot in snapshots] + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def delete_cluster_snapshot(self): + snapshot_identifier = self._get_param('SnapshotIdentifier') + snapshot = self.redshift_backend.delete_snapshot(snapshot_identifier) + + return self.get_response({ + "DeleteClusterSnapshotResponse": { + "DeleteClusterSnapshotResult": { + "Snapshot": snapshot.to_json() + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) + + def describe_tags(self): + resource_type = self._get_param('ResourceType') + if resource_type != 'Snapshot': + raise NotImplementedError( + "The describe_tags action has not been fully implemented.") + tagged_resources = \ + self.redshift_backend.describe_tags_for_resource_type(resource_type) + return self.get_response({ + "DescribeTagsResponse": { + "DescribeTagsResult": { + "TaggedResources": tagged_resources + }, + "ResponseMetadata": { + "RequestId": "384ac68d-3775-11df-8963-01868b7c937a", + } + } + }) diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py index aff3e8bed..1df503de2 100644 --- a/tests/test_redshift/test_redshift.py +++ b/tests/test_redshift/test_redshift.py @@ -9,6 +9,9 @@ from boto.redshift.exceptions import ( ClusterSubnetGroupNotFound, InvalidSubnet, ) +from botocore.exceptions import ( + ClientError +) import sure # noqa from moto import mock_ec2 @@ -36,7 +39,7 @@ def test_create_cluster(): conn = boto.redshift.connect_to_region("us-east-1") cluster_identifier = 'my_cluster' - conn.create_cluster( + cluster_response = conn.create_cluster( cluster_identifier, node_type="dw.hs1.xlarge", master_username="username", @@ -51,6 +54,8 @@ def test_create_cluster(): allow_version_upgrade=True, number_of_nodes=3, ) + cluster_response['CreateClusterResponse']['CreateClusterResult'][ + 'Cluster']['ClusterStatus'].should.equal('creating') cluster_response = conn.describe_clusters(cluster_identifier) cluster = cluster_response['DescribeClustersResponse'][ @@ -320,7 +325,6 @@ def test_modify_cluster(): cluster_identifier, cluster_type="multi-node", node_type="dw.hs1.xlarge", - number_of_nodes=2, cluster_security_groups="security_group", master_user_password="new_password", cluster_parameter_group_name="my_parameter_group", @@ -343,7 +347,8 @@ def test_modify_cluster(): 'ParameterGroupName'].should.equal("my_parameter_group") cluster['AutomatedSnapshotRetentionPeriod'].should.equal(7) cluster['AllowVersionUpgrade'].should.equal(False) - cluster['NumberOfNodes'].should.equal(2) + # This one should remain unmodified. + cluster['NumberOfNodes'].should.equal(1) @mock_redshift_deprecated @@ -523,3 +528,177 @@ def test_delete_cluster_parameter_group(): # Delete invalid id conn.delete_cluster_parameter_group.when.called_with( "not-a-parameter-group").should.throw(ClusterParameterGroupNotFound) + + +@mock_redshift +def test_create_cluster_snapshot(): + client = boto3.client('redshift', region_name='us-east-1') + cluster_identifier = 'my_cluster' + snapshot_identifier = 'my_snapshot' + + cluster_response = client.create_cluster( + DBName='test-db', + ClusterIdentifier=cluster_identifier, + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='username', + MasterUserPassword='password', + ) + cluster_response['Cluster']['NodeType'].should.equal('ds2.xlarge') + + snapshot_response = client.create_cluster_snapshot( + SnapshotIdentifier=snapshot_identifier, + ClusterIdentifier=cluster_identifier, + Tags=[{'Key': 'test-tag-key', + 'Value': 'test-tag-value'}] + ) + snapshot = snapshot_response['Snapshot'] + snapshot['SnapshotIdentifier'].should.equal(snapshot_identifier) + snapshot['ClusterIdentifier'].should.equal(cluster_identifier) + snapshot['NumberOfNodes'].should.equal(1) + snapshot['NodeType'].should.equal('ds2.xlarge') + snapshot['MasterUsername'].should.equal('username') + + +@mock_redshift +def test_delete_cluster_snapshot(): + client = boto3.client('redshift', region_name='us-east-1') + cluster_identifier = 'my_cluster' + snapshot_identifier = 'my_snapshot' + + client.create_cluster( + ClusterIdentifier=cluster_identifier, + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='username', + MasterUserPassword='password', + ) + client.create_cluster_snapshot( + SnapshotIdentifier=snapshot_identifier, + ClusterIdentifier=cluster_identifier + ) + + snapshots = client.describe_cluster_snapshots()['Snapshots'] + list(snapshots).should.have.length_of(1) + + client.delete_cluster_snapshot(SnapshotIdentifier=snapshot_identifier)[ + 'Snapshot']['Status'].should.equal('deleted') + + snapshots = client.describe_cluster_snapshots()['Snapshots'] + list(snapshots).should.have.length_of(0) + + # Delete invalid id + client.delete_cluster_snapshot.when.called_with( + SnapshotIdentifier="not-a-snapshot").should.throw(ClientError) + + +@mock_redshift +def test_cluster_snapshot_already_exists(): + client = boto3.client('redshift', region_name='us-east-1') + cluster_identifier = 'my_cluster' + snapshot_identifier = 'my_snapshot' + + client.create_cluster( + DBName='test-db', + ClusterIdentifier=cluster_identifier, + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='username', + MasterUserPassword='password', + ) + + client.create_cluster_snapshot( + SnapshotIdentifier=snapshot_identifier, + ClusterIdentifier=cluster_identifier + ) + + client.create_cluster_snapshot.when.called_with( + SnapshotIdentifier=snapshot_identifier, + ClusterIdentifier=cluster_identifier + ).should.throw(ClientError) + + +@mock_redshift +def test_create_cluster_from_snapshot(): + client = boto3.client('redshift', region_name='us-east-1') + original_cluster_identifier = 'original-cluster' + original_snapshot_identifier = 'original-snapshot' + new_cluster_identifier = 'new-cluster' + + client.create_cluster( + ClusterIdentifier=original_cluster_identifier, + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='username', + MasterUserPassword='password', + ) + client.create_cluster_snapshot( + SnapshotIdentifier=original_snapshot_identifier, + ClusterIdentifier=original_cluster_identifier + ) + response = client.restore_from_cluster_snapshot( + ClusterIdentifier=new_cluster_identifier, + SnapshotIdentifier=original_snapshot_identifier, + Port=1234 + ) + response['Cluster']['ClusterStatus'].should.equal('creating') + + response = client.describe_clusters( + ClusterIdentifier=new_cluster_identifier + ) + new_cluster = response['Clusters'][0] + new_cluster['NodeType'].should.equal('ds2.xlarge') + new_cluster['MasterUsername'].should.equal('username') + new_cluster['Endpoint']['Port'].should.equal(1234) + + +@mock_redshift +def test_create_cluster_status_update(): + client = boto3.client('redshift', region_name='us-east-1') + cluster_identifier = 'test-cluster' + + response = client.create_cluster( + ClusterIdentifier=cluster_identifier, + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='username', + MasterUserPassword='password', + ) + response['Cluster']['ClusterStatus'].should.equal('creating') + + response = client.describe_clusters( + ClusterIdentifier=cluster_identifier + ) + response['Clusters'][0]['ClusterStatus'].should.equal('available') + + +@mock_redshift +def test_describe_snapshot_tags(): + client = boto3.client('redshift', region_name='us-east-1') + cluster_identifier = 'my_cluster' + snapshot_identifier = 'my_snapshot' + tag_key = 'test-tag-key' + tag_value = 'teat-tag-value' + + client.create_cluster( + DBName='test-db', + ClusterIdentifier=cluster_identifier, + ClusterType='single-node', + NodeType='ds2.xlarge', + MasterUsername='username', + MasterUserPassword='password', + ) + + client.create_cluster_snapshot( + SnapshotIdentifier=snapshot_identifier, + ClusterIdentifier=cluster_identifier, + Tags=[{'Key': tag_key, + 'Value': tag_value}] + ) + + tags_response = client.describe_tags(ResourceType='Snapshot') + tagged_resources = tags_response['TaggedResources'] + list(tagged_resources).should.have.length_of(1) + tag = tagged_resources[0]['Tag'] + tag['Key'].should.equal(tag_key) + tag['Value'].should.equal(tag_value)