|
30 | 30 | from pynamodb.attributes import (
|
31 | 31 | UnicodeAttribute, NumberAttribute, BinaryAttribute, UTCDateTimeAttribute,
|
32 | 32 | UnicodeSetAttribute, NumberSetAttribute, BinarySetAttribute, MapAttribute,
|
33 |
| - BooleanAttribute, ListAttribute, TTLAttribute) |
| 33 | + BooleanAttribute, ListAttribute, TTLAttribute, VersionAttribute) |
34 | 34 | from .data import (
|
35 | 35 | MODEL_TABLE_DATA, GET_MODEL_ITEM_DATA, SIMPLE_MODEL_TABLE_DATA,
|
36 | 36 | DESCRIBE_TABLE_DATA_PAY_PER_REQUEST,
|
|
41 | 41 | GET_OFFICE_EMPLOYEE_ITEM_DATA, GET_OFFICE_EMPLOYEE_ITEM_DATA_WITH_NULL,
|
42 | 42 | GROCERY_LIST_MODEL_TABLE_DATA, GET_GROCERY_LIST_ITEM_DATA,
|
43 | 43 | GET_OFFICE_ITEM_DATA, OFFICE_MODEL_TABLE_DATA, COMPLEX_MODEL_TABLE_DATA, COMPLEX_MODEL_ITEM_DATA,
|
44 |
| - CAR_MODEL_TABLE_DATA, FULL_CAR_MODEL_ITEM_DATA, CAR_MODEL_WITH_NULL_ITEM_DATA, INVALID_CAR_MODEL_WITH_NULL_ITEM_DATA, |
| 44 | + CAR_MODEL_TABLE_DATA, FULL_CAR_MODEL_ITEM_DATA, CAR_MODEL_WITH_NULL_ITEM_DATA, |
| 45 | + INVALID_CAR_MODEL_WITH_NULL_ITEM_DATA, |
45 | 46 | BOOLEAN_MODEL_TABLE_DATA, BOOLEAN_MODEL_FALSE_ITEM_DATA, BOOLEAN_MODEL_TRUE_ITEM_DATA,
|
46 | 47 | TREE_MODEL_TABLE_DATA, TREE_MODEL_ITEM_DATA,
|
47 | 48 | EXPLICIT_RAW_MAP_MODEL_TABLE_DATA, EXPLICIT_RAW_MAP_MODEL_ITEM_DATA,
|
48 |
| - EXPLICIT_RAW_MAP_MODEL_AS_SUB_MAP_IN_TYPED_MAP_ITEM_DATA, EXPLICIT_RAW_MAP_MODEL_AS_SUB_MAP_IN_TYPED_MAP_TABLE_DATA |
49 |
| -) |
| 49 | + EXPLICIT_RAW_MAP_MODEL_AS_SUB_MAP_IN_TYPED_MAP_ITEM_DATA, EXPLICIT_RAW_MAP_MODEL_AS_SUB_MAP_IN_TYPED_MAP_TABLE_DATA, |
| 50 | + VERSIONED_TABLE_DATA) |
50 | 51 |
|
51 | 52 | if six.PY3:
|
52 | 53 | from unittest.mock import patch, MagicMock
|
@@ -440,6 +441,15 @@ class Meta:
|
440 | 441 | my_ttl = TTLAttribute(default_for_new=timedelta(minutes=1))
|
441 | 442 |
|
442 | 443 |
|
| 444 | +class VersionedModel(Model): |
| 445 | + class Meta: |
| 446 | + table_name = 'VersionedModel' |
| 447 | + |
| 448 | + name = UnicodeAttribute(hash_key=True) |
| 449 | + email = UnicodeAttribute() |
| 450 | + version = VersionAttribute() |
| 451 | + |
| 452 | + |
443 | 453 | class ModelTestCase(TestCase):
|
444 | 454 | """
|
445 | 455 | Tests for the models API
|
@@ -2964,6 +2974,153 @@ def fake_dynamodb(*args, **kwargs):
|
2964 | 2974 | self.assert_dict_lists_equal(actual['AttributeDefinitions'],
|
2965 | 2975 | DOG_TABLE_DATA['Table']['AttributeDefinitions'])
|
2966 | 2976 |
|
| 2977 | + def test_model_version_attribute_save(self): |
| 2978 | + self.init_table_meta(VersionedModel, VERSIONED_TABLE_DATA) |
| 2979 | + item = VersionedModel('test_user_name', email='test_user@email.com') |
| 2980 | + |
| 2981 | + with patch(PATCH_METHOD) as req: |
| 2982 | + req.return_value = {} |
| 2983 | + item.save() |
| 2984 | + args = req.call_args[0][1] |
| 2985 | + params = { |
| 2986 | + 'Item': { |
| 2987 | + 'name': { |
| 2988 | + 'S': 'test_user_name' |
| 2989 | + }, |
| 2990 | + 'email': { |
| 2991 | + 'S': 'test_user@email.com' |
| 2992 | + }, |
| 2993 | + 'version': { |
| 2994 | + 'N': '1' |
| 2995 | + }, |
| 2996 | + }, |
| 2997 | + 'ReturnConsumedCapacity': 'TOTAL', |
| 2998 | + 'TableName': 'VersionedModel', |
| 2999 | + 'ConditionExpression': 'attribute_not_exists (#0)', |
| 3000 | + 'ExpressionAttributeNames': {'#0': 'version'}, |
| 3001 | + } |
| 3002 | + |
| 3003 | + deep_eq(args, params, _assert=True) |
| 3004 | + item.version = 1 |
| 3005 | + item.name = "test_new_username" |
| 3006 | + item.save() |
| 3007 | + args = req.call_args[0][1] |
| 3008 | + |
| 3009 | + params = { |
| 3010 | + 'Item': { |
| 3011 | + 'name': { |
| 3012 | + 'S': 'test_new_username' |
| 3013 | + }, |
| 3014 | + 'email': { |
| 3015 | + 'S': 'test_user@email.com' |
| 3016 | + }, |
| 3017 | + 'version': { |
| 3018 | + 'N': '2' |
| 3019 | + }, |
| 3020 | + }, |
| 3021 | + 'ReturnConsumedCapacity': 'TOTAL', |
| 3022 | + 'TableName': 'VersionedModel', |
| 3023 | + 'ConditionExpression': '#0 = :0', |
| 3024 | + 'ExpressionAttributeNames': {'#0': 'version'}, |
| 3025 | + 'ExpressionAttributeValues': {':0': {'N': '1'}} |
| 3026 | + } |
| 3027 | + |
| 3028 | + deep_eq(args, params, _assert=True) |
| 3029 | + |
| 3030 | + def test_version_attribute_increments_on_update(self): |
| 3031 | + self.init_table_meta(VersionedModel, VERSIONED_TABLE_DATA) |
| 3032 | + item = VersionedModel('test_user_name', email='test_user@email.com') |
| 3033 | + |
| 3034 | + with patch(PATCH_METHOD) as req: |
| 3035 | + req.return_value = { |
| 3036 | + ATTRIBUTES: { |
| 3037 | + 'name': { |
| 3038 | + 'S': 'test_user_name' |
| 3039 | + }, |
| 3040 | + 'email': { |
| 3041 | + 'S': 'new@email.com' |
| 3042 | + }, |
| 3043 | + 'version': { |
| 3044 | + 'N': '1' |
| 3045 | + }, |
| 3046 | + } |
| 3047 | + } |
| 3048 | + item.update(actions=[VersionedModel.email.set('new@email.com')]) |
| 3049 | + args = req.call_args[0][1] |
| 3050 | + params = { |
| 3051 | + 'ConditionExpression': 'attribute_not_exists (#0)', |
| 3052 | + 'ExpressionAttributeNames': { |
| 3053 | + '#0': 'version', |
| 3054 | + '#1': 'email' |
| 3055 | + }, |
| 3056 | + 'ExpressionAttributeValues': { |
| 3057 | + ':0': { |
| 3058 | + 'S': 'new@email.com' |
| 3059 | + }, |
| 3060 | + ':1': { |
| 3061 | + 'N': '1' |
| 3062 | + } |
| 3063 | + }, |
| 3064 | + 'Key': { |
| 3065 | + 'name': { |
| 3066 | + 'S': 'test_user_name' |
| 3067 | + } |
| 3068 | + }, |
| 3069 | + 'ReturnConsumedCapacity': 'TOTAL', |
| 3070 | + 'ReturnValues': 'ALL_NEW', |
| 3071 | + 'TableName': 'VersionedModel', |
| 3072 | + 'UpdateExpression': 'SET #1 = :0, #0 = :1' |
| 3073 | + } |
| 3074 | + |
| 3075 | + deep_eq(args, params, _assert=True) |
| 3076 | + assert item.version == 1 |
| 3077 | + |
| 3078 | + req.return_value = { |
| 3079 | + ATTRIBUTES: { |
| 3080 | + 'name': { |
| 3081 | + 'S': 'test_user_name' |
| 3082 | + }, |
| 3083 | + 'email': { |
| 3084 | + 'S': 'newer@email.com' |
| 3085 | + }, |
| 3086 | + 'version': { |
| 3087 | + 'N': '2' |
| 3088 | + }, |
| 3089 | + } |
| 3090 | + } |
| 3091 | + |
| 3092 | + item.update(actions=[VersionedModel.email.set('newer@email.com')]) |
| 3093 | + args = req.call_args[0][1] |
| 3094 | + params = { |
| 3095 | + 'ConditionExpression': '#0 = :0', |
| 3096 | + 'ExpressionAttributeNames': { |
| 3097 | + '#0': 'version', |
| 3098 | + '#1': 'email' |
| 3099 | + }, |
| 3100 | + 'ExpressionAttributeValues': { |
| 3101 | + ':0': { |
| 3102 | + 'N': '1' |
| 3103 | + }, |
| 3104 | + ':1': { |
| 3105 | + 'S': 'newer@email.com' |
| 3106 | + }, |
| 3107 | + ':2': { |
| 3108 | + 'N': '1' |
| 3109 | + } |
| 3110 | + }, |
| 3111 | + 'Key': { |
| 3112 | + 'name': { |
| 3113 | + 'S': 'test_user_name' |
| 3114 | + } |
| 3115 | + }, |
| 3116 | + 'ReturnConsumedCapacity': 'TOTAL', |
| 3117 | + 'ReturnValues': 'ALL_NEW', |
| 3118 | + 'TableName': 'VersionedModel', |
| 3119 | + 'UpdateExpression': 'SET #1 = :1 ADD #0 :2' |
| 3120 | + } |
| 3121 | + |
| 3122 | + deep_eq(args, params, _assert=True) |
| 3123 | + |
2967 | 3124 |
|
2968 | 3125 | class ModelInitTestCase(TestCase):
|
2969 | 3126 |
|
@@ -3075,3 +3232,12 @@ def test_deserialized_with_ttl(self):
|
3075 | 3232 | req.return_value = SIMPLE_MODEL_TABLE_DATA
|
3076 | 3233 | m = TTLModel.from_raw_data({'user_name': {'S': 'mock'}, 'my_ttl': {'N': '1546300800'}})
|
3077 | 3234 | assert m.my_ttl == datetime(2019, 1, 1, tzinfo=tzutc())
|
| 3235 | + |
| 3236 | + def test_multiple_version_attributes(self): |
| 3237 | + with self.assertRaises(ValueError): |
| 3238 | + class BadVersionedModel(Model): |
| 3239 | + class Meta: |
| 3240 | + table_name = 'BadVersionedModel' |
| 3241 | + |
| 3242 | + version = VersionAttribute() |
| 3243 | + another_version = VersionAttribute() |
0 commit comments