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 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)