Source code for mongomotor.document

# -*- coding: utf-8 -*-

# Copyright 2016 Juca Crispim <juca@poraodojuca.net>

# This file is part of mongomotor.

# mongomotor is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# mongomotor is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with mongomotor. If not, see <http://www.gnu.org/licenses/>.

import inspect
from mongoengine import (Document as DocumentBase,
                         DynamicDocument as DynamicDocumentBase)
from mongoengine.document import (
    EmbeddedDocument as EmbeddedDocumentBase,
    DynamicEmbeddedDocument as DynamicEmbeddedDocumentBase
)
from mongoengine import signals
from mongoengine.common import _import_class
from mongoengine.errors import InvalidDocumentError, InvalidQueryError
from mongomotor.fields import ReferenceField, ComplexBaseField
from mongoengine.queryset import OperationError
from mongomotor.metaprogramming import (
    AsyncTopLevelDocumentMetaclass,
    AsyncDocumentMetaclass,
    Async,
    Sync,
    get_future
)
from mongomotor.queryset import QuerySet
import pymongo


[docs]class NoDerefInitMixin: """A mixin used to Documents and EmbeddedDocuments not to dereference reference fields on __init__. """ def __init__(self, *args, **kwargs): # The thing here is that if we try to dereference # references now we end with a future as the attribute so # we don't dereference here. fields = [] for name, field in self._fields.items(): if isinstance(field, ReferenceField) or ( isinstance(field, ComplexBaseField) and isinstance(field.field, ReferenceField)): fields.append((field, field._auto_dereference)) field._auto_dereference = False super().__init__(*args, **kwargs) # and here we back things to normal for field, deref in fields: field._auto_dereference = deref
[docs]class Document(NoDerefInitMixin, DocumentBase, metaclass=AsyncTopLevelDocumentMetaclass): """The base class used for defining the structure and properties of collections of documents stored in MongoDB. Inherit from this class, and add fields as class attributes to define a document's structure. Individual documents may then be created by making instances of the :class:`~mongomotor.Document` subclass. By default, the MongoDB collection used to store documents created using a :class:`~mongomotor.Document` subclass will be the name of the subclass converted to lowercase. A different collection may be specified by providing :attr:`collection` to the :attr:`meta` dictionary in the class definition. A :class:`~mongomotor.Document` subclass may be itself subclassed, to create a specialised version of the document that will be stored in the same collection. To facilitate this behaviour a `_cls` field is added to documents (hidden though the MongoEngine interface). To disable this behaviour and remove the dependence on the presence of `_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta` dictionary. A :class:`~mongomotor.Document` may use a **Capped Collection** by specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta` dictionary. :attr:`max_documents` is the maximum number of documents that is allowed to be stored in the collection, and :attr:`max_size` is the maximum size of the collection in bytes. :attr:`max_size` is rounded up to the next multiple of 256 by MongoDB internally and mongoengine before. Use also a multiple of 256 to avoid confusions. If :attr:`max_size` is not specified and :attr:`max_documents` is, :attr:`max_size` defaults to 10485760 bytes (10MB). Indexes may be created by specifying :attr:`indexes` in the :attr:`meta` dictionary. The value should be a list of field names or tuples of field names. Index direction may be specified by prefixing the field names with a **+** or **-** sign. Automatic index creation can be enabled by specifying :attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to True then indexes will be created by MongoMotor. By default, _cls will be added to the start of every index (that doesn't contain a list) if allow_inheritance is True. This can be disabled by either setting cls to False on the specific index or by setting index_cls to False on the meta dictionary for the document. By default, any extra attribute existing in stored data but not declared in your model will raise a :class:`mongoengine.FieldDoesNotExist` error. This can be disabled by setting :attr:`strict` to ``False`` in the :attr:`meta` dictionary. """ # setting it here so mongoengine will be happy even if I don't # use TopLevelDocumentMetaclass. meta = {'abstract': True, 'max_documents': None, 'max_size': None, 'ordering': [], 'indexes': [], 'id_field': None, 'index_background': False, 'index_drop_dups': False, 'index_opts': None, 'delete_rules': None, 'allow_inheritance': None, 'auto_create_index': False, 'queryset_class': QuerySet} # Methods that will run asynchronally and return a future save = Async() modify = Async() reload = Async() compare_indexes = Sync(cls_meth=True) ensure_indexes = Sync(cls_meth=True) # ensure_index = Sync(cls_meth=True)
[docs] async def delete(self, signal_kwargs=None, **write_concern): """Delete the :class:`~mongoengine.Document` from the database. This will only take effect if the document has been previously saved. :parm signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. """ signal_kwargs = signal_kwargs or {} signals.pre_delete.send(self.__class__, document=self, **signal_kwargs) # Delete FileFields separately FileField = _import_class('FileField') for name, field in self._fields.items(): if isinstance(field, FileField): getattr(self, name).delete() try: r = await self._qs.filter( **self._object_key).delete(write_concern=write_concern, _from_doc_delete=True) signals.post_delete.send( self.__class__, document=self, **signal_kwargs) except pymongo.errors.OperationFailure as err: message = 'Could not delete document (%s)' % err.message raise OperationError(message) return r
[docs] def modify(self, query={}, **update): """Perform an atomic update of the document in the database and reload the document object using updated version. Returns True if the document has been updated or False if the document in the database doesn't match the query. .. note:: All unsaved changes that have been made to the document are rejected if the method returns True. :param query: the update will be performed only if the document in the database matches the query :param update: Django-style update keyword arguments """ if self.pk is None: raise InvalidDocumentError( "The document does not have a primary key.") id_field = self._meta["id_field"] query = query.copy() if isinstance( query, dict) else query.to_query(self) if id_field not in query: query[id_field] = self.pk elif query[id_field] != self.pk: msg = "Invalid document modify query: " msg += "it must modify only this document." raise InvalidQueryError(msg) updated_future = self._qs(**query).modify(new=True, **update) ret_future = get_future(self) def updated_cb(updated_future): try: updated = updated_future.result() if updated is None: ret_future.set_result(False) return for field in self._fields_ordered: try: setattr(self, field, self._reload(field, updated[field])) except AttributeError: setattr(self, field, self._reload( field, updated._data.get(field))) self._changed_fields = updated._changed_fields self._created = False ret_future.set_result(True) return except Exception as e: ret_future.set_exception(e) updated_future.add_done_callback(updated_cb) return ret_future
[docs] @classmethod def register_delete_rule(cls, document_cls, field_name, rule): """This method registers the delete rules to apply when removing this object. """ if document_cls.__name__.startswith('Patched'): return return super().register_delete_rule(document_cls, field_name, rule)
[docs] @classmethod def drop_collection(cls): """Drops the entire collection associated with this :class:`mongomotor.Document` type from the database. """ cls._collection = None db = cls._get_db() return db.drop_collection(cls._get_collection_name())
@property def _qs(self): """ Returns the queryset to use for updating / reloading / deletions """ if not hasattr(self, '__objects'): self.__objects = QuerySet(self, self._get_collection()) return self.__objects def _reload(self, key, value): # Hack!! # What we do here is to raise the exception for futures because # we don't really want to reload references and the way mongoengine # does this stuff makes us to have a future instead of a reference in # the end, so we thow the exception here, the exception will be # handled by mongoengine and we will get a DBRef, and we finally # simply return this DBRef so in the end we can have everything # right for mongomotor if inspect.isawaitable(value): raise AttributeError return super()._reload(key, value)
[docs]class DynamicDocument(Document, DynamicDocumentBase, metaclass=AsyncTopLevelDocumentMetaclass): meta = {'abstract': True, 'max_documents': None, 'max_size': None, 'ordering': [], 'indexes': [], 'id_field': None, 'index_background': False, 'index_drop_dups': False, 'index_opts': None, 'delete_rules': None, 'allow_inheritance': None} _dynamic = True def __delattr__(self, *args, **kwargs): DynamicDocumentBase.__delattr__(self, *args, **kwargs)
[docs]class EmbeddedDocument(NoDerefInitMixin, EmbeddedDocumentBase, metaclass=AsyncDocumentMetaclass): meta = {'abstract': True, 'max_documents': None, 'max_size': None, 'ordering': [], 'indexes': [], 'id_field': None, 'index_background': False, 'index_drop_dups': False, 'index_opts': None, 'delete_rules': None, 'allow_inheritance': None}
[docs]class DynamicEmbeddedDocument(NoDerefInitMixin, DynamicEmbeddedDocumentBase, metaclass=AsyncDocumentMetaclass): meta = {'abstract': True, 'max_documents': None, 'max_size': None, 'ordering': [], 'indexes': [], 'id_field': None, 'index_background': False, 'index_drop_dups': False, 'index_opts': None, 'delete_rules': None, 'allow_inheritance': None}