Copie una entidad en el almacén de datos de Google App Engine en Python sin saber los nombres de las propiedades en el momento de la “comstackción”

En una aplicación de Google App Engine de Python que estoy escribiendo, tengo una entidad almacenada en el almacén de datos que necesito recuperar, hacer una copia exacta de la misma (con la excepción de la clave) y luego volver a ingresar esta entidad.

¿Cómo debería hacer esto? En particular, ¿hay alguna advertencia o truco que deba tener en cuenta al hacer esto para obtener una copia del tipo que espero y no otra cosa?

ETA: Bueno, lo probé y tuve problemas. Me gustaría hacer mi copia de tal manera que no tenga que saber los nombres de las propiedades cuando escribo el código. Mi pensamiento era hacer esto:

#theThing = a particular entity we pull from the datastore with model Thing copyThing = Thing(user = user) for thingProperty in theThing.properties(): copyThing.__setattr__(thingProperty[0], thingProperty[1]) 

Esto se ejecuta sin errores … hasta que bash extraer copyThing del almacén de datos, momento en el que descubro que todas las propiedades están configuradas en Ninguna (con la excepción del usuario y la clave, obviamente). Claramente, este código está haciendo algo, ya que reemplaza los valores predeterminados por Ninguno (todas las propiedades tienen un conjunto de valores predeterminado), pero no lo que quiero. Sugerencias?

Aqui tienes:

 def clone_entity(e, **extra_args): """Clones an entity, adding or overriding constructor attributes. The cloned entity will have exactly the same property values as the original entity, except where overridden. By default it will have no parent entity or key name, unless supplied. Args: e: The entity to clone extra_args: Keyword arguments to override from the cloned entity and pass to the constructor. Returns: A cloned, possibly modified, copy of entity e. """ klass = e.__class__ props = dict((k, v.__get__(e, klass)) for k, v in klass.properties().iteritems()) props.update(extra_args) return klass(**props) 

Ejemplo de uso:

 b = clone_entity(a) c = clone_entity(a, key_name='foo') d = clone_entity(a, parent=a.key().parent()) 

EDITAR: Cambios si se utiliza NDB

Combinando el comentario de Gus a continuación con una solución para las propiedades que especifican un nombre de almacén de datos diferente, el siguiente código funciona para NDB:

 def clone_entity(e, **extra_args): klass = e.__class__ props = dict((v._code_name, v.__get__(e, klass)) for v in klass._properties.itervalues() if type(v) is not ndb.ComputedProperty) props.update(extra_args) return klass(**props) 

Ejemplo de uso (nota key_name convierte en id en NDB):

 b = clone_entity(a, id='new_id_here') 

Nota al _code_name : vea el uso de _code_name para obtener el nombre de propiedad _code_name con Python. Sin esto, una propiedad como name = ndb.StringProperty('n') causaría que el constructor del modelo name = ndb.StringProperty('n') un AttributeError: type object 'foo' has no attribute 'n' .

Si está utilizando el NDB, simplemente puede copiar con: new_entity.populate(**old_entity.to_dict())

Esta es solo una extensión del excelente código de Nick Johnson para abordar los problemas destacados por Amir en los comentarios:

  1. El valor db.Key de ReferenceProperty ya no se recupera mediante un viaje de ida y vuelta innecesario al almacén de datos.
  2. Ahora puede especificar si desea omitir las propiedades de DateTime con el auto_now y / o auto_now_add .

Aquí está el código actualizado:

 def clone_entity(e, skip_auto_now=False, skip_auto_now_add=False, **extra_args): """Clones an entity, adding or overriding constructor attributes. The cloned entity will have exactly the same property values as the original entity, except where overridden. By default it will have no parent entity or key name, unless supplied. Args: e: The entity to clone skip_auto_now: If True then all DateTimeProperty propertes will be skipped which have the 'auto_now' flag set to True skip_auto_now_add: If True then all DateTimeProperty propertes will be skipped which have the 'auto_now_add' flag set to True extra_args: Keyword arguments to override from the cloned entity and pass to the constructor. Returns: A cloned, possibly modified, copy of entity e. """ klass = e.__class__ props = {} for k, v in klass.properties().iteritems(): if not (type(v) == db.DateTimeProperty and ((skip_auto_now and getattr(v, 'auto_now')) or (skip_auto_now_add and getattr(v, 'auto_now_add')))): if type(v) == db.ReferenceProperty: value = getattr(klass, k).get_value_for_datastore(e) else: value = v.__get__(e, klass) props[k] = value props.update(extra_args) return klass(**props) 

