Merge branch 'master' of git://github.com/hmarr/mongoengine

This commit is contained in:
blackbrrr
2010-01-05 12:00:07 -06:00
20 changed files with 1110 additions and 146 deletions

View File

@@ -4,9 +4,21 @@ import fields
from fields import *
import connection
from connection import *
import queryset
from queryset import *
__all__ = document.__all__ + fields.__all__ + connection.__all__
__all__ = (document.__all__ + fields.__all__ + connection.__all__ +
queryset.__all__)
__author__ = 'Harry Marr'
__version__ = '0.1'
VERSION = (0, 1, 1)
def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1])
if VERSION[2]:
version = '%s.%s' % (version, VERSION[2])
return version
__version__ = get_version()

View File

@@ -28,26 +28,15 @@ class BaseField(object):
# Get value from document instance if available, if not use default
value = instance._data.get(self.name)
if value is None:
if self.default is not None:
value = self.default
if callable(value):
value = value()
else:
raise AttributeError(self.name)
value = self.default
# Allow callable default values
if callable(value):
value = value()
return value
def __set__(self, instance, value):
"""Descriptor for assigning a value to a field in a document. Do any
necessary conversion between Python and MongoDB types.
"""Descriptor for assigning a value to a field in a document.
"""
if value is not None:
try:
self.validate(value)
except (ValueError, AttributeError, AssertionError), e:
raise ValidationError('Invalid value for field of type "' +
self.__class__.__name__ + '"')
elif self.required:
raise ValidationError('Field "%s" is required' % self.name)
instance._data[self.name] = value
def to_python(self, value):
@@ -156,6 +145,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
meta = {
'collection': collection,
'allow_inheritance': True,
'max_documents': None,
'max_size': None,
}
meta.update(attrs.get('meta', {}))
# Only simple classes - direct subclasses of Document - may set
@@ -170,7 +161,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
# Set up collection manager, needs the class to have fields so use
# DocumentMetaclass before instantiating CollectionManager object
new_class = super_new(cls, name, bases, attrs)
new_class.objects = QuerySetManager(new_class)
new_class.objects = QuerySetManager()
return new_class
@@ -186,8 +177,6 @@ class BaseDocument(object):
else:
# Use default value if present
value = getattr(self, attr_name, None)
if value is None and attr_value.required:
raise ValidationError('Field "%s" is required' % attr_name)
setattr(self, attr_name, value)
@classmethod
@@ -228,8 +217,8 @@ class BaseDocument(object):
def __contains__(self, name):
try:
getattr(self, name)
return True
val = getattr(self, name)
return val is not None
except AttributeError:
return False

View File

@@ -29,15 +29,13 @@ def _get_db():
raise ConnectionError('Not connected to database')
return _db
def connect(db=None, username=None, password=None, **kwargs):
def connect(db, username=None, password=None, **kwargs):
"""Connect to the database specified by the 'db' argument. Connection
settings may be provided here as well if the database is not running on
the default port on localhost. If authentication is needed, provide
username and password arguments as well.
"""
global _db
if db is None:
raise TypeError('"db" argument must be provided to connect()')
_connection_settings.update(kwargs)
connection = _get_connection()

View File

@@ -1,4 +1,5 @@
from base import DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
ValidationError)
from connection import _get_db
@@ -35,6 +36,14 @@ class Document(BaseDocument):
though). To disable this behaviour and remove the dependence on the
presence of `_cls` and `_types`, set :attr:`allow_inheritance` to
``False`` in the :attr:`meta` dictionary.
A :class:`~mongoengine.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. If :attr:`max_size` is not
specified and :attr:`max_documents` is, :attr:`max_size` defaults to
10000000 bytes (10MB).
"""
__metaclass__ = TopLevelDocumentMetaclass
@@ -44,15 +53,43 @@ class Document(BaseDocument):
document already exists, it will be updated, otherwise it will be
created.
"""
object_id = self.objects._collection.save(self.to_mongo())
self.id = object_id
self.validate()
object_id = self.__class__.objects._collection.save(self.to_mongo())
self.id = self._fields['id'].to_python(object_id)
def delete(self):
"""Delete the :class:`~mongoengine.Document` from the database. This
will only take effect if the document has been previously saved.
"""
object_id = self._fields['id'].to_mongo(self.id)
self.__class__.objects(_id=object_id).delete()
self.__class__.objects(id=object_id).delete()
def reload(self):
"""Reloads all attributes from the database.
"""
object_id = self._fields['id'].to_mongo(self.id)
obj = self.__class__.objects(id=object_id).first()
for field in self._fields:
setattr(self, field, getattr(obj, field))
def validate(self):
"""Ensure that all fields' values are valid and that required fields
are present.
"""
# Get a list of tuples of field names and their current values
fields = [(field, getattr(self, name))
for name, field in self._fields.items()]
# Ensure that each field is matched to a valid value
for field, value in fields:
if value is not None:
try:
field.validate(value)
except (ValueError, AttributeError, AssertionError), e:
raise ValidationError('Invalid value for field of type "' +
field.__class__.__name__ + '"')
elif field.required:
raise ValidationError('Field "%s" is required' % field.name)
@classmethod
def drop_collection(cls):

View File

@@ -34,6 +34,9 @@ class StringField(BaseField):
message = 'String value did not match validation regex'
raise ValidationError(message)
def lookup_member(self, member_name):
return None
class IntField(BaseField):
"""An integer field.
@@ -114,6 +117,9 @@ class EmbeddedDocumentField(BaseField):
raise ValidationError('Invalid embedded document instance '
'provided to an EmbeddedDocumentField')
def lookup_member(self, member_name):
return self.document._fields.get(member_name)
class ListField(BaseField):
"""A list field that wraps a standard field, allowing multiple instances
@@ -146,6 +152,9 @@ class ListField(BaseField):
raise ValidationError('All items in a list field must be of the '
'specified type')
def lookup_member(self, member_name):
return self.field.lookup_member(member_name)
class ReferenceField(BaseField):
"""A reference to a document that will be automatically dereferenced on
@@ -181,9 +190,8 @@ class ReferenceField(BaseField):
if isinstance(document, (str, unicode, pymongo.objectid.ObjectId)):
id_ = document
else:
try:
id_ = document.id
except:
id_ = document.id
if id_ is None:
raise ValidationError('You can only reference documents once '
'they have been saved to the database')
@@ -195,3 +203,6 @@ class ReferenceField(BaseField):
def validate(self, value):
assert(isinstance(value, (self.document_type, pymongo.dbref.DBRef)))
def lookup_member(self, member_name):
return self.document_type._fields.get(member_name)

