Like this project? it on

Source code for blitzdb.document

import copy
import logging
import uuid

import six

from blitzdb.fields import CharField
from blitzdb.fields.base import BaseField

logger = logging.getLogger(__name__)


if six.PY3:
    unicode = str

class DoesNotExist(BaseException):

    def __str__(self):
        message = BaseException.__str__(self)
        return u"DoesNotExist({}): {}".format(self.__class__.__name__, message)

class MultipleDocumentsReturned(BaseException):

    def __str__(self):
        message = BaseException.__str__(self)
        return u"MultipleDocumentsReturned({}): {}".format(self.__class__.__name__, message)

class MetaDocument(type):

    """
    Here we inject class-dependent exceptions into the Document class.
    """

    def __new__(meta, name, bases, dct):
        sanitized_dct = {}
        sanitized_dct['fields'] = {}

        class_type = type.__new__(meta, name, bases, dct)

        fields = {}

        #we inherit the fields from the base type(s)
        if hasattr(class_type,'fields'):
            fields.update(class_type.fields)

        for key,value in dct.items():
            if isinstance(value,BaseField):
                if value.key:
                    field_key = value.key
                else:
                    field_key = key
                fields[field_key] = value
                delattr(class_type,key)

        class_type.fields = fields

        global DoesNotExist,MultipleDocumentsReturned

        class DoesNotExist(DoesNotExist):
            pass

        class MultipleDocumentsReturned(MultipleDocumentsReturned):
            pass

        class_type.DoesNotExist = DoesNotExist
        class_type.MultipleDocumentsReturned = MultipleDocumentsReturned

        if class_type in document_classes:
            document_classes.remove(class_type)
        if name == 'Document' and bases == (object,):
            pass
        elif not (hasattr(class_type.Meta,'autoregister') and class_type.Meta.autoregister == False):
            document_classes.append(class_type)

        return class_type

document_classes = []