La primera es if expresión no es muy elegante, así que aprecio si puedes compartir una mejor manera de escribirla.

No soy Python ni el gurú de AppEngine, pero ¿no se puede obtener / establecer dinámicamente las propiedades?

 props = {} for p in Thing.properties(): props[p] = getattr(old_thing, p) new_thing = Thing(**props).put() 

Una variación inspirada en la respuesta de Nick que maneja el caso en el que su entidad tiene una Propiedad estructurada (repetida), donde la Propiedad estructurada en sí misma tiene propiedades de propiedad de cálculo. Probablemente se pueda escribir más tersamente con la comprensión de dict de alguna manera, pero aquí está la versión más larga que funcionó para mí:

 def removeComputedProps(klass,oldDicc): dicc = {} for key,propertType in klass._properties.iteritems(): if type(propertType) is ndb.StructuredProperty: purged = [] for item in oldDicc[key]: purged.append(removeComputedProps(propertType._modelclass,item)) dicc[key]=purged else: if type(propertType) is not ndb.ComputedProperty: dicc[key] = oldDicc[key] return dicc def cloneEntity(entity): oldDicc = entity.to_dict() klass = entity.__class__ dicc = removeComputedProps(klass,oldDicc) return klass(**dicc) 

Esto puede ser complicado si ha cambiado el nombre de las claves subyacentes para sus propiedades … lo que algunas personas optan por hacer en lugar de realizar cambios masivos de datos

Di que empezaste con esto:

 class Person(ndb.Model): fname = ndb.StringProperty() lname = ndb.StringProperty() 

Entonces, un día realmente decidiste que sería mejor usar primero_nombre y último nombre en su lugar … así que haces esto:

 class Person(ndb.Model): first_name = ndb.StringProperty(name="fname") last_name = ndb.StringProperty(name="lname") 

ahora, cuando haga Person._properties (o .properties () o person_instance._properties) obtendrá un diccionario con claves que coinciden con los nombres subyacentes (fname y lname) … pero no coincidirán con los nombres de propiedad reales en la clase … así que no funcionará si los pones en el constructor de una nueva instancia, o si usas el método .populate () (los ejemplos anteriores se romperán)

De todos modos, en NDB, las instancias de los modelos tienen un diccionario de valores .codificado por los nombres de las propiedades subyacentes … y puede actualizarlo directamente. Terminé con algo como esto:

  def clone(entity, **extra_args): klass = entity.__class__ clone = klass(**extra_args) original_values = dict((k,v) for k,v in entity._values.iteritems() if k not in clone._values) clone._values.update(original_values) return clone 

Esta no es realmente la forma más segura … ya que hay otros métodos de ayuda privada que hacen más trabajo (como la validación y conversión de propiedades computadas usando _store_value () y _retrieve_value () ) … pero si son modelos Bastante simple, y te gusta vivir al límite 🙂

Aquí está el código proporcionado por @zengabor con la expresión if formateada para facilitar la lectura. Puede que no sea compatible con PEP-8:

 klass = e.__class__ props = {} for k, v in klass.properties().iteritems(): if not (type(v) == db.DateTimeProperty and (( skip_auto_now and getattr(v, 'auto_now' )) or ( skip_auto_now_add and getattr(v, 'auto_now_add')))): if type(v) == db.ReferenceProperty: value = getattr(klass, k).get_value_for_datastore(e) else: value = v.__get__(e, klass) props[k] = value props.update(extra_args) return klass(**props)