Important to any ODM/ORM is a way to provide utility and validation for the documents that your application is storing. The goals of micromongo’s models are to:
Despite mongodb’s schemaless nature, there is often still a need to maintain some type and value guarantees on certain fields or maintain field requirements on certain documents. For instance, an Image being persisted to a database might contain all different kinds of EXIF keys and values, but an integer filesize (in bytes), URI, filetype, etc. could be required of all such documents.
All user defined models should inherit from micromongo.models.Model, which is also imported into the top level micromongo namespace:
Micromongo Model object.
Create a new instance of this model based on its spec and either a map or the provided kwargs.
Run a find on this model’s collection. The arguments to Model.find are the same as to pymongo.Collection.find.
Save this object to the database. Behaves very similarly to whatever collection.save(document) would, ie. does upserts on _id presence. If methods pre_save or post_save are defined, those are called. If there is a spec document, then the document is validated against it after the pre_save hook but before the save.
Validate this object based on its spec document.
The following keys cannot be used without colliding with names that Models use internally or externally. You should avoid these names if possible in your documents:
new, find, save, validate, keys, items, values, iterkeys, iteritems, itervalues, update, clear, pre_save, post_save, collection, database, spec
Many of these are to maintain a dict-like interface. You can use a micromongo model in anything that accepts map-like objects, but they do not inherit from dict, so be careful when passing into C-code that expects an explicit dict.
Spec documents are dictionaries defined on Model classes that are a mapping of document keys to Field objects that describe arbitrary validation on those fields as well as requirements for any document of the model’s type.
A base Field type, which itself can be used pretty reasonably to get type coersion, field defaults, etc. If required is True, then validation will fail if this field is NOT present in the document. default is the default value to give this field in a new document; if it is None and the field is not required, the field is not added to new documents. type is an object that can perform validation on this field; see documentation for Field.typecheck.
Create a typecheck from some value t. This behaves differently depending on what t is. It should take a value and return True if the typecheck passes, or False otherwise. Override pre_validate in a child class to do type coercion.
If none of these conditions are met, a TypeError is raised.
The way this looks in a micromongo Model is:
from pprint import pprint
from micromongo import Model, Field
class TestModel(Model):
collection = 'test.test'
spec = {
'docid': Field(type=int, default=0),
'req': Field(required=True),
'enum': Field(type=['foo', 'bar'], default='foo'),
'baz': Field(type=float),
}
t = TestModel.new()
# t.items() => [('req', None), ('docid', 0), ('enum', 'foo')]
As you can see, docid and enum were given their defaults. req was given a default of None because it is required but no default was given. baz was not added to the document, but if it is, its value will have to be an instance of float, or else saving the document will fail during validation.
You can create your own custom Field types by subclassing from Field. Suppose your needs include some kind of distance from a known point which must be 0 or more:
class PositiveNumberField(Field):
"""A field that checks that values are positive numbers, and also
coerces values set on the field to float if it isn't already a
number."""
def __init__(self, **kwargs):
def _type(value):
if isinstance(value, (int, long, float)) and value >= 0:
return True
return False
kwargs['type'] = _type
super(PositiveIntegerField, self).__init__(**kwargs)
def pre_validate(self, value):
if isinstance(value, (int, long, float)):
return value
try:
return float(value)
except ValueError:
return value
This field will still take required and default, but now the typecheck is embedded within the field class itself. It also overrides the pre_validate method to provide some type coercion:
A hook called before a value is validated. Implement this to do type coercion; say, to create an IntegerField that attempts to turn values into integers, or other more complex manipulations.
Sometimes, validation has to take into consideration the values of more than one field. Suppose you have a User model where you want either a twitter_id or an email_address field to be present and not the empty string. You can do this by implementing a pre_save hook on your Model object:
class User(Model):
collection = 'auth.user'
spec = {
'username' : Field(required=True, default="", type=str),
'password' : Field(required=True, default="", type=str),
'twitter_id' : Field(required=True, default=""),
'email_address': Field(required=True, default=""),
}
def pre_save(self):
if not any((self.twitter_id, self.email_address)):
raise ValueError("User objects require either a twitter_id or"
"email_address.")
Validation failures in general raise ValidationError, so if you are doing validation in your pre_save hook, you will want to raise this on failures too.
You can also use pre_save to cache or calculate certain fields based off of others in the model. A text document meant to be saved in markdown can have a calculated field prerendered that is set in this hook.
Micromongo keeps track of all models that have been registered. For applications that might want to use this data, a function providing a mapping of their database location to the model class is provided:
Return the AccountingMeta’s model mapping, which is a dictionary of keys in the form of “db.collection” to model classes.