def validate(cls, data): """Validates data against the model. Args: data (dict): Data to validate, may be None. Returns: dict: Validated data or None if `data` is None. Raises: ModelError: The given data doesn't fit the model. """ if data is None: return None for key in data: if key not in cls.attributes: raise ModelError(cls, "no attribute named '%s'" % key) validated = {} for attr, props in cls.attributes.iteritems(): # Check required fields and defaults try: validated[attr] = data[attr] except KeyError: if 'required' in props: if props['required']: raise ModelError( cls, "'%s' is required but was not defined" % attr) elif 'default' in props: validated[attr] = props['default'] # Check collections if 'collection' in props: value = data.get(attr, []) if not value: value = [] elif not isinstance(value, list): raise ModelError( cls, "Value supplied for '%s' is not a list: %r" % (attr, value)) else: for eid in value: try: int(eid) except ValueError: raise ModelError( cls, "Invalid non-integer ID '%s' in '%s'" % (eid, attr)) validated[attr] = value # Check model associations elif 'model' in props: value = data.get(attr, None) if value is not None: try: if int(value) != value: raise ValueError except ValueError: raise ModelError( cls, "Invalid non-integer ID '%s' in '%s'" % (value, attr)) validated[attr] = value return validated
def _construct_relationships(cls): primary_key = None for attr, props in cls.attributes.iteritems(): model_attr_name = cls.name + "." + attr if 'collection' in props and 'via' not in props: raise ModelError(cls, "%s: collection does not define 'via'" % model_attr_name) if 'via' in props and not ('collection' in props or 'model' in props): raise ModelError(cls, "%s: defines 'via' property but not 'model' or 'collection'" % model_attr_name) if not isinstance(props.get('unique', False), bool): raise ModelError(cls, "%s: invalid value for 'unique'" % model_attr_name) if not isinstance(props.get('description', ''), basestring): raise ModelError(cls, "%s: invalid value for 'description'" % model_attr_name) if props.get('primary_key', False): if primary_key is not None: raise ModelError(cls, "%s: primary key previously specified as %s" % (model_attr_name, primary_key)) primary_key = attr via = props.get('via', None) foreign_cls = props.get('model', props.get('collection', None)) if not foreign_cls: continue try: if not issubclass(foreign_cls, Model): raise TypeError except TypeError: raise ModelError(cls, "%s: Invalid foreign model controller: %r" % (model_attr_name, foreign_cls)) forward = (foreign_cls, via) reverse = (cls, attr) if not via: foreign_cls.references.add(reverse) else: foreign_cls.associations[via] = reverse foreign_model_attr_name = foreign_cls.name + "." + via try: via_props = foreign_cls.attributes[via] except KeyError: raise ModelError(cls, "%s: 'via' references undefined attribute '%s'" % (model_attr_name, foreign_model_attr_name)) via_attr_model = via_props.get('model', via_props.get('collection', None)) if not via_attr_model: raise ModelError(cls, "%s: 'via' on non-model attribute '%s'" % (model_attr_name, foreign_model_attr_name)) if via_attr_model is not cls: raise ModelError(cls, "Attribute '%s' referenced by 'via' in '%s' " "does not define 'collection' or 'model' of type '%s'" % (foreign_model_attr_name, attr, cls.name)) try: existing = cls.associations[attr] except KeyError: cls.associations[attr] = forward else: if existing != forward: raise ModelError(cls, "%s: conflicting associations: '%s' vs. '%s'" % (model_attr_name, existing, forward))
def _associate(self, record, foreign_model, affected, via): """Associates a record with another record. Args: record (Record): Record to associate. foreign_model (Model): Foreign record's data model. affected (list): Identifiers for the records that will be updated to associate with `record`. via (str): The name of the associated foreign attribute. """ _heavy_debug("Adding %s to '%s' in %s(eids=%s)", record.eid, via, foreign_model.name, affected) if not isinstance(affected, list): affected = [affected] with self.storage as database: for key in affected: foreign_record = database.get(key, table_name=foreign_model.name) if not foreign_record: raise ModelError(foreign_model, "No record with ID '%s'" % key) if 'model' in foreign_model.attributes[via]: updated = record.eid elif 'collection' in foreign_model.attributes[via]: updated = list(set(foreign_record[via] + [record.eid])) else: raise InternalError( "%s.%s has neither 'model' nor 'collection'" % (foreign_model.name, via)) foreign_model.controller(database).update({via: updated}, key)
def update(self, data, keys): """Change recorded data and update associations. The behavior depends on the type of `keys`: * Record.ElementIdentifier: update the record with that element identifier. * dict: update all records with attributes matching `keys`. * list or tuple: apply update to all records matching the elements of `keys`. * ``bool(keys) == False``: raise ValueError. Invokes the `on_update` callback **after** the data is modified. If this callback raises an exception then the operation is reverted. Args: data (dict): New data for existing records. keys: Fields or element identifiers to match. """ for attr in data: if attr not in self.model.attributes: raise ModelError(self.model, "no attribute named '%s'" % attr) with self.storage as database: # Get the list of affected records **before** updating the data so foreign keys are correct old_records = self.search(keys) database.update(data, keys, table_name=self.model.name) changes = {} for model in old_records: changes[model.eid] = { attr: (model.get(attr), new_value) for attr, new_value in data.iteritems() if not (attr in model and model.get(attr) == new_value) } for attr, foreign in self.model.associations.iteritems(): try: # 'collection' attribute is iterable new_foreign_keys = set(data[attr]) except TypeError: # 'model' attribute is not iterable, so make a tuple new_foreign_keys = set((data[attr], )) except KeyError: continue try: # 'collection' attribute is iterable old_foreign_keys = set(model[attr]) except TypeError: # 'model' attribute is not iterable, so make a tuple old_foreign_keys = set((model[attr], )) except KeyError: old_foreign_keys = set() foreign_cls, via = foreign added = list(new_foreign_keys - old_foreign_keys) deled = list(old_foreign_keys - new_foreign_keys) if added: self._associate(model, foreign_cls, added, via) if deled: self._disassociate(model, foreign_cls, deled, via) updated_records = self.search(keys) for model in updated_records: model.check_compatibility(model) model.on_update(changes[model.eid])
def key_attribute(cls): # pylint: disable=attribute-defined-outside-init try: return cls._key_attribute except AttributeError: for attr, props in cls.attributes.iteritems(): if 'primary_key' in props: cls._key_attribute = attr break else: raise ModelError(cls, "No attribute has the 'primary_key' property set to 'True'") return cls._key_attribute
def _populate_attribute(self, model, attr, defaults): try: props = model.attributes[attr] except KeyError: raise ModelError(model, "no attribute '%s'" % attr) if not defaults or 'default' not in props: value = model[attr] else: value = model.get(attr, props['default']) try: foreign = props['model'] except KeyError: try: foreign = props['collection'] except KeyError: return value else: return foreign.controller(self.storage).search(value) else: return foreign.controller(self.storage).one(value)
def unset(self, fields, keys): """Unset recorded data fields and update associations. The behavior depends on the type of `keys`: * Record.ElementIdentifier: update the record with that element identifier. * dict: update all records with attributes matching `keys`. * list or tuple: apply update to all records matching the elements of `keys`. * ``bool(keys) == False``: raise ValueError. Invokes the `on_update` callback **after** the data is modified. If this callback raises an exception then the operation is reverted. Args: fields (list): Names of fields to unset. keys: Fields or element identifiers to match. """ for attr in fields: if attr not in self.model.attributes: raise ModelError(self.model, "no attribute named '%s'" % attr) with self.storage as database: # Get the list of affected records **before** updating the data so foreign keys are correct old_records = self.search(keys) database.unset(fields, keys, table_name=self.model.name) changes = {} for model in old_records: changes[model.eid] = { attr: (model.get(attr), None) for attr in fields if attr in model } for attr, foreign in self.model.associations.iteritems(): if attr in fields: foreign_cls, via = foreign old_foreign_keys = model.get(attr, None) if old_foreign_keys: self._disassociate(model, foreign_cls, old_foreign_keys, via) updated_records = self.search(keys) for model in updated_records: model.check_compatibility(model) model.on_update(changes[model.eid])