def test_condition_converts_eq_null(self): """ Conditional converts eq=None to null=True """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a'}) update = ItemUpdate.put('foo', set([1, 2]), eq=set()) self.dynamo.update_item('foobar', {'id': 'a'}, [update]) update = ItemUpdate.put('foo', set([2]), eq=set()) with self.assertRaises(CheckFailed): self.dynamo.update_item('foobar', {'id': 'a'}, [update])
def test_atomic_add_set(self): """ Update can atomically add to a set """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a'}) self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.add('foo', set([1]))]) self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.add('foo', set([1, 2]))]) item = list(self.dynamo.scan('foobar'))[0] self.assertEqual(item, {'id': 'a', 'foo': set([1, 2])})
def test_atomic_add_num(self): """ Update can atomically add to a number """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a'}) self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.add('foo', 1)]) self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.add('foo', 2)]) item = list(self.dynamo.scan('foobar'))[0] self.assertEqual(item, {'id': 'a', 'foo': 3})
def test_expect_not_exists_deprecated(self): """ Update can expect a field to not exist """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a', 'foo': 'bar'}) update = ItemUpdate.put('foo', 'baz', expected=None) with self.assertRaises(CheckFailed): self.dynamo.update_item('foobar', {'id': 'a'}, [update])
def test_expect_condition(self): """ Update can expect a field to meet a condition """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a', 'foo': 5}) update = ItemUpdate.put('foo', 10, lt=5) with self.assertRaises(CheckFailed): self.dynamo.update_item('foobar', {'id': 'a'}, [update])
def test_expect_condition_or(self): """ Expected conditionals can be OR'd together """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a', 'foo': 5}) update = ItemUpdate.put('foo', 10, lt=5) self.dynamo.update_item('foobar', {'id': 'a'}, [update], expect_or=True, baz__null=True)
def test_expect_field_deprecated(self): """ Update can expect a field to have a value """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a', 'foo': 'bar'}) update = ItemUpdate.put('foo', 'baz', expected='wat') with self.assertRaises(CheckFailed): self.dynamo.update_item('foobar', {'id': 'a'}, [update])
def test_return_item(self): """ Update can return the updated item """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a'}) ret = self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.put('foo', 'bar')], returns=ALL_NEW) self.assertEqual(ret, {'id': 'a', 'foo': 'bar'})
def test_delete_field(self): """ Update can delete fields from an item """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a', 'foo': 'bar'}) self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.delete('foo')]) item = list(self.dynamo.scan('foobar'))[0] self.assertEqual(item, {'id': 'a'})
def test_update_field(self): """ Update an item field """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a'}) self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.put('foo', 'bar')]) item = list(self.dynamo.scan('foobar'))[0] self.assertEqual(item, {'id': 'a', 'foo': 'bar'})
def test_write_converts_none(self): """ Write operation converts None values to a DELETE """ hash_key = DynamoKey('id', data_type=STRING) self.dynamo.create_table('foobar', hash_key=hash_key) self.dynamo.put_item('foobar', {'id': 'a', 'foo': 'bar'}) update = ItemUpdate.put('foo', None) self.dynamo.update_item('foobar', {'id': 'a'}, [update]) ret = list(self.dynamo.scan('foobar')) self.assertItemsEqual(ret, [{'id': 'a'}])
def test_sync_only_updates_changed(self): """ Sync only updates fields that have been changed """ with patch.object(self.engine, 'dynamo') as dynamo: captured_updates = [] def update_item(_, __, updates, *___, **____): """ Mock update_item and capture the passed updateds """ captured_updates.extend(updates) return {} dynamo.update_item.side_effect = update_item p = Post('a', 'b', 4) self.engine.save(p) p.foobar = set('a') p.points = Decimal('2') p.sync(raise_on_conflict=False) self.assertEqual(len(captured_updates), 2) self.assertTrue(ItemUpdate.put('foobar', ANY) in captured_updates) self.assertTrue(ItemUpdate.put('points', ANY) in captured_updates)
def test_sync_only_updates_changed(self): """ Sync only updates fields that have been changed """ with patch.object(self.engine, 'dynamo') as dynamo: captured_updates = [] def update_item(_, __, updates, *___, **____): """ Mock update_item and capture the passed updateds """ captured_updates.extend(updates) return {} dynamo.update_item.side_effect = update_item p = Post('a', 'b', 4) self.engine.save(p) p.foobar = set('a') p.ts = 4 p.points = Decimal('2') p.sync(raise_on_conflict=False) self.assertEqual(len(captured_updates), 2) self.assertTrue(ItemUpdate.put('foobar', ANY) in captured_updates) self.assertTrue(ItemUpdate.put('points', ANY) in captured_updates)
def update_field(self, item, name, value=NO_ARG, action=ItemUpdate.PUT, constraints=None): """ Update the value of a single field Note that this method bypasses field validators and will ignore any special behavior around Composite fields. Parameters ---------- item : :class:`~flywheel.models.Model` The model to update name : str The name of the field to update value : object, optional The new value for the field. Default will use the value currently on the model. action : str, optional PUT, ADD, or DELETE. (default PUT) constraints : list, optional List of constraints that must pass for the update to complete. Format is the same as query filters (e.g. Model.fieldname > 5) """ if value is NO_ARG: if action == ItemUpdate.DELETE: value = None elif action == ItemUpdate.ADD: raise ValueError("Must specify a value when using the " "actions ADD or DELETE") else: value = getattr(item, name) keywords = {} if constraints is not None: for constraint in constraints: keywords.update(constraint.scan_kwargs()) updates = [ ItemUpdate(action, name, item.field_(name).ddb_dump_for_query(value)) ] ret = self.dynamo.update_item( item.meta_.ddb_tablename(self.namespace), item.pk_dict_, updates, returns=UPDATED_NEW, **keywords) with item.partial_loading_(): for key, val in six.iteritems(ret): item.set_ddb_val_(key, val) # If we didn't see the field in the response, # it must have been deleted. if name not in ret: item.set_ddb_val_(name, None) item.post_save_fields_(set([name]))
def test_return_metadata(self): """ The Update return value contains capacity metadata """ self.make_table() self.dynamo.put_item('foobar', {'id': 'a'}) ret = self.dynamo.update_item('foobar', {'id': 'a'}, [ItemUpdate.put('foo', 'bar')], returns=ALL_NEW, return_capacity=TOTAL) self.assertTrue(is_number(ret.capacity)) self.assertTrue(is_number(ret.table_capacity)) self.assertTrue(isinstance(ret.indexes, dict)) self.assertTrue(isinstance(ret.global_indexes, dict))
def test_write_add_require_value(self): """ Doing an ADD requires a non-null value """ with self.assertRaises(ValueError): ItemUpdate.add('foo', None)
def test_expect_dupe_fail2(self): """ Update cannot expect a field to meet multiple constraints """ self.make_table() update = ItemUpdate.put('foo', 10, lt=5) with self.assertRaises(ValueError): self.dynamo.update_item('foobar', {'id': 'a'}, [update], foo__gt=1)
def test_expect_dupe_fail(self): """ Update cannot expect a field to meet multiple constraints """ self.make_table() with self.assertRaises(ValueError): update = ItemUpdate.put('foo', 10, lt=5, gt=1)
def sync(self, items, raise_on_conflict=None, consistent=False, constraints=None): """ Sync model changes back to database This will push any updates to the database, and ensure that all of the synced items have the most up-to-date data. Parameters ---------- items : list or :class:`~flywheel.models.Model` Models to sync raise_on_conflict : bool, optional If True, raise exception if any of the fields that are being updated were concurrently changed in the database (default set by :attr:`.default_conflict`) consistent : bool, optional If True, force a consistent read from the db. This will only take effect if the sync is only performing a read. (default False) constraints : list, optional List of more complex constraints that must pass for the update to complete. Must be used with raise_on_conflict=True. Format is the same as query filters (e.g. Model.fieldname > 5) Raises ------ exc : :class:`dynamo3.CheckFailed` If raise_on_conflict=True and the model changed underneath us """ if raise_on_conflict is None: raise_on_conflict = self.default_conflict in ('update', 'raise') if constraints is not None and not raise_on_conflict: raise ValueError("Cannot pass constraints to sync() when raise_on_conflict is False") if isinstance(items, Model): items = [items] refresh_models = [] for item in items: # Look for any mutable fields (e.g. sets) that have changed for name in item.keys_(): if name in item.__dirty__ or name in item.__incrs__: continue field = item.meta_.fields.get(name) if field is None: value = item.get_(name) if Field.is_overflow_mutable(value): if value != item.cached_(name): item.__dirty__.add(name) elif field.is_mutable: cached_var = item.cached_(name) if field.resolve(item) != cached_var: for related in item.meta_.related_fields[name]: item.__dirty__.add(related) if not item.__dirty__ and not item.__incrs__: refresh_models.append(item) continue fields = item.__dirty__ item.pre_save_(self) # If the model has changed any field that is part of a composite # field, FORCE the sync to raise on conflict. This prevents the # composite key from potentially getting into an inconsistent state _raise_on_conflict = raise_on_conflict for name in itertools.chain(item.__incrs__, fields): for related_name in item.meta_.related_fields.get(name, []): field = item.meta_.fields[related_name] if field.composite: _raise_on_conflict = True break keywords = {} constrained_fields = set() if _raise_on_conflict and constraints is not None: for constraint in constraints: constrained_fields.update(constraint.eq_fields.keys()) constrained_fields.update(constraint.fields.keys()) keywords.update(constraint.scan_kwargs()) updates = [] # Set dynamo keys for name in fields: field = item.meta_.fields.get(name) value = getattr(item, name) kwargs = {} if _raise_on_conflict and name not in constrained_fields: kwargs = {'eq': item.ddb_dump_cached_(name)} update = ItemUpdate.put(name, item.ddb_dump_field_(name), **kwargs) updates.append(update) # Atomic increment fields for name, value in six.iteritems(item.__incrs__): kwargs = {} # We don't need to ddb_dump because we know they're all native if isinstance(value, SetDelta): update = ItemUpdate(value.action, name, value.values) else: update = ItemUpdate.add(name, value) updates.append(update) # Perform sync ret = self.dynamo.update_item( item.meta_.ddb_tablename(self.namespace), item.pk_dict_, updates, returns=ALL_NEW, **keywords) # Load updated data back into object with item.loading_(self): for key, val in six.iteritems(ret): item.set_ddb_val_(key, val) item.post_save_() # Handle items that didn't have any fields to update # If the item isn't known to exist in the db, try to save it first for item in refresh_models: if not item.persisted_: try: self.save(item, overwrite=False) except CheckFailed: pass # Refresh item data self.refresh(refresh_models, consistent=consistent)
def test_item_update_eq(self): """ ItemUpdates should be equal """ a, b = ItemUpdate.put('foo', 'bar'), ItemUpdate.put('foo', 'bar') self.assertEqual(a, b) self.assertEqual(hash(a), hash(b)) self.assertFalse(a != b)