View File

@@ -3,6 +3,13 @@ from connection import _get_db
import pymongo
__all__ = ['queryset_manager', 'InvalidQueryError', 'InvalidCollectionError']
class InvalidQueryError(Exception):
pass
class QuerySet(object):
"""A set of results returned from a query. Wraps a MongoDB cursor,
providing :class:`~mongoengine.Document` objects as the results.
@@ -36,7 +43,8 @@ class QuerySet(object):
"""Filter the selected documents by calling the
:class:`~mongoengine.QuerySet` with a query.
"""
self._query.update(QuerySet._transform_query(**query))
query = QuerySet._transform_query(_doc_cls=self._document, **query)
self._query.update(query)
return self
@property
@@ -44,9 +52,30 @@ class QuerySet(object):
if not self._cursor_obj:
self._cursor_obj = self._collection.find(self._query)
return self._cursor_obj
@classmethod
def _translate_field_name(cls, document, parts):
"""Translate a field attribute name to a database field name.
"""
if not isinstance(parts, (list, tuple)):
parts = [parts]
field_names = []
field = None
for field_name in parts:
if field is None:
# Look up first field from the document
field = document._fields[field_name]
else:
# Look up subfield on the previous field
field = field.lookup_member(field_name)
if field is None:
raise InvalidQueryError('Cannot resolve field "%s"'
% field_name)
field_names.append(field.name)
return field_names
@classmethod
def _transform_query(cls, **query):
def _transform_query(cls, _doc_cls=None, **query):
"""Transform a query from Django-style format to Mongo format.
"""
operators = ['neq', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
@@ -61,6 +90,10 @@ class QuerySet(object):
op = parts.pop()
value = {'$' + op: value}
# Switch field names to proper names [set in Field(name='foo')]
if _doc_cls:
parts = QuerySet._translate_field_name(_doc_cls, parts)
key = '.'.join(parts)
if op is None or key not in mongo_query:
mongo_query[key] = value
@@ -72,9 +105,10 @@ class QuerySet(object):
def first(self):
"""Retrieve the first object matching the query.
"""
result = self._collection.find_one(self._query)
if result is not None:
result = self._document._from_son(result)
try:
result = self[0]
except IndexError:
result = None
return result
def with_id(self, object_id):
@@ -98,6 +132,9 @@ class QuerySet(object):
"""
return self._cursor.count()
def __len__(self):
return self.count()
def limit(self, n):
"""Limit the number of returned documents to `n`. This may also be
achieved using array-slicing syntax (e.g. ``User.objects[:5]``).
@@ -161,15 +198,99 @@ class QuerySet(object):
def __iter__(self):
return self
def exec_js(self, code, *fields, **options):
"""Execute a Javascript function on the server. A list of fields may be
provided, which will be translated to their correct names and supplied
as the arguments to the function. A few extra variables are added to
the function's scope: ``collection``, which is the name of the
collection in use; ``query``, which is an object representing the
current query; and ``options``, which is an object containing any
options specified as keyword arguments.
"""
fields = [QuerySet._translate_field_name(self._document, f)
for f in fields]
collection = self._document._meta['collection']
scope = {
'collection': collection,
'query': self._query,
'options': options or {},
}
code = pymongo.code.Code(code, scope=scope)
db = _get_db()
return db.eval(code, *fields)
def sum(self, field):
"""Sum over the values of the specified field.
"""
sum_func = """
function(sumField) {
var total = 0.0;
db[collection].find(query).forEach(function(doc) {
total += (doc[sumField] || 0.0);
});
return total;
}
"""
return self.exec_js(sum_func, field)
def average(self, field):
"""Average over the values of the specified field.
"""
average_func = """
function(averageField) {
var total = 0.0;
var num = 0;
db[collection].find(query).forEach(function(doc) {
if (doc[averageField]) {
total += doc[averageField];
num += 1;
}
});
return total / num;
}
"""
return self.exec_js(average_func, field)
def item_frequencies(self, list_field, normalize=False):
"""Returns a dictionary of all items present in a list field across
the whole queried set of documents, and their corresponding frequency.
This is useful for generating tag clouds, or searching documents.
"""
freq_func = """
function(listField) {
if (options.normalize) {
var total = 0.0;
db[collection].find(query).forEach(function(doc) {
total += doc[listField].length;
});
}
var frequencies = {};
var inc = 1.0;
if (options.normalize) {
inc /= total;
}
db[collection].find(query).forEach(function(doc) {
doc[listField].forEach(function(item) {
frequencies[item] = inc + (frequencies[item] || 0);
});
});
return frequencies;
}
"""
return self.exec_js(freq_func, list_field, normalize=normalize)
class InvalidCollectionError(Exception):
pass
class QuerySetManager(object):
def __init__(self, document):
db = _get_db()
self._document = document
self._collection_name = document._meta['collection']
# This will create the collection if it doesn't exist
self._collection = db[self._collection_name]
def __init__(self, manager_func=None):
self._manager_func = manager_func
self._collection = None
def __get__(self, instance, owner):
"""Descriptor for instantiating a new QuerySet object when
@@ -178,6 +299,47 @@ class QuerySetManager(object):
if instance is not None:
# Document class being used rather than a document object
return self
if self._collection is None:
db = _get_db()
collection = owner._meta['collection']
# Create collection as a capped collection if specified
if owner._meta['max_size'] or owner._meta['max_documents']:
# Get max document limit and max byte size from meta
max_size = owner._meta['max_size'] or 10000000 # 10MB default
max_documents = owner._meta['max_documents']
if collection in db.collection_names():
self._collection = db[collection]
# The collection already exists, check if its capped
# options match the specified capped options
options = self._collection.options()
if options.get('max') != max_documents or \
options.get('size') != max_size:
msg = ('Cannot create collection "%s" as a capped '
'collection as it already exists') % collection
raise InvalidCollectionError(msg)
else:
# Create the collection as a capped collection
opts = {'capped': True, 'size': max_size}
if max_documents:
opts['max'] = max_documents
self._collection = db.create_collection(collection, opts)
else:
self._collection = db[collection]
# self._document should be the same as owner
return QuerySet(self._document, self._collection)
# owner is the document that contains the QuerySetManager
queryset = QuerySet(owner, self._collection)
if self._manager_func:
queryset = self._manager_func(queryset)
return queryset
def queryset_manager(func):
"""Decorator that allows you to define custom QuerySet managers on
:class:`~mongoengine.Document` classes. The manager must be a function that
accepts a :class:`~mongoengine.queryset.QuerySet` as its only argument, and
returns a :class:`~mongoengine.queryset.QuerySet`, probably the same one
but modified in some way.
"""
return QuerySetManager(func)