MongoKat

MongoKat is a minimalist MongoDB ORM/ODM, inspired by the “hands off” API of MongoKit. It was created at Pricing Assistant, drawing from our experience managing a large Python codebase.

It differs from MongoKit in a few ways:

  • Less features: we focus on basic Collection & Document methods.
  • Less magic: MongoKit’s use of complex Python features like __mro__ and __metaclass__ made bugs and memory leaks hard to debug.
  • Cleaner design: We enforce a separation between collection-level methods (find, aggregate, ...) and document-level methods (save, reload, ...)
  • Better performance: The Cursor class is not wrapped anymore so the overhead of instanciating Documents instead of dicts is now close to zero.
  • Requires pymongo 3.0+, taking advantage of its new features. To make transition to 3.0 easier (lots of pymongo’s APIs got renamed or deprecated) MongoKat still supports some 2.x-style parameters and method names.
  • Support for simple hooks: before_delete, after_delete, after_save. Useful for keeping data up-to-date in ElasticSearch for instance, on a best-effort basis (some hooks may be lost under high load when using methods like update_many).
  • Support for protected fields that can’t be updated directly. Useful for making sure developers to use specific methods of a Document.

Installation

You can either clone the code from GitHub or install it via pip:

` pip install mongokat `

Code sample

# First, declare a Document/Collection pair (a "model"):

from mongokat import Collection, Document

class SampleDocument(Document):

        def my_sum(self):
                return self["a"] + self["b"]

class SampleCollection(Collection):
        document_class = SampleDocument

        def find_by_a(self, a_value):
                return self.find_one({"a": a_value})

# Then use it in your code like this:

from pymongo import MongoClient
client = MongoClient()
Sample = SampleCollection(collection=client.my_db.my_col)

Sample.insert({"a": 1, "b": 2})
Sample.insert({"a": 2, "b": 3})

assert Sample.count() == 2

five = Sample.find_by_a(2)
assert five.my_sum() == 5

By the way, this is an actual test!

Migration guide from MongoKit

First you should get familiar with the new CRUD methods) from PyMongo 3.0. All of them work as expected in MongoKat.

We have generally tried to limit the changes needed for a migration to the models themselves, while the code actually using them should work without major changes.

Here is a list of things you should be aware of:

  • You will have to split your Models into Document and Collection classes. For instance, find() belongs to a Collection, whereas reload() belongs to a Document.
  • Initialization logic is different/cleaner, models are not magically registered everywhere, you have to explicitly instanciate them.
  • Structures are not inherited.

API Reference

mongokat.collection.Collection

class mongokat.collection.Collection(collection=None, database=None, client=None)[source]

mongokat.Collection wraps a pymongo.collection.Collection

document_class

alias of Document

structure = None
immutable = False
protected_fields = ()
exists(query, **args)[source]

Returns True if the search matches at least one document

count(*args, **kwargs)[source]
distinct(*args, **kwargs)[source]
group(*args, **kwargs)[source]
aggregate(*args, **kwargs)[source]
find(*args, **kwargs)[source]
find_one(*args, **kwargs)[source]
find_by_id(*args, **kwargs)[source]
find_by_ids(*args, **kwargs)[source]
find_by_b64id(*args, **kwargs)[source]
find_by_b64ids(*args, **kwargs)[source]
list_column(*args, **kwargs)[source]

Return one field as a list

iter_column(query=None, field='_id', **kwargs)[source]

Return one field as an iterator. Beware that if your query returns records where the field is not set, it will raise a KeyError.

find_random(**kwargs)[source]

return one random document from the collection

one(*args, **kwargs)[source]
insert(data, return_object=False)[source]

Inserts the data as a new document.

bulk_write(*args, **kwargs)[source]

Hook are not supported for this method!

insert_one(document, **kwargs)[source]
insert_many(documents, **kwargs)[source]
replace_one(filter, replacement, **kwargs)[source]
update_one(filter, update, **kwargs)[source]
update_many(filter, update, **kwargs)[source]
delete_one(filter, **kwargs)[source]
delete_many(filter, **kwargs)[source]
find_one_and_delete(*args, **kwargs)[source]
find_one_and_replace(*args, **kwargs)[source]
find_one_and_update(*args, **kwargs)[source]
has_trigger(event)[source]

Does this trigger need to run?

trigger(event, filter=None, update=None, documents=None, ids=None, replacements=None)[source]

Trigger the after_save hook on documents, if present.

connection
db
save(to_save, **kwargs)[source]
update(spec, document, **kwargs)[source]
remove(spec_or_id=None, **kwargs)[source]
find_and_modify(query={}, update=None, **kwargs)[source]
get_from_id(_id)[source]
fetch(spec=None, *args, **kwargs)[source]

return all document which match the structure of the object fetch() takes the same arguments than the the pymongo.collection.find method. The query is launch against the db and collection of the object.

fetch_one(*args, **kwargs)[source]

return one document which match the structure of the object fetch_one() takes the same arguments than the the pymongo.collection.find method. If multiple documents are found, raise a MultipleResultsFound exception. If no document is found, return None The query is launch against the db and collection of the object.

mongokat.document.Document

class mongokat.document.Document(doc=None, mongokat_collection=None, fetched_fields=None, gen_skel=None)[source]
mongokat_collection = None
gen_skel = True
b64id

Returns the document’s _id as a base64-encoded string

ensure_fields(fields, force_refetch=False)[source]

Makes sure we fetched the fields, and populate them if not.

refetch_fields(missing_fields)[source]

Refetches a list of fields from the DB

unset_fields(fields)[source]

Removes this list of fields from both the local object and the DB.

reload()[source]

allow to refresh the document, so after using update(), it could reload its value from the database.

Be carreful : reload() will erase all unsaved values.

If no _id is set in the document, a KeyError is raised.

delete()[source]

delete the document from the collection from his _id.

save(force=False, uuid=False, **kwargs)[source]

REPLACES the object in DB. This is forbidden with objects from find() methods unless force=True is given.

save_partial(data=None, allow_protected_fields=False, **kwargs)[source]

Saves just the currently set fields in the database.

generate_skeleton()[source]
get_size()[source]

return the size of the underlying bson object

validate()[source]

We do not support validation yet.

Credits