summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkyleknap <kyleknap@amazon.com>2015-01-19 16:21:23 -0800
committerkyleknap <kyleknap@amazon.com>2015-01-19 16:21:23 -0800
commit3213c250967c820b5ecf48acf9c9c4d367e37b69 (patch)
tree09fdf2fb5a828f8798075f9e05f540369c03df25
parent350edd84d3033f01ef4ea31e5dfc4ccfb4e99310 (diff)
parent41e10af1c17d8105941f8fbec221ae34c135b68f (diff)
downloadboto-3213c250967c820b5ecf48acf9c9c4d367e37b69.tar.gz
Merge branch 'release-2.35.2'2.35.2
-rw-r--r--README.rst4
-rw-r--r--boto/__init__.py2
-rw-r--r--boto/cloudformation/connection.py53
-rw-r--r--boto/dynamodb/layer2.py6
-rw-r--r--boto/dynamodb/types.py59
-rw-r--r--boto/dynamodb2/items.py4
-rw-r--r--boto/dynamodb2/table.py65
-rw-r--r--boto/dynamodb2/types.py6
-rw-r--r--boto/ec2/autoscale/__init__.py8
-rw-r--r--boto/ec2/autoscale/launchconfig.py32
-rw-r--r--docs/source/dynamodb2_tut.rst4
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/releasenotes/v2.35.2.rst16
-rw-r--r--tests/integration/dynamodb2/test_highlevel.py73
-rw-r--r--tests/unit/cloudformation/test_connection.py4
-rw-r--r--tests/unit/dynamodb/test_types.py16
-rw-r--r--tests/unit/ec2/autoscale/test_group.py13
17 files changed, 302 insertions, 64 deletions
diff --git a/README.rst b/README.rst
index b2be3fe1..1ca6992d 100644
--- a/README.rst
+++ b/README.rst
@@ -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'])