Like this project? it on
Source code for blitzdb.document
import copy
import uuid
import logging
logger = logging.getLogger(__name__)
import six
if six.PY3:
unicode = str
class MetaDocument(type):
"""
Here we inject class-dependent exceptions into the Document class.
"""
def __new__(meta, name, bases, dct):
class DoesNotExist(BaseException):
def __str__(self):
return "DoesNotExist(%s)" % name
class MultipleDocumentsReturned(BaseException):
def __str__(self):
return "MultipleDocumentsReturned(%s)" % name
dct['DoesNotExist'] = DoesNotExist
dct['MultipleDocumentsReturned'] = MultipleDocumentsReturned
class_type = type.__new__(meta, name, bases, dct)
if class_type in document_classes:
document_classes.remove(class_type)
if name == 'Document' and bases == (BaseDocument,):
pass
else:
document_classes.append(class_type)
return class_type
document_classes = []
class BaseDocument(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 `default_backend` has been
specified and that the `pk` attribute is set.
:param default_backend: the default 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.
"""
class Meta:
primary_key = "pk"
def __init__(self, attributes=None, lazy=False, default_backend=None, autoload=True):
"""
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 `default_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 default_backend: the default backend for use in the `save`, `delete` and `revert` functions.
"""
if not attributes:
attributes = {}
self.__dict__['_attributes'] = attributes
self.__dict__['embed'] = False
self.__dict__['_autoload'] = autoload
self._default_backend = default_backend
if self.pk is None:
self.pk = None
if not lazy:
self._lazy = False
self.initialize()
else:
self._lazy = True
def __getitem__(self,key):
try:
lazy = super(BaseDocument,self).__getattribute__('_lazy')
except AttributeError:
lazy = False
if lazy:
if key in self.lazy_attributes:
return self.lazy_attributes[key]
else:
self.revert()
self._lazy = False
return self.attributes[key]
def __getattribute__(self,key):
"""
Checks if the `_lazy` attribute of the document is set. If this is the case, the function
lazily loads the document from the database by calling `revert` and sets `_lazy = False'
after doing so.
"""
try:
lazy = super(BaseDocument, self).__getattribute__('_lazy')
except AttributeError:
lazy = False
if lazy:
if key == 'lazy_attributes':
return super(BaseDocument, self).__getattribute__('_attributes')
# If we demand the attributes, we load the object from the DB in any case.
if key in ('attributes',):
if self._autoload:
self.revert()
self._lazy = False
else:
return super(BaseDocument, self).__getattribute__('_attributes')
try:
return super(BaseDocument, self).__getattribute__(key)
except AttributeError:
pass
if key in self.lazy_attributes:
return self.lazy_attributes[key]
elif self._autoload:
self.revert()
self._lazy = False
return super(BaseDocument,self).__getattribute__(key)
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()
def __contains__(self, key):
return True if key in self.attributes else False
def __getattr__(self, key):
try:
super(BaseDocument, self).__getattr__(key)
except AttributeError:
try:
return self.attributes[key]
except KeyError:
raise AttributeError(key)
def __setattr__(self, key, value):
if key.startswith('_'):
return super(BaseDocument, self).__setattr__(key, value)
elif key == 'pk':
# this is ugly, should find a better solution for handling properties...
super(BaseDocument, self).__setattr__(key, value)
else:
self.attributes[key] = value
def __delattr__(self, key):
if key.startswith('_'):
return super(BaseDocument, 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, default_backend = self._default_backend)
return d
def __deepcopy__(self, memo):
d = self.__class__(copy.deepcopy(self.attributes, memo), lazy=self._lazy, default_backend=self._default_backend)
return d
def __hash__(self):
return id(self)
def __ne__(self, other):
return not self.__eq__(other)
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__ + "({'pk' : '%s'},lazy = %s)" % (str(self.pk), str(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 dict([(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
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
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.uuid1().hex`
to generate a (statistically) unique primary key for the object (`more about UUIDs <http://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.uuid1().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[self.Meta.primary_key]
return None
@property
def eager(self):
self.load_if_lazy()
return self
@pk.setter
def pk(self, value):
self._attributes[self.Meta.primary_key] = value
@property
def attributes(self):
"""
Returns a reference to the attributes of the document. The attributes are the *"unique source of truth"*
about the state of a document.
"""
return self._attributes
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._default_backend:
raise AttributeError("No default backend defined!")
return self._default_backend.save(self)
return backend.save(self)
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._default_backend:
raise AttributeError("No default backend defined!")
return self._default_backend.delete(self)
backend.delete(self)
def revert(self, backend=None):
"""
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.
.. 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.
"""
logger.debug("Reverting to database state (%s, %s)" % (self.__class__.__name__, self.pk))
backend = backend or self._default_backend
if not backend:
raise AttributeError("No backend given!")
if self.pk == None:
raise self.DoesNotExist("No primary key given!")
obj = self._default_backend.get(self.__class__, {'pk': self.pk})
self._attributes = obj.attributes
self.initialize()
def load_if_lazy(self):
try:
lazy = super(BaseDocument, self).__getattribute__('_lazy')
except AttributeError:
lazy = False
if lazy:
self._lazy = False
self.revert()
Document = MetaDocument('Document', (BaseDocument,), {})