Renamed project to mongoengine
This commit is contained in:
12
mongoengine/__init__.py
Normal file
12
mongoengine/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import document
|
||||
from document import *
|
||||
import fields
|
||||
from fields import *
|
||||
import connection
|
||||
from connection import *
|
||||
|
||||
__all__ = document.__all__ + fields.__all__ + connection.__all__
|
||||
|
||||
__author__ = 'Harry Marr'
|
||||
__version__ = '0.1'
|
||||
|
||||
250
mongoengine/base.py
Normal file
250
mongoengine/base.py
Normal file
@@ -0,0 +1,250 @@
|
||||
from collection import CollectionManager
|
||||
|
||||
import pymongo
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseField(object):
|
||||
"""A base class for fields in a MongoDB document. Instances of this class
|
||||
may be added to subclasses of `Document` to define a document's schema.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, required=False, default=None):
|
||||
self.name = name
|
||||
self.required = required
|
||||
self.default = default
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor for retrieving a value from a field in a document. Do
|
||||
any necessary conversion between Python and MongoDB types.
|
||||
"""
|
||||
if instance is None:
|
||||
# Document class being used rather than a document object
|
||||
return self
|
||||
|
||||
# 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)
|
||||
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.
|
||||
"""
|
||||
if value is not None:
|
||||
try:
|
||||
value = self._to_python(value)
|
||||
self._validate(value)
|
||||
except (ValueError, AttributeError, AssertionError):
|
||||
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):
|
||||
"""Convert a MongoDB-compatible type to a Python type.
|
||||
"""
|
||||
return unicode(value)
|
||||
|
||||
def _to_mongo(self, value):
|
||||
"""Convert a Python type to a MongoDB-compatible type.
|
||||
"""
|
||||
return self._to_python(value)
|
||||
|
||||
def _validate(self, value):
|
||||
"""Perform validation on a value.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ObjectIdField(BaseField):
|
||||
"""An field wrapper around MongoDB's ObjectIds.
|
||||
"""
|
||||
|
||||
def _to_python(self, value):
|
||||
return str(value)
|
||||
|
||||
def _to_mongo(self, value):
|
||||
if not isinstance(value, pymongo.objectid.ObjectId):
|
||||
return pymongo.objectid.ObjectId(value)
|
||||
return value
|
||||
|
||||
def _validate(self, value):
|
||||
try:
|
||||
pymongo.objectid.ObjectId(str(value))
|
||||
except:
|
||||
raise ValidationError('Invalid Object ID')
|
||||
|
||||
|
||||
class DocumentMetaclass(type):
|
||||
"""Metaclass for all documents.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
metaclass = attrs.get('__metaclass__')
|
||||
super_new = super(DocumentMetaclass, cls).__new__
|
||||
if metaclass and issubclass(metaclass, DocumentMetaclass):
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
doc_fields = {}
|
||||
class_name = [name]
|
||||
superclasses = {}
|
||||
for base in bases:
|
||||
# Include all fields present in superclasses
|
||||
if hasattr(base, '_fields'):
|
||||
doc_fields.update(base._fields)
|
||||
class_name.append(base._class_name)
|
||||
# Get superclasses from superclass
|
||||
superclasses[base._class_name] = base
|
||||
superclasses.update(base._superclasses)
|
||||
attrs['_class_name'] = '.'.join(reversed(class_name))
|
||||
attrs['_superclasses'] = superclasses
|
||||
|
||||
# Add the document's fields to the _fields attribute
|
||||
for attr_name, attr_value in attrs.items():
|
||||
if issubclass(attr_value.__class__, BaseField):
|
||||
if not attr_value.name:
|
||||
attr_value.name = attr_name
|
||||
doc_fields[attr_name] = attr_value
|
||||
attrs['_fields'] = doc_fields
|
||||
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
"""Metaclass for top-level documents (i.e. documents that have their own
|
||||
collection in the database.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
super_new = super(TopLevelDocumentMetaclass, cls).__new__
|
||||
# Classes defined in this package are abstract and should not have
|
||||
# their own metadata with DB collection, etc.
|
||||
# __metaclass__ is only set on the class with the __metaclass__
|
||||
# attribute (i.e. it is not set on subclasses). This differentiates
|
||||
# 'real' documents from the 'Document' class
|
||||
if attrs.get('__metaclass__') == TopLevelDocumentMetaclass:
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
collection = name.lower()
|
||||
# Subclassed documents inherit collection from superclass
|
||||
for base in bases:
|
||||
if hasattr(base, '_meta') and 'collection' in base._meta:
|
||||
collection = base._meta['collection']
|
||||
|
||||
meta = {
|
||||
'collection': collection,
|
||||
}
|
||||
meta.update(attrs.get('meta', {}))
|
||||
attrs['_meta'] = meta
|
||||
|
||||
attrs['_id'] = ObjectIdField()
|
||||
|
||||
# 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 = CollectionManager(new_class)
|
||||
|
||||
return new_class
|
||||
|
||||
|
||||
class BaseDocument(object):
|
||||
|
||||
def __init__(self, **values):
|
||||
self._data = {}
|
||||
# Assign initial values to instance
|
||||
for attr_name, attr_value in self._fields.items():
|
||||
if attr_name in values:
|
||||
setattr(self, attr_name, values.pop(attr_name))
|
||||
else:
|
||||
if attr_value.required:
|
||||
raise ValidationError('Field "%s" is required' % attr_name)
|
||||
# Use default value
|
||||
setattr(self, attr_name, getattr(self, attr_name, None))
|
||||
|
||||
@classmethod
|
||||
def _get_subclasses(cls):
|
||||
"""Return a dictionary of all subclasses (found recursively).
|
||||
"""
|
||||
try:
|
||||
subclasses = cls.__subclasses__()
|
||||
except:
|
||||
subclasses = cls.__subclasses__(cls)
|
||||
|
||||
all_subclasses = {}
|
||||
for subclass in subclasses:
|
||||
all_subclasses[subclass._class_name] = subclass
|
||||
all_subclasses.update(subclass._get_subclasses())
|
||||
return all_subclasses
|
||||
|
||||
def __iter__(self):
|
||||
# Use _data rather than _fields as iterator only looks at names so
|
||||
# values don't need to be converted to Python types
|
||||
return iter(self._data)
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""Dictionary-style field access, return a field's value if present.
|
||||
"""
|
||||
try:
|
||||
return getattr(self, name)
|
||||
except AttributeError:
|
||||
raise KeyError(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
"""Dictionary-style field access, set a field's value.
|
||||
"""
|
||||
# Ensure that the field exists before settings its value
|
||||
if name not in self._fields:
|
||||
raise KeyError(name)
|
||||
return setattr(self, name, value)
|
||||
|
||||
def __contains__(self, name):
|
||||
try:
|
||||
getattr(self, name)
|
||||
return True
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def _to_mongo(self):
|
||||
"""Return data dictionary ready for use with MongoDB.
|
||||
"""
|
||||
data = {}
|
||||
for field_name, field in self._fields.items():
|
||||
value = getattr(self, field_name, None)
|
||||
if value is not None:
|
||||
data[field_name] = field._to_mongo(value)
|
||||
data['_cls'] = self._class_name
|
||||
data['_types'] = self._superclasses.keys() + [self._class_name]
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _from_son(cls, son):
|
||||
"""Create an instance of a Document (subclass) from a PyMongo SOM.
|
||||
"""
|
||||
class_name = son[u'_cls']
|
||||
data = dict((str(key), value) for key, value in son.items())
|
||||
del data['_cls']
|
||||
|
||||
# Return correct subclass for document type
|
||||
if class_name != cls._class_name:
|
||||
subclasses = cls._get_subclasses()
|
||||
if class_name not in subclasses:
|
||||
# Type of document is probably more generic than the class
|
||||
# that has been queried to return this SON
|
||||
return None
|
||||
cls = subclasses[class_name]
|
||||
return cls(**data)
|
||||
|
||||
101
mongoengine/collection.py
Normal file
101
mongoengine/collection.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from connection import _get_db
|
||||
|
||||
import pymongo
|
||||
|
||||
|
||||
class QuerySet(object):
|
||||
"""A set of results returned from a query. Wraps a MongoDB cursor,
|
||||
providing Document objects as the results.
|
||||
"""
|
||||
|
||||
def __init__(self, document, cursor):
|
||||
self._document = document
|
||||
self._cursor = cursor
|
||||
|
||||
def next(self):
|
||||
"""Wrap the result in a Document object.
|
||||
"""
|
||||
return self._document._from_son(self._cursor.next())
|
||||
|
||||
def count(self):
|
||||
"""Count the selected elements in the query.
|
||||
"""
|
||||
return self._cursor.count()
|
||||
|
||||
def limit(self, n):
|
||||
"""Limit the number of returned documents to.
|
||||
"""
|
||||
self._cursor.limit(n)
|
||||
# Return self to allow chaining
|
||||
return self
|
||||
|
||||
def skip(self, n):
|
||||
"""Skip n documents before returning the results.
|
||||
"""
|
||||
self._cursor.skip(n)
|
||||
return self
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
class CollectionManager(object):
|
||||
|
||||
def __init__(self, document):
|
||||
"""Set up the collection manager for a specific 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 _save_document(self, document):
|
||||
"""Save the provided document to the collection.
|
||||
"""
|
||||
_id = self._collection.save(document._to_mongo())
|
||||
document._id = _id
|
||||
|
||||
def _transform_query(self, **query):
|
||||
"""Transform a query from Django-style format to Mongo format.
|
||||
"""
|
||||
operators = ['neq', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
||||
'all', 'size', 'exists']
|
||||
|
||||
mongo_query = {}
|
||||
for key, value in query.items():
|
||||
parts = key.split('__')
|
||||
# Check for an operator and transform to mongo-style if there is
|
||||
if parts[-1] in operators:
|
||||
op = parts.pop()
|
||||
value = {'$' + op: value}
|
||||
|
||||
key = '.'.join(parts)
|
||||
mongo_query[key] = value
|
||||
|
||||
return mongo_query
|
||||
|
||||
def find(self, **query):
|
||||
"""Query the collection for documents matching the provided query.
|
||||
"""
|
||||
query = self._transform_query(**query)
|
||||
query['_types'] = self._document._class_name
|
||||
return QuerySet(self._document, self._collection.find(query))
|
||||
|
||||
def find_one(self, object_id=None, **query):
|
||||
"""Query the collection for document matching the provided query.
|
||||
"""
|
||||
if object_id:
|
||||
# Use just object_id if provided
|
||||
if not isinstance(object_id, pymongo.objectid.ObjectId):
|
||||
object_id = pymongo.objectid.ObjectId(object_id)
|
||||
query = object_id
|
||||
else:
|
||||
# Otherwise, use the query provided
|
||||
query = self._transform_query(**query)
|
||||
query['_types'] = self._document._class_name
|
||||
|
||||
result = self._collection.find_one(query)
|
||||
if result is not None:
|
||||
result = self._document._from_son(result)
|
||||
return result
|
||||
48
mongoengine/connection.py
Normal file
48
mongoengine/connection.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from pymongo import Connection
|
||||
|
||||
|
||||
__all__ = ['ConnectionError', 'connect']
|
||||
|
||||
|
||||
_connection_settings = {
|
||||
'host': 'localhost',
|
||||
'port': 27017,
|
||||
'pool_size': 1,
|
||||
}
|
||||
_connection = None
|
||||
_db = None
|
||||
|
||||
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _get_connection():
|
||||
global _connection
|
||||
if _connection is None:
|
||||
_connection = Connection(**_connection_settings)
|
||||
return _connection
|
||||
|
||||
def _get_db():
|
||||
global _db
|
||||
if _db is None:
|
||||
raise ConnectionError('Not connected to database')
|
||||
return _db
|
||||
|
||||
def connect(db=None, 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()
|
||||
# Get DB from connection and auth if necessary
|
||||
_db = connection[db]
|
||||
if username is not None and password is not None:
|
||||
_db.authenticate(username, password)
|
||||
|
||||
20
mongoengine/document.py
Normal file
20
mongoengine/document.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from base import DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument
|
||||
|
||||
|
||||
__all__ = ['Document', 'EmbeddedDocument']
|
||||
|
||||
|
||||
class EmbeddedDocument(BaseDocument):
|
||||
|
||||
__metaclass__ = DocumentMetaclass
|
||||
|
||||
|
||||
class Document(BaseDocument):
|
||||
|
||||
__metaclass__ = TopLevelDocumentMetaclass
|
||||
|
||||
def save(self):
|
||||
"""Save the document to the database. If the document already exists,
|
||||
it will be updated, otherwise it will be created.
|
||||
"""
|
||||
self.objects._save_document(self)
|
||||
170
mongoengine/fields.py
Normal file
170
mongoengine/fields.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from base import BaseField, ObjectIdField, ValidationError
|
||||
from document import Document, EmbeddedDocument
|
||||
from connection import _get_db
|
||||
|
||||
import re
|
||||
import pymongo
|
||||
|
||||
|
||||
__all__ = ['StringField', 'IntField', 'EmbeddedDocumentField', 'ListField',
|
||||
'ObjectIdField', 'ReferenceField', 'ValidationError']
|
||||
|
||||
|
||||
class StringField(BaseField):
|
||||
"""A unicode string field.
|
||||
"""
|
||||
|
||||
def __init__(self, regex=None, max_length=None, **kwargs):
|
||||
self.regex = re.compile(regex) if regex else None
|
||||
self.max_length = max_length
|
||||
super(StringField, self).__init__(**kwargs)
|
||||
|
||||
def _to_python(self, value):
|
||||
assert(isinstance(value, (str, unicode)))
|
||||
return unicode(value)
|
||||
|
||||
def _validate(self, value):
|
||||
assert(isinstance(value, (str, unicode)))
|
||||
|
||||
if self.max_length is not None and len(value) > self.max_length:
|
||||
raise ValidationError('String value is too long')
|
||||
|
||||
if self.regex is not None and self.regex.match(value) is None:
|
||||
message = 'String value did not match validation regex'
|
||||
raise ValidationError(message)
|
||||
|
||||
|
||||
class IntField(BaseField):
|
||||
"""An integer field.
|
||||
"""
|
||||
|
||||
def __init__(self, min_value=None, max_value=None, **kwargs):
|
||||
self.min_value, self.max_value = min_value, max_value
|
||||
super(IntField, self).__init__(**kwargs)
|
||||
|
||||
def _to_python(self, value):
|
||||
assert(isinstance(value, int))
|
||||
return int(value)
|
||||
|
||||
def _validate(self, value):
|
||||
assert(isinstance(value, int))
|
||||
|
||||
if self.min_value is not None and value < self.min_value:
|
||||
raise ValidationError('Integer value is too small')
|
||||
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
raise ValidationError('Integer value is too large')
|
||||
|
||||
|
||||
class EmbeddedDocumentField(BaseField):
|
||||
"""An embedded document field. Only valid values are subclasses of
|
||||
EmbeddedDocument.
|
||||
"""
|
||||
|
||||
def __init__(self, document, **kwargs):
|
||||
if not issubclass(document, EmbeddedDocument):
|
||||
raise ValidationError('Invalid embedded document class provided '
|
||||
'to an EmbeddedDocumentField')
|
||||
self.document = document
|
||||
super(EmbeddedDocumentField, self).__init__(**kwargs)
|
||||
|
||||
def _to_python(self, value):
|
||||
if not isinstance(value, self.document):
|
||||
assert(isinstance(value, (dict, pymongo.son.SON)))
|
||||
return self.document._from_son(value)
|
||||
return value
|
||||
|
||||
def _to_mongo(self, value):
|
||||
return self.document._to_mongo(value)
|
||||
|
||||
def _validate(self, value):
|
||||
"""Make sure that the document instance is an instance of the
|
||||
EmbeddedDocument subclass provided when the document was defined.
|
||||
"""
|
||||
# Using isinstance also works for subclasses of self.document
|
||||
if not isinstance(value, self.document):
|
||||
raise ValidationError('Invalid embedded document instance '
|
||||
'provided to an EmbeddedDocumentField')
|
||||
|
||||
|
||||
class ListField(BaseField):
|
||||
"""A list field that wraps a standard field, allowing multiple instances
|
||||
of the field to be used as a list in the database.
|
||||
"""
|
||||
|
||||
def __init__(self, field, **kwargs):
|
||||
if not isinstance(field, BaseField):
|
||||
raise ValidationError('Argument to ListField constructor must be '
|
||||
'a valid field')
|
||||
self.field = field
|
||||
super(ListField, self).__init__(**kwargs)
|
||||
|
||||
def _to_python(self, value):
|
||||
assert(isinstance(value, (list, tuple)))
|
||||
return [self.field._to_python(item) for item in value]
|
||||
|
||||
def _to_mongo(self, value):
|
||||
return [self.field._to_mongo(item) for item in value]
|
||||
|
||||
def _validate(self, value):
|
||||
"""Make sure that a list of valid fields is being used.
|
||||
"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValidationError('Only lists and tuples may be used in a '
|
||||
'list field')
|
||||
|
||||
try:
|
||||
[self.field._validate(item) for item in value]
|
||||
except:
|
||||
raise ValidationError('All items in a list field must be of the '
|
||||
'specified type')
|
||||
|
||||
|
||||
class ReferenceField(BaseField):
|
||||
"""A reference to a document that will be automatically dereferenced on
|
||||
access (lazily).
|
||||
"""
|
||||
|
||||
def __init__(self, document_type, **kwargs):
|
||||
if not issubclass(document_type, Document):
|
||||
raise ValidationError('Argument to ReferenceField constructor '
|
||||
'must be a top level document class')
|
||||
self.document_type = document_type
|
||||
self.document_obj = None
|
||||
super(ReferenceField, self).__init__(**kwargs)
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor to allow lazy dereferencing.
|
||||
"""
|
||||
if instance is None:
|
||||
# Document class being used rather than a document object
|
||||
return self
|
||||
|
||||
# Get value from document instance if available
|
||||
value = instance._data.get(self.name)
|
||||
# Dereference DBRefs
|
||||
if isinstance(value, (pymongo.dbref.DBRef)):
|
||||
value = _get_db().dereference(value)
|
||||
instance._data[self.name] = self.document_type._from_son(value)
|
||||
|
||||
return super(ReferenceField, self).__get__(instance, owner)
|
||||
|
||||
def _to_python(self, document):
|
||||
assert(isinstance(document, (self.document_type, pymongo.dbref.DBRef)))
|
||||
return document
|
||||
|
||||
def _to_mongo(self, document):
|
||||
if isinstance(document, (str, unicode, pymongo.objectid.ObjectId)):
|
||||
_id = document
|
||||
else:
|
||||
try:
|
||||
_id = document._id
|
||||
except:
|
||||
raise ValidationError('You can only reference documents once '
|
||||
'they have been saved to the database')
|
||||
|
||||
if not isinstance(_id, pymongo.objectid.ObjectId):
|
||||
_id = pymongo.objectid.ObjectId(_id)
|
||||
|
||||
collection = self.document_type._meta['collection']
|
||||
return pymongo.dbref.DBRef(collection, _id)
|
||||
Reference in New Issue
Block a user