diff options
author | kyleknap <kyleknap@amazon.com> | 2015-01-19 16:21:23 -0800 |
---|---|---|
committer | kyleknap <kyleknap@amazon.com> | 2015-01-19 16:21:23 -0800 |
commit | 3213c250967c820b5ecf48acf9c9c4d367e37b69 (patch) | |
tree | 09fdf2fb5a828f8798075f9e05f540369c03df25 | |
parent | 350edd84d3033f01ef4ea31e5dfc4ccfb4e99310 (diff) | |
parent | 41e10af1c17d8105941f8fbec221ae34c135b68f (diff) | |
download | boto-3213c250967c820b5ecf48acf9c9c4d367e37b69.tar.gz |
Merge branch 'release-2.35.2'2.35.2
-rw-r--r-- | README.rst | 4 | ||||
-rw-r--r-- | boto/__init__.py | 2 | ||||
-rw-r--r-- | boto/cloudformation/connection.py | 53 | ||||
-rw-r--r-- | boto/dynamodb/layer2.py | 6 | ||||
-rw-r--r-- | boto/dynamodb/types.py | 59 | ||||
-rw-r--r-- | boto/dynamodb2/items.py | 4 | ||||
-rw-r--r-- | boto/dynamodb2/table.py | 65 | ||||
-rw-r--r-- | boto/dynamodb2/types.py | 6 | ||||
-rw-r--r-- | boto/ec2/autoscale/__init__.py | 8 | ||||
-rw-r--r-- | boto/ec2/autoscale/launchconfig.py | 32 | ||||
-rw-r--r-- | docs/source/dynamodb2_tut.rst | 4 | ||||
-rw-r--r-- | docs/source/index.rst | 1 | ||||
-rw-r--r-- | docs/source/releasenotes/v2.35.2.rst | 16 | ||||
-rw-r--r-- | tests/integration/dynamodb2/test_highlevel.py | 73 | ||||
-rw-r--r-- | tests/unit/cloudformation/test_connection.py | 4 | ||||
-rw-r--r-- | tests/unit/dynamodb/test_types.py | 16 | ||||
-rw-r--r-- | tests/unit/ec2/autoscale/test_group.py | 13 |
17 files changed, 302 insertions, 64 deletions
@@ -1,9 +1,9 @@ #### boto #### -boto 2.35.1 +boto 2.35.2 -Released: 09-Jan-2015 +Released: 19-Jan-2015 .. image:: https://travis-ci.org/boto/boto.svg?branch=develop :target: https://travis-ci.org/boto/boto diff --git a/boto/__init__.py b/boto/__init__.py index f4f157ab..b076f2f1 100644 --- a/boto/__init__.py +++ b/boto/__init__.py @@ -38,7 +38,7 @@ import logging.config from boto.compat import urlparse from boto.exception import InvalidUriError -__version__ = '2.35.1' +__version__ = '2.35.2' Version = __version__ # for backware compatibility # http://bugs.python.org/issue7980 diff --git a/boto/cloudformation/connection.py b/boto/cloudformation/connection.py index 84b4ea6e..1407c570 100644 --- a/boto/cloudformation/connection.py +++ b/boto/cloudformation/connection.py @@ -98,7 +98,8 @@ class CloudFormationConnection(AWSQueryConnection): def _build_create_or_update_params(self, stack_name, template_body, template_url, parameters, disable_rollback, timeout_in_minutes, notification_arns, capabilities, on_failure, stack_policy_body, - stack_policy_url, tags, stack_policy_during_update_body=None, + stack_policy_url, tags, use_previous_template=None, + stack_policy_during_update_body=None, stack_policy_during_update_url=None): """ Helper that creates JSON parameters needed by a Stack Create or @@ -117,16 +118,20 @@ class CloudFormationConnection(AWSQueryConnection): :param template_body: Structure containing the template body. (For more information, go to `Template Anatomy`_ in the AWS CloudFormation User Guide.) - Conditional: You must pass `TemplateBody` or `TemplateURL`. If both are - passed, only `TemplateBody` is used. + Conditional: You must pass either `UsePreviousTemplate` or one of + `TemplateBody` or `TemplateUrl`. If both `TemplateBody` and + `TemplateUrl` are passed, only `TemplateBody` is used. + `TemplateBody`. :type template_url: string :param template_url: Location of file containing the template body. The URL must point to a template (max size: 307,200 bytes) located in an S3 bucket in the same region as the stack. For more information, go to the `Template Anatomy`_ in the AWS CloudFormation User Guide. - Conditional: You must pass `TemplateURL` or `TemplateBody`. If both are - passed, only `TemplateBody` is used. + Conditional: You must pass either `UsePreviousTemplate` or one of + `TemplateBody` or `TemplateUrl`. If both `TemplateBody` and + `TemplateUrl` are passed, only `TemplateBody` is used. + `TemplateBody`. :type parameters: list :param parameters: A list of key/value tuples that specify input @@ -186,6 +191,13 @@ class CloudFormationConnection(AWSQueryConnection): propagated to EC2 resources that are created as part of the stack. A maximum number of 10 tags can be specified. + :type use_previous_template: boolean + :param use_previous_template: Set to `True` to use the previous + template instead of uploading a new one via `TemplateBody` or + `TemplateURL`. + Conditional: You must pass either `UsePreviousTemplate` or one of + `TemplateBody` or `TemplateUrl`. + :type stack_policy_during_update_body: string :param stack_policy_during_update_body: Structure containing the temporary overriding stack policy body. If you pass @@ -217,6 +229,8 @@ class CloudFormationConnection(AWSQueryConnection): params['TemplateBody'] = template_body if template_url: params['TemplateURL'] = template_url + if use_previous_template is not None: + params['UsePreviousTemplate'] = self.encode_bool(use_previous_template) if template_body and template_url: boto.log.warning("If both TemplateBody and TemplateURL are" " specified, only TemplateBody will be honored by the API") @@ -383,6 +397,7 @@ class CloudFormationConnection(AWSQueryConnection): def update_stack(self, stack_name, template_body=None, template_url=None, parameters=None, notification_arns=None, disable_rollback=False, timeout_in_minutes=None, capabilities=None, tags=None, + use_previous_template=None, stack_policy_during_update_body=None, stack_policy_during_update_url=None, stack_policy_body=None, stack_policy_url=None): @@ -421,16 +436,26 @@ class CloudFormationConnection(AWSQueryConnection): :param template_body: Structure containing the template body. (For more information, go to `Template Anatomy`_ in the AWS CloudFormation User Guide.) - Conditional: You must pass `TemplateBody` or `TemplateURL`. If both are - passed, only `TemplateBody` is used. + Conditional: You must pass either `UsePreviousTemplate` or one of + `TemplateBody` or `TemplateUrl`. If both `TemplateBody` and + `TemplateUrl` are passed, only `TemplateBody` is used. :type template_url: string :param template_url: Location of file containing the template body. The - URL must point to a template located in an S3 bucket in the same - region as the stack. For more information, go to `Template - Anatomy`_ in the AWS CloudFormation User Guide. - Conditional: You must pass `TemplateURL` or `TemplateBody`. If both are - passed, only `TemplateBody` is used. + URL must point to a template (max size: 307,200 bytes) located in + an S3 bucket in the same region as the stack. For more information, + go to the `Template Anatomy`_ in the AWS CloudFormation User Guide. + Conditional: You must pass either `UsePreviousTemplate` or one of + `TemplateBody` or `TemplateUrl`. If both `TemplateBody` and + `TemplateUrl` are passed, only `TemplateBody` is used. + `TemplateBody`. + + :type use_previous_template: boolean + :param use_previous_template: Set to `True` to use the previous + template instead of uploading a new one via `TemplateBody` or + `TemplateURL`. + Conditional: You must pass either `UsePreviousTemplate` or one of + `TemplateBody` or `TemplateUrl`. :type parameters: list :param parameters: A list of key/value tuples that specify input @@ -497,8 +522,8 @@ class CloudFormationConnection(AWSQueryConnection): params = self._build_create_or_update_params(stack_name, template_body, template_url, parameters, disable_rollback, timeout_in_minutes, notification_arns, capabilities, None, stack_policy_body, - stack_policy_url, tags, stack_policy_during_update_body, - stack_policy_during_update_url) + stack_policy_url, tags, use_previous_template, + stack_policy_during_update_body, stack_policy_during_update_url) body = self._do_request('UpdateStack', params, '/', 'POST') return body['UpdateStackResponse']['UpdateStackResult']['StackId'] diff --git a/boto/dynamodb/layer2.py b/boto/dynamodb/layer2.py index fa0e545f..9510d499 100644 --- a/boto/dynamodb/layer2.py +++ b/boto/dynamodb/layer2.py @@ -26,7 +26,7 @@ from boto.dynamodb.schema import Schema from boto.dynamodb.item import Item from boto.dynamodb.batch import BatchList, BatchWriteList from boto.dynamodb.types import get_dynamodb_type, Dynamizer, \ - LossyFloatDynamizer + LossyFloatDynamizer, NonBooleanDynamizer class TableGenerator(object): @@ -154,7 +154,7 @@ class Layer2(object): profile_name=profile_name) self.dynamizer = dynamizer() - def use_decimals(self): + def use_decimals(self, use_boolean=False): """ Use the ``decimal.Decimal`` type for encoding/decoding numeric types. @@ -164,7 +164,7 @@ class Layer2(object): """ # Eventually this should be made the default dynamizer. - self.dynamizer = Dynamizer() + self.dynamizer = Dynamizer() if use_boolean else NonBooleanDynamizer() def dynamize_attribute_updates(self, pending_updates): """ diff --git a/boto/dynamodb/types.py b/boto/dynamodb/types.py index 2049c219..6a48ae5f 100644 --- a/boto/dynamodb/types.py +++ b/boto/dynamodb/types.py @@ -27,6 +27,7 @@ Python types and vice-versa. import base64 from decimal import (Decimal, DecimalException, Context, Clamped, Overflow, Inexact, Underflow, Rounded) +from collections import Mapping from boto.dynamodb.exceptions import DynamoDBNumberError from boto.compat import filter, map, six, long_type @@ -51,8 +52,12 @@ def float_to_decimal(f): return result -def is_num(n): - types = (int, long_type, float, bool, Decimal) +def is_num(n, boolean_as_int=True): + if boolean_as_int: + types = (int, long_type, float, Decimal, bool) + else: + types = (int, long_type, float, Decimal) + return isinstance(n, types) or n in types @@ -94,15 +99,20 @@ def convert_binary(n): return Binary(base64.b64decode(n)) -def get_dynamodb_type(val): +def get_dynamodb_type(val, use_boolean=True): """ Take a scalar Python value and return a string representing the corresponding Amazon DynamoDB type. If the value passed in is not a supported type, raise a TypeError. """ dynamodb_type = None - if is_num(val): - dynamodb_type = 'N' + if val is None: + dynamodb_type = 'NULL' + elif is_num(val): + if isinstance(val, bool) and use_boolean: + dynamodb_type = 'BOOL' + else: + dynamodb_type = 'N' elif is_str(val): dynamodb_type = 'S' elif isinstance(val, (set, frozenset)): @@ -114,6 +124,10 @@ def get_dynamodb_type(val): dynamodb_type = 'BS' elif is_binary(val): dynamodb_type = 'B' + elif isinstance(val, Mapping): + dynamodb_type = 'M' + elif isinstance(val, list): + dynamodb_type = 'L' if dynamodb_type is None: msg = 'Unsupported type "%s" for value "%s"' % (type(val), val) raise TypeError(msg) @@ -301,6 +315,18 @@ class Dynamizer(object): def _encode_bs(self, attr): return [self._encode_b(n) for n in attr] + def _encode_null(self, attr): + return True + + def _encode_bool(self, attr): + return attr + + def _encode_m(self, attr): + return dict([(k, self.encode(v)) for k, v in attr.items()]) + + def _encode_l(self, attr): + return [self.encode(i) for i in attr] + def decode(self, attr): """ Takes the format returned by DynamoDB and constructs @@ -338,8 +364,29 @@ class Dynamizer(object): def _decode_bs(self, attr): return set(map(self._decode_b, attr)) + def _decode_null(self, attr): + return None + + def _decode_bool(self, attr): + return attr + + def _decode_m(self, attr): + return dict([(k, self.decode(v)) for k, v in attr.items()]) + + def _decode_l(self, attr): + return [self.decode(i) for i in attr] + + +class NonBooleanDynamizer(Dynamizer): + """Casting boolean type to numeric types. + + This class is provided for backward compatibility. + """ + def _get_dynamodb_type(self, attr): + return get_dynamodb_type(attr, use_boolean=False) + -class LossyFloatDynamizer(Dynamizer): +class LossyFloatDynamizer(NonBooleanDynamizer): """Use float/int instead of Decimal for numeric types. This class is provided for backwards compatibility. Instead of diff --git a/boto/dynamodb2/items.py b/boto/dynamodb2/items.py index b1d72139..b1b535f6 100644 --- a/boto/dynamodb2/items.py +++ b/boto/dynamodb2/items.py @@ -1,7 +1,5 @@ from copy import deepcopy -from boto.dynamodb2.types import Dynamizer - class NEWVALUE(object): # A marker for new data added. @@ -70,7 +68,7 @@ class Item(object): self._loaded = loaded self._orig_data = {} self._data = data - self._dynamizer = Dynamizer() + self._dynamizer = table._dynamizer if isinstance(self._data, Item): self._data = self._data._data diff --git a/boto/dynamodb2/table.py b/boto/dynamodb2/table.py index cddd296d..d72604e3 100644 --- a/boto/dynamodb2/table.py +++ b/boto/dynamodb2/table.py @@ -7,8 +7,8 @@ from boto.dynamodb2.fields import (HashKey, RangeKey, from boto.dynamodb2.items import Item from boto.dynamodb2.layer1 import DynamoDBConnection from boto.dynamodb2.results import ResultSet, BatchGetResultSet -from boto.dynamodb2.types import (Dynamizer, FILTER_OPERATORS, QUERY_OPERATORS, - STRING) +from boto.dynamodb2.types import (NonBooleanDynamizer, Dynamizer, FILTER_OPERATORS, + QUERY_OPERATORS, STRING) from boto.exception import JSONResponseError @@ -24,6 +24,18 @@ class Table(object): """ max_batch_get = 100 + _PROJECTION_TYPE_TO_INDEX = dict( + global_indexes=dict( + ALL=GlobalAllIndex, + KEYS_ONLY=GlobalKeysOnlyIndex, + INCLUDE=GlobalIncludeIndex, + ), local_indexes=dict( + ALL=AllIndex, + KEYS_ONLY=KeysOnlyIndex, + INCLUDE=IncludeIndex, + ) + ) + def __init__(self, table_name, schema=None, throughput=None, indexes=None, global_indexes=None, connection=None): """ @@ -109,6 +121,9 @@ class Table(object): if throughput is not None: self.throughput = throughput + self._dynamizer = NonBooleanDynamizer() + + def use_boolean(self): self._dynamizer = Dynamizer() @classmethod @@ -264,25 +279,25 @@ class Table(object): return schema - def _introspect_indexes(self, raw_indexes): + def _introspect_all_indexes(self, raw_indexes, map_indexes_projection): """ - Given a raw index structure back from a DynamoDB response, parse - out & build the high-level Python objects that represent them. + Given a raw index/global index structure back from a DynamoDB response, + parse out & build the high-level Python objects that represent them. """ indexes = [] for field in raw_indexes: - index_klass = AllIndex + index_klass = map_indexes_projection.get('ALL') kwargs = { 'parts': [] } if field['Projection']['ProjectionType'] == 'ALL': - index_klass = AllIndex + index_klass = map_indexes_projection.get('ALL') elif field['Projection']['ProjectionType'] == 'KEYS_ONLY': - index_klass = KeysOnlyIndex + index_klass = map_indexes_projection.get('KEYS_ONLY') elif field['Projection']['ProjectionType'] == 'INCLUDE': - index_klass = IncludeIndex + index_klass = map_indexes_projection.get('INCLUDE') kwargs['includes'] = field['Projection']['NonKeyAttributes'] else: raise exceptions.UnknownIndexFieldError( @@ -297,16 +312,33 @@ class Table(object): return indexes + def _introspect_indexes(self, raw_indexes): + """ + Given a raw index structure back from a DynamoDB response, parse + out & build the high-level Python objects that represent them. + """ + return self._introspect_all_indexes( + raw_indexes, self._PROJECTION_TYPE_TO_INDEX.get('local_indexes')) + + def _introspect_global_indexes(self, raw_global_indexes): + """ + Given a raw global index structure back from a DynamoDB response, parse + out & build the high-level Python objects that represent them. + """ + return self._introspect_all_indexes( + raw_global_indexes, + self._PROJECTION_TYPE_TO_INDEX.get('global_indexes')) + def describe(self): """ Describes the current structure of the table in DynamoDB. - This information will be used to update the ``schema``, ``indexes`` - and ``throughput`` information on the ``Table``. Some calls, such as - those involving creating keys or querying, will require this - information to be populated. + This information will be used to update the ``schema``, ``indexes``, + ``global_indexes`` and ``throughput`` information on the ``Table``. Some + calls, such as those involving creating keys or querying, will require + this information to be populated. - It also returns the full raw datastructure from DynamoDB, in the + It also returns the full raw data structure from DynamoDB, in the event you'd like to parse out additional information (such as the ``ItemCount`` or usage information). @@ -339,6 +371,11 @@ class Table(object): raw_indexes = result['Table'].get('LocalSecondaryIndexes', []) self.indexes = self._introspect_indexes(raw_indexes) + if not self.global_indexes: + # Build the global index information as well. + raw_global_indexes = result['Table'].get('GlobalSecondaryIndexes', []) + self.global_indexes = self._introspect_global_indexes(raw_global_indexes) + # This is leaky. return result diff --git a/boto/dynamodb2/types.py b/boto/dynamodb2/types.py index fc67aa01..1216621a 100644 --- a/boto/dynamodb2/types.py +++ b/boto/dynamodb2/types.py @@ -1,7 +1,7 @@ # Shadow the DynamoDB v1 bits. # This way, no end user should have to cross-import between versions & we # reserve the namespace to extend v2 if it's ever needed. -from boto.dynamodb.types import Dynamizer +from boto.dynamodb.types import NonBooleanDynamizer, Dynamizer # Some constants for our use. @@ -11,6 +11,10 @@ BINARY = 'B' STRING_SET = 'SS' NUMBER_SET = 'NS' BINARY_SET = 'BS' +NULL = 'NULL' +BOOLEAN = 'BOOL' +MAP = 'M' +LIST = 'L' QUERY_OPERATORS = { 'eq': 'EQ', diff --git a/boto/ec2/autoscale/__init__.py b/boto/ec2/autoscale/__init__.py index 5a58748d..ea6c083c 100644 --- a/boto/ec2/autoscale/__init__.py +++ b/boto/ec2/autoscale/__init__.py @@ -259,6 +259,14 @@ class AutoScaleConnection(AWSQueryConnection): params['DeleteOnTermination'] = 'false' if launch_config.iops: params['Iops'] = launch_config.iops + if launch_config.classic_link_vpc_id: + params['ClassicLinkVPCId'] = launch_config.classic_link_vpc_id + if launch_config.classic_link_vpc_security_groups: + self.build_list_params( + params, + launch_config.classic_link_vpc_security_groups, + 'ClassicLinkVPCSecurityGroups' + ) return self.get_object('CreateLaunchConfiguration', params, Request, verb='POST') diff --git a/boto/ec2/autoscale/launchconfig.py b/boto/ec2/autoscale/launchconfig.py index 6a94f7db..090f5f43 100644 --- a/boto/ec2/autoscale/launchconfig.py +++ b/boto/ec2/autoscale/launchconfig.py @@ -103,7 +103,9 @@ class LaunchConfiguration(object): instance_monitoring=False, spot_price=None, instance_profile_name=None, ebs_optimized=False, associate_public_ip_address=None, volume_type=None, - delete_on_termination=True, iops=None, use_block_device_types=False): + delete_on_termination=True, iops=None, + use_block_device_types=False, classic_link_vpc_id=None, + classic_link_vpc_security_groups=None): """ A launch configuration. @@ -160,21 +162,13 @@ class LaunchConfiguration(object): :param associate_public_ip_address: Used for Auto Scaling groups that launch instances into an Amazon Virtual Private Cloud. Specifies whether to assign a public IP address to each instance launched in a Amazon VPC. - :type volume_type: str - :param volume_type: The type of the volume. - Valid values are: standard | io1 | gp2. - - :type delete_on_termination: bool - :param delete_on_termination: Whether the device will be deleted - when the instance is terminated. - - :type iops: int - :param iops: The provisioned IOPs you want to associate with this volume. - - :type use_block_device_types: bool - :param use_block_device_types: Specifies whether to return - described Launch Configs with block device mappings containing. + :type classic_link_vpc_id: str + :param classic_link_vpc_id: ID of ClassicLink enabled VPC. + :type classic_link_vpc_security_groups: list + :param classic_link_vpc_security_groups: Security group + id's of the security groups with which to associate the + ClassicLink VPC instances. """ self.connection = connection self.name = name @@ -199,6 +193,10 @@ class LaunchConfiguration(object): self.delete_on_termination = delete_on_termination self.iops = iops self.use_block_device_types = use_block_device_types + self.classic_link_vpc_id = classic_link_vpc_id + classic_link_vpc_sec_groups = classic_link_vpc_security_groups or [] + self.classic_link_vpc_security_groups = \ + ListElement(classic_link_vpc_sec_groups) if connection is not None: self.use_block_device_types = connection.use_block_device_types @@ -209,6 +207,8 @@ class LaunchConfiguration(object): def startElement(self, name, attrs, connection): if name == 'SecurityGroups': return self.security_groups + elif name == 'ClassicLinkVPCSecurityGroups': + return self.classic_link_vpc_security_groups elif name == 'BlockDeviceMappings': if self.use_block_device_types: self.block_device_mappings = BDM() @@ -260,6 +260,8 @@ class LaunchConfiguration(object): self.delete_on_termination = False elif name == 'Iops': self.iops = int(value) + elif name == 'ClassicLinkVPCId': + self.classic_link_vpc_id = value else: setattr(self, name, value) diff --git a/docs/source/dynamodb2_tut.rst b/docs/source/dynamodb2_tut.rst index ae012a58..449890c5 100644 --- a/docs/source/dynamodb2_tut.rst +++ b/docs/source/dynamodb2_tut.rst @@ -89,8 +89,8 @@ A full example:: ... HashKey('account_type', data_type=NUMBER), ... ]) ... ], - ... # If you need to specify custom parameters like keys or region info... - ... connection= boto.dynamodb2.connect_to_region('us-east-1')) + ... # If you need to specify custom parameters, such as credentials or region, use the following: + ... Table.create('users', connection=boto.dynamodb2.connect_to_region('us-east-1')) Using an Existing Table diff --git a/docs/source/index.rst b/docs/source/index.rst index 39338944..76b7e93f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -136,6 +136,7 @@ Release Notes .. toctree:: :titlesonly: + releasenotes/v2.35.2 releasenotes/v2.35.1 releasenotes/v2.35.0 releasenotes/v2.34.0 diff --git a/docs/source/releasenotes/v2.35.2.rst b/docs/source/releasenotes/v2.35.2.rst new file mode 100644 index 00000000..ca7ed9ab --- /dev/null +++ b/docs/source/releasenotes/v2.35.2.rst @@ -0,0 +1,16 @@ +boto v2.32.2 +============ + +:date: 2015/01/19 + +This release adds ClassicLink support for Auto Scaling and fixes a few issues. + + +Changes +------- +* Add support for new data types in DynamoDB. (:issue:`2667`, :sha:`68ad513`) +* Expose cloudformation `UsePreviousTemplate` parameter. (:issue:`2843`, :issue:`2628`, :sha:`873e89c`) +* Fix documentation around using custom connections for DynamoDB tables. (:issue:`2842`, :issue:`1585`, :sha:`71d677f`) +* Fixed bug that unable call query_2 after call describe method on dynamodb2 module. (:issue:`2829`, :sha:`66addce`) + + diff --git a/tests/integration/dynamodb2/test_highlevel.py b/tests/integration/dynamodb2/test_highlevel.py index 8e8b1a05..f44de368 100644 --- a/tests/integration/dynamodb2/test_highlevel.py +++ b/tests/integration/dynamodb2/test_highlevel.py @@ -29,7 +29,8 @@ import time from tests.unit import unittest from boto.dynamodb2 import exceptions from boto.dynamodb2.fields import (HashKey, RangeKey, KeysOnlyIndex, - GlobalKeysOnlyIndex, GlobalIncludeIndex) + GlobalKeysOnlyIndex, GlobalIncludeIndex, + GlobalAllIndex) from boto.dynamodb2.items import Item from boto.dynamodb2.table import Table from boto.dynamodb2.types import NUMBER @@ -645,3 +646,73 @@ class DynamoDBv2Test(unittest.TestCase): '2013-12-24T15:22:22', ] ) + + def test_query_after_describe_with_gsi(self): + # Create a table to using gsi to reproduce the error mentioned on issue + # https://github.com/boto/boto/issues/2828 + users = Table.create('more_gsi_query_users', schema=[ + HashKey('user_id') + ], throughput={ + 'read': 5, + 'write': 5 + }, global_indexes=[ + GlobalAllIndex('EmailGSIIndex', parts=[ + HashKey('email') + ], throughput={ + 'read': 1, + 'write': 1 + }) + ]) + + # Add this function to be called after tearDown() + self.addCleanup(users.delete) + + # Wait for it. + time.sleep(60) + + # populate a couple of items in it + users.put_item(data={ + 'user_id': '7', + 'username': 'johndoe', + 'first_name': 'John', + 'last_name': 'Doe', + 'email': 'johndoe@johndoe.com', + }) + users.put_item(data={ + 'user_id': '24', + 'username': 'alice', + 'first_name': 'Alice', + 'last_name': 'Expert', + 'email': 'alice@alice.com', + }) + users.put_item(data={ + 'user_id': '35', + 'username': 'jane', + 'first_name': 'Jane', + 'last_name': 'Doe', + 'email': 'jane@jane.com', + }) + + # Try the GSI. it should work. + rs = users.query_2( + email__eq='johndoe@johndoe.com', + index='EmailGSIIndex' + ) + + for rs_item in rs: + self.assertEqual(rs_item['username'], ['johndoe']) + + # The issue arises when we're introspecting the table and try to + # query_2 after call describe method + users_hit_api = Table('more_gsi_query_users') + users_hit_api.describe() + + # Try the GSI. This is what os going wrong on #2828 issue. It should + # work fine now. + rs = users_hit_api.query_2( + email__eq='johndoe@johndoe.com', + index='EmailGSIIndex' + ) + + for rs_item in rs: + self.assertEqual(rs_item['username'], ['johndoe']) diff --git a/tests/unit/cloudformation/test_connection.py b/tests/unit/cloudformation/test_connection.py index 61d9fe7d..f4e23f37 100644 --- a/tests/unit/cloudformation/test_connection.py +++ b/tests/unit/cloudformation/test_connection.py @@ -138,7 +138,8 @@ class TestCloudFormationUpdateStack(CloudFormationConnectionBase): tags={'TagKey': 'TagValue'}, notification_arns=['arn:notify1', 'arn:notify2'], disable_rollback=True, - timeout_in_minutes=20 + timeout_in_minutes=20, + use_previous_template=True ) self.assert_request_parameters({ 'Action': 'UpdateStack', @@ -155,6 +156,7 @@ class TestCloudFormationUpdateStack(CloudFormationConnectionBase): 'TimeoutInMinutes': 20, 'TemplateBody': SAMPLE_TEMPLATE, 'TemplateURL': 'http://url', + 'UsePreviousTemplate': 'true', }) def test_update_stack_with_minimum_args(self): diff --git a/tests/unit/dynamodb/test_types.py b/tests/unit/dynamodb/test_types.py index 25b3f78f..ed72cc39 100644 --- a/tests/unit/dynamodb/test_types.py +++ b/tests/unit/dynamodb/test_types.py @@ -45,6 +45,12 @@ class TestDynamizer(unittest.TestCase): {'B': 'AQ=='}) self.assertEqual(dynamizer.encode(set([types.Binary(b'\x01')])), {'BS': ['AQ==']}) + self.assertEqual(dynamizer.encode(['foo', 54, [1]]), + {'L': [{'S': 'foo'}, {'N': '54'}, {'L': [{'N': '1'}]}]}) + self.assertEqual(dynamizer.encode({'foo': 'bar', 'hoge': {'sub': 1}}), + {'M': {'foo': {'S': 'bar'}, 'hoge': {'M': {'sub': {'N': '1'}}}}}) + self.assertEqual(dynamizer.encode(None), {'NULL': True}) + self.assertEqual(dynamizer.encode(False), {'BOOL': False}) def test_decoding_to_dynamodb(self): dynamizer = types.Dynamizer() @@ -58,6 +64,12 @@ class TestDynamizer(unittest.TestCase): self.assertEqual(dynamizer.decode({'B': 'AQ=='}), types.Binary(b'\x01')) self.assertEqual(dynamizer.decode({'BS': ['AQ==']}), set([types.Binary(b'\x01')])) + self.assertEqual(dynamizer.decode({'L': [{'S': 'foo'}, {'N': '54'}, {'L': [{'N': '1'}]}]}), + ['foo', 54, [1]]) + self.assertEqual(dynamizer.decode({'M': {'foo': {'S': 'bar'}, 'hoge': {'M': {'sub': {'N': '1'}}}}}), + {'foo': 'bar', 'hoge': {'sub': 1}}) + self.assertEqual(dynamizer.decode({'NULL': True}), None) + self.assertEqual(dynamizer.decode({'BOOL': False}), False) def test_float_conversion_errors(self): dynamizer = types.Dynamizer() @@ -68,6 +80,10 @@ class TestDynamizer(unittest.TestCase): with self.assertRaises(DynamoDBNumberError): dynamizer.encode(1.1) + def test_non_boolean_conversions(self): + dynamizer = types.NonBooleanDynamizer() + self.assertEqual(dynamizer.encode(True), {'N': '1'}) + def test_lossy_float_conversions(self): dynamizer = types.LossyFloatDynamizer() # Just testing the differences here, specifically float conversions: diff --git a/tests/unit/ec2/autoscale/test_group.py b/tests/unit/ec2/autoscale/test_group.py index 5c5b4dcd..08d672bc 100644 --- a/tests/unit/ec2/autoscale/test_group.py +++ b/tests/unit/ec2/autoscale/test_group.py @@ -294,6 +294,10 @@ class TestLaunchConfigurationDescribe(AWSMockServiceTestCase): <Enabled>true</Enabled> </InstanceMonitoring> <EbsOptimized>false</EbsOptimized> + <ClassicLinkVPCId>vpc-12345</ClassicLinkVPCId> + <ClassicLinkVPCSecurityGroups> + <member>sg-1234</member> + </ClassicLinkVPCSecurityGroups> </member> </LaunchConfigurations> </DescribeLaunchConfigurationsResult> @@ -320,6 +324,9 @@ class TestLaunchConfigurationDescribe(AWSMockServiceTestCase): self.assertEqual(response[0].instance_monitoring.enabled, 'true') self.assertEqual(response[0].ebs_optimized, False) self.assertEqual(response[0].block_device_mappings, []) + self.assertEqual(response[0].classic_link_vpc_id, 'vpc-12345') + self.assertEqual(response[0].classic_link_vpc_security_groups, + ['sg-1234']) self.assert_request_parameters({ 'Action': 'DescribeLaunchConfigurations', @@ -367,7 +374,9 @@ class TestLaunchConfiguration(AWSMockServiceTestCase): associate_public_ip_address=True, volume_type='atype', delete_on_termination=False, - iops=3000 + iops=3000, + classic_link_vpc_id='vpc-1234', + classic_link_vpc_security_groups=['classic_link_group'] ) response = self.service_connection.create_launch_configuration(lc) @@ -389,6 +398,8 @@ class TestLaunchConfiguration(AWSMockServiceTestCase): 'VolumeType': 'atype', 'DeleteOnTermination': 'false', 'Iops': 3000, + 'ClassicLinkVPCId': 'vpc-1234', + 'ClassicLinkVPCSecurityGroups.member.1': 'classic_link_group' }, ignore_params_values=['Version']) |