Merge pull request #35 from spulec/master

Merge upstream
This commit is contained in:
Bert Blommers 2020-03-20 12:10:24 +00:00 committed by GitHub
commit 66b26cd7b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 291 additions and 33 deletions

View File

@ -8,6 +8,7 @@ from boto3 import Session
from moto.compat import OrderedDict from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_without_milliseconds
from .parsing import ResourceMap, OutputMap from .parsing import ResourceMap, OutputMap
from .utils import ( from .utils import (
@ -240,6 +241,7 @@ class FakeStack(BaseModel):
self.output_map = self._create_output_map() self.output_map = self._create_output_map()
self._add_stack_event("CREATE_COMPLETE") self._add_stack_event("CREATE_COMPLETE")
self.status = "CREATE_COMPLETE" self.status = "CREATE_COMPLETE"
self.creation_time = datetime.utcnow()
def _create_resource_map(self): def _create_resource_map(self):
resource_map = ResourceMap( resource_map = ResourceMap(
@ -259,6 +261,10 @@ class FakeStack(BaseModel):
output_map.create() output_map.create()
return output_map return output_map
@property
def creation_time_iso_8601(self):
return iso_8601_datetime_without_milliseconds(self.creation_time)
def _add_stack_event( def _add_stack_event(
self, resource_status, resource_status_reason=None, resource_properties=None self, resource_status, resource_status_reason=None, resource_properties=None
): ):

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import functools import functools
import json
import logging import logging
import copy import copy
import warnings import warnings
@ -24,7 +25,8 @@ from moto.rds import models as rds_models
from moto.rds2 import models as rds2_models from moto.rds2 import models as rds2_models
from moto.redshift import models as redshift_models from moto.redshift import models as redshift_models
from moto.route53 import models as route53_models from moto.route53 import models as route53_models
from moto.s3 import models as s3_models from moto.s3 import models as s3_models, s3_backend
from moto.s3.utils import bucket_and_name_from_url
from moto.sns import models as sns_models from moto.sns import models as sns_models
from moto.sqs import models as sqs_models from moto.sqs import models as sqs_models
from moto.core import ACCOUNT_ID from moto.core import ACCOUNT_ID
@ -150,6 +152,9 @@ def clean_json(resource_json, resources_map):
map_path = resource_json["Fn::FindInMap"][1:] map_path = resource_json["Fn::FindInMap"][1:]
result = resources_map[map_name] result = resources_map[map_name]
for path in map_path: for path in map_path:
if "Fn::Transform" in result:
result = resources_map[clean_json(path, resources_map)]
else:
result = result[clean_json(path, resources_map)] result = result[clean_json(path, resources_map)]
return result return result
@ -470,6 +475,17 @@ class ResourceMap(collections_abc.Mapping):
def load_mapping(self): def load_mapping(self):
self._parsed_resources.update(self._template.get("Mappings", {})) self._parsed_resources.update(self._template.get("Mappings", {}))
def transform_mapping(self):
for k, v in self._template.get("Mappings", {}).items():
if "Fn::Transform" in v:
name = v["Fn::Transform"]["Name"]
params = v["Fn::Transform"]["Parameters"]
if name == "AWS::Include":
location = params["Location"]
bucket_name, name = bucket_and_name_from_url(location)
key = s3_backend.get_key(bucket_name, name)
self._parsed_resources.update(json.loads(key.value))
def load_parameters(self): def load_parameters(self):
parameter_slots = self._template.get("Parameters", {}) parameter_slots = self._template.get("Parameters", {})
for parameter_name, parameter in parameter_slots.items(): for parameter_name, parameter in parameter_slots.items():
@ -515,6 +531,7 @@ class ResourceMap(collections_abc.Mapping):
def create(self): def create(self):
self.load_mapping() self.load_mapping()
self.transform_mapping()
self.load_parameters() self.load_parameters()
self.load_conditions() self.load_conditions()

View File

@ -662,7 +662,7 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
<member> <member>
<StackName>{{ stack.name }}</StackName> <StackName>{{ stack.name }}</StackName>
<StackId>{{ stack.stack_id }}</StackId> <StackId>{{ stack.stack_id }}</StackId>
<CreationTime>2010-07-27T22:28:28Z</CreationTime> <CreationTime>{{ stack.creation_time_iso_8601 }}</CreationTime>
<StackStatus>{{ stack.status }}</StackStatus> <StackStatus>{{ stack.status }}</StackStatus>
{% if stack.notification_arns %} {% if stack.notification_arns %}
<NotificationARNs> <NotificationARNs>
@ -803,7 +803,7 @@ LIST_STACKS_RESPONSE = """<ListStacksResponse>
<StackId>{{ stack.stack_id }}</StackId> <StackId>{{ stack.stack_id }}</StackId>
<StackStatus>{{ stack.status }}</StackStatus> <StackStatus>{{ stack.status }}</StackStatus>
<StackName>{{ stack.name }}</StackName> <StackName>{{ stack.name }}</StackName>
<CreationTime>2011-05-23T15:47:44Z</CreationTime> <CreationTime>{{ stack.creation_time_iso_8601 }}</CreationTime>
<TemplateDescription>{{ stack.description }}</TemplateDescription> <TemplateDescription>{{ stack.description }}</TemplateDescription>
</member> </member>
{% endfor %} {% endfor %}

View File

@ -5,6 +5,7 @@ from boto3 import Session
from moto.core.utils import iso_8601_datetime_without_milliseconds from moto.core.utils import iso_8601_datetime_without_milliseconds
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
from moto.core.exceptions import RESTError from moto.core.exceptions import RESTError
from moto.logs import logs_backends
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil.tz import tzutc from dateutil.tz import tzutc
from uuid import uuid4 from uuid import uuid4
@ -428,12 +429,9 @@ class LogGroup(BaseModel):
cls, resource_name, cloudformation_json, region_name cls, resource_name, cloudformation_json, region_name
): ):
properties = cloudformation_json["Properties"] properties = cloudformation_json["Properties"]
spec = {"LogGroupName": properties["LogGroupName"]} log_group_name = properties["LogGroupName"]
optional_properties = "Tags".split() tags = properties.get("Tags", {})
for prop in optional_properties: return logs_backends[region_name].create_log_group(log_group_name, tags)
if prop in properties:
spec[prop] = properties[prop]
return LogGroup(spec)
cloudwatch_backends = {} cloudwatch_backends = {}

View File

@ -146,6 +146,9 @@ class DynamoType(object):
def __eq__(self, other): def __eq__(self, other):
return self.type == other.type and self.value == other.value return self.type == other.type and self.value == other.value
def __ne__(self, other):
return self.type != other.type or self.value != other.value
def __lt__(self, other): def __lt__(self, other):
return self.cast_value < other.cast_value return self.cast_value < other.cast_value
@ -679,6 +682,10 @@ class Table(BaseModel):
self.throughput["NumberOfDecreasesToday"] = 0 self.throughput["NumberOfDecreasesToday"] = 0
self.indexes = indexes self.indexes = indexes
self.global_indexes = global_indexes if global_indexes else [] self.global_indexes = global_indexes if global_indexes else []
for index in self.global_indexes:
index[
"IndexStatus"
] = "ACTIVE" # One of 'CREATING'|'UPDATING'|'DELETING'|'ACTIVE'
self.created_at = datetime.datetime.utcnow() self.created_at = datetime.datetime.utcnow()
self.items = defaultdict(dict) self.items = defaultdict(dict)
self.table_arn = self._generate_arn(table_name) self.table_arn = self._generate_arn(table_name)
@ -981,8 +988,13 @@ class Table(BaseModel):
if index_name: if index_name:
if index_range_key: if index_range_key:
# Convert to float if necessary to ensure proper ordering
def conv(x):
return float(x.value) if x.type == "N" else x.value
results.sort( results.sort(
key=lambda item: item.attrs[index_range_key["AttributeName"]].value key=lambda item: conv(item.attrs[index_range_key["AttributeName"]])
if item.attrs.get(index_range_key["AttributeName"]) if item.attrs.get(index_range_key["AttributeName"])
else None else None
) )

View File

@ -1,9 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools
import copy
import json import json
import six
import re import re
import itertools
import six
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores, amzn_request_id from moto.core.utils import camelcase_to_underscores, amzn_request_id
from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSizeTooLarge from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSizeTooLarge
@ -711,7 +714,8 @@ class DynamoHandler(BaseResponse):
attribute_updates = self.body.get("AttributeUpdates") attribute_updates = self.body.get("AttributeUpdates")
expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_names = self.body.get("ExpressionAttributeNames", {})
expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) expression_attribute_values = self.body.get("ExpressionAttributeValues", {})
existing_item = self.dynamodb_backend.get_item(name, key) # We need to copy the item in order to avoid it being modified by the update_item operation
existing_item = copy.deepcopy(self.dynamodb_backend.get_item(name, key))
if existing_item: if existing_item:
existing_attributes = existing_item.to_json()["Attributes"] existing_attributes = existing_item.to_json()["Attributes"]
else: else:
@ -797,14 +801,39 @@ class DynamoHandler(BaseResponse):
k: v for k, v in existing_attributes.items() if k in changed_attributes k: v for k, v in existing_attributes.items() if k in changed_attributes
} }
elif return_values == "UPDATED_NEW": elif return_values == "UPDATED_NEW":
item_dict["Attributes"] = { item_dict["Attributes"] = self._build_updated_new_attributes(
k: v existing_attributes, item_dict["Attributes"]
for k, v in item_dict["Attributes"].items() )
if k in changed_attributes
}
return dynamo_json_dump(item_dict) return dynamo_json_dump(item_dict)
def _build_updated_new_attributes(self, original, changed):
if type(changed) != type(original):
return changed
else:
if type(changed) is dict:
return {
key: self._build_updated_new_attributes(
original.get(key, None), changed[key]
)
for key in changed.keys()
if changed[key] != original.get(key, None)
}
elif type(changed) in (set, list):
if len(changed) != len(original):
return changed
else:
return [
self._build_updated_new_attributes(
original[index], changed[index]
)
for index in range(len(changed))
]
elif changed != original:
return changed
else:
return None
def describe_limits(self): def describe_limits(self):
return json.dumps( return json.dumps(
{ {

View File

@ -405,6 +405,7 @@ class LogsBackend(BaseBackend):
if log_group_name in self.groups: if log_group_name in self.groups:
raise ResourceAlreadyExistsException() raise ResourceAlreadyExistsException()
self.groups[log_group_name] = LogGroup(self.region_name, log_group_name, tags) self.groups[log_group_name] = LogGroup(self.region_name, log_group_name, tags)
return self.groups[log_group_name]
def ensure_log_group(self, log_group_name, tags): def ensure_log_group(self, log_group_name, tags):
if log_group_name in self.groups: if log_group_name in self.groups:

View File

@ -12,6 +12,7 @@ import codecs
import random import random
import string import string
import tempfile import tempfile
import threading
import sys import sys
import time import time
import uuid import uuid
@ -110,6 +111,7 @@ class FakeKey(BaseModel):
self._value_buffer = tempfile.SpooledTemporaryFile(max_size=max_buffer_size) self._value_buffer = tempfile.SpooledTemporaryFile(max_size=max_buffer_size)
self._max_buffer_size = max_buffer_size self._max_buffer_size = max_buffer_size
self.value = value self.value = value
self.lock = threading.Lock()
@property @property
def version_id(self): def version_id(self):
@ -117,8 +119,12 @@ class FakeKey(BaseModel):
@property @property
def value(self): def value(self):
self.lock.acquire()
self._value_buffer.seek(0) self._value_buffer.seek(0)
return self._value_buffer.read() r = self._value_buffer.read()
r = copy.copy(r)
self.lock.release()
return r
@value.setter @value.setter
def value(self, new_value): def value(self, new_value):
@ -130,6 +136,7 @@ class FakeKey(BaseModel):
if isinstance(new_value, six.text_type): if isinstance(new_value, six.text_type):
new_value = new_value.encode(DEFAULT_TEXT_ENCODING) new_value = new_value.encode(DEFAULT_TEXT_ENCODING)
self._value_buffer.write(new_value) self._value_buffer.write(new_value)
self.contentsize = len(new_value)
def copy(self, new_name=None, new_is_versioned=None): def copy(self, new_name=None, new_is_versioned=None):
r = copy.deepcopy(self) r = copy.deepcopy(self)
@ -157,6 +164,7 @@ class FakeKey(BaseModel):
self.acl = acl self.acl = acl
def append_to_value(self, value): def append_to_value(self, value):
self.contentsize += len(value)
self._value_buffer.seek(0, os.SEEK_END) self._value_buffer.seek(0, os.SEEK_END)
self._value_buffer.write(value) self._value_buffer.write(value)
@ -229,8 +237,7 @@ class FakeKey(BaseModel):
@property @property
def size(self): def size(self):
self._value_buffer.seek(0, os.SEEK_END) return self.contentsize
return self._value_buffer.tell()
@property @property
def storage_class(self): def storage_class(self):
@ -249,6 +256,7 @@ class FakeKey(BaseModel):
state = self.__dict__.copy() state = self.__dict__.copy()
state["value"] = self.value state["value"] = self.value
del state["_value_buffer"] del state["_value_buffer"]
del state["lock"]
return state return state
def __setstate__(self, state): def __setstate__(self, state):
@ -258,6 +266,7 @@ class FakeKey(BaseModel):
max_size=self._max_buffer_size max_size=self._max_buffer_size
) )
self.value = state["value"] self.value = state["value"]
self.lock = threading.Lock()
class FakeMultipart(BaseModel): class FakeMultipart(BaseModel):
@ -284,7 +293,7 @@ class FakeMultipart(BaseModel):
etag = etag.replace('"', "") etag = etag.replace('"', "")
if part is None or part_etag != etag: if part is None or part_etag != etag:
raise InvalidPart() raise InvalidPart()
if last is not None and len(last.value) < UPLOAD_PART_MIN_SIZE: if last is not None and last.contentsize < UPLOAD_PART_MIN_SIZE:
raise EntityTooSmall() raise EntityTooSmall()
md5s.extend(decode_hex(part_etag)[0]) md5s.extend(decode_hex(part_etag)[0])
total.extend(part.value) total.extend(part.value)

View File

@ -35,6 +35,17 @@ def bucket_name_from_url(url):
return None return None
# 'owi-common-cf', 'snippets/test.json' = bucket_and_name_from_url('s3://owi-common-cf/snippets/test.json')
def bucket_and_name_from_url(url):
prefix = "s3://"
if url.startswith(prefix):
bucket_name = url[len(prefix) : url.index("/", len(prefix))]
key = url[url.index("/", len(prefix)) + 1 :]
return bucket_name, key
else:
return None, None
REGION_URL_REGEX = re.compile( REGION_URL_REGEX = re.compile(
r"^https?://(s3[-\.](?P<region1>.+)\.amazonaws\.com/(.+)|" r"^https?://(s3[-\.](?P<region1>.+)\.amazonaws\.com/(.+)|"
r"(.+)\.s3[-\.](?P<region2>.+)\.amazonaws\.com)/?" r"(.+)\.s3[-\.](?P<region2>.+)\.amazonaws\.com)/?"

View File

@ -91,9 +91,11 @@ class SESBackend(BaseBackend):
return host in self.domains return host in self.domains
def verify_email_identity(self, address): def verify_email_identity(self, address):
_, address = parseaddr(address)
self.addresses.append(address) self.addresses.append(address)
def verify_email_address(self, address): def verify_email_address(self, address):
_, address = parseaddr(address)
self.email_addresses.append(address) self.email_addresses.append(address)
def verify_domain(self, domain): def verify_domain(self, domain):

View File

@ -2,6 +2,8 @@ from __future__ import unicode_literals
import json import json
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta
import pytz
import boto3 import boto3
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
@ -911,6 +913,10 @@ def test_describe_stack_by_name():
stack = cf_conn.describe_stacks(StackName="test_stack")["Stacks"][0] stack = cf_conn.describe_stacks(StackName="test_stack")["Stacks"][0]
stack["StackName"].should.equal("test_stack") stack["StackName"].should.equal("test_stack")
two_secs_ago = datetime.now(tz=pytz.UTC) - timedelta(seconds=2)
assert (
two_secs_ago < stack["CreationTime"] < datetime.now(tz=pytz.UTC)
), "Stack should have been created recently"
@mock_cloudformation @mock_cloudformation

View File

@ -32,12 +32,14 @@ from moto import (
mock_iam_deprecated, mock_iam_deprecated,
mock_kms, mock_kms,
mock_lambda, mock_lambda,
mock_logs,
mock_rds_deprecated, mock_rds_deprecated,
mock_rds2, mock_rds2,
mock_rds2_deprecated, mock_rds2_deprecated,
mock_redshift, mock_redshift,
mock_redshift_deprecated, mock_redshift_deprecated,
mock_route53_deprecated, mock_route53_deprecated,
mock_s3,
mock_sns_deprecated, mock_sns_deprecated,
mock_sqs, mock_sqs,
mock_sqs_deprecated, mock_sqs_deprecated,
@ -2332,3 +2334,48 @@ def test_stack_dynamodb_resources_integration():
response["Item"]["Sales"].should.equal(Decimal("10")) response["Item"]["Sales"].should.equal(Decimal("10"))
response["Item"]["NumberOfSongs"].should.equal(Decimal("5")) response["Item"]["NumberOfSongs"].should.equal(Decimal("5"))
response["Item"]["Album"].should.equal("myAlbum") response["Item"]["Album"].should.equal("myAlbum")
@mock_cloudformation
@mock_logs
@mock_s3
def test_create_log_group_using_fntransform():
s3_resource = boto3.resource("s3")
s3_resource.create_bucket(
Bucket="owi-common-cf",
CreateBucketConfiguration={"LocationConstraint": "us-west-2"},
)
s3_resource.Object("owi-common-cf", "snippets/test.json").put(
Body=json.dumps({"lgname": {"name": "some-log-group"}})
)
template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Mappings": {
"EnvironmentMapping": {
"Fn::Transform": {
"Name": "AWS::Include",
"Parameters": {"Location": "s3://owi-common-cf/snippets/test.json"},
}
}
},
"Resources": {
"LogGroup": {
"Properties": {
"LogGroupName": {
"Fn::FindInMap": ["EnvironmentMapping", "lgname", "name"]
},
"RetentionInDays": 90,
},
"Type": "AWS::Logs::LogGroup",
}
},
}
cf_conn = boto3.client("cloudformation", "us-west-2")
cf_conn.create_stack(
StackName="test_stack", TemplateBody=json.dumps(template),
)
logs_conn = boto3.client("logs", region_name="us-west-2")
log_group = logs_conn.describe_log_groups()["logGroups"][0]
log_group["logGroupName"].should.equal("some-log-group")

View File

@ -3431,13 +3431,18 @@ def test_update_supports_list_append():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"SHA256": {"S": "sha-of-file"}}, Key={"SHA256": {"S": "sha-of-file"}},
UpdateExpression="SET crontab = list_append(crontab, :i)", UpdateExpression="SET crontab = list_append(crontab, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal(
{"crontab": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}
)
# Verify item is appended to the existing list # Verify item is appended to the existing list
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"SHA256": {"S": "sha-of-file"}} TableName="TestTable", Key={"SHA256": {"S": "sha-of-file"}}
@ -3470,15 +3475,19 @@ def test_update_supports_nested_list_append():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "nested_list_append"}}, Key={"id": {"S": "nested_list_append"}},
UpdateExpression="SET a.#b = list_append(a.#b, :i)", UpdateExpression="SET a.#b = list_append(a.#b, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ExpressionAttributeNames={"#b": "b"}, ExpressionAttributeNames={"#b": "b"},
ReturnValues="UPDATED_NEW",
) )
# Verify item is appended to the existing list # Verify updated item is correct
updated_item["Attributes"].should.equal(
{"a": {"M": {"b": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}}}
)
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"id": {"S": "nested_list_append"}} TableName="TestTable", Key={"id": {"S": "nested_list_append"}}
)["Item"] )["Item"]
@ -3510,14 +3519,19 @@ def test_update_supports_multiple_levels_nested_list_append():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "nested_list_append"}}, Key={"id": {"S": "nested_list_append"}},
UpdateExpression="SET a.#b.c = list_append(a.#b.#c, :i)", UpdateExpression="SET a.#b.c = list_append(a.#b.#c, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ExpressionAttributeNames={"#b": "b", "#c": "c"}, ExpressionAttributeNames={"#b": "b", "#c": "c"},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal(
{"a": {"M": {"b": {"M": {"c": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}}}}}
)
# Verify item is appended to the existing list # Verify item is appended to the existing list
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"id": {"S": "nested_list_append"}} TableName="TestTable", Key={"id": {"S": "nested_list_append"}}
@ -3551,14 +3565,19 @@ def test_update_supports_nested_list_append_onto_another_list():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "list_append_another"}}, Key={"id": {"S": "list_append_another"}},
UpdateExpression="SET a.#c = list_append(a.#b, :i)", UpdateExpression="SET a.#c = list_append(a.#b, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ExpressionAttributeNames={"#b": "b", "#c": "c"}, ExpressionAttributeNames={"#b": "b", "#c": "c"},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal(
{"a": {"M": {"c": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}}}
)
# Verify item is appended to the existing list # Verify item is appended to the existing list
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"id": {"S": "list_append_another"}} TableName="TestTable", Key={"id": {"S": "list_append_another"}}
@ -3601,13 +3620,18 @@ def test_update_supports_list_append_maps():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "nested_list_append"}, "rid": {"S": "range_key"}}, Key={"id": {"S": "nested_list_append"}, "rid": {"S": "range_key"}},
UpdateExpression="SET a = list_append(a, :i)", UpdateExpression="SET a = list_append(a, :i)",
ExpressionAttributeValues={":i": {"L": [{"M": {"b": {"S": "bar2"}}}]}}, ExpressionAttributeValues={":i": {"L": [{"M": {"b": {"S": "bar2"}}}]}},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal(
{"a": {"L": [{"M": {"b": {"S": "bar1"}}}, {"M": {"b": {"S": "bar2"}}}]}}
)
# Verify item is appended to the existing list # Verify item is appended to the existing list
result = client.query( result = client.query(
TableName="TestTable", TableName="TestTable",
@ -3643,11 +3667,18 @@ def test_update_supports_list_append_with_nested_if_not_exists_operation():
table = dynamo.Table(table_name) table = dynamo.Table(table_name)
table.put_item(Item={"Id": "item-id", "nest1": {"nest2": {}}}) table.put_item(Item={"Id": "item-id", "nest1": {"nest2": {}}})
table.update_item( updated_item = table.update_item(
Key={"Id": "item-id"}, Key={"Id": "item-id"},
UpdateExpression="SET nest1.nest2.event_history = list_append(if_not_exists(nest1.nest2.event_history, :empty_list), :new_value)", UpdateExpression="SET nest1.nest2.event_history = list_append(if_not_exists(nest1.nest2.event_history, :empty_list), :new_value)",
ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_value"]}, ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_value"]},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal(
{"nest1": {"nest2": {"event_history": ["some_value"]}}}
)
table.get_item(Key={"Id": "item-id"})["Item"].should.equal( table.get_item(Key={"Id": "item-id"})["Item"].should.equal(
{"Id": "item-id", "nest1": {"nest2": {"event_history": ["some_value"]}}} {"Id": "item-id", "nest1": {"nest2": {"event_history": ["some_value"]}}}
) )
@ -3668,11 +3699,18 @@ def test_update_supports_list_append_with_nested_if_not_exists_operation_and_pro
table = dynamo.Table(table_name) table = dynamo.Table(table_name)
table.put_item(Item={"Id": "item-id", "event_history": ["other_value"]}) table.put_item(Item={"Id": "item-id", "event_history": ["other_value"]})
table.update_item( updated_item = table.update_item(
Key={"Id": "item-id"}, Key={"Id": "item-id"},
UpdateExpression="SET event_history = list_append(if_not_exists(event_history, :empty_list), :new_value)", UpdateExpression="SET event_history = list_append(if_not_exists(event_history, :empty_list), :new_value)",
ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_value"]}, ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_value"]},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal(
{"event_history": ["other_value", "some_value"]}
)
table.get_item(Key={"Id": "item-id"})["Item"].should.equal( table.get_item(Key={"Id": "item-id"})["Item"].should.equal(
{"Id": "item-id", "event_history": ["other_value", "some_value"]} {"Id": "item-id", "event_history": ["other_value", "some_value"]}
) )
@ -3759,11 +3797,16 @@ def test_update_nested_item_if_original_value_is_none():
) )
table = dynamo.Table("origin-rbu-dev") table = dynamo.Table("origin-rbu-dev")
table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}}) table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}})
table.update_item( updated_item = table.update_item(
Key={"job_id": "a"}, Key={"job_id": "a"},
UpdateExpression="SET job_details.job_name = :output", UpdateExpression="SET job_details.job_name = :output",
ExpressionAttributeValues={":output": "updated"}, ExpressionAttributeValues={":output": "updated"},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal({"job_details": {"job_name": "updated"}})
table.scan()["Items"][0]["job_details"]["job_name"].should.equal("updated") table.scan()["Items"][0]["job_details"]["job_name"].should.equal("updated")
@ -3779,11 +3822,16 @@ def test_allow_update_to_item_with_different_type():
table = dynamo.Table("origin-rbu-dev") table = dynamo.Table("origin-rbu-dev")
table.put_item(Item={"job_id": "a", "job_details": {"job_name": {"nested": "yes"}}}) table.put_item(Item={"job_id": "a", "job_details": {"job_name": {"nested": "yes"}}})
table.put_item(Item={"job_id": "b", "job_details": {"job_name": {"nested": "yes"}}}) table.put_item(Item={"job_id": "b", "job_details": {"job_name": {"nested": "yes"}}})
table.update_item( updated_item = table.update_item(
Key={"job_id": "a"}, Key={"job_id": "a"},
UpdateExpression="SET job_details.job_name = :output", UpdateExpression="SET job_details.job_name = :output",
ExpressionAttributeValues={":output": "updated"}, ExpressionAttributeValues={":output": "updated"},
ReturnValues="UPDATED_NEW",
) )
# Verify updated item is correct
updated_item["Attributes"].should.equal({"job_details": {"job_name": "updated"}})
table.get_item(Key={"job_id": "a"})["Item"]["job_details"][ table.get_item(Key={"job_id": "a"})["Item"]["job_details"][
"job_name" "job_name"
].should.be.equal("updated") ].should.be.equal("updated")
@ -4026,3 +4074,61 @@ def test_valid_transact_get_items():
"Table": {"CapacityUnits": 2.0, "ReadCapacityUnits": 2.0,}, "Table": {"CapacityUnits": 2.0, "ReadCapacityUnits": 2.0,},
} }
) )
@mock_dynamodb2
def test_gsi_verify_negative_number_order():
table_schema = {
"KeySchema": [{"AttributeName": "partitionKey", "KeyType": "HASH"}],
"GlobalSecondaryIndexes": [
{
"IndexName": "GSI-K1",
"KeySchema": [
{"AttributeName": "gsiK1PartitionKey", "KeyType": "HASH"},
{"AttributeName": "gsiK1SortKey", "KeyType": "RANGE"},
],
"Projection": {"ProjectionType": "KEYS_ONLY",},
}
],
"AttributeDefinitions": [
{"AttributeName": "partitionKey", "AttributeType": "S"},
{"AttributeName": "gsiK1PartitionKey", "AttributeType": "S"},
{"AttributeName": "gsiK1SortKey", "AttributeType": "N"},
],
}
item1 = {
"partitionKey": "pk-1",
"gsiK1PartitionKey": "gsi-k1",
"gsiK1SortKey": Decimal("-0.6"),
}
item2 = {
"partitionKey": "pk-2",
"gsiK1PartitionKey": "gsi-k1",
"gsiK1SortKey": Decimal("-0.7"),
}
item3 = {
"partitionKey": "pk-3",
"gsiK1PartitionKey": "gsi-k1",
"gsiK1SortKey": Decimal("0.7"),
}
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
dynamodb.create_table(
TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
)
table = dynamodb.Table("test-table")
table.put_item(Item=item3)
table.put_item(Item=item1)
table.put_item(Item=item2)
resp = table.query(
KeyConditionExpression=Key("gsiK1PartitionKey").eq("gsi-k1"),
IndexName="GSI-K1",
)
# Items should be ordered with the lowest number first
[float(item["gsiK1SortKey"]) for item in resp["Items"]].should.equal(
[-0.7, -0.6, 0.7]
)

View File

@ -574,6 +574,7 @@ def test_create_with_global_indexes():
"ReadCapacityUnits": 6, "ReadCapacityUnits": 6,
"WriteCapacityUnits": 1, "WriteCapacityUnits": 1,
}, },
"IndexStatus": "ACTIVE",
} }
] ]
) )

View File

@ -214,3 +214,16 @@ def test_send_raw_email_without_source_or_from():
kwargs = dict(RawMessage={"Data": message.as_string()}) kwargs = dict(RawMessage={"Data": message.as_string()})
conn.send_raw_email.when.called_with(**kwargs).should.throw(ClientError) conn.send_raw_email.when.called_with(**kwargs).should.throw(ClientError)
@mock_ses
def test_send_email_notification_with_encoded_sender():
sender = "Foo <foo@bar.baz>"
conn = boto3.client("ses", region_name="us-east-1")
conn.verify_email_identity(EmailAddress=sender)
response = conn.send_email(
Source=sender,
Destination={"ToAddresses": ["your.friend@hotmail.com"]},
Message={"Subject": {"Data": "hi",}, "Body": {"Text": {"Data": "there",}}},
)
response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)