Skip to content

Commit 9ccd683

Browse files
authored
Allows specifying timedeltas in expressions involving TTLAttribute (#665)
For example, the following will be supported: ``` class MyModel(Model): ... ttl = TTLAttribute() model = MyModel() model.update(actions=[ MyModel.ttl.set(timedelta(days=42)), ]) ```
1 parent 797cfad commit 9ccd683

File tree

3 files changed

+31
-13
lines changed

3 files changed

+31
-13
lines changed

pynamodb/attributes.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -503,30 +503,35 @@ class TTLAttribute(Attribute):
503503
"""
504504
attr_type = NUMBER
505505

506-
def __set__(self, instance, value):
506+
def _normalize(self, value):
507507
"""
508-
Converts assigned values to a UTC datetime
508+
Converts value to a UTC datetime
509509
"""
510+
if value is None:
511+
return
510512
if isinstance(value, timedelta):
511513
value = int(time.time() + value.total_seconds())
512514
elif isinstance(value, datetime):
513515
if value.tzinfo is None:
514516
raise ValueError("datetime must be timezone-aware")
515517
value = calendar.timegm(value.utctimetuple())
516-
elif value is not None:
518+
else:
517519
raise ValueError("TTLAttribute value must be a timedelta or datetime")
518-
attr_name = instance._dynamo_to_python_attrs.get(self.attr_name, self.attr_name)
519-
if value is not None:
520-
value = datetime.utcfromtimestamp(value).replace(tzinfo=tzutc())
521-
instance.attribute_values[attr_name] = value
520+
return datetime.utcfromtimestamp(value).replace(tzinfo=tzutc())
521+
522+
def __set__(self, instance, value):
523+
"""
524+
Converts assigned values to a UTC datetime
525+
"""
526+
super(TTLAttribute, self).__set__(instance, self._normalize(value))
522527

523528
def serialize(self, value):
524529
"""
525530
Serializes a datetime as a timestamp (Unix time).
526531
"""
527532
if value is None:
528533
return None
529-
return json.dumps(calendar.timegm(value.utctimetuple()))
534+
return json.dumps(calendar.timegm(self._normalize(value).utctimetuple()))
530535

531536
def deserialize(self, value):
532537
"""

tests/test_attributes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,11 @@ def test_ttl_attribute_wrong_type(self):
517517
model = AttributeTestModel()
518518
model.ttl_attr = 'wrong type'
519519

520+
@patch('time.time')
521+
def test_serialize_timedelta(self, mock_time):
522+
mock_time.side_effect = [1559692800] # 2019-06-05 00:00:00 UTC
523+
assert TTLAttribute().serialize(timedelta(seconds=60)) == str(1559692800 + 60)
524+
520525
def test_serialize_none(self):
521526
model = AttributeTestModel()
522527
model.ttl_attr = None

tests/test_model.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ class Meta:
190190
views = NumberAttribute(null=True)
191191
is_active = BooleanAttribute(null=True)
192192
signature = UnicodeAttribute(null=True)
193+
ttl = TTLAttribute(null=True)
193194

194195

195196
class CustomAttrIndex(LocalSecondaryIndex):
@@ -806,10 +807,12 @@ def test_delete_doesnt_do_validation_on_null_attributes(self):
806807
car = CarModel('foo')
807808
batch.delete(car)
808809

809-
def test_update(self):
810+
@patch('time.time')
811+
def test_update(self, mock_time):
810812
"""
811813
Model.update
812814
"""
815+
mock_time.side_effect = [1559692800] # 2019-06-05 00:00:00 UTC
813816
self.init_table_meta(SimpleUserModel, SIMPLE_MODEL_TABLE_DATA)
814817
item = SimpleUserModel('foo', is_active=True, email='[email protected]', signature='foo')
815818

@@ -839,7 +842,8 @@ def test_update(self):
839842
SimpleUserModel.is_active.set(None),
840843
SimpleUserModel.signature.set(None),
841844
SimpleUserModel.custom_aliases.set(['bob']),
842-
SimpleUserModel.numbers.delete(0, 1)
845+
SimpleUserModel.numbers.delete(0, 1),
846+
SimpleUserModel.ttl.set(timedelta(seconds=60)),
843847
])
844848

845849
args = req.call_args[0][1]
@@ -851,14 +855,15 @@ def test_update(self):
851855
'S': 'foo'
852856
}
853857
},
854-
'UpdateExpression': 'SET #0 = :0, #1 = :1, #2 = :2, #3 = :3 REMOVE #4 DELETE #5 :4',
858+
'UpdateExpression': 'SET #0 = :0, #1 = :1, #2 = :2, #3 = :3, #4 = :4 REMOVE #5 DELETE #6 :5',
855859
'ExpressionAttributeNames': {
856860
'#0': 'email',
857861
'#1': 'is_active',
858862
'#2': 'signature',
859863
'#3': 'aliases',
860-
'#4': 'views',
861-
'#5': 'numbers'
864+
'#4': 'ttl',
865+
'#5': 'views',
866+
'#6': 'numbers'
862867
},
863868
'ExpressionAttributeValues': {
864869
':0': {
@@ -874,6 +879,9 @@ def test_update(self):
874879
'SS': ['bob']
875880
},
876881
':4': {
882+
'N': str(1559692800 + 60)
883+
},
884+
':5': {
877885
'NS': ['0', '1']
878886
}
879887
},

0 commit comments

Comments
 (0)