[docs]@six.add_metaclass(MetaDocument) class Document(object): """ The Document object is the base class for all documents stored in the database. The name of the collection can be set by defining a :class:`Document.Meta` class within the class and setting its `collection` attribute. :param attributes: the attributes of the document instance. Expects a Python dictionary. :param lazy: if set to `True`, will lazily load the document from the backend when an attribute is requested. This requires that `backend` has been specified and that the `pk` attribute is set. :param backend: the backend to be used for saving and loading the document. **The `Meta` attribute** You can use the `Meta` attribute of the class to specify the primary_key (defaults to `pk`) or change the collection name for a given class. example:: class Actor(Document): class Meta(Document.Meta): pk = 'name' collection = 'hollywood_actors' **Accessing Document Attributes** Document attributes are accessible as class attributes: .. code-block:: python marlon = Actor({'name' : 'Marlon Brando', 'birth_year' : 1924}) print("%s was born in %d" % (marlon.name,marlon.birth_year)) In case one of your attributes shadows a class attribute or function, you can still access it using the `attributes` attribute. example:: fail = Document({'delete': False,'save' : True}) print(fail.delete) #will print <bound method Document.save ...> print(fail.attributes['delete']) #will print 'False' **Defining "non-database" attributes** Attributes that begin with an underscore (_) will not be stored in the :py:meth:`attributes` dictionary but as normal instance attributes of the document. This is useful if you need to define e.g. some helper variables that you don't want to store in the database. """ abstract = True class Meta: PkType = CharField(length = 32,primary_key = True,indexed = True,nullable = False) primary_key = "pk" indexes = {} def __init__(self, attributes=None, lazy=False, backend=None, autoload=True, db_loader = None): """ Initializes a document instance with the given attributes. If `lazy = True`, a *lazy* document will be created, which means that the attributes of the document will be loaded from the database only if they are requested. Lazy loading requires that the `backend` variable is set. :param attributes: the attributes of the document instance. :param lazy: specifies if the document is *lazy*, i.e. if it should be loaded on demand when its attributes get accessed for the first time. :param autoload: if True, will automatically fetch the document from the database if it is lazy and the user tries to access an attribute that does not yet exist. :param backend: the backend for use in the `save`, `delete` and `revert` functions. """ if not attributes: attributes = {} self._attributes = attributes self._autoload = autoload self._backend = backend self._properties = {} self._db_loader = db_loader if not lazy: self._lazy = False else: self._lazy = True self._embed = False self.initialize() def __getitem__(self,key): try: lazy = super(Document,self).__getattribute__('_lazy') except AttributeError: lazy = False if lazy: if key in self.lazy_attributes: return self.lazy_attributes[key] self.revert(implicit=True) return self.attributes[key] @property def lazy(self): return self._lazy @lazy.setter def lazy(self,lazy): self._lazy = lazy @property def lazy_attributes(self): return self._attributes @property def attributes(self): if self._lazy: self.revert(implicit=True) return self._attributes @attributes.setter def attributes(self,value): self._attributes = value def get(self,key,default = None): return self[key] if key in self else default def has_key(self,key): return True if key in self else False def keys(self): return self.attributes.keys() def clear(self): self.attributes.clear() def values(self): return self.attributes.values() def items(self): return self.attributes.items() @property def properties(self): return self._properties @properties.setter def properties(self,value): self._properties = value def __contains__(self, key): return True if (key in self.lazy_attributes or key in self.attributes) else False def __iter__(self): for key in self.keys(): yield key def get_lazy_attribute(self,key): #we make sure not to revert the document... return object.__getattribute__(self,key) def __getattr__(self, key,load_if_lazy = True): try: return super(Document,self).__getattr__(key) except AttributeError: pass try: if key in self._properties: return self._properties[key] if key in self._attributes: return self._attributes[key] if self._lazy: self.revert(implicit=True) return self._attributes[key] except KeyError: raise AttributeError(key) def __setattr__(self, key, value): if key.startswith('_') or key in ('attributes','pk','lazy','backend'): return super(Document, self).__setattr__(key, value) else: self.attributes[key] = value def __delattr__(self, key): if key.startswith('_'): return super(Document, self).__delattr__(key) try: del self.attributes[key] except KeyError: raise AttributeError(key) __setitem__ = __setattr__ def __delitem__(self, key): try: return self.__delattr__(key) except AttributeError: raise KeyError(key) def __copy__(self): d = self.__class__(self.attributes.copy(), lazy=self._lazy, backend = self._backend) return d def __deepcopy__(self, memo): d = self.__class__(copy.deepcopy(self.attributes, memo), lazy=self._lazy, backend =self._backend) return d def __hash__(self): return id(self) def __ne__(self, other): return not self.__eq__(other) def __nonzero__(self): if self.pk: return True return False
[docs] def __eq__(self, other): """ Compares the document instance to another object. The comparison rules are as follows: * If the Python `id` of the objects are identical, return `True` * If the types of the objects differ, return `False` * If the types match and the primary keys are identical, return `True` * If the types and attributes of the objects match, return `True` * Otherwise, return `False` """ if id(self) == id(other): return True if type(self) != type(other): return False if self.pk != None or other.pk != None: if self.pk == other.pk: return True if self.attributes == other.attributes: return True return False
def __unicode__(self): return self.__class__.__name__ + "({{{0} : '{1}'}},lazy = {2})".format(self.get_pk_name(), self.pk, self._lazy) if six.PY3: __str__ = __unicode__ else: def __str__(self): return unicode(self).encode("utf-8") def _represent(self, n=1): if n < 0: return self.__class__.__name__ + "({...})" def truncate_dict(d, n=n): if isinstance(d, dict): out = {} return {key: truncate_dict(value, n - 1) for key, value in d.items()} elif isinstance(d, list) or isinstance(d, set): return [truncate_dict(v, n - 1) for v in d] elif isinstance(d, Document): return d._represent(n - 1) else: return d return self.__class__.__name__ + "(" + str(truncate_dict(self._attributes)) + ")" __repr__ = _represent
[docs] def initialize(self): """ Gets called when **after** the object attributes get loaded from the database. Redefine it in your document class to perform object initialization tasks. .. admonition:: Keep in Mind The function also get called after invoking the `revert` function, which resets the object attributes to those in the database, so do not assume that the function will get called only once during the lifetime of the object. Likewise, you should **not** perform any initialization in the `__init__` function to initialize your object, since this can possibly break lazy loading and `revert` operations. """ pass
[docs] def autogenerate_pk(self): """ Autogenerates a primary key for this document. This function gets called by the backend if you save a document without a primary key field. By default, it uses `uuid.uuid4().hex` to generate a (statistically) unique primary key for the object (`more about UUIDs <https://docs.python.org/2/library/uuid.html>`_). If you want to define your own primary key generation mechanism, just redefine this function in your document class. """ self.pk = uuid.uuid4().hex
@classmethod def get_pk_name(cls): return cls.Meta.primary_key if hasattr(cls.Meta, 'primary_key') else Document.Meta.primary_key @property def pk(self): """ Returns (or sets) the primary key of the document, which is stored in the `attributes` dict along with all other attributes. The name of the primary key defaults to `pk` and can be redefine in the `Meta` class. This function provides a standardized way to retrieve and set the primary key of a document and is used by the backend and a few other classes. If possible, always use this function to access the primary key of a document. .. admonition:: Automatic primary key generation If you save a document to the database that has an empty primary key field, Blitz will create a default primary-key by calling the `autogenerate_pk` function of the document. To generate your own primary keys, just redefine this function in your derived document class. """ primary_key = self.get_pk_name() if primary_key in self._attributes: return self._attributes[primary_key] #if there is no pk value but a _db_loader, we load the object lazily to retrieve the pk if self._lazy and self._db_loader: self.revert(implicit=True) if primary_key in self._attributes: return self._attributes[primary_key] return None @property def embed(self): return self._embed @property def eager(self): return self.load_if_lazy() @pk.setter def pk(self, value): self._attributes[self.get_pk_name()] = value @property def backend(self): return self._backend @backend.setter def backend(self,backend): self._backend = backend
[docs] def save(self, backend=None): """ Saves a document to the database. If the `backend` argument is not specified, the function resorts to the *default backend* as defined during object instantiation. If no such backend is defined, an `AttributeError` exception will be thrown. :param backend: the backend in which to store the document. """ if not backend: if not self._backend: raise AttributeError("No default backend defined!") return self._backend.save(self) self._backend = backend return backend.save(self)
[docs] def delete(self, backend=None): """ Deletes a document from the database. If the `backend` argument is not specified, the function resorts to the *default backend* as defined during object instantiation. If no such backend is defined, an `AttributeError` exception will be thrown. :param backend: the backend from which to delete the document. """ if not backend: if not self._backend: raise AttributeError("No default backend defined!") return self._backend.delete(self) backend.delete(self)
[docs] def revert(self, backend=None, implicit=False): """ Reverts the state of the document to that contained in the database. If the `backend` argument is not specified, the function resorts to the *default backend* as defined during object instantiation. If no such backend is defined, an `AttributeError` exception will be thrown. :param backend: the backend from which to delete the document. :param implicit: whether the loading was triggered implicitly (e.g. by accessing attributes) .. admonition:: Keep in Mind This function will call the `initialize` function after loading the object, which allows you to perform document-specific initialization tasks if needed. """ if implicit and not self._autoload: logger.debug("Autoloading is disabled, not reverting the document implicitly...") return self._lazy = False logger.debug("Reverting to database state (%s, %s)" % (self.__class__.__name__, str(self.pk))) if self._db_loader: obj = self._db_loader() else: backend = backend or self._backend if not backend: raise AttributeError("No backend given!") if self.pk is None: return obj = backend.get(self.__class__, {self.get_pk_name(): self.pk}) self._attributes = obj.attributes self.initialize()
def load_if_lazy(self, implicit=False): if self._lazy: self.revert(implicit=implicit) return self