Welcome to nanomongo’s documentation!¶
nanomongo is a minimal MongoDB Object-Document Mapper for Python.
It does not attempt to be a feature-complete ODM but if you like
using pymongo
api with python dictionaries and often find yourself
writing validators and pymongo.Collection
wrappers, nanomongo
might suit your needs.
nanomongo has full test coverage.
Quick Links: Source (github) - Documentation (rtd) - Packages (PyPi)
Version 0.4: Utility methods dbref_field_getters, get_dbref()
and Bugfix Python23 text type compatibility
Version 0.3: nanomongo is now python2 compatible (with syntactic difference when defining your Document, see Defining Your Document below).
Installation¶
$ pip install nanomongo
Quick Tutorial¶
Defining Your Document¶
import pymongo
from nanomongo import Index, Field, BaseDocument
mclient = pymongo.MongoClient()
class Py23CompatibleDoc(BaseDocument):
client = mclient
db = 'dbname'
dot_notation = True
foo = Field(str)
bar = Field(int, required=False)
# Python3 only
class Py3Doc(BaseDocument, dot_notation=True, client=mclient, db='dbname'):
foo = Field(str)
bar = Field(int, required=False)
__indexes__ = [
Index('foo'),
Index([('bar', 1), ('foo', -1)], unique=True),
]
You don’t have to declare client
or db
as shown above, you can
register()
(and I definitely prefer it on
python2) your document later as such:
client = pymongo.MongoClient()
MyDoc.register(client=client, db='dbname', collection='mydoc')
If you omit collection
when defining/registering your document,
__name__.lower()
will be used by default
Creating, Inserting, Saving¶
doc = MyDoc(foo='42') # or MyDoc({'foo': '42'})
doc.bar = 42 # attribute style access because dot_notation=True
doc.insert()
insert()
is a wrapper around
pymongo.Collection().insert()
and has the same return value (_id
)
unless you explicitly set w=0
doc.foo = 'new foo' # this change is recorded
del doc.bar # this is recored as well
doc.save() # save only does partial updates
save()
uses pymongo.Collection().update()
with the changed data. The above will run
update({'_id': doc['_id']}, {'$set': {'foo': 'new foo'}, '$unset': {'bar': 1}})
Querying¶
Doc.find({'bar': 42})
Doc.find_one({'foo': 'new foo'})
find()
and find_one()
methods are essentially wrappers around respective methods of pymongo.Collection()
and they take the same arguments.
Extensive Example¶
Advanced Features¶
$addToSet¶
MongoDB $addToSet
update modifier is very useful. nanomongo implements it.
addToSet()
will do the add-to-field-if-doesnt-exist
on your document instance and record the change to be applied later when
save()
is called.
# lets expand our MyDoc
class NewDoc(MyDoc):
list_field = Field(list)
dict_field = Field(dict)
NewDoc.register(client=client, db='dbname')
doc_id = NewDoc(list_field=[42], dict_field={'foo':[]}).insert()
doc = NewDoc.find_one({'_id': doc_id})
doc.addToSet('list_field', 1337)
doc.addToSet('dict_field.foo', 'like a boss')
doc.save()
Both of the above addToSet
are applied to the NewDoc
instance like MongoDB does it eg.
- create list field with new value if it doesn’t exist
- add new value to list field if it’s missing (append)
- complain if it is not a list field
When save is called, query becomes:
update({'$addToSet': {'list_field': {'$each': [1337]}},
'dict_field.foo': {'$each': ['like a boss']}})
Undefined fields or field type mismatch raises ValidationError
.
QuerySpec check¶
find()
and find_one()
has a simple check against queries that can not match, logging warnings. This is
an experimental feature at the moment and only does type checks as such:
{'foo': 1}
will log warnings if
- Document has no field named
foo
(field existence) foo
field is not of typeint
(field data type)
or {'foo.bar': 1}
will log warnings if
foo
field is not of typedict
orlist
(dotted field type)
dbref_field_getters¶
Documents that define bson.DBRef
fields automatically generate getter methods
through nanomongo.document.ref_getter_maker()
where the generated methods
have names such as get_<field_name>_field
.
class MyDoc(BaseDocument):
# document_class with full path
source = Field(DBRef, document_class='some_module.Source')
# must be defined in same module as this will use
# mydoc_instance.__class__.__module__
destination = Field(DBRef, document_class='Destination')
# autodiscover
user = Field(DBRef)
nanomongo tries to guess the document_class
if it’s not provided by looking at
registered subclasses of BaseDocument
. If it matches two (for example
when two document classes use the same collection), it will raise
UnsupportedOperation
.
pymongo & motor¶
Throughout the documentation, pymongo
is referenced but all features work the
same when using motor transparently if you
register the document class with a motor.MotorClient
.
import motor
from nanomongo import Field, BaseDocument
class MyDoc(BaseDocument, dot_notation=True):
foo = Field(str)
bar = Field(list, required=False)
client = motor.MotorClient().open_sync()
MyDoc.register(client=client, db='dbname')
# and now some async motor queries (using @gen.engine)
doc_id = yield motor.Op(MyDoc(foo=42).insert)
doc = yield motor.Op(MyDoc.find_one, {'foo': 42})
doc.addToSet('bar', 1337)
yield motor.Op(doc.save)
Note however that pymongo vs motor behaviour is not necessarily identical.
Asynchronous methods require tornado.ioloop.IOLoop
. For example,
register()
runs ensure_index
but the query will not be sent
to MongoDB until IOLoop.start()
is called.