10
10
11
11
from six import add_metaclass
12
12
from pynamodb .exceptions import DoesNotExist , TableDoesNotExist , TableError , InvalidStateError
13
- from pynamodb .attributes import Attribute , AttributeContainer , AttributeContainerMeta , MapAttribute , TTLAttribute
13
+ from pynamodb .attributes import (
14
+ Attribute , AttributeContainer , AttributeContainerMeta , MapAttribute , TTLAttribute , VersionAttribute
15
+ )
14
16
from pynamodb .connection .table import TableConnection
15
17
from pynamodb .connection .util import pythonic
16
18
from pynamodb .types import HASH , RANGE
@@ -209,6 +211,14 @@ def __init__(cls, name, bases, attrs):
209
211
if len (ttl_attr_names ) > 1 :
210
212
raise ValueError ("The model has more than one TTL attribute: {}" .format (", " .join (ttl_attr_names )))
211
213
214
+ version_attribute_names = [
215
+ name for name , attr_obj in attrs .items () if isinstance (attr_obj , VersionAttribute )
216
+ ]
217
+ if len (version_attribute_names ) > 1 :
218
+ raise ValueError (
219
+ "The model has more than one Version attribute: {}" .format (", " .join (version_attribute_names ))
220
+ )
221
+
212
222
if META_CLASS_NAME not in attrs :
213
223
setattr (cls , META_CLASS_NAME , DefaultMeta )
214
224
@@ -334,6 +344,10 @@ def delete(self, condition=None):
334
344
Deletes this object from dynamodb
335
345
"""
336
346
args , kwargs = self ._get_save_args (attributes = False , null_check = False )
347
+ version_condition = self ._handle_version_attribute (kwargs )
348
+ if version_condition is not None :
349
+ condition &= version_condition
350
+
337
351
kwargs .update (condition = condition )
338
352
return self ._get_connection ().delete_item (* args , ** kwargs )
339
353
@@ -348,6 +362,9 @@ def update(self, actions, condition=None):
348
362
raise TypeError ("the value of `actions` is expected to be a non-empty list" )
349
363
350
364
args , save_kwargs = self ._get_save_args (null_check = False )
365
+ version_condition = self ._handle_version_attribute (save_kwargs , actions = actions )
366
+ if version_condition is not None :
367
+ condition &= version_condition
351
368
kwargs = {
352
369
pythonic (RETURN_VALUES ): ALL_NEW ,
353
370
}
@@ -371,6 +388,9 @@ def save(self, condition=None):
371
388
Save this object to dynamodb
372
389
"""
373
390
args , kwargs = self ._get_save_args ()
391
+ version_condition = self ._handle_version_attribute (serialized_attributes = kwargs )
392
+ if version_condition is not None :
393
+ condition &= version_condition
374
394
kwargs .update (condition = condition )
375
395
return self ._get_connection ().put_item (* args , ** kwargs )
376
396
@@ -395,6 +415,10 @@ def get_operation_kwargs_from_instance(self,
395
415
return_values_on_condition_failure = None ):
396
416
is_update = actions is not None
397
417
args , save_kwargs = self ._get_save_args (null_check = not is_update )
418
+ version_condition = self ._handle_version_attribute (serialized_attributes = save_kwargs ,
419
+ actions = actions )
420
+ if version_condition is not None :
421
+ condition &= version_condition
398
422
kwargs = dict (
399
423
key = key ,
400
424
actions = actions ,
@@ -872,6 +896,7 @@ def _get_save_args(self, attributes=True, null_check=True):
872
896
"""
873
897
kwargs = {}
874
898
serialized = self ._serialize (null_check = null_check )
899
+
875
900
hash_key = serialized .get (HASH )
876
901
range_key = serialized .get (RANGE , None )
877
902
args = (hash_key , )
@@ -881,6 +906,40 @@ def _get_save_args(self, attributes=True, null_check=True):
881
906
kwargs [pythonic (ATTRIBUTES )] = serialized [pythonic (ATTRIBUTES )]
882
907
return args , kwargs
883
908
909
+ def _handle_version_attribute (self , serialized_attributes , actions = None ):
910
+ """
911
+ Handles modifying the request to set or increment the version attribute.
912
+
913
+ :param serialized_attributes: A dictionary mapping attribute names to serialized values.
914
+ :param actions: A non-empty list when performing an update, otherwise None.
915
+ """
916
+ version_condition = None
917
+
918
+ for name , attr in self .get_attributes ().items ():
919
+ value = getattr (self , name )
920
+ if isinstance (attr , VersionAttribute ):
921
+ # We don't modify the attribute except in the serialized payload so that
922
+ # the local object is not modified on failure.
923
+ if not value :
924
+ version_condition = attr .does_not_exist ()
925
+ if actions :
926
+ actions .append (attr .set (1 ))
927
+ elif pythonic (ATTRIBUTES ) in serialized_attributes :
928
+ serialized_attributes [pythonic (ATTRIBUTES )][attr .attr_name ] = self ._serialize_value (
929
+ attr , 1 , null_check = True
930
+ )
931
+ else :
932
+ version_condition = attr == value
933
+ if actions :
934
+ actions .append (attr .add (1 ))
935
+ elif pythonic (ATTRIBUTES ) in serialized_attributes :
936
+ serialized_attributes [pythonic (ATTRIBUTES )][attr .attr_name ] = self ._serialize_value (
937
+ attr , value + 1 , null_check = True
938
+ )
939
+
940
+ break
941
+ return version_condition
942
+
884
943
@classmethod
885
944
def _hash_key_attribute (cls ):
886
945
"""
@@ -1002,7 +1061,6 @@ def _serialize(self, attr_map=False, null_check=True):
1002
1061
if isinstance (value , MapAttribute ):
1003
1062
if not value .validate ():
1004
1063
raise ValueError ("Attribute '{}' is not correctly typed" .format (attr .attr_name ))
1005
-
1006
1064
serialized = self ._serialize_value (attr , value , null_check )
1007
1065
if NULL in serialized :
1008
1066
continue
0 commit comments