Renamed project to mongoengine

This commit is contained in:
Harry Marr
2009-11-21 18:41:10 +00:00
parent 5fa01d89a5
commit 3017dc78ed
9 changed files with 10 additions and 10 deletions

12
mongoengine/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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)