Using Ops for dynamodb expected dicts
This commit is contained in:
parent
1a2fc66f84
commit
2712654518
@ -19,6 +19,63 @@ def get_filter_expression(expr, names, values):
|
|||||||
return parser.parse()
|
return parser.parse()
|
||||||
|
|
||||||
|
|
||||||
|
def get_expected(expected):
|
||||||
|
"""
|
||||||
|
Parse a filter expression into an Op.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
expr = 'Id > 5 AND attribute_exists(test) AND Id BETWEEN 5 AND 6 OR length < 6 AND contains(test, 1) AND 5 IN (4,5, 6) OR (Id < 5 AND 5 > Id)'
|
||||||
|
expr = 'Id > 5 AND Subs < 7'
|
||||||
|
"""
|
||||||
|
ops = {
|
||||||
|
'EQ': OpEqual,
|
||||||
|
'NE': OpNotEqual,
|
||||||
|
'LE': OpLessThanOrEqual,
|
||||||
|
'LT': OpLessThan,
|
||||||
|
'GE': OpGreaterThanOrEqual,
|
||||||
|
'GT': OpGreaterThan,
|
||||||
|
'NOT_NULL': FuncAttrExists,
|
||||||
|
'NULL': FuncAttrNotExists,
|
||||||
|
'CONTAINS': FuncContains,
|
||||||
|
'NOT_CONTAINS': FuncNotContains,
|
||||||
|
'BEGINS_WITH': FuncBeginsWith,
|
||||||
|
'IN': FuncIn,
|
||||||
|
'BETWEEN': FuncBetween,
|
||||||
|
}
|
||||||
|
|
||||||
|
# NOTE: Always uses ConditionalOperator=AND
|
||||||
|
conditions = []
|
||||||
|
for key, cond in expected.items():
|
||||||
|
path = AttributePath([key])
|
||||||
|
if 'Exists' in cond:
|
||||||
|
if cond['Exists']:
|
||||||
|
conditions.append(FuncAttrExists(path))
|
||||||
|
else:
|
||||||
|
conditions.append(FuncAttrNotExists(path))
|
||||||
|
elif 'Value' in cond:
|
||||||
|
conditions.append(OpEqual(path, AttributeValue(cond['Value'])))
|
||||||
|
elif 'ComparisonOperator' in cond:
|
||||||
|
operator_name = cond['ComparisonOperator']
|
||||||
|
values = [
|
||||||
|
AttributeValue(v)
|
||||||
|
for v in cond.get("AttributeValueList", [])]
|
||||||
|
print(path, values)
|
||||||
|
OpClass = ops[operator_name]
|
||||||
|
conditions.append(OpClass(path, *values))
|
||||||
|
|
||||||
|
# NOTE: Ignore ConditionalOperator
|
||||||
|
ConditionalOp = OpAnd
|
||||||
|
if conditions:
|
||||||
|
output = conditions[0]
|
||||||
|
for condition in conditions[1:]:
|
||||||
|
output = ConditionalOp(output, condition)
|
||||||
|
else:
|
||||||
|
return OpDefault(None, None)
|
||||||
|
|
||||||
|
print("EXPECTED:", expected, output)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
class Op(object):
|
class Op(object):
|
||||||
"""
|
"""
|
||||||
Base class for a FilterExpression operator
|
Base class for a FilterExpression operator
|
||||||
@ -782,14 +839,19 @@ class AttributePath(Operand):
|
|||||||
self.path = path
|
self.path = path
|
||||||
|
|
||||||
def _get_attr(self, item):
|
def _get_attr(self, item):
|
||||||
|
if item is None:
|
||||||
|
return None
|
||||||
|
|
||||||
base = self.path[0]
|
base = self.path[0]
|
||||||
if base not in item.attrs:
|
if base not in item.attrs:
|
||||||
return None
|
return None
|
||||||
attr = item.attrs[base]
|
attr = item.attrs[base]
|
||||||
|
|
||||||
for name in self.path[1:]:
|
for name in self.path[1:]:
|
||||||
attr = attr.child_attr(name)
|
attr = attr.child_attr(name)
|
||||||
if attr is None:
|
if attr is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
def expr(self, item):
|
def expr(self, item):
|
||||||
@ -807,7 +869,7 @@ class AttributePath(Operand):
|
|||||||
return attr.type
|
return attr.type
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.path
|
return ".".join(self.path)
|
||||||
|
|
||||||
|
|
||||||
class AttributeValue(Operand):
|
class AttributeValue(Operand):
|
||||||
@ -821,23 +883,27 @@ class AttributeValue(Operand):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self.type = list(value.keys())[0]
|
self.type = list(value.keys())[0]
|
||||||
if 'N' in value:
|
self.value = value[self.type]
|
||||||
self.value = float(value['N'])
|
|
||||||
elif 'BOOL' in value:
|
|
||||||
self.value = value['BOOL']
|
|
||||||
elif 'S' in value:
|
|
||||||
self.value = value['S']
|
|
||||||
elif 'NS' in value:
|
|
||||||
self.value = tuple(value['NS'])
|
|
||||||
elif 'SS' in value:
|
|
||||||
self.value = tuple(value['SS'])
|
|
||||||
elif 'L' in value:
|
|
||||||
self.value = tuple(value['L'])
|
|
||||||
else:
|
|
||||||
# TODO: Handle all attribute types
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def expr(self, item):
|
def expr(self, item):
|
||||||
|
# TODO: Reuse DynamoType code
|
||||||
|
if self.type == 'N':
|
||||||
|
try:
|
||||||
|
return int(self.value)
|
||||||
|
except ValueError:
|
||||||
|
return float(self.value)
|
||||||
|
elif self.type in ['SS', 'NS', 'BS']:
|
||||||
|
sub_type = self.type[0]
|
||||||
|
return set([AttributeValue({sub_type: v}).expr(item)
|
||||||
|
for v in self.value])
|
||||||
|
elif self.type == 'L':
|
||||||
|
return [AttributeValue(v).expr(item) for v in self.value]
|
||||||
|
elif self.type == 'M':
|
||||||
|
return dict([
|
||||||
|
(k, AttributeValue(v).expr(item))
|
||||||
|
for k, v in self.value.items()])
|
||||||
|
else:
|
||||||
|
return self.value
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def get_type(self, item):
|
def get_type(self, item):
|
||||||
@ -976,15 +1042,8 @@ class FuncAttrExists(Func):
|
|||||||
return self.attr.get_type(item) is not None
|
return self.attr.get_type(item) is not None
|
||||||
|
|
||||||
|
|
||||||
class FuncAttrNotExists(Func):
|
def FuncAttrNotExists(attribute):
|
||||||
FUNC = 'attribute_not_exists'
|
return OpNot(FuncAttrExists(attribute), None)
|
||||||
|
|
||||||
def __init__(self, attribute):
|
|
||||||
self.attr = attribute
|
|
||||||
super().__init__(attribute)
|
|
||||||
|
|
||||||
def expr(self, item):
|
|
||||||
return self.attr.get_type(item) is None
|
|
||||||
|
|
||||||
|
|
||||||
class FuncAttrType(Func):
|
class FuncAttrType(Func):
|
||||||
@ -1024,13 +1083,20 @@ class FuncContains(Func):
|
|||||||
super().__init__(attribute, operand)
|
super().__init__(attribute, operand)
|
||||||
|
|
||||||
def expr(self, item):
|
def expr(self, item):
|
||||||
if self.attr.get_type(item) in ('S', 'SS', 'NS', 'BS', 'L', 'M'):
|
if self.attr.get_type(item) in ('S', 'SS', 'NS', 'BS', 'L'):
|
||||||
|
try:
|
||||||
return self.operand.expr(item) in self.attr.expr(item)
|
return self.operand.expr(item) in self.attr.expr(item)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def FuncNotContains(attribute, operand):
|
||||||
|
return OpNot(FuncContains(attribute, operand), None)
|
||||||
|
|
||||||
|
|
||||||
class FuncSize(Func):
|
class FuncSize(Func):
|
||||||
FUNC = 'contains'
|
FUNC = 'size'
|
||||||
|
|
||||||
def __init__(self, attribute):
|
def __init__(self, attribute):
|
||||||
self.attr = attribute
|
self.attr = attribute
|
||||||
|
@ -13,6 +13,9 @@ from moto.core import BaseBackend, BaseModel
|
|||||||
from moto.core.utils import unix_time
|
from moto.core.utils import unix_time
|
||||||
from moto.core.exceptions import JsonRESTError
|
from moto.core.exceptions import JsonRESTError
|
||||||
from .comparisons import get_comparison_func, get_filter_expression, Op
|
from .comparisons import get_comparison_func, get_filter_expression, Op
|
||||||
|
from .comparisons import get_comparison_func
|
||||||
|
from .comparisons import get_filter_expression
|
||||||
|
from .comparisons import get_expected
|
||||||
from .exceptions import InvalidIndexNameError
|
from .exceptions import InvalidIndexNameError
|
||||||
|
|
||||||
|
|
||||||
@ -557,29 +560,9 @@ class Table(BaseModel):
|
|||||||
self.range_key_type, item_attrs)
|
self.range_key_type, item_attrs)
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
if current is None:
|
if not get_expected(expected).expr(current):
|
||||||
current_attr = {}
|
|
||||||
elif hasattr(current, 'attrs'):
|
|
||||||
current_attr = current.attrs
|
|
||||||
else:
|
|
||||||
current_attr = current
|
|
||||||
|
|
||||||
for key, val in expected.items():
|
|
||||||
if 'Exists' in val and val['Exists'] is False \
|
|
||||||
or 'ComparisonOperator' in val and val['ComparisonOperator'] == 'NULL':
|
|
||||||
if key in current_attr:
|
|
||||||
raise ValueError("The conditional request failed")
|
|
||||||
elif key not in current_attr:
|
|
||||||
raise ValueError("The conditional request failed")
|
|
||||||
elif 'Value' in val and DynamoType(val['Value']).value != current_attr[key].value:
|
|
||||||
raise ValueError("The conditional request failed")
|
|
||||||
elif 'ComparisonOperator' in val:
|
|
||||||
dynamo_types = [
|
|
||||||
DynamoType(ele) for ele in
|
|
||||||
val.get("AttributeValueList", [])
|
|
||||||
]
|
|
||||||
if not current_attr[key].compare(val['ComparisonOperator'], dynamo_types):
|
|
||||||
raise ValueError('The conditional request failed')
|
raise ValueError('The conditional request failed')
|
||||||
|
|
||||||
if range_value:
|
if range_value:
|
||||||
self.items[hash_value][range_value] = item
|
self.items[hash_value][range_value] = item
|
||||||
else:
|
else:
|
||||||
@ -1024,31 +1007,10 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
|
|
||||||
item = table.get_item(hash_value, range_value)
|
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:
|
if not expected:
|
||||||
expected = {}
|
expected = {}
|
||||||
|
|
||||||
for key, val in expected.items():
|
if not get_expected(expected).expr(item):
|
||||||
if 'Exists' in val and val['Exists'] is False \
|
|
||||||
or 'ComparisonOperator' in val and val['ComparisonOperator'] == 'NULL':
|
|
||||||
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:
|
|
||||||
dynamo_types = [
|
|
||||||
DynamoType(ele) for ele in
|
|
||||||
val.get("AttributeValueList", [])
|
|
||||||
]
|
|
||||||
if not item_attr[key].compare(val['ComparisonOperator'], dynamo_types):
|
|
||||||
raise ValueError('The conditional request failed')
|
raise ValueError('The conditional request failed')
|
||||||
|
|
||||||
# Update does not fail on new items, so create one
|
# Update does not fail on new items, so create one
|
||||||
|
Loading…
x
Reference in New Issue
Block a user