:leaves: Powerful schema-less ODM for MongoDB and Python (sync + async)
[pymongo]: https://github.com/mongodb/mongo-python-driver [thingy]: https://github.com/Refty/thingy [mongomock]: https://github.com/mongomock/mongomock [montydb]: https://github.com/davidlatwe/montydb [motor]: https://github.com/mongodb/motor [mongomock-motor]: https://github.com/michaelkryukov/mongomock_motor
**Mongo-Thingy is the most idiomatic and friendly-yet-powerful way to use MongoDB with Python.**
It is an "Object-Document Mapper" that gives you full advantage of MongoDB schema-less design by not asking you to define schemas in your code.
What you'll get:
- a simple and robust pure-Python code base, with 100% coverage and few
- [PyMongo][pymongo] query language - no need to learn yet another one;
- both sync and async support! choose what suits you best;
- [Thingy][thingy] views - control what to show, and create fields based on
- swappable backend - wanna use SQLite behind the scenes? well, you can;
- versioning (optional) - rollback to any point in any thingy history;
- and more!
Compatibility
We support all Python and MongoDB versions supported by [PyMongo][pymongo], namely:
- CPython 3.7+ and PyPy3.7+
- MongoDB 3.6, 4.0, 4.2, 4.4, and 5.0.
- Synchronous:
- Asynchronous:
Install
pip install mongo-thingy
Examples
First steps
Connect, insert and find thingies
>>> from mongo_thingy import connect, Thingy
>>> connect("mongodb://localhost/test")
>>> class User(Thingy): ... pass
>>> user = User({"name": "Mr. Foo", "age": 42}).save() >>> User.count_documents() 1 >>> User.find_one({"age": 42}) User({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 42})
In an AsyncIO (or Tornado) environment, use the asynchronous class instead:
>>> from mongo_thingy import connect, AsyncThingy
>>> connect("mongodb://localhost/test")
>>> class User(AsyncThingy): ... pass
>>> user = await User({"name": "Mr. Foo", "age": 42}).save() >>> await User.count_documents() 1 >>> await User.find_one({"age": 42}) User({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 42})
To use another backend than the default ones, just pass its client class with `client_cls:
<pre><code class="lang-python">>>> import mongomock >>> connect(client_cls=mongomock.MongoClient)</code></pre>
Update a thingy
<pre><code class="lang-python">>>> user.age 42 >>> user.age = 1337 >>> user.save() User({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337})</code></pre>
Thingy views power
Complete information with properties
<pre><code class="lang-python">>>> class User(Thingy): ... @property ... def username(self): ... return "".join(char for char in self.name if char.isalpha())
>>> User.add_view(name="everything", defaults=True, include="username") >>> user = User.find_one() >>> user.view("everything") {'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337, 'username': 'MrFoo'}</code></pre>
Hide sensitive stuff
<pre><code class="lang-python">>>> User.add_view(name="public", defaults=True, exclude="password") >>> user.password = "t0ps3cr3t" >>> user.view() {'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337, 'password': 't0ps3cr3t'} >>> user.view("public") {'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337}</code></pre>
Only use certain fields/properties
<pre><code class="lang-python">>>> User.add_view(name="credentials", include=["username", "password"]) >>> user.view("credentials") {'username': 'MrFoo', 'password': 't0ps3cr3t'}</code></pre>
Apply views on cursors
<pre><code class="lang-python">>>> cursor = User.find() >>> for credentials in cursor.view("credentials"): ... print(credentials) {'username': 'MrFoo', 'password': 't0ps3cr3t'} {'username': 'MrsBar', 'password': '123456789'} ...</code></pre>
And if your cursor is already exhausted, you can still apply a view!
<pre><code class="lang-python">>>> users = User.find().to_list(None) >>> for credentials in users.view("credentials"): ... print(credentials) {'username': 'MrFoo', 'password': 't0ps3cr3t'} {'username': 'MrsBar', 'password': '123456789'} ...</code></pre>
Versioning
<pre><code class="lang-python">>>> from mongo_thingy.versioned import Versioned
>>> class Article(Versioned, Thingy): ... pass
>>> article = Article(c) >>> article.version 0
>>> article.save() Article({'_id': ObjectId('...'), 'content': 'Cogito ergo sum'}) >>> article.version 1
>>> article.content = "Sum ergo cogito" >>> article.save() Article({'_id': ObjectId('...'), 'content': 'Sum ergo cogito'}) >>> article.version 2
>>> article.revert() Article({'_id': ObjectId('...'), 'content': 'Cogito ergo sum'}) >>> article.version 3</code></pre>
Database/collection "discovery"
Default behaviour
<pre><code class="lang-python">>>> class AuthenticationGroup(Thingy): ... pass
>>> connect("mongodb://localhost/") >>> AuthenticationGroup.collection Collection(Database(MongoClient(host=['localhost:27017'], ...), 'authentication'), 'group')</code></pre>
Use mismatching names for Thingy class and database collection
You can either specify the collection name:
<pre><code class="lang-python">>>> class Foo(Thingy): ... collection_name = "bar"</code></pre>
or the collection directly:
<pre><code class="lang-python">>>> class Foo(Thingy): ... collection = db.bar</code></pre>
You can then check what collection is being used with:
<pre><code class="lang-python">>>> Foo.collection Collection(Database(MongoClient('localhost', 27017), 'database'), 'bar')</code></pre>
Indexes
Create an index
<pre><code class="lang-python">>>> User.create_index("email", sparse=True, unique=True)</code></pre>
Add one or more indexes, create later
<pre><code class="lang-python">>>> User.add_index("email", sparse=True, unique=True) >>> User.add_index("username")
>>> User.create_indexes()</code></pre>
Create all indexes of all thingies at once
<pre><code class="lang-python">>>> from mongothingy import createindexes >>> create_indexes()</code></pre>
Dealing with camelCase data
<pre><code class="lang-python">>>> from mongo_thingy.camelcase import CamelCase
>>> class SystemUser(CamelCase, Thingy): ... collection_name = "systemUsers"
>>> user = SystemUser.find_one() >>> user.view() {'_id': ObjectId(...), 'firstName': 'John', 'lastName': 'Doe'}
>>> user.first_name 'John' >>> user.first_name = "Jonny" >>> user.save() SystemUser({'_id': ObjectId(...), firstName: 'Jonny', lastName: 'Doe'})</code></pre>
Tests
To run the tests suite:
- make sure you have a MongoDB database running on localhost:27017 (you can spawn one with docker compose up -d); - install developers requirements with pip install -r requirements.txt; - run pytest`.

