It's small and simple ODM for use with MongoDB.
YADM support MongoDB version 3.x only. MongoDB 2.x is not supported.
Minimal version of python — 3.6.
from datetime import datetime
import pymongo
from yadm import Database
from yadm import Document, EmbeddedDocument
from yadm import fields
from yadm.serialize import to_mongo
class User(Document):
__collection__ = 'users'
name = fields.StringField()
email = fields.EmailField()
class PostLogItem(EmbeddedDocument):
op = fields.StringField(choices=['created', 'comment_added'])
at = fields.DatetimeField()
data = fields.MongoMapField()
class Post(Document):
__collection__ = 'posts'
user = fields.ReferenceField(User)
created_at = fields.DatetimeField(auto_now=True)
title = fields.StringField()
body = fields.StringField()
log = fields.ListField(fields.EmbeddedDocumentField(PostLogItem))
class Comment(Document):
__collection__ = 'comments'
user = fields.ReferenceField(User)
created_at = fields.DatetimeField(auto_now=True)
post = fields.ReferenceField(Post)
text = fields.StringField()All documents creates from class Document. You can use multiple inheritance.
__collection__ magic attribute setups the collection name for documents of model.
client = pymongo.MongoClient('mongodb://localhost:27017')
db = Database(client, 'blog')Database object is a wrapper about pymongo or motor Database.
user = User(name='Bill', email='bill@galactic.hero')
db.insert_one(user)Just insert document to database.
post = Post()
post.user = user
post.title = 'Small post'
post.body = 'Bla-bla-bla...'
post.log = [PostLogItem(op='created', at=datetime.utcnow())]
db.insert_one(post)You can fill documents as above.
comment = Comment()
comment.user = user
comment.post = post
comment.text = "RE: Bla-bla-bla..."
db.insert_one(comment)
db.update_one(post, push={
'log': to_mongo(PostLogItem(op='comment_added',
at=comment.created_at,
data={
'comment': comment.id,
'user': comment.user.id,
}))
})We add log item to post's log. This is very usefull case.
qs = db(Post).find({'title': {'$regex': '^S'}})
assert qs.count() > 0db(Post)creates theQuerySetobject;findmethod get the raw-query and return newQuerySetobject with updated criteria;countmethod make the query to database and return value.
for post in qs:
assert post.title.startswith('S')__iter__ method make the find-query and returns the generator of documents.
Get the first finded document.
post = db(Post).find_one({'user': user.id})Get the document by id from primary.
user = db.get_document(User, user.id)user = post.userGet attribute with reference makes the query to referred collection. Warning: N+1 problem!
We have a cache in QuerySet object and get one referred document only once for one queryset.
comments = db(Comment).find({'post': post.id}).sort(('created_at', 1))
for comment in comments.lookup('user'):
print(comment.user.name, comment.text)This code create the aggregate query with $lookup statement for resolve the references.
agg = (db.aggregate(Comment)
.match(user=user.id)
.group(_id='post', count={'$sum': 1})
.sort(count=-1))
for item in agg:
print(item)Or traditional MongoDB syntax:
agg = db.aggregate(Comment, pipeline=[
{'match': {'user': user.id}},
{'group': {'_id': 'post', 'count': {'$sum': 1}}},
{'sort': {'count': -1}},
])- Asyncio support for testing.
- Some bugfixes.
- Add
Aggregation.hintmethod.
- Add
Database.estimated_document_countmethod for quickly count documents in the collection.
- Add
QuerySet.hintfor specify index for query.
- A Big Rewrite document logic:
Document.__raw__now contains only data from pymongo, without anyAttributeNotSetorNotLoaded;Document.__changed__is removed: all changes reflects toDocument.__cache__;Document.__not_loaded__frozenset of fields whitch not loaded by projection;Document.__new_document__flag isTruefor document's objects whitch created directly in your code;Document.__log__list-like container with log of document changes (unstable API at now);Document.__data__is removed as deprecated;- Now is not allow to set fields as classes;
- Defaults is not lazy and creates with document instance;
- Update for minimal versions of pymongo (3.7) and motor (2.0):
- Add
Database.bulk_write; - Add
Database.insert_one,Database.insert_manyandDatabase.delete_one; - Deprecate
Database.insert,Database.remove; - Remove
Database.bulk(without deprecation period, sorry); - Add
QuerySet.count_documents; - Add
QuerySet.update_oneandQuerySet.update_many; - Add
QuerySet.delete_oneandQuerySet.delete_many; - Add
QuerySet.find_one_and_update,QuerySet.find_one_and_replaceandQuerySet.find_one_and_delete; - Deprecate
QuerySet.count; - Deprecate
QuerySet.update,QuerySet.removeandQuerySet.find_and_modify; - Remove deprecated
QuerySet.with_id;
- Add
- Simple interface for build lookups:
QuerySet.lookup; - Remove
bccargument fromMoneyField; - Add
Decimal128Field.
- Experimental
asynciosupport; - Add
ReferencesListFieldfor lists of references.
- Add
projectionargument toDatabase.get_documentandDatabase.reload; - Add
Document.__default_projection__attribute.
- Add
EnumFieldfor saveenum.Enum; - Add
EnumStateFieldfor simple state machines based onenum.Enum.
- Add
QuerySet.batch_sizemethod for setup batch size for cursor; - Some minor fixes.
ReferenceField.from_mongotry to get document from primary- if not found by default.
- Add
QuerySet.read_primarymethod for simple setupread_preference.Primary.
- Add
TimedeltaFieldfor stores durations; - Add
SimpleEmbeddedDocumentFieldfor simply create embedded documents.
class Doc(Document):
embedded = SimpleEmbeddedDocumentField({
'i': IntegerField(),
's': StringField(),
})- Add
StaticFieldfor static data.
- Additional arguments (like
write_concern) for write operations; create_fakesave the documents with write concern "majority" by default.
- Drop pymongo 2 support;
- Additional options for databases and collections;
- Add
Database.get_document; - Add
TypedEmbeddedDocumentField; reloadargument ofDatabase.update_onemust be keyword- (may be backward incompotable).
- Change raw data for
Money;
- Add currency support to
Money: - Totaly rewrite
Moneytype. Now it is not subclass ofDecimal; - Add storage for currencies:
yadm.fields.money.currency.DEFAULT_CURRENCY_STORAGE;
- Totaly rewrite
- Add currency support to
- Add
QuerySet.find_infor$inqueries with specified order;
- Drop MongoDB 2.X suport;
- Objects for update and remove results;
- Use Faker instead fake-factory.
- Add some features to
Bulk: Bulk.update_one(document, **kw): method for add update one document in bulk;Bulk.find(query).update(**kw): update many documents by query;Bulk.find(query).upsert().update(**kw): upsert document;Bulk.find(query).remove(**kw): remove documents;
- Add some features to
- Add
QuerySet.idsmethod for get only documents id's from queryset; - Add
Money.total_centsmethod andMoney.from_centsclassmethod;
- Add cacheing on queryset level and use it for
ReferenceField; - Add mongo aggregation framework support;
- Add
read_preferencesetting; - Add
excargument toQuerySet.find_onefor raise exception if not found; - Add
multiargument toQuerySet.remove; - Deprecate
QuerySet.with_id; - Refactoring.
- Change document structure. No more bad BaseDocument.__data__ attribute:
- BaseDocument.__raw__: raw data from mongo;
- BaseDocument.__cache__: cached objects, casted with fields;
- BaseDocument.__changed__: changed objects.
- Changes api for custom fields:
- Not more need create field descriptors for every field;
- prepare_value called only for setattr;
- to_mongo called only for save objects to mongo;
- from_mongo called only for load values from BaseDocument.__raw__;
- Remove Field.default attribute. Use Field.get_default method;
- Add Field.get_if_not_loaded and Field.get_if_attribute_not_set method;
- By default raise NotLoadedError if field not loaded from projection;
- Changes in ReferenceField:
- Raise BrokenReference if link is bloken;
- Raise NotBindingToDatabase if document not saved to database;
- smart_null keyword for Field;
- Fields in document must be instances (not classes!);
- Remove ArrayContainer and ArrayContainerField;
- Remove old MapIntKeysField and MapObjectIdKeysField. Use new MapCustomKeysField;
- Add Database.update_one method for run simple update query with specified document;
- Add QuerySet.distinct;
- serialize.from_mongo now accept not_loaded sequence with filed names who must mark as not loaded, parent and name;
- serialize.to_mongo do not call FieldDescriptor.__set__;
- Fakers! Subsystem for generate test objects;
- Tests now use pytest;
- And more, and more...