1
1
from copy import copy
2
- from pynamodb .constants import AND , BETWEEN
2
+ from pynamodb .constants import AND , ATTR_TYPE_MAP , BETWEEN , IN , OR , SHORT_ATTR_TYPES , STRING_SHORT
3
3
from pynamodb .expressions .util import get_value_placeholder , substitute_names
4
+ from six .moves import range
4
5
5
6
6
- class Path (object ):
7
-
8
- def __init__ (self , path , attribute_name = False ):
9
- self .path = path
10
- self .attribute_name = attribute_name
11
-
12
- def __getitem__ (self , idx ):
13
- # list dereference operator
14
- if not isinstance (idx , int ):
15
- raise TypeError ("list indices must be integers, not {0}" .format (type (idx ).__name__ ))
16
- element_path = copy (self )
17
- element_path .path = '{0}[{1}]' .format (self .path , idx )
18
- return element_path
7
+ class Operand (object ):
8
+ """
9
+ Operand is the base class for objects that support creating conditions from comparators.
10
+ """
19
11
20
12
def __eq__ (self , other ):
21
13
return self ._compare ('=' , other )
22
14
15
+ def __ne__ (self , other ):
16
+ return self ._compare ('<>' , other )
17
+
23
18
def __lt__ (self , other ):
24
19
return self ._compare ('<' , other )
25
20
@@ -41,13 +36,85 @@ def between(self, lower, upper):
41
36
# work but similar expressions like value1 <= attribute & attribute < value2 fail seems too brittle.
42
37
return Between (self , self ._serialize (lower ), self ._serialize (upper ))
43
38
39
+ def is_in (self , * values ):
40
+ values = [self ._serialize (value ) for value in values ]
41
+ return In (self , * values )
42
+
43
+ def _serialize (self , value ):
44
+ # Check to see if value is already serialized
45
+ if isinstance (value , dict ) and len (value ) == 1 and list (value .keys ())[0 ] in SHORT_ATTR_TYPES :
46
+ return value
47
+ # Serialize value based on its type
48
+ from pynamodb .attributes import _get_class_for_serialize
49
+ attr_class = _get_class_for_serialize (value )
50
+ return {ATTR_TYPE_MAP [attr_class .attr_type ]: attr_class .serialize (value )}
51
+
52
+
53
+ class Size (Operand ):
54
+ """
55
+ Size is a special operand that represents the result of calling the 'size' function on a Path operand.
56
+ """
57
+
58
+ def __init__ (self , path ):
59
+ # prevent circular import -- AttributePath imports Path
60
+ from pynamodb .attributes import Attribute , AttributePath
61
+ if isinstance (path , Path ):
62
+ self .path = Path
63
+ elif isinstance (path , Attribute ):
64
+ self .path = AttributePath (path )
65
+ else :
66
+ self .path = Path (path )
67
+
68
+ def _serialize (self , value ):
69
+ if not isinstance (value , int ):
70
+ raise TypeError ("size must be compared to an integer, not {0}" .format (type (value ).__name__ ))
71
+ return {'N' : str (value )}
72
+
73
+ def __str__ (self ):
74
+ return "size({0})" .format (self .path )
75
+
76
+ def __repr__ (self ):
77
+ return "Size({0})" .format (repr (self .path ))
78
+
79
+
80
+ # match dynamo function syntax: size(path)
81
+ def size (path ):
82
+ return Size (path )
83
+
84
+
85
+ class Path (Operand ):
86
+ """
87
+ Path is an operand that represents either an attribute name or document path.
88
+ In addition to supporting comparisons, Path also supports creating conditions from functions.
89
+ """
90
+
91
+ def __init__ (self , path , attribute_name = False ):
92
+ self .path = path
93
+ self .attribute_name = attribute_name
94
+
95
+ def __getitem__ (self , idx ):
96
+ # list dereference operator
97
+ if not isinstance (idx , int ):
98
+ raise TypeError ("list indices must be integers, not {0}" .format (type (idx ).__name__ ))
99
+ element_path = copy (self )
100
+ element_path .path = '{0}[{1}]' .format (self .path , idx )
101
+ return element_path
102
+
103
+ def exists (self ):
104
+ return Exists (self )
105
+
106
+ def not_exists (self ):
107
+ return NotExists (self )
108
+
109
+ def is_type (self , attr_type ):
110
+ return IsType (self , attr_type )
111
+
44
112
def startswith (self , prefix ):
45
113
# A 'pythonic' replacement for begins_with to match string behavior (e.g. "foo".startswith("f"))
46
114
return BeginsWith (self , self ._serialize (prefix ))
47
115
48
- def _serialize (self , value ):
49
- # Allow subclasses to define value serialization.
50
- return value
116
+ def contains (self , item ):
117
+ return Contains (self , self ._serialize (item ))
51
118
52
119
def __str__ (self ):
53
120
if self .attribute_name and '.' in self .path :
@@ -67,34 +134,47 @@ def __init__(self, path, operator, *values):
67
134
self .path = path
68
135
self .operator = operator
69
136
self .values = values
70
- self .logical_operator = None
71
- self .other_condition = None
72
137
73
138
def serialize (self , placeholder_names , expression_attribute_values ):
74
- split = not self .path .attribute_name
75
- path = substitute_names (self .path .path , placeholder_names , split = split )
76
- values = [get_value_placeholder (value , expression_attribute_values ) for value in self .values ]
77
- condition = self .format_string .format (* values , path = path , operator = self .operator )
78
- if self .logical_operator :
79
- other_condition = self .other_condition .serialize (placeholder_names , expression_attribute_values )
80
- return '{0} {1} {2}' .format (condition , self .logical_operator , other_condition )
81
- return condition
139
+ path = self ._get_path (self .path , placeholder_names )
140
+ values = self ._get_values (placeholder_names , expression_attribute_values )
141
+ return self .format_string .format (* values , path = path , operator = self .operator )
142
+
143
+ def _get_path (self , path , placeholder_names ):
144
+ if isinstance (path , Path ):
145
+ split = not path .attribute_name
146
+ return substitute_names (path .path , placeholder_names , split = split )
147
+ elif isinstance (path , Size ):
148
+ return "size ({0})" .format (self ._get_path (path .path , placeholder_names ))
149
+ else :
150
+ return path
151
+
152
+ def _get_values (self , placeholder_names , expression_attribute_values ):
153
+ return [
154
+ value .serialize (placeholder_names , expression_attribute_values )
155
+ if isinstance (value , Condition )
156
+ else get_value_placeholder (value , expression_attribute_values )
157
+ for value in self .values
158
+ ]
82
159
83
160
def __and__ (self , other ):
84
161
if not isinstance (other , Condition ):
85
162
raise TypeError ("unsupported operand type(s) for &: '{0}' and '{1}'" ,
86
163
self .__class__ .__name__ , other .__class__ .__name__ )
87
- self .logical_operator = AND
88
- self .other_condition = other
89
- return self
164
+ return And (self , other )
165
+
166
+ def __or__ (self , other ):
167
+ if not isinstance (other , Condition ):
168
+ raise TypeError ("unsupported operand type(s) for |: '{0}' and '{1}'" ,
169
+ self .__class__ .__name__ , other .__class__ .__name__ )
170
+ return Or (self , other )
171
+
172
+ def __invert__ (self ):
173
+ return Not (self )
90
174
91
175
def __repr__ (self ):
92
- values = [value .items ()[0 ][1 ] for value in self .values ]
93
- condition = self .format_string .format (* values , path = self .path , operator = self .operator )
94
- if self .logical_operator :
95
- other_conditions = repr (self .other_condition )
96
- return '{0} {1} {2}' .format (condition , self .logical_operator , other_conditions )
97
- return condition
176
+ values = [repr (value ) if isinstance (value , Condition ) else value .items ()[0 ][1 ] for value in self .values ]
177
+ return self .format_string .format (* values , path = self .path , operator = self .operator )
98
178
99
179
def __nonzero__ (self ):
100
180
# Prevent users from accidentally comparing the condition object instead of the attribute instance
@@ -112,8 +192,70 @@ def __init__(self, path, lower, upper):
112
192
super (Between , self ).__init__ (path , BETWEEN , lower , upper )
113
193
114
194
195
+ class In (Condition ):
196
+ def __init__ (self , path , * values ):
197
+ super (In , self ).__init__ (path , IN , * values )
198
+ list_format = ', ' .join ('{' + str (i ) + '}' for i in range (len (values )))
199
+ self .format_string = '{path} {operator} (' + list_format + ')'
200
+
201
+
202
+ class Exists (Condition ):
203
+ format_string = '{operator} ({path})'
204
+
205
+ def __init__ (self , path ):
206
+ super (Exists , self ).__init__ (path , 'attribute_exists' )
207
+
208
+
209
+ class NotExists (Condition ):
210
+ format_string = '{operator} ({path})'
211
+
212
+ def __init__ (self , path ):
213
+ super (NotExists , self ).__init__ (path , 'attribute_not_exists' )
214
+
215
+
216
+ class IsType (Condition ):
217
+ format_string = '{operator} ({path}, {0})'
218
+
219
+ def __init__ (self , path , attr_type ):
220
+ if attr_type not in SHORT_ATTR_TYPES :
221
+ raise ValueError ("{0} is not a valid attribute type. Must be one of {1}" .format (
222
+ attr_type , SHORT_ATTR_TYPES ))
223
+ super (IsType , self ).__init__ (path , 'attribute_type' , {STRING_SHORT : attr_type })
224
+
225
+
115
226
class BeginsWith (Condition ):
116
227
format_string = '{operator} ({path}, {0})'
117
228
118
229
def __init__ (self , path , prefix ):
119
230
super (BeginsWith , self ).__init__ (path , 'begins_with' , prefix )
231
+
232
+
233
+ class Contains (Condition ):
234
+ format_string = '{operator} ({path}, {0})'
235
+
236
+ def __init__ (self , path , item ):
237
+ (attr_type , value ), = item .items ()
238
+ if attr_type != STRING_SHORT :
239
+ raise ValueError ("{0} must be a string" .format (value ))
240
+ super (Contains , self ).__init__ (path , 'contains' , item )
241
+
242
+
243
+ class And (Condition ):
244
+ format_string = '({0} {operator} {1})'
245
+
246
+ def __init__ (self , condition1 , condition2 ):
247
+ super (And , self ).__init__ (None , AND , condition1 , condition2 )
248
+
249
+
250
+ class Or (Condition ):
251
+ format_string = '({0} {operator} {1})'
252
+
253
+ def __init__ (self , condition1 , condition2 ):
254
+ super (Or , self ).__init__ (None , OR , condition1 , condition2 )
255
+
256
+
257
+ class Not (Condition ):
258
+ format_string = '({operator} {0})'
259
+
260
+ def __init__ (self , condition ):
261
+ super (Not , self ).__init__ (None , 'NOT' , condition )
0 commit comments