2

J'essaie de simplifier la sérialisation JSON et la dé-sérialisation de KeyProperties ndb en utilisant un validateur personnalisé qui convertit une chaîne en Key du type respectif.BadValueError lors du stockage de KeyProperty répété avec validateur personnalisé

L'idée est d'avoir une propriété comme celui-ci:

def key_validator(kind): 
    def validator(prop, value): 
    if not isinstance(value, ndb.Key): 
     return ndb.Key(kind, value) 
    return value 
    return validator 

class Bar(ndb.Model): 

    foo = ndb.KeyProperty('Foo', validator=key_validator('Foo')) 

Comme vous pouvez le voir le validateur convertit une chaîne en Key du genre donné. L'objectif est d'être en mesure de passer un objet JSON qui contient l'identifiant d'une clé à la méthode populate comme ceci:

bar = Bar() 
bar.populate(json.loads('{"foo": "1234"}')) 

Ce qui devrait effectivement faire:

bar = Bar() 
bar.foo = ndb.Key("Foo", "1234") 

Le problème est que ceci nécessite de surcharger KeyProperty parce que le validateur est appelé après une validation de base qui échoue, car "1234" n'est apparemment pas un Key, voir issue 268.

Donc, pour faire ce travail, je l'ai créé un « ValidationMixin » et une nouvelle KeyProperty qui appelle le validateur avant toute autre validation a lieu (et sérialise aussi Key juste l'id).

class ValidationMixin(object): 
    # make sure to call _validator before we do as the very first validation step 
    def _do_validate(self, value): 
    if self._validator is not None: 
     newvalue = self._validator(self, value) 
     if newvalue is not None: 
     value = newvalue 
    return super(ValidationMixin, self)._do_validate(value) 

# A KeyProperty that allows a validator to generate a Key. 
# In addition it serializes to just the id of the key 
class KeyProperty(ValidationMixin, ndb.KeyProperty): 
    # return just the id of the key 
    def _get_for_dict(self, entity): 
    value = self._get_value(entity) 
    if self._repeated: 
     return [v.id() for v in value] 
    elif value is not None: 
     return value.id() 
    return value 

L'utilisation de ce KeyProperty fonctionne comme un charme pour les propriétés non répétées. Malheureusement, il échoue mal avec les propriétés qui ont repeated=True.

L'exception suivante est levée lorsque j'appelle bar.populate(json.loads('[{"foo": "1234"}]')) suivie put():

File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 3451, in _put 
    return self._put_async(**ctx_options).get_result() 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/tasklets.py", line 383, in get_result 
    self.check_success() 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/tasklets.py", line 427, in _help_tasklet_along 
    value = gen.throw(exc.__class__, exc, tb) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/context.py", line 824, in put 
    key = yield self._put_batcher.add(entity, options) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/tasklets.py", line 430, in _help_tasklet_along 
    value = gen.send(val) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/context.py", line 358, in _put_tasklet 
    keys = yield self._conn.async_put(options, datastore_entities) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/datastore/datastore_rpc.py", line 1852, in async_put 
    pbs = [entity_to_pb(entity) for entity in entities] 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 697, in entity_to_pb 
    pb = ent._to_pb() 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 3167, in _to_pb 
    prop._serialize(self, pb, projection=self._projection) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1422, in _serialize 
    values = self._get_base_value_unwrapped_as_list(entity) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1192, in _get_base_value_unwrapped_as_list 
    wrapped = self._get_base_value(entity) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1180, in _get_base_value 
    return self._apply_to_values(entity, self._opt_call_to_base_type) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1355, in _apply_to_values 
    newvalue = function(value) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1234, in _opt_call_to_base_type 
    value = _BaseValue(self._call_to_base_type(value)) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1255, in _call_to_base_type 
    return call(value) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 1331, in call 
    newvalue = method(self, value) 
    File "/base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/ext/ndb/model.py", line 2013, in _validate 
    raise datastore_errors.BadValueError('Expected Key, got %r' % (value,)) 
BadValueError: Expected Key, got [Key('Foo', '486944fe896a44c689275e6f19e3084a')] 

Comme vous pouvez le voir se plaint de la valeur étant une liste au lieu d'un seul Key. Notez que l'exception est levée dans put() pas dans populate, donc la validation initiale effectuée par _set_value a réussi. Donc, ma question est la suivante: mon approche est-elle brisée ou cela devrait-il fonctionner? Si cela devrait fonctionner, pourquoi cela ne fonctionne-t-il pas et comment peut-il être réparé?

mise à jour

Selon la pile trace l'exécution de code passe model.py, line 1355, ce qui est étrange, parce que la propriété est répétée et si l'autre branche dans model.py, line 1347

mise à jour 2

Je viens de découvrir cela fonctionne quand je retire l'anthère non répétée KeyProperty du modèle. Il semble que la sérialisation est rompue et la mauvaise instance KeyProperty est transmise à la méthode _seralize

Répondre

1

Ok, vous l'avez trouvé. KeyProperty a ce constructeur vraiment bizarre "signature magique" (model.py, line 1963).

Le fait est que si le premier paramètre est une chaîne, il devient le nom de la propriété, pas le type!Si vous voulez spécifier le type par chaîne, vous devez utiliser un argument mot-clé, sinon le paramètre kind doit être le type réel et pas seulement le nom. Corrigez-moi si je me trompe, mais cela ne fait pas partie de la documentation publique. C'est vraiment déroutant, car avec un ndb.Key vous pouvez réellement spécifier le type comme une chaîne comme le premier paramètre positionnel.

En fait, j'avais 3 KeyProperties avec le même type, mais des noms d'attributs différents. Cependant, puisque j'ai spécifié le genre comme une chaîne, c'est devenu le nom. Donc, les trois propriétés utilisaient le même nom. Par conséquent, la valeur de propriété répétée a été sérialisée avec l'instance non répétée KeyProperty, provoquant ce blocage.

La solution était de préciser le genre avec un argument mot-clé:

foo = ndb.KeyProperty(kind='Foo', validator=key_validator('Foo')) 

sérialisation les KeyProperties de/vers JSON fonctionne bien maintenant.