Merge branch 'master' of git://github.com/hmarr/mongoengine
This commit is contained in:
commit
bb4444f54d
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 Harry Marr
|
Copyright (c) 2009-2010 Harry Marr
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
Permission is hereby granted, free of charge, to any person
|
||||||
obtaining a copy of this software and associated documentation
|
obtaining a copy of this software and associated documentation
|
||||||
|
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
include README.rst
|
||||||
|
include LICENSE
|
||||||
|
recursive-include docs *
|
||||||
|
prune docs/_build/*
|
||||||
|
recursive-include tests *
|
||||||
|
recursive-exclude * *.pyc *.swp
|
89
README.rst
89
README.rst
@ -1,8 +1,89 @@
|
|||||||
|
===========
|
||||||
MongoEngine
|
MongoEngine
|
||||||
===========
|
===========
|
||||||
MongoEngine is an ORM-like layer on top of PyMongo.
|
:Info: MongoEngine is an ORM-like layer on top of PyMongo.
|
||||||
|
:Author: Harry Marr (http://github.com/hmarr)
|
||||||
|
|
||||||
Tutorial available at http://hmarr.com/mongoengine/
|
About
|
||||||
|
=====
|
||||||
|
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
|
||||||
|
Documentation available at http://hmarr.com/mongoengine/ - there is currently
|
||||||
|
a `tutorial <http://hmarr.com/mongoengine/tutorial.html>`_, a `user guide
|
||||||
|
<http://hmarr.com/mongoengine/userguide.html>`_ and an `API reference
|
||||||
|
<http://hmarr.com/mongoengine/apireference.html>`_.
|
||||||
|
|
||||||
**Warning:** this software is still in development and should *not* be used
|
Installation
|
||||||
in production.
|
============
|
||||||
|
If you have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
|
||||||
|
you can use ``easy_install mongoengine``. Otherwise, you can download the
|
||||||
|
source from `GitHub <http://github.com/hmarr/mongoengine>`_ and run ``python
|
||||||
|
setup.py install``.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
============
|
||||||
|
- pymongo 1.1+
|
||||||
|
- sphinx (optional - for documentation generation)
|
||||||
|
|
||||||
|
Examples
|
||||||
|
========
|
||||||
|
Some simple examples of what MongoEngine code looks like::
|
||||||
|
|
||||||
|
class BlogPost(Document):
|
||||||
|
title = StringField(required=True, max_length=200)
|
||||||
|
posted = DateTimeField(default=datetime.datetime.now)
|
||||||
|
tags = ListField(StringField(max_length=50))
|
||||||
|
|
||||||
|
class TextPost(BlogPost):
|
||||||
|
content = StringField(required=True)
|
||||||
|
|
||||||
|
class LinkPost(BlogPost):
|
||||||
|
url = StringField(required=True)
|
||||||
|
|
||||||
|
# Create a text-based post
|
||||||
|
>>> post1 = TextPost(title='Using MongoEngine', content='See the tutorial')
|
||||||
|
>>> post1.tags = ['mongodb', 'mongoengine']
|
||||||
|
>>> post1.save()
|
||||||
|
|
||||||
|
# Create a link-based post
|
||||||
|
>>> post2 = LinkPost(title='MongoEngine Docs', url='hmarr.com/mongoengine')
|
||||||
|
>>> post2.tags = ['mongoengine', 'documentation']
|
||||||
|
>>> post2.save()
|
||||||
|
|
||||||
|
# Iterate over all posts using the BlogPost superclass
|
||||||
|
>>> for post in BlogPost.objects:
|
||||||
|
... print '===', post.title, '==='
|
||||||
|
... if isinstance(post, TextPost):
|
||||||
|
... print post.content
|
||||||
|
... elif isinstance(post, LinkPost):
|
||||||
|
... print 'Link:', post.url
|
||||||
|
... print
|
||||||
|
...
|
||||||
|
=== Using MongoEngine ===
|
||||||
|
See the tutorial
|
||||||
|
|
||||||
|
=== MongoEngine Docs ===
|
||||||
|
Link: hmarr.com/mongoengine
|
||||||
|
|
||||||
|
>>> len(BlogPost.objects)
|
||||||
|
2
|
||||||
|
>>> len(HtmlPost.objects)
|
||||||
|
1
|
||||||
|
>>> len(LinkPost.objects)
|
||||||
|
1
|
||||||
|
|
||||||
|
# Find tagged posts
|
||||||
|
>>> len(BlogPost.objects(tags='mongoengine'))
|
||||||
|
2
|
||||||
|
>>> len(BlogPost.objects(tags='mongodb'))
|
||||||
|
1
|
||||||
|
|
||||||
|
Tests
|
||||||
|
=====
|
||||||
|
To run the test suite, ensure you are running a local instance of MongoDB on
|
||||||
|
the standard port, and run ``python setup.py test``.
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
The source is available on `GitHub <http://github.com/hmarr/mongoengine>`_ - to
|
||||||
|
contribute to the project, fork it on GitHub and send a pull request, all
|
||||||
|
contributions and suggestions are welcome!
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
=============
|
||||||
API Reference
|
API Reference
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Connecting
|
Connecting
|
||||||
----------
|
==========
|
||||||
|
|
||||||
.. autofunction:: mongoengine.connect
|
.. autofunction:: mongoengine.connect
|
||||||
|
|
||||||
Documents
|
Documents
|
||||||
---------
|
=========
|
||||||
|
|
||||||
.. autoclass:: mongoengine.Document
|
.. autoclass:: mongoengine.Document
|
||||||
:members:
|
:members:
|
||||||
@ -21,13 +22,15 @@ Documents
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
Querying
|
Querying
|
||||||
--------
|
========
|
||||||
|
|
||||||
.. autoclass:: mongoengine.queryset.QuerySet
|
.. autoclass:: mongoengine.queryset.QuerySet
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autofunction:: mongoengine.queryset.queryset_manager
|
||||||
|
|
||||||
Fields
|
Fields
|
||||||
------
|
======
|
||||||
|
|
||||||
.. autoclass:: mongoengine.StringField
|
.. autoclass:: mongoengine.StringField
|
||||||
|
|
||||||
|
7
docs/changelog.rst
Normal file
7
docs/changelog.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
=========
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
Changes in v0.1.1
|
||||||
|
=================
|
||||||
|
- Documents may now use capped collections
|
@ -44,10 +44,11 @@ copyright = u'2009, Harry Marr'
|
|||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
|
import mongoengine
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.1'
|
version = mongoengine.get_version()
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.1'
|
release = mongoengine.get_version()
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
@ -128,7 +129,7 @@ html_static_path = ['_static']
|
|||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
# typographically correct entities.
|
# typographically correct entities.
|
||||||
#html_use_smartypants = True
|
html_use_smartypants = True
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
# Custom sidebar templates, maps document names to template names.
|
||||||
#html_sidebars = {}
|
#html_sidebars = {}
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
.. MongoEngine documentation master file, created by
|
|
||||||
sphinx-quickstart on Sun Nov 22 18:14:13 2009.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
MongoEngine User Documentation
|
MongoEngine User Documentation
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
Contents:
|
MongoEngine is an Object-Document Mapper, written in Python for working with
|
||||||
|
MongoDB. To install it, simply run
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# easy_install mongoengine
|
||||||
|
|
||||||
|
The source is available on `GitHub <http://github.com/hmarr/mongoengine>`_.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
tutorial.rst
|
tutorial
|
||||||
apireference.rst
|
userguide
|
||||||
|
apireference
|
||||||
|
changelog
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
========
|
||||||
Tutorial
|
Tutorial
|
||||||
========
|
========
|
||||||
This tutorial introduces **MongoEngine** by means of example --- we will walk
|
This tutorial introduces **MongoEngine** by means of example --- we will walk
|
||||||
@ -9,14 +10,14 @@ application. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
|||||||
focus on the data-modelling side of the application, leaving out a user
|
focus on the data-modelling side of the application, leaving out a user
|
||||||
interface.
|
interface.
|
||||||
|
|
||||||
Connecting to MongoDB
|
Getting started
|
||||||
---------------------
|
===============
|
||||||
Before we start, make sure that a copy of MongoDB is running in an accessible
|
Before we start, make sure that a copy of MongoDB is running in an accessible
|
||||||
location --- running it locally will be easier, but if that is not an option
|
location --- running it locally will be easier, but if that is not an option
|
||||||
then it may be run on a remote server.
|
then it may be run on a remote server.
|
||||||
|
|
||||||
Before we can start using MongoEngine, we need to tell it how to connect to our
|
Before we can start using MongoEngine, we need to tell it how to connect to our
|
||||||
instance of **mongod**. For this we use the :func:`mongoengine.connect`
|
instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect`
|
||||||
function. The only argument we need to provide is the name of the MongoDB
|
function. The only argument we need to provide is the name of the MongoDB
|
||||||
database to use::
|
database to use::
|
||||||
|
|
||||||
@ -24,14 +25,10 @@ database to use::
|
|||||||
|
|
||||||
connect('tumblelog')
|
connect('tumblelog')
|
||||||
|
|
||||||
This will connect to a mongod instance running locally on the default port. To
|
For more information about connecting to MongoDB see :ref:`guide-connecting`.
|
||||||
connect to a mongod instance running elsewhere, specify the host and port
|
|
||||||
explicitly::
|
|
||||||
|
|
||||||
connect('tumblelog', host='192.168.1.35', port=12345)
|
|
||||||
|
|
||||||
Defining our documents
|
Defining our documents
|
||||||
----------------------
|
======================
|
||||||
MongoDB is *schemaless*, which means that no schema is enforced by the database
|
MongoDB is *schemaless*, which means that no schema is enforced by the database
|
||||||
--- we may add and remove fields however we want and MongoDB won't complain.
|
--- we may add and remove fields however we want and MongoDB won't complain.
|
||||||
This makes life a lot easier in many regards, especially when there is a change
|
This makes life a lot easier in many regards, especially when there is a change
|
||||||
@ -50,7 +47,7 @@ specified tag. Finally, it would be nice if **comments** could be added to
|
|||||||
posts. We'll start with **users**, as the others are slightly more involved.
|
posts. We'll start with **users**, as the others are slightly more involved.
|
||||||
|
|
||||||
Users
|
Users
|
||||||
^^^^^
|
-----
|
||||||
Just as if we were using a relational database with an ORM, we need to define
|
Just as if we were using a relational database with an ORM, we need to define
|
||||||
which fields a :class:`User` may have, and what their types will be::
|
which fields a :class:`User` may have, and what their types will be::
|
||||||
|
|
||||||
@ -65,7 +62,7 @@ MongoDB --- this will only be enforced at the application level. Also, the User
|
|||||||
documents will be stored in a MongoDB *collection* rather than a table.
|
documents will be stored in a MongoDB *collection* rather than a table.
|
||||||
|
|
||||||
Posts, Comments and Tags
|
Posts, Comments and Tags
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
------------------------
|
||||||
Now we'll think about how to store the rest of the information. If we were
|
Now we'll think about how to store the rest of the information. If we were
|
||||||
using a relational database, we would most likely have a table of **posts**, a
|
using a relational database, we would most likely have a table of **posts**, a
|
||||||
table of **comments** and a table of **tags**. To associate the comments with
|
table of **comments** and a table of **tags**. To associate the comments with
|
||||||
@ -77,7 +74,7 @@ several ways we can achieve this, but each of them have their problems --- none
|
|||||||
of them stand out as particularly intuitive solutions.
|
of them stand out as particularly intuitive solutions.
|
||||||
|
|
||||||
Posts
|
Posts
|
||||||
"""""
|
^^^^^
|
||||||
But MongoDB *isn't* a relational database, so we're not going to do it that
|
But MongoDB *isn't* a relational database, so we're not going to do it that
|
||||||
way. As it turns out, we can use MongoDB's schemaless nature to provide us with
|
way. As it turns out, we can use MongoDB's schemaless nature to provide us with
|
||||||
a much nicer solution. We will store all of the posts in *one collection* ---
|
a much nicer solution. We will store all of the posts in *one collection* ---
|
||||||
@ -103,12 +100,12 @@ this kind of modelling out of the box::
|
|||||||
link_url = StringField()
|
link_url = StringField()
|
||||||
|
|
||||||
We are storing a reference to the author of the posts using a
|
We are storing a reference to the author of the posts using a
|
||||||
:class:`mongoengine.ReferenceField` object. These are similar to foreign key
|
:class:`~mongoengine.ReferenceField` object. These are similar to foreign key
|
||||||
fields in traditional ORMs, and are automatically translated into references
|
fields in traditional ORMs, and are automatically translated into references
|
||||||
when they are saved, and dereferenced when they are loaded.
|
when they are saved, and dereferenced when they are loaded.
|
||||||
|
|
||||||
Tags
|
Tags
|
||||||
""""
|
^^^^
|
||||||
Now that we have our Post models figured out, how will we attach tags to them?
|
Now that we have our Post models figured out, how will we attach tags to them?
|
||||||
MongoDB allows us to store lists of items natively, so rather than having a
|
MongoDB allows us to store lists of items natively, so rather than having a
|
||||||
link table, we can just store a list of tags in each post. So, for both
|
link table, we can just store a list of tags in each post. So, for both
|
||||||
@ -124,13 +121,13 @@ size of our database. So let's take a look that the code our modified
|
|||||||
author = ReferenceField(User)
|
author = ReferenceField(User)
|
||||||
tags = ListField(StringField(max_length=30))
|
tags = ListField(StringField(max_length=30))
|
||||||
|
|
||||||
The :class:`mongoengine.ListField` object that is used to define a Post's tags
|
The :class:`~mongoengine.ListField` object that is used to define a Post's tags
|
||||||
takes a field object as its first argument --- this means that you can have
|
takes a field object as its first argument --- this means that you can have
|
||||||
lists of any type of field (including lists). Note that we don't need to
|
lists of any type of field (including lists). Note that we don't need to
|
||||||
modify the specialised post types as they all inherit from :class:`Post`.
|
modify the specialised post types as they all inherit from :class:`Post`.
|
||||||
|
|
||||||
Comments
|
Comments
|
||||||
""""""""
|
^^^^^^^^
|
||||||
A comment is typically associated with *one* post. In a relational database, to
|
A comment is typically associated with *one* post. In a relational database, to
|
||||||
display a post with its comments, we would have to retrieve the post from the
|
display a post with its comments, we would have to retrieve the post from the
|
||||||
database, then query the database again for the comments associated with the
|
database, then query the database again for the comments associated with the
|
||||||
@ -156,7 +153,7 @@ We can then store a list of comment documents in our post document::
|
|||||||
comments = ListField(EmbeddedDocumentField(Comment))
|
comments = ListField(EmbeddedDocumentField(Comment))
|
||||||
|
|
||||||
Adding data to our Tumblelog
|
Adding data to our Tumblelog
|
||||||
----------------------------
|
============================
|
||||||
Now that we've defined how our documents will be structured, let's start adding
|
Now that we've defined how our documents will be structured, let's start adding
|
||||||
some documents to the database. Firstly, we'll need to create a :class:`User`
|
some documents to the database. Firstly, we'll need to create a :class:`User`
|
||||||
object::
|
object::
|
||||||
@ -164,8 +161,7 @@ object::
|
|||||||
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
|
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
|
||||||
john.save()
|
john.save()
|
||||||
|
|
||||||
Note that only fields with ``required=True`` need to be specified in the
|
Note that we could have also defined our user using attribute syntax::
|
||||||
constructor, we could have also defined our user using attribute syntax::
|
|
||||||
|
|
||||||
john = User(email='jdoe@example.com')
|
john = User(email='jdoe@example.com')
|
||||||
john.first_name = 'John'
|
john.first_name = 'John'
|
||||||
@ -188,10 +184,10 @@ Note that if you change a field on a object that has already been saved, then
|
|||||||
call :meth:`save` again, the document will be updated.
|
call :meth:`save` again, the document will be updated.
|
||||||
|
|
||||||
Accessing our data
|
Accessing our data
|
||||||
------------------
|
==================
|
||||||
So now we've got a couple of posts in our database, how do we display them?
|
So now we've got a couple of posts in our database, how do we display them?
|
||||||
Each document class (i.e. any class that inherits either directly or indirectly
|
Each document class (i.e. any class that inherits either directly or indirectly
|
||||||
from :class:`mongoengine.Document`) has an :attr:`objects` attribute, which is
|
from :class:`~mongoengine.Document`) has an :attr:`objects` attribute, which is
|
||||||
used to access the documents in the database collection associated with that
|
used to access the documents in the database collection associated with that
|
||||||
class. So let's see how we can get our posts' titles::
|
class. So let's see how we can get our posts' titles::
|
||||||
|
|
||||||
@ -199,7 +195,7 @@ class. So let's see how we can get our posts' titles::
|
|||||||
print post.title
|
print post.title
|
||||||
|
|
||||||
Retrieving type-specific information
|
Retrieving type-specific information
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
------------------------------------
|
||||||
This will print the titles of our posts, one on each line. But What if we want
|
This will print the titles of our posts, one on each line. But What if we want
|
||||||
to access the type-specific data (link_url, content, etc.)? One way is simply
|
to access the type-specific data (link_url, content, etc.)? One way is simply
|
||||||
to use the :attr:`objects` attribute of a subclass of :class:`Post`::
|
to use the :attr:`objects` attribute of a subclass of :class:`Post`::
|
||||||
@ -209,7 +205,7 @@ to use the :attr:`objects` attribute of a subclass of :class:`Post`::
|
|||||||
|
|
||||||
Using TextPost's :attr:`objects` attribute only returns documents that were
|
Using TextPost's :attr:`objects` attribute only returns documents that were
|
||||||
created using :class:`TextPost`. Actually, there is a more general rule here:
|
created using :class:`TextPost`. Actually, there is a more general rule here:
|
||||||
the :attr:`objects` attribute of any subclass of :class:`mongoengine.Document`
|
the :attr:`objects` attribute of any subclass of :class:`~mongoengine.Document`
|
||||||
only looks for documents that were created using that subclass or one of its
|
only looks for documents that were created using that subclass or one of its
|
||||||
subclasses.
|
subclasses.
|
||||||
|
|
||||||
@ -237,20 +233,21 @@ This would print the title of each post, followed by the content if it was a
|
|||||||
text post, and "Link: <url>" if it was a link post.
|
text post, and "Link: <url>" if it was a link post.
|
||||||
|
|
||||||
Searching our posts by tag
|
Searching our posts by tag
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
--------------------------
|
||||||
The :attr:`objects` attribute of a :class:`mongoengine.Document` is actually a
|
The :attr:`objects` attribute of a :class:`~mongoengine.Document` is actually a
|
||||||
:class:`mongoengine.QuerySet` object. This lazily queries the database only
|
:class:`~mongoengine.queryset.QuerySet` object. This lazily queries the
|
||||||
when you need the data. It may also be filtered to narrow down your query.
|
database only when you need the data. It may also be filtered to narrow down
|
||||||
Let's adjust our query so that only posts with the tag "mongodb" are returned::
|
your query. Let's adjust our query so that only posts with the tag "mongodb"
|
||||||
|
are returned::
|
||||||
|
|
||||||
for post in Post.objects(tags='mongodb'):
|
for post in Post.objects(tags='mongodb'):
|
||||||
print post.title
|
print post.title
|
||||||
|
|
||||||
There are also methods available on :class:`mongoengine.QuerySet` objects that
|
There are also methods available on :class:`~mongoengine.queryset.QuerySet`
|
||||||
allow different results to be returned, for example, calling :meth:`first` on
|
objects that allow different results to be returned, for example, calling
|
||||||
the :attr:`objects` attribute will return a single document, the first matched
|
:meth:`first` on the :attr:`objects` attribute will return a single document,
|
||||||
by the query you provide. Aggregation functions may also be used on
|
the first matched by the query you provide. Aggregation functions may also be
|
||||||
:class:`mongoengine.QuerySet` objects::
|
used on :class:`~mongoengine.queryset.QuerySet` objects::
|
||||||
|
|
||||||
num_posts = Post.objects(tags='mongodb').count()
|
num_posts = Post.objects(tags='mongodb').count()
|
||||||
print 'Found % posts with tag "mongodb"' % num_posts
|
print 'Found % posts with tag "mongodb"' % num_posts
|
||||||
|
385
docs/userguide.rst
Normal file
385
docs/userguide.rst
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
==========
|
||||||
|
User Guide
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. _guide-connecting:
|
||||||
|
|
||||||
|
Installing
|
||||||
|
==========
|
||||||
|
MongoEngine is available on PyPI, so to use it you can use
|
||||||
|
:program:`easy_install`
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# easy_install mongoengine
|
||||||
|
|
||||||
|
Alternatively, if you don't have setuptools installed, `download it from PyPi
|
||||||
|
<http://pypi.python.org/pypi/mongoengine/>`_ and run
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# python setup.py install
|
||||||
|
|
||||||
|
Connecting to MongoDB
|
||||||
|
=====================
|
||||||
|
To connect to a running instance of :program:`mongod`, use the
|
||||||
|
:func:`~mongoengine.connect` function. The first argument is the name of the
|
||||||
|
database to connect to. If the database does not exist, it will be created. If
|
||||||
|
the database requires authentication, :attr:`username` and :attr:`password`
|
||||||
|
arguments may be provided::
|
||||||
|
|
||||||
|
from mongoengine import connect
|
||||||
|
connect('project1', username='webapp', password='pwd123')
|
||||||
|
|
||||||
|
By default, MongoEngine assumes that the :program:`mongod` instance is running
|
||||||
|
on **localhost** on port **27017**. If MongoDB is running elsewhere, you may
|
||||||
|
provide :attr:`host` and :attr:`port` arguments to
|
||||||
|
:func:`~mongoengine.connect`::
|
||||||
|
|
||||||
|
connect('project1', host='192.168.1.35', port=12345)
|
||||||
|
|
||||||
|
Defining documents
|
||||||
|
==================
|
||||||
|
In MongoDB, a **document** is roughly equivalent to a **row** in an RDBMS. When
|
||||||
|
working with relational databases, rows are stored in **tables**, which have a
|
||||||
|
strict **schema** that the rows follow. MongoDB stores documents in
|
||||||
|
**collections** rather than tables - the principle difference is that no schema
|
||||||
|
is enforced at a database level.
|
||||||
|
|
||||||
|
Defining a document's schema
|
||||||
|
----------------------------
|
||||||
|
MongoEngine allows you to define schemata for documents as this helps to reduce
|
||||||
|
coding errors, and allows for utility methods to be defined on fields which may
|
||||||
|
be present.
|
||||||
|
|
||||||
|
To define a schema for a document, create a class that inherits from
|
||||||
|
:class:`~mongoengine.Document`. Fields are specified by adding **field
|
||||||
|
objects** as class attributes to the document class::
|
||||||
|
|
||||||
|
from mongoengine import *
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
class Page(Document):
|
||||||
|
title = StringField(max_length=200, required=True)
|
||||||
|
date_modified = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
|
Fields
|
||||||
|
------
|
||||||
|
By default, fields are not required. To make a field mandatory, set the
|
||||||
|
:attr:`required` keyword argument of a field to ``True``. Fields also may have
|
||||||
|
validation constraints available (such as :attr:`max_length` in the example
|
||||||
|
above). Fields may also take default values, which will be used if a value is
|
||||||
|
not provided. Default values may optionally be a callable, which will be called
|
||||||
|
to retrieve the value (such as in the above example). The field types available
|
||||||
|
are as follows:
|
||||||
|
|
||||||
|
* :class:`~mongoengine.StringField`
|
||||||
|
* :class:`~mongoengine.IntField`
|
||||||
|
* :class:`~mongoengine.FloatField`
|
||||||
|
* :class:`~mongoengine.DateTimeField`
|
||||||
|
* :class:`~mongoengine.ListField`
|
||||||
|
* :class:`~mongoengine.ObjectIdField`
|
||||||
|
* :class:`~mongoengine.EmbeddedDocumentField`
|
||||||
|
* :class:`~mongoengine.ReferenceField`
|
||||||
|
|
||||||
|
List fields
|
||||||
|
^^^^^^^^^^^
|
||||||
|
MongoDB allows the storage of lists of items. To add a list of items to a
|
||||||
|
:class:`~mongoengine.Document`, use the :class:`~mongoengine.ListField` field
|
||||||
|
type. :class:`~mongoengine.ListField` takes another field object as its first
|
||||||
|
argument, which specifies which type elements may be stored within the list::
|
||||||
|
|
||||||
|
class Page(Document):
|
||||||
|
tags = ListField(StringField(max_length=50))
|
||||||
|
|
||||||
|
Embedded documents
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
MongoDB has the ability to embed documents within other documents. Schemata may
|
||||||
|
be defined for these embedded documents, just as they may be for regular
|
||||||
|
documents. To create an embedded document, just define a document as usual, but
|
||||||
|
inherit from :class:`~mongoengine.EmbeddedDocument` rather than
|
||||||
|
:class:`~mongoengine.Document`::
|
||||||
|
|
||||||
|
class Comment(EmbeddedDocument):
|
||||||
|
content = StringField()
|
||||||
|
|
||||||
|
To embed the document within another document, use the
|
||||||
|
:class:`~mongoengine.EmbeddedDocumentField` field type, providing the embedded
|
||||||
|
document class as the first argument::
|
||||||
|
|
||||||
|
class Page(Document):
|
||||||
|
comments = ListField(EmbeddedDocumentField(Comment))
|
||||||
|
|
||||||
|
comment1 = Comment('Good work!')
|
||||||
|
comment2 = Comment('Nice article!')
|
||||||
|
page = Page(comments=[comment1, comment2])
|
||||||
|
|
||||||
|
Reference fields
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
References may be stored to other documents in the database using the
|
||||||
|
:class:`~mongoengine.ReferenceField`. Pass in another document class as the
|
||||||
|
first argument to the constructor, then simply assign document objects to the
|
||||||
|
field::
|
||||||
|
|
||||||
|
class User(Document):
|
||||||
|
name = StringField()
|
||||||
|
|
||||||
|
class Page(Document):
|
||||||
|
content = StringField()
|
||||||
|
author = ReferenceField(User)
|
||||||
|
|
||||||
|
john = User(name="John Smith")
|
||||||
|
john.save()
|
||||||
|
|
||||||
|
post = Page(content="Test Page")
|
||||||
|
post.author = john
|
||||||
|
post.save()
|
||||||
|
|
||||||
|
The :class:`User` object is automatically turned into a reference behind the
|
||||||
|
scenes, and dereferenced when the :class:`Page` object is retrieved.
|
||||||
|
|
||||||
|
Document collections
|
||||||
|
--------------------
|
||||||
|
Document classes that inherit **directly** from :class:`~mongoengine.Document`
|
||||||
|
will have their own **collection** in the database. The name of the collection
|
||||||
|
is by default the name of the class, coverted to lowercase (so in the example
|
||||||
|
above, the collection would be called `page`). If you need to change the name
|
||||||
|
of the collection (e.g. to use MongoEngine with an existing database), then
|
||||||
|
create a class dictionary attribute called :attr:`meta` on your document, and
|
||||||
|
set :attr:`collection` to the name of the collection that you want your
|
||||||
|
document class to use::
|
||||||
|
|
||||||
|
class Page(Document):
|
||||||
|
title = StringField(max_length=200, required=True)
|
||||||
|
meta = {'collection': 'cmsPage'}
|
||||||
|
|
||||||
|
Capped collections
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
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).
|
||||||
|
The following example shows a :class:`Log` document that will be limited to
|
||||||
|
1000 entries and 2MB of disk space::
|
||||||
|
|
||||||
|
class Log(Document):
|
||||||
|
ip_address = StringField()
|
||||||
|
meta = {'max_documents': 1000, 'max_size': 2000000}
|
||||||
|
|
||||||
|
Document inheritance
|
||||||
|
--------------------
|
||||||
|
To create a specialised type of a :class:`~mongoengine.Document` you have
|
||||||
|
defined, you may subclass it and add any extra fields or methods you may need.
|
||||||
|
As this is new class is not a direct subclass of
|
||||||
|
:class:`~mongoengine.Document`, it will not be stored in its own collection; it
|
||||||
|
will use the same collection as its superclass uses. This allows for more
|
||||||
|
convenient and efficient retrieval of related documents::
|
||||||
|
|
||||||
|
# Stored in a collection named 'page'
|
||||||
|
class Page(Document):
|
||||||
|
title = StringField(max_length=200, required=True)
|
||||||
|
|
||||||
|
# Also stored in the collection named 'page'
|
||||||
|
class DatedPage(Page):
|
||||||
|
date = DateTimeField()
|
||||||
|
|
||||||
|
Working with existing data
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
To enable correct retrieval of documents involved in this kind of heirarchy,
|
||||||
|
two extra attributes are stored on each document in the database: :attr:`_cls`
|
||||||
|
and :attr:`_types`. These are hidden from the user through the MongoEngine
|
||||||
|
interface, but may not be present if you are trying to use MongoEngine with
|
||||||
|
an existing database. For this reason, you may disable this inheritance
|
||||||
|
mechansim, removing the dependency of :attr:`_cls` and :attr:`_types`, enabling
|
||||||
|
you to work with existing databases. To disable inheritance on a document
|
||||||
|
class, set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
|
||||||
|
dictionary::
|
||||||
|
|
||||||
|
# Will work with data in an existing collection named 'cmsPage'
|
||||||
|
class Page(Document):
|
||||||
|
title = StringField(max_length=200, required=True)
|
||||||
|
meta = {
|
||||||
|
'collection': 'cmsPage',
|
||||||
|
'allow_inheritance': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
Documents instances
|
||||||
|
===================
|
||||||
|
To create a new document object, create an instance of the relevant document
|
||||||
|
class, providing values for its fields as its constructor keyword arguments.
|
||||||
|
You may provide values for any of the fields on the document::
|
||||||
|
|
||||||
|
>>> page = Page(title="Test Page")
|
||||||
|
>>> page.title
|
||||||
|
'Test Page'
|
||||||
|
|
||||||
|
You may also assign values to the document's fields using standard object
|
||||||
|
attribute syntax::
|
||||||
|
|
||||||
|
>>> page.title = "Example Page"
|
||||||
|
>>> page.title
|
||||||
|
'Example Page'
|
||||||
|
|
||||||
|
Saving and deleting documents
|
||||||
|
-----------------------------
|
||||||
|
To save the document to the database, call the
|
||||||
|
:meth:`~mongoengine.Document.save` method. If the document does not exist in
|
||||||
|
the database, it will be created. If it does already exist, it will be
|
||||||
|
updated.
|
||||||
|
|
||||||
|
To delete a document, call the :meth:`~mongoengine.Document.delete` method.
|
||||||
|
Note that this will only work if the document exists in the database and has a
|
||||||
|
valide :attr:`id`.
|
||||||
|
|
||||||
|
Document IDs
|
||||||
|
------------
|
||||||
|
Each document in the database has a unique id. This may be accessed through the
|
||||||
|
:attr:`id` attribute on :class:`~mongoengine.Document` objects. Usually, the id
|
||||||
|
will be generated automatically by the database server when the object is save,
|
||||||
|
meaning that you may only access the :attr:`id` field once a document has been
|
||||||
|
saved::
|
||||||
|
|
||||||
|
>>> page = Page(title="Test Page")
|
||||||
|
>>> page.id
|
||||||
|
>>> page.save()
|
||||||
|
>>> page.id
|
||||||
|
ObjectId('123456789abcdef000000000')
|
||||||
|
|
||||||
|
Alternatively, you may explicitly set the :attr:`id` before you save the
|
||||||
|
document, but the id must be a valid PyMongo :class:`ObjectId`.
|
||||||
|
|
||||||
|
Querying the database
|
||||||
|
=====================
|
||||||
|
:class:`~mongoengine.Document` classes have an :attr:`objects` attribute, which
|
||||||
|
is used for accessing the objects in the database associated with the class.
|
||||||
|
The :attr:`objects` attribute is actually a
|
||||||
|
:class:`~mongoengine.queryset.QuerySetManager`, which creates and returns a new
|
||||||
|
a new :class:`~mongoengine.queryset.QuerySet` object on access. The
|
||||||
|
:class:`~mongoengine.queryset.QuerySet` object may may be iterated over to
|
||||||
|
fetch documents from the database::
|
||||||
|
|
||||||
|
# Prints out the names of all the users in the database
|
||||||
|
for user in User.objects:
|
||||||
|
print user.name
|
||||||
|
|
||||||
|
Filtering queries
|
||||||
|
-----------------
|
||||||
|
The query may be filtered by calling the
|
||||||
|
:class:`~mongoengine.queryset.QuerySet` object with field lookup keyword
|
||||||
|
arguments. The keys in the keyword arguments correspond to fields on the
|
||||||
|
:class:`~mongoengine.Document` you are querying::
|
||||||
|
|
||||||
|
# This will return a QuerySet that will only iterate over users whose
|
||||||
|
# 'country' field is set to 'uk'
|
||||||
|
uk_users = User.objects(country='uk')
|
||||||
|
|
||||||
|
Fields on embedded documents may also be referred to using field lookup syntax
|
||||||
|
by using a double-underscore in place of the dot in object attribute access
|
||||||
|
syntax::
|
||||||
|
|
||||||
|
# This will return a QuerySet that will only iterate over pages that have
|
||||||
|
# been written by a user whose 'country' field is set to 'uk'
|
||||||
|
uk_pages = Page.objects(author__country='uk')
|
||||||
|
|
||||||
|
Querying lists
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
On most fields, this syntax will look up documents where the field specified
|
||||||
|
matches the given value exactly, but when the field refers to a
|
||||||
|
:class:`~mongoengine.ListField`, a single item may be provided, in which case
|
||||||
|
lists that contain that item will be matched::
|
||||||
|
|
||||||
|
class Page(Document):
|
||||||
|
tags = ListField(StringField())
|
||||||
|
|
||||||
|
# This will match all pages that have the word 'coding' as an item in the
|
||||||
|
# 'tags' list
|
||||||
|
Page.objects(tags='coding')
|
||||||
|
|
||||||
|
Query operators
|
||||||
|
---------------
|
||||||
|
Operators other than equality may also be used in queries; just attach the
|
||||||
|
operator name to a key with a double-underscore::
|
||||||
|
|
||||||
|
# Only find users whose age is 18 or less
|
||||||
|
young_users = Users.objects(age__lte=18)
|
||||||
|
|
||||||
|
Available operators are as follows:
|
||||||
|
|
||||||
|
* ``neq`` -- not equal to
|
||||||
|
* ``lt`` -- less than
|
||||||
|
* ``lte`` -- less than or equal to
|
||||||
|
* ``gt`` -- greater than
|
||||||
|
* ``gte`` -- greater than or equal to
|
||||||
|
* ``in`` -- value is in list (a list of values should be provided)
|
||||||
|
* ``nin`` -- value is not in list (a list of values should be provided)
|
||||||
|
* ``mod`` -- ``value % x == y``, where ``x`` and ``y`` are two provided values
|
||||||
|
* ``all`` -- every item in array is in list of values provided
|
||||||
|
* ``size`` -- the size of the array is
|
||||||
|
* ``exists`` -- value for field exists
|
||||||
|
|
||||||
|
Limiting and skipping results
|
||||||
|
-----------------------------
|
||||||
|
Just as with traditional ORMs, you may limit the number of results returned, or
|
||||||
|
skip a number or results in you query.
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.limit` and
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on
|
||||||
|
:class:`~mongoengine.queryset.QuerySet` objects, but the prefered syntax for
|
||||||
|
achieving this is using array-slicing syntax::
|
||||||
|
|
||||||
|
# Only the first 5 people
|
||||||
|
users = User.objects[:5]
|
||||||
|
|
||||||
|
# All except for the first 5 people
|
||||||
|
users = User.objects[5:]
|
||||||
|
|
||||||
|
# 5 users, starting from the 10th user found
|
||||||
|
users = User.objects[10:15]
|
||||||
|
|
||||||
|
Aggregation
|
||||||
|
-----------
|
||||||
|
MongoDB provides some aggregation methods out of the box, but there are not as
|
||||||
|
many as you typically get with an RDBMS. MongoEngine provides a wrapper around
|
||||||
|
the built-in methods and provides some of its own, which are implemented as
|
||||||
|
Javascript code that is executed on the database server.
|
||||||
|
|
||||||
|
Counting results
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
Just as with limiting and skipping results, there is a method on
|
||||||
|
:class:`~mongoengine.queryset.QuerySet` objects --
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.count`, but there is also a more Pythonic
|
||||||
|
way of achieving this::
|
||||||
|
|
||||||
|
num_users = len(User.objects)
|
||||||
|
|
||||||
|
Further aggregation
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
You may sum over the values of a specific field on documents using
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.sum`::
|
||||||
|
|
||||||
|
yearly_expense = Employee.objects.sum('salary')
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If the field isn't present on a document, that document will be ignored from
|
||||||
|
the sum.
|
||||||
|
|
||||||
|
To get the average (mean) of a field on a collection of documents, use
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.average`::
|
||||||
|
|
||||||
|
mean_age = User.objects.average('age')
|
||||||
|
|
||||||
|
As MongoDB provides native lists, MongoEngine provides a helper method to get a
|
||||||
|
dictionary of the frequencies of items in lists across an entire collection --
|
||||||
|
:meth:`~mongoengine.queryset.QuerySet.item_frequencies`. An example of its use
|
||||||
|
would be generating "tag-clouds"::
|
||||||
|
|
||||||
|
class Article(Document):
|
||||||
|
tag = ListField(StringField())
|
||||||
|
|
||||||
|
# After adding some tagged articles...
|
||||||
|
tag_freqs = Article.objects.item_frequencies('tag', normalize=True)
|
||||||
|
|
||||||
|
from operator import itemgetter
|
||||||
|
top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10]
|
||||||
|
|
@ -4,9 +4,21 @@ import fields
|
|||||||
from fields import *
|
from fields import *
|
||||||
import connection
|
import connection
|
||||||
from connection import *
|
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'
|
__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()
|
||||||
|
|
||||||
|
@ -28,26 +28,15 @@ class BaseField(object):
|
|||||||
# Get value from document instance if available, if not use default
|
# Get value from document instance if available, if not use default
|
||||||
value = instance._data.get(self.name)
|
value = instance._data.get(self.name)
|
||||||
if value is None:
|
if value is None:
|
||||||
if self.default is not None:
|
value = self.default
|
||||||
value = self.default
|
# Allow callable default values
|
||||||
if callable(value):
|
if callable(value):
|
||||||
value = value()
|
value = value()
|
||||||
else:
|
|
||||||
raise AttributeError(self.name)
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
"""Descriptor for assigning a value to a field in a document. Do any
|
"""Descriptor for assigning a value to a field in a document.
|
||||||
necessary conversion between Python and MongoDB types.
|
|
||||||
"""
|
"""
|
||||||
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
|
instance._data[self.name] = value
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
@ -156,6 +145,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
meta = {
|
meta = {
|
||||||
'collection': collection,
|
'collection': collection,
|
||||||
'allow_inheritance': True,
|
'allow_inheritance': True,
|
||||||
|
'max_documents': None,
|
||||||
|
'max_size': None,
|
||||||
}
|
}
|
||||||
meta.update(attrs.get('meta', {}))
|
meta.update(attrs.get('meta', {}))
|
||||||
# Only simple classes - direct subclasses of Document - may set
|
# 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
|
# Set up collection manager, needs the class to have fields so use
|
||||||
# DocumentMetaclass before instantiating CollectionManager object
|
# DocumentMetaclass before instantiating CollectionManager object
|
||||||
new_class = super_new(cls, name, bases, attrs)
|
new_class = super_new(cls, name, bases, attrs)
|
||||||
new_class.objects = QuerySetManager(new_class)
|
new_class.objects = QuerySetManager()
|
||||||
|
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
@ -186,8 +177,6 @@ class BaseDocument(object):
|
|||||||
else:
|
else:
|
||||||
# Use default value if present
|
# Use default value if present
|
||||||
value = getattr(self, attr_name, None)
|
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)
|
setattr(self, attr_name, value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -228,8 +217,8 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
def __contains__(self, name):
|
def __contains__(self, name):
|
||||||
try:
|
try:
|
||||||
getattr(self, name)
|
val = getattr(self, name)
|
||||||
return True
|
return val is not None
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -29,15 +29,13 @@ def _get_db():
|
|||||||
raise ConnectionError('Not connected to database')
|
raise ConnectionError('Not connected to database')
|
||||||
return _db
|
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
|
"""Connect to the database specified by the 'db' argument. Connection
|
||||||
settings may be provided here as well if the database is not running on
|
settings may be provided here as well if the database is not running on
|
||||||
the default port on localhost. If authentication is needed, provide
|
the default port on localhost. If authentication is needed, provide
|
||||||
username and password arguments as well.
|
username and password arguments as well.
|
||||||
"""
|
"""
|
||||||
global _db
|
global _db
|
||||||
if db is None:
|
|
||||||
raise TypeError('"db" argument must be provided to connect()')
|
|
||||||
|
|
||||||
_connection_settings.update(kwargs)
|
_connection_settings.update(kwargs)
|
||||||
connection = _get_connection()
|
connection = _get_connection()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from base import DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument
|
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
|
||||||
|
ValidationError)
|
||||||
from connection import _get_db
|
from connection import _get_db
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +36,14 @@ class Document(BaseDocument):
|
|||||||
though). To disable this behaviour and remove the dependence on the
|
though). To disable this behaviour and remove the dependence on the
|
||||||
presence of `_cls` and `_types`, set :attr:`allow_inheritance` to
|
presence of `_cls` and `_types`, set :attr:`allow_inheritance` to
|
||||||
``False`` in the :attr:`meta` dictionary.
|
``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
|
__metaclass__ = TopLevelDocumentMetaclass
|
||||||
@ -44,15 +53,43 @@ class Document(BaseDocument):
|
|||||||
document already exists, it will be updated, otherwise it will be
|
document already exists, it will be updated, otherwise it will be
|
||||||
created.
|
created.
|
||||||
"""
|
"""
|
||||||
object_id = self.objects._collection.save(self.to_mongo())
|
self.validate()
|
||||||
self.id = object_id
|
object_id = self.__class__.objects._collection.save(self.to_mongo())
|
||||||
|
self.id = self._fields['id'].to_python(object_id)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete the :class:`~mongoengine.Document` from the database. This
|
"""Delete the :class:`~mongoengine.Document` from the database. This
|
||||||
will only take effect if the document has been previously saved.
|
will only take effect if the document has been previously saved.
|
||||||
"""
|
"""
|
||||||
object_id = self._fields['id'].to_mongo(self.id)
|
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
|
@classmethod
|
||||||
def drop_collection(cls):
|
def drop_collection(cls):
|
||||||
|
@ -34,6 +34,9 @@ class StringField(BaseField):
|
|||||||
message = 'String value did not match validation regex'
|
message = 'String value did not match validation regex'
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
|
|
||||||
|
def lookup_member(self, member_name):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class IntField(BaseField):
|
class IntField(BaseField):
|
||||||
"""An integer field.
|
"""An integer field.
|
||||||
@ -114,6 +117,9 @@ class EmbeddedDocumentField(BaseField):
|
|||||||
raise ValidationError('Invalid embedded document instance '
|
raise ValidationError('Invalid embedded document instance '
|
||||||
'provided to an EmbeddedDocumentField')
|
'provided to an EmbeddedDocumentField')
|
||||||
|
|
||||||
|
def lookup_member(self, member_name):
|
||||||
|
return self.document._fields.get(member_name)
|
||||||
|
|
||||||
|
|
||||||
class ListField(BaseField):
|
class ListField(BaseField):
|
||||||
"""A list field that wraps a standard field, allowing multiple instances
|
"""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 '
|
raise ValidationError('All items in a list field must be of the '
|
||||||
'specified type')
|
'specified type')
|
||||||
|
|
||||||
|
def lookup_member(self, member_name):
|
||||||
|
return self.field.lookup_member(member_name)
|
||||||
|
|
||||||
|
|
||||||
class ReferenceField(BaseField):
|
class ReferenceField(BaseField):
|
||||||
"""A reference to a document that will be automatically dereferenced on
|
"""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)):
|
if isinstance(document, (str, unicode, pymongo.objectid.ObjectId)):
|
||||||
id_ = document
|
id_ = document
|
||||||
else:
|
else:
|
||||||
try:
|
id_ = document.id
|
||||||
id_ = document.id
|
if id_ is None:
|
||||||
except:
|
|
||||||
raise ValidationError('You can only reference documents once '
|
raise ValidationError('You can only reference documents once '
|
||||||
'they have been saved to the database')
|
'they have been saved to the database')
|
||||||
|
|
||||||
@ -195,3 +203,6 @@ class ReferenceField(BaseField):
|
|||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
assert(isinstance(value, (self.document_type, pymongo.dbref.DBRef)))
|
assert(isinstance(value, (self.document_type, pymongo.dbref.DBRef)))
|
||||||
|
|
||||||
|
def lookup_member(self, member_name):
|
||||||
|
return self.document_type._fields.get(member_name)
|
||||||
|
@ -3,6 +3,13 @@ from connection import _get_db
|
|||||||
import pymongo
|
import pymongo
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['queryset_manager', 'InvalidQueryError', 'InvalidCollectionError']
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQueryError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class QuerySet(object):
|
class QuerySet(object):
|
||||||
"""A set of results returned from a query. Wraps a MongoDB cursor,
|
"""A set of results returned from a query. Wraps a MongoDB cursor,
|
||||||
providing :class:`~mongoengine.Document` objects as the results.
|
providing :class:`~mongoengine.Document` objects as the results.
|
||||||
@ -36,7 +43,8 @@ class QuerySet(object):
|
|||||||
"""Filter the selected documents by calling the
|
"""Filter the selected documents by calling the
|
||||||
:class:`~mongoengine.QuerySet` with a query.
|
: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
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -44,9 +52,30 @@ class QuerySet(object):
|
|||||||
if not self._cursor_obj:
|
if not self._cursor_obj:
|
||||||
self._cursor_obj = self._collection.find(self._query)
|
self._cursor_obj = self._collection.find(self._query)
|
||||||
return self._cursor_obj
|
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
|
@classmethod
|
||||||
def _transform_query(cls, **query):
|
def _transform_query(cls, _doc_cls=None, **query):
|
||||||
"""Transform a query from Django-style format to Mongo format.
|
"""Transform a query from Django-style format to Mongo format.
|
||||||
"""
|
"""
|
||||||
operators = ['neq', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
operators = ['neq', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
||||||
@ -61,6 +90,10 @@ class QuerySet(object):
|
|||||||
op = parts.pop()
|
op = parts.pop()
|
||||||
value = {'$' + op: value}
|
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)
|
key = '.'.join(parts)
|
||||||
if op is None or key not in mongo_query:
|
if op is None or key not in mongo_query:
|
||||||
mongo_query[key] = value
|
mongo_query[key] = value
|
||||||
@ -72,9 +105,10 @@ class QuerySet(object):
|
|||||||
def first(self):
|
def first(self):
|
||||||
"""Retrieve the first object matching the query.
|
"""Retrieve the first object matching the query.
|
||||||
"""
|
"""
|
||||||
result = self._collection.find_one(self._query)
|
try:
|
||||||
if result is not None:
|
result = self[0]
|
||||||
result = self._document._from_son(result)
|
except IndexError:
|
||||||
|
result = None
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def with_id(self, object_id):
|
def with_id(self, object_id):
|
||||||
@ -98,6 +132,9 @@ class QuerySet(object):
|
|||||||
"""
|
"""
|
||||||
return self._cursor.count()
|
return self._cursor.count()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.count()
|
||||||
|
|
||||||
def limit(self, n):
|
def limit(self, n):
|
||||||
"""Limit the number of returned documents to `n`. This may also be
|
"""Limit the number of returned documents to `n`. This may also be
|
||||||
achieved using array-slicing syntax (e.g. ``User.objects[:5]``).
|
achieved using array-slicing syntax (e.g. ``User.objects[:5]``).
|
||||||
@ -161,15 +198,99 @@ class QuerySet(object):
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return 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):
|
class QuerySetManager(object):
|
||||||
|
|
||||||
def __init__(self, document):
|
def __init__(self, manager_func=None):
|
||||||
db = _get_db()
|
self._manager_func = manager_func
|
||||||
self._document = document
|
self._collection = None
|
||||||
self._collection_name = document._meta['collection']
|
|
||||||
# This will create the collection if it doesn't exist
|
|
||||||
self._collection = db[self._collection_name]
|
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
"""Descriptor for instantiating a new QuerySet object when
|
"""Descriptor for instantiating a new QuerySet object when
|
||||||
@ -178,6 +299,47 @@ class QuerySetManager(object):
|
|||||||
if instance is not None:
|
if instance is not None:
|
||||||
# Document class being used rather than a document object
|
# Document class being used rather than a document object
|
||||||
return self
|
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
|
# owner is the document that contains the QuerySetManager
|
||||||
return QuerySet(self._document, self._collection)
|
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)
|
||||||
|
37
setup.py
Normal file
37
setup.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
VERSION = '0.1.1'
|
||||||
|
|
||||||
|
DESCRIPTION = "A Python Document-Object Mapper for working with MongoDB"
|
||||||
|
|
||||||
|
LONG_DESCRIPTION = None
|
||||||
|
try:
|
||||||
|
LONG_DESCRIPTION = open('README.rst').read()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
CLASSIFIERS = [
|
||||||
|
'Development Status :: 4 - Beta',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Topic :: Database',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(name='mongoengine',
|
||||||
|
version=VERSION,
|
||||||
|
packages=find_packages(),
|
||||||
|
author='Harry Marr',
|
||||||
|
author_email='harry.marr@{nospam}gmail.com',
|
||||||
|
url='http://hmarr.com/mongoengine/',
|
||||||
|
license='MIT',
|
||||||
|
include_package_data=True,
|
||||||
|
description=DESCRIPTION,
|
||||||
|
long_description=LONG_DESCRIPTION,
|
||||||
|
platforms=['any'],
|
||||||
|
classifiers=CLASSIFIERS,
|
||||||
|
install_requires=['pymongo'],
|
||||||
|
test_suite='tests',
|
||||||
|
)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@ -1,4 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
import datetime
|
||||||
import pymongo
|
import pymongo
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
@ -156,6 +157,70 @@ class DocumentTest(unittest.TestCase):
|
|||||||
meta = {'allow_inheritance': False}
|
meta = {'allow_inheritance': False}
|
||||||
self.assertRaises(ValueError, create_employee_class)
|
self.assertRaises(ValueError, create_employee_class)
|
||||||
|
|
||||||
|
def test_collection_name(self):
|
||||||
|
"""Ensure that a collection with a specified name may be used.
|
||||||
|
"""
|
||||||
|
collection = 'personCollTest'
|
||||||
|
if collection in self.db.collection_names():
|
||||||
|
self.db.drop_collection(collection)
|
||||||
|
|
||||||
|
class Person(Document):
|
||||||
|
name = StringField()
|
||||||
|
meta = {'collection': collection}
|
||||||
|
|
||||||
|
user = Person(name="Test User")
|
||||||
|
user.save()
|
||||||
|
self.assertTrue(collection in self.db.collection_names())
|
||||||
|
|
||||||
|
user_obj = self.db[collection].find_one()
|
||||||
|
self.assertEqual(user_obj['name'], "Test User")
|
||||||
|
|
||||||
|
user_obj = Person.objects[0]
|
||||||
|
self.assertEqual(user_obj.name, "Test User")
|
||||||
|
|
||||||
|
Person.drop_collection()
|
||||||
|
self.assertFalse(collection in self.db.collection_names())
|
||||||
|
|
||||||
|
def test_capped_collection(self):
|
||||||
|
"""Ensure that capped collections work properly.
|
||||||
|
"""
|
||||||
|
class Log(Document):
|
||||||
|
date = DateTimeField(default=datetime.datetime.now)
|
||||||
|
meta = {
|
||||||
|
'max_documents': 10,
|
||||||
|
'max_size': 90000,
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.drop_collection()
|
||||||
|
|
||||||
|
# Ensure that the collection handles up to its maximum
|
||||||
|
for i in range(10):
|
||||||
|
Log().save()
|
||||||
|
|
||||||
|
self.assertEqual(len(Log.objects), 10)
|
||||||
|
|
||||||
|
# Check that extra documents don't increase the size
|
||||||
|
Log().save()
|
||||||
|
self.assertEqual(len(Log.objects), 10)
|
||||||
|
|
||||||
|
options = Log.objects._collection.options()
|
||||||
|
self.assertEqual(options['capped'], True)
|
||||||
|
self.assertEqual(options['max'], 10)
|
||||||
|
self.assertEqual(options['size'], 90000)
|
||||||
|
|
||||||
|
# Check that the document cannot be redefined with different options
|
||||||
|
def recreate_log_document():
|
||||||
|
class Log(Document):
|
||||||
|
date = DateTimeField(default=datetime.datetime.now)
|
||||||
|
meta = {
|
||||||
|
'max_documents': 11,
|
||||||
|
}
|
||||||
|
# Create the collection by accessing Document.objects
|
||||||
|
Log.objects
|
||||||
|
self.assertRaises(InvalidCollectionError, recreate_log_document)
|
||||||
|
|
||||||
|
Log.drop_collection()
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
"""Ensure that document may be created using keyword arguments.
|
"""Ensure that document may be created using keyword arguments.
|
||||||
"""
|
"""
|
||||||
@ -163,6 +228,24 @@ class DocumentTest(unittest.TestCase):
|
|||||||
self.assertEqual(person.name, "Test User")
|
self.assertEqual(person.name, "Test User")
|
||||||
self.assertEqual(person.age, 30)
|
self.assertEqual(person.age, 30)
|
||||||
|
|
||||||
|
def test_reload(self):
|
||||||
|
"""Ensure that attributes may be reloaded.
|
||||||
|
"""
|
||||||
|
person = self.Person(name="Test User", age=20)
|
||||||
|
person.save()
|
||||||
|
|
||||||
|
person_obj = self.Person.objects.first()
|
||||||
|
person_obj.name = "Mr Test User"
|
||||||
|
person_obj.age = 21
|
||||||
|
person_obj.save()
|
||||||
|
|
||||||
|
self.assertEqual(person.name, "Test User")
|
||||||
|
self.assertEqual(person.age, 20)
|
||||||
|
|
||||||
|
person.reload()
|
||||||
|
self.assertEqual(person.name, "Mr Test User")
|
||||||
|
self.assertEqual(person.age, 21)
|
||||||
|
|
||||||
def test_dictionary_access(self):
|
def test_dictionary_access(self):
|
||||||
"""Ensure that dictionary-style field access works properly.
|
"""Ensure that dictionary-style field access works properly.
|
||||||
"""
|
"""
|
||||||
@ -204,16 +287,16 @@ class DocumentTest(unittest.TestCase):
|
|||||||
person_obj = collection.find_one({'name': 'Test User'})
|
person_obj = collection.find_one({'name': 'Test User'})
|
||||||
self.assertEqual(person_obj['name'], 'Test User')
|
self.assertEqual(person_obj['name'], 'Test User')
|
||||||
self.assertEqual(person_obj['age'], 30)
|
self.assertEqual(person_obj['age'], 30)
|
||||||
self.assertEqual(person_obj['_id'], person.id)
|
self.assertEqual(str(person_obj['_id']), person.id)
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
"""Ensure that document may be deleted using the delete method.
|
"""Ensure that document may be deleted using the delete method.
|
||||||
"""
|
"""
|
||||||
person = self.Person(name="Test User", age=30)
|
person = self.Person(name="Test User", age=30)
|
||||||
person.save()
|
person.save()
|
||||||
self.assertEqual(self.Person.objects.count(), 1)
|
self.assertEqual(len(self.Person.objects), 1)
|
||||||
person.delete()
|
person.delete()
|
||||||
self.assertEqual(self.Person.objects.count(), 0)
|
self.assertEqual(len(self.Person.objects), 0)
|
||||||
|
|
||||||
def test_save_custom_id(self):
|
def test_save_custom_id(self):
|
||||||
"""Ensure that a document may be saved with a custom _id.
|
"""Ensure that a document may be saved with a custom _id.
|
||||||
|
113
tests/fields.py
113
tests/fields.py
@ -31,13 +31,10 @@ class FieldTest(unittest.TestCase):
|
|||||||
age = IntField(required=True)
|
age = IntField(required=True)
|
||||||
userid = StringField()
|
userid = StringField()
|
||||||
|
|
||||||
self.assertRaises(ValidationError, Person, name="Test User")
|
person = Person(name="Test User")
|
||||||
self.assertRaises(ValidationError, Person, age=30)
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
person = Person(age=30)
|
||||||
person = Person(name="Test User", age=30, userid="testuser")
|
self.assertRaises(ValidationError, person.validate)
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'name', None)
|
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'age', None)
|
|
||||||
person.userid = None
|
|
||||||
|
|
||||||
def test_object_id_validation(self):
|
def test_object_id_validation(self):
|
||||||
"""Ensure that invalid values cannot be assigned to string fields.
|
"""Ensure that invalid values cannot be assigned to string fields.
|
||||||
@ -46,10 +43,16 @@ class FieldTest(unittest.TestCase):
|
|||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
person = Person(name='Test User')
|
person = Person(name='Test User')
|
||||||
self.assertRaises(AttributeError, getattr, person, 'id')
|
self.assertEqual(person.id, None)
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'id', 47)
|
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'id', 'abc')
|
person.id = 47
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
|
person.id = 'abc'
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
person.id = '497ce96f395f2f052a494fd4'
|
person.id = '497ce96f395f2f052a494fd4'
|
||||||
|
person.validate()
|
||||||
|
|
||||||
def test_string_validation(self):
|
def test_string_validation(self):
|
||||||
"""Ensure that invalid values cannot be assigned to string fields.
|
"""Ensure that invalid values cannot be assigned to string fields.
|
||||||
@ -58,20 +61,23 @@ class FieldTest(unittest.TestCase):
|
|||||||
name = StringField(max_length=20)
|
name = StringField(max_length=20)
|
||||||
userid = StringField(r'[0-9a-z_]+$')
|
userid = StringField(r'[0-9a-z_]+$')
|
||||||
|
|
||||||
person = Person()
|
person = Person(name=34)
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'name', 34)
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
# Test regex validation on userid
|
# Test regex validation on userid
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'userid',
|
person = Person(userid='test.User')
|
||||||
'test.User')
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
person.userid = 'test_user'
|
person.userid = 'test_user'
|
||||||
self.assertEqual(person.userid, 'test_user')
|
self.assertEqual(person.userid, 'test_user')
|
||||||
|
person.validate()
|
||||||
|
|
||||||
# Test max length validation on name
|
# Test max length validation on name
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'name',
|
person = Person(name='Name that is more than twenty characters')
|
||||||
'Name that is more than twenty characters')
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
person.name = 'Shorter name'
|
person.name = 'Shorter name'
|
||||||
self.assertEqual(person.name, 'Shorter name')
|
person.validate()
|
||||||
|
|
||||||
def test_int_validation(self):
|
def test_int_validation(self):
|
||||||
"""Ensure that invalid values cannot be assigned to int fields.
|
"""Ensure that invalid values cannot be assigned to int fields.
|
||||||
@ -81,9 +87,14 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
person = Person()
|
person = Person()
|
||||||
person.age = 50
|
person.age = 50
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'age', -1)
|
person.validate()
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'age', 120)
|
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'age', 'ten')
|
person.age = -1
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
person.age = 120
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
person.age = 'ten'
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
def test_float_validation(self):
|
def test_float_validation(self):
|
||||||
"""Ensure that invalid values cannot be assigned to float fields.
|
"""Ensure that invalid values cannot be assigned to float fields.
|
||||||
@ -93,9 +104,14 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
person = Person()
|
person = Person()
|
||||||
person.height = 1.89
|
person.height = 1.89
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'height', 2)
|
person.validate()
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'height', 0.01)
|
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'height', 4.0)
|
person.height = 2
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
person.height = 0.01
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
person.height = 4.0
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
def test_datetime_validation(self):
|
def test_datetime_validation(self):
|
||||||
"""Ensure that invalid values cannot be assigned to datetime fields.
|
"""Ensure that invalid values cannot be assigned to datetime fields.
|
||||||
@ -104,9 +120,13 @@ class FieldTest(unittest.TestCase):
|
|||||||
time = DateTimeField()
|
time = DateTimeField()
|
||||||
|
|
||||||
log = LogEntry()
|
log = LogEntry()
|
||||||
self.assertRaises(ValidationError, log.__setattr__, 'time', -1)
|
|
||||||
self.assertRaises(ValidationError, log.__setattr__, 'time', '1pm')
|
|
||||||
log.time = datetime.datetime.now()
|
log.time = datetime.datetime.now()
|
||||||
|
log.validate()
|
||||||
|
|
||||||
|
log.time = -1
|
||||||
|
self.assertRaises(ValidationError, log.validate)
|
||||||
|
log.time = '1pm'
|
||||||
|
self.assertRaises(ValidationError, log.validate)
|
||||||
|
|
||||||
def test_list_validation(self):
|
def test_list_validation(self):
|
||||||
"""Ensure that a list field only accepts lists with valid elements.
|
"""Ensure that a list field only accepts lists with valid elements.
|
||||||
@ -120,16 +140,26 @@ class FieldTest(unittest.TestCase):
|
|||||||
tags = ListField(StringField())
|
tags = ListField(StringField())
|
||||||
|
|
||||||
post = BlogPost(content='Went for a walk today...')
|
post = BlogPost(content='Went for a walk today...')
|
||||||
self.assertRaises(ValidationError, post.__setattr__, 'tags', 'fun')
|
post.validate()
|
||||||
self.assertRaises(ValidationError, post.__setattr__, 'tags', [1, 2])
|
|
||||||
|
post.tags = 'fun'
|
||||||
|
self.assertRaises(ValidationError, post.validate)
|
||||||
|
post.tags = [1, 2]
|
||||||
|
self.assertRaises(ValidationError, post.validate)
|
||||||
|
|
||||||
post.tags = ['fun', 'leisure']
|
post.tags = ['fun', 'leisure']
|
||||||
|
post.validate()
|
||||||
post.tags = ('fun', 'leisure')
|
post.tags = ('fun', 'leisure')
|
||||||
|
post.validate()
|
||||||
|
|
||||||
comments = [Comment(content='Good for you'), Comment(content='Yay.')]
|
comments = [Comment(content='Good for you'), Comment(content='Yay.')]
|
||||||
self.assertRaises(ValidationError, post.__setattr__, 'comments', ['a'])
|
|
||||||
self.assertRaises(ValidationError, post.__setattr__, 'comments', 'Yay')
|
|
||||||
self.assertRaises(ValidationError, post.__setattr__, 'comments', 'Yay')
|
|
||||||
post.comments = comments
|
post.comments = comments
|
||||||
|
post.validate()
|
||||||
|
|
||||||
|
post.comments = ['a']
|
||||||
|
self.assertRaises(ValidationError, post.validate)
|
||||||
|
post.comments = 'yay'
|
||||||
|
self.assertRaises(ValidationError, post.validate)
|
||||||
|
|
||||||
def test_embedded_document_validation(self):
|
def test_embedded_document_validation(self):
|
||||||
"""Ensure that invalid embedded documents cannot be assigned to
|
"""Ensure that invalid embedded documents cannot be assigned to
|
||||||
@ -147,12 +177,15 @@ class FieldTest(unittest.TestCase):
|
|||||||
preferences = EmbeddedDocumentField(PersonPreferences)
|
preferences = EmbeddedDocumentField(PersonPreferences)
|
||||||
|
|
||||||
person = Person(name='Test User')
|
person = Person(name='Test User')
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'preferences',
|
person.preferences = 'My Preferences'
|
||||||
'My preferences')
|
self.assertRaises(ValidationError, person.validate)
|
||||||
self.assertRaises(ValidationError, person.__setattr__, 'preferences',
|
|
||||||
Comment(content='Nice blog post...'))
|
person.preferences = Comment(content='Nice blog post...')
|
||||||
|
self.assertRaises(ValidationError, person.validate)
|
||||||
|
|
||||||
person.preferences = PersonPreferences(food='Cheese', number=47)
|
person.preferences = PersonPreferences(food='Cheese', number=47)
|
||||||
self.assertEqual(person.preferences.food, 'Cheese')
|
self.assertEqual(person.preferences.food, 'Cheese')
|
||||||
|
person.validate()
|
||||||
|
|
||||||
def test_embedded_document_inheritance(self):
|
def test_embedded_document_inheritance(self):
|
||||||
"""Ensure that subclasses of embedded documents may be provided to
|
"""Ensure that subclasses of embedded documents may be provided to
|
||||||
@ -173,8 +206,8 @@ class FieldTest(unittest.TestCase):
|
|||||||
post.author = PowerUser(name='Test User', power=47)
|
post.author = PowerUser(name='Test User', power=47)
|
||||||
|
|
||||||
def test_reference_validation(self):
|
def test_reference_validation(self):
|
||||||
"""Ensure that invalid embedded documents cannot be assigned to
|
"""Ensure that invalid docment objects cannot be assigned to reference
|
||||||
embedded document fields.
|
fields.
|
||||||
"""
|
"""
|
||||||
class User(Document):
|
class User(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
@ -187,19 +220,23 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
user = User(name='Test User')
|
user = User(name='Test User')
|
||||||
|
|
||||||
|
# Ensure that the referenced object must have been saved
|
||||||
post1 = BlogPost(content='Chips and gravy taste good.')
|
post1 = BlogPost(content='Chips and gravy taste good.')
|
||||||
post1.author = user
|
post1.author = user
|
||||||
self.assertRaises(ValidationError, post1.save)
|
self.assertRaises(ValidationError, post1.save)
|
||||||
|
|
||||||
|
# Check that an invalid object type cannot be used
|
||||||
post2 = BlogPost(content='Chips and chilli taste good.')
|
post2 = BlogPost(content='Chips and chilli taste good.')
|
||||||
self.assertRaises(ValidationError, post1.__setattr__, 'author', post2)
|
post1.author = post2
|
||||||
|
self.assertRaises(ValidationError, post1.validate)
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
post1.author = user
|
post1.author = user
|
||||||
post1.save()
|
post1.save()
|
||||||
|
|
||||||
post2.save()
|
post2.save()
|
||||||
self.assertRaises(ValidationError, post1.__setattr__, 'author', post2)
|
post1.author = post2
|
||||||
|
self.assertRaises(ValidationError, post1.validate)
|
||||||
|
|
||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
@ -50,7 +50,7 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Find all people in the collection
|
# Find all people in the collection
|
||||||
people = self.Person.objects
|
people = self.Person.objects
|
||||||
self.assertEqual(people.count(), 2)
|
self.assertEqual(len(people), 2)
|
||||||
results = list(people)
|
results = list(people)
|
||||||
self.assertTrue(isinstance(results[0], self.Person))
|
self.assertTrue(isinstance(results[0], self.Person))
|
||||||
self.assertTrue(isinstance(results[0].id, (pymongo.objectid.ObjectId,
|
self.assertTrue(isinstance(results[0].id, (pymongo.objectid.ObjectId,
|
||||||
@ -62,7 +62,7 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Use a query to filter the people found to just person1
|
# Use a query to filter the people found to just person1
|
||||||
people = self.Person.objects(age=20)
|
people = self.Person.objects(age=20)
|
||||||
self.assertEqual(people.count(), 1)
|
self.assertEqual(len(people), 1)
|
||||||
person = people.next()
|
person = people.next()
|
||||||
self.assertEqual(person.name, "User A")
|
self.assertEqual(person.name, "User A")
|
||||||
self.assertEqual(person.age, 20)
|
self.assertEqual(person.age, 20)
|
||||||
@ -158,13 +158,13 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
self.Person(name="User B", age=30).save()
|
self.Person(name="User B", age=30).save()
|
||||||
self.Person(name="User C", age=40).save()
|
self.Person(name="User C", age=40).save()
|
||||||
|
|
||||||
self.assertEqual(self.Person.objects.count(), 3)
|
self.assertEqual(len(self.Person.objects), 3)
|
||||||
|
|
||||||
self.Person.objects(age__lt=30).delete()
|
self.Person.objects(age__lt=30).delete()
|
||||||
self.assertEqual(self.Person.objects.count(), 2)
|
self.assertEqual(len(self.Person.objects), 2)
|
||||||
|
|
||||||
self.Person.objects.delete()
|
self.Person.objects.delete()
|
||||||
self.assertEqual(self.Person.objects.count(), 0)
|
self.assertEqual(len(self.Person.objects), 0)
|
||||||
|
|
||||||
def test_order_by(self):
|
def test_order_by(self):
|
||||||
"""Ensure that QuerySets may be ordered.
|
"""Ensure that QuerySets may be ordered.
|
||||||
@ -185,6 +185,121 @@ class QuerySetTest(unittest.TestCase):
|
|||||||
ages = [p.age for p in self.Person.objects.order_by('-name')]
|
ages = [p.age for p in self.Person.objects.order_by('-name')]
|
||||||
self.assertEqual(ages, [30, 40, 20])
|
self.assertEqual(ages, [30, 40, 20])
|
||||||
|
|
||||||
|
def test_item_frequencies(self):
|
||||||
|
"""Ensure that item frequencies are properly generated from lists.
|
||||||
|
"""
|
||||||
|
class BlogPost(Document):
|
||||||
|
hits = IntField()
|
||||||
|
tags = ListField(StringField(), name='blogTags')
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
BlogPost(hits=1, tags=['music', 'film', 'actors']).save()
|
||||||
|
BlogPost(hits=2, tags=['music']).save()
|
||||||
|
BlogPost(hits=3, tags=['music', 'actors']).save()
|
||||||
|
|
||||||
|
f = BlogPost.objects.item_frequencies('tags')
|
||||||
|
f = dict((key, int(val)) for key, val in f.items())
|
||||||
|
self.assertEqual(set(['music', 'film', 'actors']), set(f.keys()))
|
||||||
|
self.assertEqual(f['music'], 3)
|
||||||
|
self.assertEqual(f['actors'], 2)
|
||||||
|
self.assertEqual(f['film'], 1)
|
||||||
|
|
||||||
|
# Ensure query is taken into account
|
||||||
|
f = BlogPost.objects(hits__gt=1).item_frequencies('tags')
|
||||||
|
f = dict((key, int(val)) for key, val in f.items())
|
||||||
|
self.assertEqual(set(['music', 'actors']), set(f.keys()))
|
||||||
|
self.assertEqual(f['music'], 2)
|
||||||
|
self.assertEqual(f['actors'], 1)
|
||||||
|
|
||||||
|
# Check that normalization works
|
||||||
|
f = BlogPost.objects.item_frequencies('tags', normalize=True)
|
||||||
|
self.assertAlmostEqual(f['music'], 3.0/6.0)
|
||||||
|
self.assertAlmostEqual(f['actors'], 2.0/6.0)
|
||||||
|
self.assertAlmostEqual(f['film'], 1.0/6.0)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
def test_average(self):
|
||||||
|
"""Ensure that field can be averaged correctly.
|
||||||
|
"""
|
||||||
|
ages = [23, 54, 12, 94, 27]
|
||||||
|
for i, age in enumerate(ages):
|
||||||
|
self.Person(name='test%s' % i, age=age).save()
|
||||||
|
|
||||||
|
avg = float(sum(ages)) / len(ages)
|
||||||
|
self.assertAlmostEqual(int(self.Person.objects.average('age')), avg)
|
||||||
|
|
||||||
|
self.Person(name='ageless person').save()
|
||||||
|
self.assertEqual(int(self.Person.objects.average('age')), avg)
|
||||||
|
|
||||||
|
def test_sum(self):
|
||||||
|
"""Ensure that field can be summed over correctly.
|
||||||
|
"""
|
||||||
|
ages = [23, 54, 12, 94, 27]
|
||||||
|
for i, age in enumerate(ages):
|
||||||
|
self.Person(name='test%s' % i, age=age).save()
|
||||||
|
|
||||||
|
self.assertEqual(int(self.Person.objects.sum('age')), sum(ages))
|
||||||
|
|
||||||
|
self.Person(name='ageless person').save()
|
||||||
|
self.assertEqual(int(self.Person.objects.sum('age')), sum(ages))
|
||||||
|
|
||||||
|
def test_custom_manager(self):
|
||||||
|
"""Ensure that custom QuerySetManager instances work as expected.
|
||||||
|
"""
|
||||||
|
class BlogPost(Document):
|
||||||
|
tags = ListField(StringField())
|
||||||
|
|
||||||
|
@queryset_manager
|
||||||
|
def music_posts(queryset):
|
||||||
|
return queryset(tags='music')
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
post1 = BlogPost(tags=['music', 'film'])
|
||||||
|
post1.save()
|
||||||
|
post2 = BlogPost(tags=['music'])
|
||||||
|
post2.save()
|
||||||
|
post3 = BlogPost(tags=['film', 'actors'])
|
||||||
|
post3.save()
|
||||||
|
|
||||||
|
self.assertEqual([p.id for p in BlogPost.objects],
|
||||||
|
[post1.id, post2.id, post3.id])
|
||||||
|
self.assertEqual([p.id for p in BlogPost.music_posts],
|
||||||
|
[post1.id, post2.id])
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
def test_query_field_name(self):
|
||||||
|
"""Ensure that the correct field name is used when querying.
|
||||||
|
"""
|
||||||
|
class Comment(EmbeddedDocument):
|
||||||
|
content = StringField(name='commentContent')
|
||||||
|
|
||||||
|
class BlogPost(Document):
|
||||||
|
title = StringField(name='postTitle')
|
||||||
|
comments = ListField(EmbeddedDocumentField(Comment),
|
||||||
|
name='postComments')
|
||||||
|
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
|
data = {'title': 'Post 1', 'comments': [Comment(content='test')]}
|
||||||
|
BlogPost(**data).save()
|
||||||
|
|
||||||
|
self.assertTrue('postTitle' in
|
||||||
|
BlogPost.objects(title=data['title'])._query)
|
||||||
|
self.assertFalse('title' in
|
||||||
|
BlogPost.objects(title=data['title'])._query)
|
||||||
|
self.assertEqual(len(BlogPost.objects(title=data['title'])), 1)
|
||||||
|
|
||||||
|
self.assertTrue('postComments.commentContent' in
|
||||||
|
BlogPost.objects(comments__content='test')._query)
|
||||||
|
self.assertEqual(len(BlogPost.objects(comments__content='test')), 1)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.Person.drop_collection()
|
self.Person.drop_collection()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user