[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](https://socialify.git.ci/Refty/mongo-thingy/image?font=Bitter&language=1&logo=https%3A%2F%2Fi.imgur.com%2FLeNC7Zb.png&owner=1&pattern=Charlie%20Brown&theme=Light)
<div align="center">
<a href="https://pypi.org/project/mongo-thingy"><img src="https://img.shields.io/pypi/v/mongo-thingy.svg" alt="PyPI"></a>
<img src="https://img.shields.io/pypi/pyversions/mongo-thingy" alt="Supported Python Versions">
<a href="LICENSE"><img src="https://img.shields.io/github/license/refty/mongo-thingy" alt="License"></a>
<a href="https://github.com/ambv/black"><img src="https://img.shields.io/badge/code%20style-black-black" alt="Code style"></a>
<br/>
<a href="https://github.com/Refty/mongo-thingy/actions"><img src="https://img.shields.io/github/actions/workflow/status/Refty/mongo-thingy/tests.yml?branch=master" alt="Tests"></a>
<a href="https://coveralls.io/github/Refty/mongo-thingy"><img src="https://img.shields.io/coveralls/Refty/mongo-thingy.svg" alt="Tests"></a>
<a href="http://mongo-thingy.readthedocs.io"><img src="https://readthedocs.org/projects/mongo-thingy/badge" alt="Docs"></a>
<br /><br />
</div>
**_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
dependencies;
- [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
other fields;
- 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.
As a backend, Mongo-Thingy supports the following libraries:
- Synchronous:
* [PyMongo][pymongo] (default)
* [Mongomock][mongomock]
* [MontyDB][montydb]
- Asynchronous:
* [Motor][motor] (default when Motor is installed)
* [Motor][motor] with Tornado (default when Motor and Tornado are installed)
* [Mongomock-Motor][mongomock-motor]
# Install
```sh
pip install mongo-thingy
```
# Examples
## First steps
### Connect, insert and find thingies
```python
>>> 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:
```python
>>> 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``:
```python
>>> import mongomock
>>> connect(client_cls=mongomock.MongoClient)
```
### Update a thingy
```python
>>> user.age
42
>>> user.age = 1337
>>> user.save()
User({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337})
```
## Thingy views power
### Complete information with properties
```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'}
```
### Hide sensitive stuff
```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}
```
### Only use certain fields/properties
```python
>>> User.add_view(name="credentials", include=["username", "password"])
>>> user.view("credentials")
{'username': 'MrFoo', 'password': 't0ps3cr3t'}
```
### Apply views on cursors
```python
>>> cursor = User.find()
>>> for credentials in cursor.view("credentials"):
... print(credentials)
{'username': 'MrFoo', 'password': 't0ps3cr3t'}
{'username': 'MrsBar', 'password': '123456789'}
...
```
And if your cursor is already exhausted, you can still apply a view!
```python
>>> users = User.find().to_list(None)
>>> for credentials in users.view("credentials"):
... print(credentials)
{'username': 'MrFoo', 'password': 't0ps3cr3t'}
{'username': 'MrsBar', 'password': '123456789'}
...
```
## Versioning
```python
>>> from mongo_thingy.versioned import Versioned
>>> class Article(Versioned, Thingy):
... pass
>>> article = Article(content="Cogito ergo sum")
>>> 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
```
## Database/collection "discovery"
### Default behaviour
```python
>>> class AuthenticationGroup(Thingy):
... pass
>>> connect("mongodb://localhost/")
>>> AuthenticationGroup.collection
Collection(Database(MongoClient(host=['localhost:27017'], ...), 'authentication'), 'group')
```
### Use mismatching names for Thingy class and database collection
You can either specify the collection name:
```python
>>> class Foo(Thingy):
... collection_name = "bar"
```
or the collection directly:
```python
>>> class Foo(Thingy):
... collection = db.bar
```
You can then check what collection is being used with:
```python
>>> Foo.collection
Collection(Database(MongoClient('localhost', 27017), 'database'), 'bar')
```
## Indexes
### Create an index
```python
>>> User.create_index("email", sparse=True, unique=True)
```
### Add one or more indexes, create later
```python
>>> User.add_index("email", sparse=True, unique=True)
>>> User.add_index("username")
>>> User.create_indexes()
```
### Create all indexes of all thingies at once
```python
>>> from mongo_thingy import create_indexes
>>> create_indexes()
```
## Dealing with camelCase data
```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'})
```
# 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`.
# Sponsors
<div align="center">
<a href="https://numberly.com/"><img src="https://raw.githubusercontent.com/Refty/mongo-thingy/master/img/numberly.png" alt="Numberly"></a>
<a href="https://refty.co/"><img src="https://raw.githubusercontent.com/Refty/mongo-thingy/master/img/refty.png" alt="Refty"></a>
</div>
Raw data
{
"_id": null,
"home_page": "https://github.com/Refty/mongo-thingy",
"name": "Mongo-Thingy",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": null,
"author": "Guillaume Gelin",
"author_email": "guillaume@refty.co",
"download_url": "https://files.pythonhosted.org/packages/48/65/4794eb1f8045c6d446d7ad3c1e4d2d56ba433cd560cf1f75697ff022e4ea/Mongo-Thingy-0.17.2.tar.gz",
"platform": "any",
"description": "[pymongo]: https://github.com/mongodb/mongo-python-driver\n[thingy]: https://github.com/Refty/thingy\n[mongomock]: https://github.com/mongomock/mongomock\n[montydb]: https://github.com/davidlatwe/montydb\n[motor]: https://github.com/mongodb/motor\n[mongomock-motor]: https://github.com/michaelkryukov/mongomock_motor\n\n![Mongo-Thingy](https://socialify.git.ci/Refty/mongo-thingy/image?font=Bitter&language=1&logo=https%3A%2F%2Fi.imgur.com%2FLeNC7Zb.png&owner=1&pattern=Charlie%20Brown&theme=Light)\n\n<div align=\"center\">\n <a href=\"https://pypi.org/project/mongo-thingy\"><img src=\"https://img.shields.io/pypi/v/mongo-thingy.svg\" alt=\"PyPI\"></a>\n <img src=\"https://img.shields.io/pypi/pyversions/mongo-thingy\" alt=\"Supported Python Versions\">\n <a href=\"LICENSE\"><img src=\"https://img.shields.io/github/license/refty/mongo-thingy\" alt=\"License\"></a>\n <a href=\"https://github.com/ambv/black\"><img src=\"https://img.shields.io/badge/code%20style-black-black\" alt=\"Code style\"></a>\n <br/>\n <a href=\"https://github.com/Refty/mongo-thingy/actions\"><img src=\"https://img.shields.io/github/actions/workflow/status/Refty/mongo-thingy/tests.yml?branch=master\" alt=\"Tests\"></a>\n <a href=\"https://coveralls.io/github/Refty/mongo-thingy\"><img src=\"https://img.shields.io/coveralls/Refty/mongo-thingy.svg\" alt=\"Tests\"></a>\n <a href=\"http://mongo-thingy.readthedocs.io\"><img src=\"https://readthedocs.org/projects/mongo-thingy/badge\" alt=\"Docs\"></a>\n <br /><br />\n</div>\n\n**_Mongo-Thingy_ is the most idiomatic and friendly-yet-powerful way to use\nMongoDB with Python.**\n\nIt is an _\"Object-Document Mapper\"_ that gives you full advantage of MongoDB\nschema-less design by **not** asking you to define schemas in your code.\n\nWhat you'll get:\n\n- a simple and robust pure-Python code base, with 100% coverage and few\n dependencies;\n- [PyMongo][pymongo] query language - no need to learn yet another one;\n- both sync and async support! choose what suits you best;\n- [Thingy][thingy] views - control what to show, and create fields based on\n other fields;\n- swappable backend - wanna use SQLite behind the scenes? well, you can;\n- versioning *(optional)* - rollback to any point in any thingy history;\n- and more!\n\n# Compatibility\n\nWe support all Python and MongoDB versions supported by [PyMongo][pymongo],\nnamely:\n\n- CPython 3.7+ and PyPy3.7+\n- MongoDB 3.6, 4.0, 4.2, 4.4, and 5.0.\n\nAs a backend, Mongo-Thingy supports the following libraries:\n\n- Synchronous:\n\n * [PyMongo][pymongo] (default)\n * [Mongomock][mongomock]\n * [MontyDB][montydb]\n\n- Asynchronous:\n\n * [Motor][motor] (default when Motor is installed)\n * [Motor][motor] with Tornado (default when Motor and Tornado are installed)\n * [Mongomock-Motor][mongomock-motor]\n\n# Install\n\n```sh\npip install mongo-thingy\n```\n\n# Examples\n\n## First steps\n\n### Connect, insert and find thingies\n\n```python\n>>> from mongo_thingy import connect, Thingy\n>>> connect(\"mongodb://localhost/test\")\n\n>>> class User(Thingy):\n... pass\n\n>>> user = User({\"name\": \"Mr. Foo\", \"age\": 42}).save()\n>>> User.count_documents()\n1\n>>> User.find_one({\"age\": 42})\nUser({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 42})\n```\n\nIn an AsyncIO (or Tornado) environment, use the asynchronous class instead:\n\n```python\n>>> from mongo_thingy import connect, AsyncThingy\n>>> connect(\"mongodb://localhost/test\")\n\n>>> class User(AsyncThingy):\n... pass\n\n>>> user = await User({\"name\": \"Mr. Foo\", \"age\": 42}).save()\n>>> await User.count_documents()\n1\n>>> await User.find_one({\"age\": 42})\nUser({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 42})\n```\n\nTo use another backend than the default ones, just pass its client class with\n``client_cls``:\n\n```python\n>>> import mongomock\n>>> connect(client_cls=mongomock.MongoClient)\n```\n\n### Update a thingy\n\n```python\n>>> user.age\n42\n>>> user.age = 1337\n>>> user.save()\nUser({'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337})\n```\n\n## Thingy views power\n\n### Complete information with properties\n\n```python\n>>> class User(Thingy):\n... @property\n... def username(self):\n... return \"\".join(char for char in self.name if char.isalpha())\n\n>>> User.add_view(name=\"everything\", defaults=True, include=\"username\")\n>>> user = User.find_one()\n>>> user.view(\"everything\")\n{'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337, 'username': 'MrFoo'}\n```\n\n### Hide sensitive stuff\n\n```python\n>>> User.add_view(name=\"public\", defaults=True, exclude=\"password\")\n>>> user.password = \"t0ps3cr3t\"\n>>> user.view()\n{'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337, 'password': 't0ps3cr3t'}\n>>> user.view(\"public\")\n{'_id': ObjectId(...), 'name': 'Mr. Foo', 'age': 1337}\n```\n\n### Only use certain fields/properties\n\n```python\n>>> User.add_view(name=\"credentials\", include=[\"username\", \"password\"])\n>>> user.view(\"credentials\")\n{'username': 'MrFoo', 'password': 't0ps3cr3t'}\n```\n\n### Apply views on cursors\n\n```python\n>>> cursor = User.find()\n>>> for credentials in cursor.view(\"credentials\"):\n... print(credentials)\n{'username': 'MrFoo', 'password': 't0ps3cr3t'}\n{'username': 'MrsBar', 'password': '123456789'}\n...\n```\n\nAnd if your cursor is already exhausted, you can still apply a view!\n\n```python\n>>> users = User.find().to_list(None)\n>>> for credentials in users.view(\"credentials\"):\n... print(credentials)\n{'username': 'MrFoo', 'password': 't0ps3cr3t'}\n{'username': 'MrsBar', 'password': '123456789'}\n...\n```\n\n## Versioning\n\n```python\n>>> from mongo_thingy.versioned import Versioned\n\n>>> class Article(Versioned, Thingy):\n... pass\n\n>>> article = Article(content=\"Cogito ergo sum\")\n>>> article.version\n0\n\n>>> article.save()\nArticle({'_id': ObjectId('...'), 'content': 'Cogito ergo sum'})\n>>> article.version\n1\n\n>>> article.content = \"Sum ergo cogito\"\n>>> article.save()\nArticle({'_id': ObjectId('...'), 'content': 'Sum ergo cogito'})\n>>> article.version\n2\n\n>>> article.revert()\nArticle({'_id': ObjectId('...'), 'content': 'Cogito ergo sum'})\n>>> article.version\n3\n```\n\n## Database/collection \"discovery\"\n\n### Default behaviour\n\n```python\n>>> class AuthenticationGroup(Thingy):\n... pass\n\n>>> connect(\"mongodb://localhost/\")\n>>> AuthenticationGroup.collection\nCollection(Database(MongoClient(host=['localhost:27017'], ...), 'authentication'), 'group')\n```\n\n### Use mismatching names for Thingy class and database collection\n\nYou can either specify the collection name:\n\n```python\n>>> class Foo(Thingy):\n... collection_name = \"bar\"\n```\n\nor the collection directly:\n\n```python\n>>> class Foo(Thingy):\n... collection = db.bar\n```\n\nYou can then check what collection is being used with:\n\n```python\n>>> Foo.collection\nCollection(Database(MongoClient('localhost', 27017), 'database'), 'bar')\n```\n\n## Indexes\n\n### Create an index\n\n```python\n>>> User.create_index(\"email\", sparse=True, unique=True)\n```\n\n### Add one or more indexes, create later\n\n```python\n>>> User.add_index(\"email\", sparse=True, unique=True)\n>>> User.add_index(\"username\")\n\n>>> User.create_indexes()\n```\n\n### Create all indexes of all thingies at once\n\n```python\n>>> from mongo_thingy import create_indexes\n>>> create_indexes()\n```\n\n## Dealing with camelCase data\n\n```python\n>>> from mongo_thingy.camelcase import CamelCase\n\n>>> class SystemUser(CamelCase, Thingy):\n... collection_name = \"systemUsers\"\n\n>>> user = SystemUser.find_one()\n>>> user.view()\n{'_id': ObjectId(...), 'firstName': 'John', 'lastName': 'Doe'}\n\n>>> user.first_name\n'John'\n>>> user.first_name = \"Jonny\"\n>>> user.save()\nSystemUser({'_id': ObjectId(...), firstName: 'Jonny', lastName: 'Doe'})\n```\n\n# Tests\n\nTo run the tests suite:\n\n - make sure you have a MongoDB database running on `localhost:27017` (you can\n spawn one with `docker-compose up -d`);\n - install developers requirements with `pip install -r requirements.txt`;\n - run `pytest`.\n\n# Sponsors\n\n<div align=\"center\">\n \n <a href=\"https://numberly.com/\"><img src=\"https://raw.githubusercontent.com/Refty/mongo-thingy/master/img/numberly.png\" alt=\"Numberly\"></a>\n \n \n <a href=\"https://refty.co/\"><img src=\"https://raw.githubusercontent.com/Refty/mongo-thingy/master/img/refty.png\" alt=\"Refty\"></a>\n \n</div>\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "The most idiomatic and friendly-yet-powerful way to use MongoDB with Python",
"version": "0.17.2",
"project_urls": {
"Homepage": "https://github.com/Refty/mongo-thingy"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "1ee5f7aa6e34468b9b9c15ca48d4f70e960837a1c378a779f02fc1ac617238d0",
"md5": "ac1b90ddc5b09a644a47bd6c3d1e49b3",
"sha256": "bf9546821125665bab2b3e14b79b3af9b5a4ed2cbdc34e1c2c6592d3a2c11d8f"
},
"downloads": -1,
"filename": "Mongo_Thingy-0.17.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "ac1b90ddc5b09a644a47bd6c3d1e49b3",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 9786,
"upload_time": "2024-04-08T17:00:30",
"upload_time_iso_8601": "2024-04-08T17:00:30.294545Z",
"url": "https://files.pythonhosted.org/packages/1e/e5/f7aa6e34468b9b9c15ca48d4f70e960837a1c378a779f02fc1ac617238d0/Mongo_Thingy-0.17.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "48654794eb1f8045c6d446d7ad3c1e4d2d56ba433cd560cf1f75697ff022e4ea",
"md5": "a9df53bbe59fd6dfec668329df7de2ec",
"sha256": "f83cae3364b688da8985c1d178fb22a2bd930863b9163d08f9778e5ecf87335f"
},
"downloads": -1,
"filename": "Mongo-Thingy-0.17.2.tar.gz",
"has_sig": false,
"md5_digest": "a9df53bbe59fd6dfec668329df7de2ec",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 9301,
"upload_time": "2024-04-08T17:00:32",
"upload_time_iso_8601": "2024-04-08T17:00:32.369164Z",
"url": "https://files.pythonhosted.org/packages/48/65/4794eb1f8045c6d446d7ad3c1e4d2d56ba433cd560cf1f75697ff022e4ea/Mongo-Thingy-0.17.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-04-08 17:00:32",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Refty",
"github_project": "mongo-thingy",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "thingy",
"specs": [
[
"==",
"0.10.0"
]
]
},
{
"name": "pymongo",
"specs": [
[
"==",
"4.6.3"
]
]
},
{
"name": "mongomock",
"specs": [
[
"==",
"4.0.0"
]
]
},
{
"name": "mongomock-motor",
"specs": [
[
"==",
"0.0.14"
]
]
},
{
"name": "montydb",
"specs": [
[
"==",
"2.4.0"
]
]
},
{
"name": "tornado",
"specs": [
[
"==",
"6.3.3"
]
]
},
{
"name": "motor",
"specs": [
[
"==",
"3.0.0"
]
]
},
{
"name": "pytest",
"specs": [
[
"==",
"7.0.1"
]
]
},
{
"name": "pytest-cov",
"specs": [
[
"==",
"3.0.0"
]
]
},
{
"name": "pytest-asyncio",
"specs": [
[
"==",
"0.18.3"
]
]
},
{
"name": "sphinx",
"specs": [
[
"==",
"4.4.0"
]
]
},
{
"name": "m2r2",
"specs": [
[
"==",
"0.3.2"
]
]
},
{
"name": "pre-commit",
"specs": [
[
"==",
"2.19.0"
]
]
}
],
"lcname": "mongo-thingy"
}