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() > 0
db(Post)
creates theQuerySet
object;find
method get the raw-query and return newQuerySet
object with updated criteria;count
method 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.user
Get 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.hint
method.
- Add
Database.estimated_document_count
method for quickly count documents in the collection.
- Add
QuerySet.hint
for specify index for query.
- A Big Rewrite document logic:
Document.__raw__
now contains only data from pymongo, without anyAttributeNotSet
orNotLoaded
;Document.__changed__
is removed: all changes reflects toDocument.__cache__
;Document.__not_loaded__
frozenset of fields whitch not loaded by projection;Document.__new_document__
flag isTrue
for 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_many
andDatabase.delete_one
; - Deprecate
Database.insert
,Database.remove
; - Remove
Database.bulk
(without deprecation period, sorry); - Add
QuerySet.count_documents
; - Add
QuerySet.update_one
andQuerySet.update_many
; - Add
QuerySet.delete_one
andQuerySet.delete_many
; - Add
QuerySet.find_one_and_update
,QuerySet.find_one_and_replace
andQuerySet.find_one_and_delete
; - Deprecate
QuerySet.count
; - Deprecate
QuerySet.update
,QuerySet.remove
andQuerySet.find_and_modify
; - Remove deprecated
QuerySet.with_id
;
- Add
- Simple interface for build lookups:
QuerySet.lookup
; - Remove
bcc
argument fromMoneyField
; - Add
Decimal128Field
.
- Experimental
asyncio
support; - Add
ReferencesListField
for lists of references.
- Add
projection
argument toDatabase.get_document
andDatabase.reload
; - Add
Document.__default_projection__
attribute.
- Add
EnumField
for saveenum.Enum
; - Add
EnumStateField
for simple state machines based onenum.Enum
.
- Add
QuerySet.batch_size
method for setup batch size for cursor; - Some minor fixes.
ReferenceField.from_mongo
try to get document from primary- if not found by default.
- Add
QuerySet.read_primary
method for simple setupread_preference.Primary
.
- Add
TimedeltaField
for stores durations; - Add
SimpleEmbeddedDocumentField
for simply create embedded documents.
class Doc(Document):
embedded = SimpleEmbeddedDocumentField({
'i': IntegerField(),
's': StringField(),
})
- Add
StaticField
for static data.
- Additional arguments (like
write_concern
) for write operations; create_fake
save the documents with write concern "majority" by default.
- Drop pymongo 2 support;
- Additional options for databases and collections;
- Add
Database.get_document
; - Add
TypedEmbeddedDocumentField
; reload
argument ofDatabase.update_one
must be keyword- (may be backward incompotable).
- Change raw data for
Money
;
- Add currency support to
Money
: - Totaly rewrite
Money
type. 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_in
for$in
queries 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.ids
method for get only documents id's from queryset; - Add
Money.total_cents
method andMoney.from_cents
classmethod;
- Add cacheing on queryset level and use it for
ReferenceField
; - Add mongo aggregation framework support;
- Add
read_preference
setting; - Add
exc
argument toQuerySet.find_one
for raise exception if not found; - Add
multi
argument 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...