# Django model objects and querysets dehydration/hydration # Dehydrates objects that can be dictionaries, lists or tuples containing django # model objects or django querysets. For each of those, it creates a # smaller/dehydrated version of it for saving in cache or pickling. The reverse # operation is also provided so dehydrated objects can also be re-hydrated. # Example: # >>> import pickle # >>> users = list(User.objects.all()[:20]) # >>> print users # [, , , , , , ...] # >>> pickled_users = pickle.dumps(users) # >>> print len(pickled_users) # 17546 # >>> dehydrated_users = dehydrate(users) # >>> pickled_dehydrated_users = pickle.dumps(dehydrated_users) # >>> rehydrated_users = hydrate(pickle.loads(pickled_dehydrated_users)) # >>> print rehydrated_users # [, , , , , , ...] # >>> print len(pickled_dehydrated_users) # 1471 import sys from django.db import models from django.db.models.query import QuerySet from django.contrib.contenttypes.models import ContentType def _walk(obj, fnct, raise_exc=False, delayed=None, maxlevels=10, level=0, context=None): if context is None: context = {} if not obj: return obj if maxlevels and level >= maxlevels: return obj objid = id(obj) if objid in context: return obj typ = type(obj) if typ is str: return obj if issubclass(typ, dict): if objid in context: return obj context[objid] = True ret = {} for k, v in obj.items(): ret[k] = _walk(v, fnct, raise_exc, delayed, maxlevels, level + 1, context) del context[objid] return ret if issubclass(typ, list) or issubclass(typ, tuple) or issubclass(typ, set): context[objid] = True ret = [] for o in obj: ret.append(_walk(o, fnct, raise_exc, delayed, maxlevels, level + 1, context)) del context[objid] if issubclass(typ, tuple): return tuple(ret) if issubclass(typ, set): return set(ret) return ret return fnct(obj, raise_exc, delayed) class Dehydrated(object): def __init__(self, obj): self._obj = obj def __repr__(self): try: u = unicode(self) except (UnicodeEncodeError, UnicodeDecodeError): u = '[Bad Unicode data]' return str(u'<%s: %s>' % (self.__class__.__name__, u)) def __str__(self): if hasattr(self, '__unicode__'): return unicode(self).encode('utf-8') return '%s object' % (self.__class__.__name__,) def __getstate__(self): ret = self.__dict__.copy() ret.pop('_obj', None) return ret class DehydratedModel(Dehydrated): def __init__(self, obj): super(DehydratedModel, self).__init__(obj) self.app_label, self.object_name = obj._meta.app_label, obj._meta.object_name self.pk_attrname = obj._meta.pk.attname setattr(self, self.pk_attrname, getattr(obj, '_pk', obj.pk)) if self.pk: fields = [f.name for f in obj._meta.fields] self.data = dict((k, v) for k, v in obj.__dict__.items() if not k.startswith('_') and not k.endswith('_id') and k not in fields) else: self.data = dict((k, v) for k, v in obj.__dict__.items() if not k.startswith('_')) def __str__(self): if hasattr(self, '__unicode__'): return unicode(self).encode('utf-8') return '%s.%s.%s' % (self.app_label, self.object_name, self.pk) def __eq__(self, other): return isinstance(other, self.__class__) and self.pk == other.pk def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.pk) def _get_pk_val(self, meta=None): return getattr(self, self.pk_attrname) def _set_pk_val(self, value): return setattr(self, self.pk_attrname, value) pk = property(_get_pk_val, _set_pk_val) @staticmethod def delayed_hydrate(raise_exc, delayed): for Model, objects in delayed.items(): for _obj in Model._default_manager.filter(pk__in=objects.keys()): obj, dobjs = objects[_obj.pk] obj.__dict__.update(_obj.__dict__) for dobj in dobjs: dobj._obj = obj for pk, objs in objects.items(): if not objs[0].pk: msg = "%s matching query does not exist. Cannot hydrate model instance %s.%s.%s." % ( Model._meta.object_name, Model._meta.app_label, Model._meta.object_name, pk, ) if raise_exc: raise Model.DoesNotExist(msg) sys.stderr.write("%s\n" % msg) def hydrate(self, raise_exc=False, delayed=None): if hasattr(self, '_obj'): obj = self._obj else: content_type = ContentType.objects.get_by_natural_key(self.app_label, self.object_name.lower()) Model = content_type.model_class() if self.pk: if delayed is not None: delayed.setdefault(Model, {}) if self.pk in delayed[Model]: obj, dobjs = delayed[Model][self.pk] dobjs.append(self) else: obj = Model() obj._pk = self.pk delayed[Model][self.pk] = (obj, [self]) else: obj = Model._default_manager.get(pk=self.pk) self._obj = obj else: obj = Model() self._obj = obj if hasattr(self, 'data'): obj.__dict__.update(self.data) return obj class DehydratedQuerySet(Dehydrated): def __init__(self, qs): super(DehydratedQuerySet, self).__init__(qs) self.app_label, self.object_name = qs.model._meta.app_label, qs.model._meta.object_name self.query = qs.query @staticmethod def delayed_hydrate(raise_exc, delayed): pass def hydrate(self, raise_exc=False, delayed=None): if not hasattr(self, '_obj'): content_type = ContentType.objects.get_by_natural_key(self.app_label, self.object_name.lower()) Model = content_type.model_class() qs = Model._default_manager.all() qs.query = self.query self._obj = qs return self._obj def _dehydrate(obj, raise_exc, delayed): typ = type(obj) if issubclass(typ, models.Model): return DehydratedModel(obj) if issubclass(typ, QuerySet): return DehydratedQuerySet(obj) return obj def _hydrate(obj, raise_exc, delayed): typ = type(obj) if issubclass(typ, DehydratedModel): if delayed is not None: delayed.setdefault(DehydratedModel, {}) delayed = delayed[DehydratedModel] return obj.hydrate(raise_exc, delayed) if issubclass(typ, DehydratedQuerySet): if delayed is not None: delayed.setdefault(DehydratedQuerySet, {}) delayed = delayed[DehydratedQuerySet] return obj.hydrate(raise_exc, delayed) return obj def dehydrate(obj, raise_exc=False): """ Dehydrates objects containing django model objects and querysets. """ return _walk(obj, _dehydrate, raise_exc) def hydrate(obj, raise_exc=False): """ Hydrates objects containing dehydrated django model objects and querysets. """ delayed = {} ret = _walk(obj, _hydrate, raise_exc, delayed) for klass, _delayed in delayed.items(): klass.delayed_hydrate(raise_exc, _delayed) return ret