././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/0000755000175100001730000000000014400345501013502 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/AUTHORS0000644000175100001730000001775614400345475014604 0ustar00runnerdockerThe PRIMARY AUTHORS are (and/or have been): Ross Lawley Harry Marr Matt Dennewitz Deepak Thukral Florian Schlachter Steve Challis Wilson Júnior Dan Crosta https://github.com/dcrosta Laine Herron https://github.com/LaineHerron CONTRIBUTORS Derived from the git logs, inevitably incomplete but all of whom and others have submitted patches, reported bugs and generally helped make MongoEngine that much better: * blackbrrr * Florian Schlachter * Vincent Driessen * Steve Challis * flosch * Deepak Thukral * Colin Howe * Wilson Júnior (https://github.com/wpjunior) * Alistair Roche * Dan Crosta * Viktor Kerkez * Stephan Jaekel * Rached Ben Mustapha * Greg Turner * Daniel Hasselrot * Mircea Pasoi * Matt Chisholm * James Punteney * TimothéePeignier * Stuart Rackham * Serge Matveenko * Matt Dennewitz * Don Spaulding * Ales Zoulek * sshwsfc * sib * Samuel Clay * Nick Vlku * martin * Flavio Amieiro * Анхбаяр Лхагвадорж * Zak Johnson * Victor Farazdagi * vandersonmota * Theo Julienne * sp * Slavi Pantaleev * Richard Henry * Nicolas Perriault * Nick Vlku Jr * Michael Henson * Leo Honkanen * kuno * Josh Ourisman * Jaime * Igor Ivanov * Gregg Lind * Gareth Lloyd * Albert Choi * John Arnfield * grubberr * Paul Aliagas * Paul Cunnane * Julien Rebetez * Marc Tamlyn * Karim Allah * Adam Parrish * jpfarias * jonrscott * Alice Zoë Bevan-McGregor (https://github.com/amcgregor/) * Stephen Young * tkloc * aid * yamaneko1212 * dave mankoff * Alexander G. Morano * jwilder * Joe Shaw * Adam Flynn * Ankhbayar * Jan Schrewe * David Koblas * Crittercism * Alvin Liang * andrewmlevy * Chris Faulkner * Ashwin Purohit * Shalabh Aggarwal * Chris Williams * Robert Kajic * Jacob Peddicord * Nils Hasenbanck * mostlystatic * Greg Banks * swashbuckler * Adam Reeve * Anthony Nemitz * deignacio * Shaun Duncan * Meir Kriheli * Andrey Fedoseev * aparajita * Tristan Escalada * Alexander Koshelev * Jaime Irurzun * Alexandre González * Thomas Steinacher * Tommi Komulainen * Peter Landry * biszkoptwielki * Anton Kolechkin * Sergey Nikitin * psychogenic * Stefan Wójcik (https://github.com/wojcikstefan) * dimonb * Garry Polley * James Slagle * Adrian Scott * Peter Teichman * Jakub Kot * Jorge Bastida * Aleksandr Sorokoumov * Yohan Graterol * bool-dev * Russ Weeks * Paul Swartz * Sundar Raman * Benoit Louy * Loic Raucy (https://github.com/lraucy) * hellysmile * Jaepil Jeong * Daniil Sharou * Pete Campton * Martyn Smith * Marcelo Anton * Aleksey Porfirov (https://github.com/lexqt) * Nicolas Trippar * Manuel Hermann * Gustavo Gawryszewski * Max Countryman * caitifbrito * lcya86 刘春洋 * Martin Alderete (https://github.com/malderete) * Nick Joyce * Jared Forsyth * Kenneth Falck * Lukasz Balcerzak * Nicolas Cortot * Alex (https://github.com/kelsta) * Jin Zhang * Daniel Axtens * Leo-Naeka * Ryan Witt (https://github.com/ryanwitt) * Jiequan (https://github.com/Jiequan) * hensom (https://github.com/hensom) * zhy0216 (https://github.com/zhy0216) * istinspring (https://github.com/istinspring) * Massimo Santini (https://github.com/mapio) * Nigel McNie (https://github.com/nigelmcnie) * ygbourhis (https://github.com/ygbourhis) * Bob Dickinson (https://github.com/BobDickinson) * Michael Bartnett (https://github.com/michaelbartnett) * Alon Horev (https://github.com/alonho) * Kelvin Hammond (https://github.com/kelvinhammond) * Jatin Chopra (https://github.com/jatin) * Paul Uithol (https://github.com/PaulUithol) * Thom Knowles (https://github.com/fleat) * Paul (https://github.com/squamous) * Olivier Cortès (https://github.com/Karmak23) * crazyzubr (https://github.com/crazyzubr) * FrankSomething (https://github.com/FrankSomething) * Alexandr Morozov (https://github.com/LK4D4) * mishudark (https://github.com/mishudark) * Joe Friedl (https://github.com/grampajoe) * Daniel Ward (https://github.com/danielward) * Aniket Deshpande (https://github.com/anicake) * rfkrocktk (https://github.com/rfkrocktk) * Gustavo Andrés Angulo (https://github.com/woakas) * Dmytro Popovych (https://github.com/drudim) * Tom (https://github.com/tomprimozic) * j0hnsmith (https://github.com/j0hnsmith) * Damien Churchill (https://github.com/damoxc) * Jonathan Simon Prates (https://github.com/jonathansp) * Thiago Papageorgiou (https://github.com/tmpapageorgiou) * Omer Katz (https://github.com/thedrow) * Falcon Dai (https://github.com/falcondai) * Polyrabbit (https://github.com/polyrabbit) * Sagiv Malihi (https://github.com/sagivmalihi) * Dmitry Konishchev (https://github.com/KonishchevDmitry) * Martyn Smith (https://github.com/martynsmith) * Andrei Zbikowski (https://github.com/b1naryth1ef) * Ronald van Rij (https://github.com/ronaldvanrij) * François Schmidts (https://github.com/jaesivsm) * Eric Plumb (https://github.com/professorplumb) * Damien Churchill (https://github.com/damoxc) * Aleksandr Sorokoumov (https://github.com/Gerrrr) * Clay McClure (https://github.com/claymation) * Bruno Rocha (https://github.com/rochacbruno) * Norberto Leite (https://github.com/nleite) * Bob Cribbs (https://github.com/bocribbz) * Jay Shirley (https://github.com/jshirley) * David Bordeynik (https://github.com/DavidBord) * Axel Haustant (https://github.com/noirbizarre) * David Czarnecki (https://github.com/czarneckid) * Vyacheslav Murashkin (https://github.com/a4tunado) * André Ericson https://github.com/aericson) * Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky) * Diego Berrocal (https://github.com/cestdiego) * Matthew Ellison (https://github.com/seglberg) * Jimmy Shen (https://github.com/jimmyshen) * J. Fernando Sánchez (https://github.com/balkian) * Michael Chase (https://github.com/rxsegrxup) * Eremeev Danil (https://github.com/elephanter) * Catstyle Lee (https://github.com/Catstyle) * Kiryl Yermakou (https://github.com/rma4ok) * Matthieu Rigal (https://github.com/MRigal) * Charanpal Dhanjal (https://github.com/charanpald) * Emmanuel Leblond (https://github.com/touilleMan) * Breeze.Kay (https://github.com/9nix00) * Vicki Donchenko (https://github.com/kivistein) * Emile Caron (https://github.com/emilecaron) * Amit Lichtenberg (https://github.com/amitlicht) * Gang Li (https://github.com/iici-gli) * Lars Butler (https://github.com/larsbutler) * George Macon (https://github.com/gmacon) * Ashley Whetter (https://github.com/AWhetter) * Paul-Armand Verhaegen (https://github.com/paularmand) * Steven Rossiter (https://github.com/BeardedSteve) * Luo Peng (https://github.com/RussellLuo) * Bryan Bennett (https://github.com/bbenne10) * Gilb's Gilb's (https://github.com/gilbsgilbs) * Joshua Nedrud (https://github.com/Neurostack) * Shu Shen (https://github.com/shushen) * xiaost7 (https://github.com/xiaost7) * Victor Varvaryuk * Stanislav Kaledin (https://github.com/sallyruthstruik) * Dmitry Yantsen (https://github.com/mrTable) * Renjianxin (https://github.com/Davidrjx) * Erdenezul Batmunkh (https://github.com/erdenezul) * Andy Yankovsky (https://github.com/werat) * Bastien Gérard (https://github.com/bagerard) * Trevor Hall (https://github.com/tjhall13) * Gleb Voropaev (https://github.com/buggyspace) * Paulo Amaral (https://github.com/pauloAmaral) * Gaurav Dadhania (https://github.com/GVRV) * Yurii Andrieiev (https://github.com/yandrieiev) * Filip Kucharczyk (https://github.com/Pacu2) * Eric Timmons (https://github.com/daewok) * Matthew Simpson (https://github.com/mcsimps2) * Leonardo Domingues (https://github.com/leodmgs) * Agustin Barto (https://github.com/abarto) * Stankiewicz Mateusz (https://github.com/mas15) * Felix Schultheiß (https://github.com/felix-smashdocs) * Jan Stein (https://github.com/janste63) * Timothé Perez (https://github.com/AchilleAsh) * oleksandr-l5 (https://github.com/oleksandr-l5) * Ido Shraga (https://github.com/idoshr) * Terence Honles (https://github.com/terencehonles) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/LICENSE0000644000175100001730000000203714400345475014523 0ustar00runnerdockerCopyright (c) 2009 See AUTHORS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/MANIFEST.in0000644000175100001730000000016214400345475015251 0ustar00runnerdockerinclude MANIFEST.in include README.rst include LICENSE include AUTHORS recursive-include docs * prune docs/_build ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/PKG-INFO0000644000175100001730000001503514400345501014603 0ustar00runnerdockerMetadata-Version: 2.1 Name: mongoengine Version: 0.27.0 Summary: MongoEngine is a Python Object-Document Mapper for working with MongoDB. Home-page: http://mongoengine.org/ Author: Harry Marr Author-email: harry.marr@gmail.com Maintainer: Stefan Wojcik Maintainer-email: wojcikstefan@gmail.com License: MIT Download-URL: https://github.com/MongoEngine/mongoengine/tarball/master Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.7 License-File: LICENSE License-File: AUTHORS =========== MongoEngine =========== :Info: MongoEngine is an ORM-like layer on top of PyMongo. :Repository: https://github.com/MongoEngine/mongoengine :Author: Harry Marr (http://github.com/hmarr) :Maintainer: Stefan Wójcik (http://github.com/wojcikstefan) .. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master :target: https://travis-ci.org/MongoEngine/mongoengine .. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master :target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black .. image:: https://pepy.tech/badge/mongoengine/month :target: https://pepy.tech/project/mongoengine .. image:: https://img.shields.io/pypi/v/mongoengine.svg :target: https://pypi.python.org/pypi/mongoengine About ===== MongoEngine is a Python Object-Document Mapper for working with MongoDB. Documentation is available at https://mongoengine-odm.readthedocs.io - there is currently a `tutorial `_, a `user guide `_, and an `API reference `_. Supported MongoDB Versions ========================== MongoEngine is currently tested against MongoDB v3.6, v4.0, v4.4 and v5.0. Future versions should be supported as well, but aren't actively tested at the moment. Make sure to open an issue or submit a pull request if you experience any problems with a more recent MongoDB versions. Installation ============ We recommend the use of `virtualenv `_ and of `pip `_. You can then use ``python -m pip install -U mongoengine``. You may also have `setuptools `_ and thus you can use ``easy_install -U mongoengine``. Another option is `pipenv `_. You can then use ``pipenv install mongoengine`` to both create the virtual environment and install the package. Otherwise, you can download the source from `GitHub `_ and run ``python setup.py install``. The support for Python2 was dropped with MongoEngine 0.20.0 Dependencies ============ All of the dependencies can easily be installed via `python -m pip `_. At the very least, you'll need these two packages to use MongoEngine: - pymongo>=3.4 If you utilize a ``DateTimeField``, you might also use a more flexible date parser: - dateutil>=2.1.0 If you need to use an ``ImageField`` or ``ImageGridFsProxy``: - Pillow>=2.0.0 If you need to use signals: - blinker>=1.3 Examples ======== Some simple examples of what MongoEngine code looks like: .. code :: python from mongoengine import * connect('mydb') class BlogPost(Document): title = StringField(required=True, max_length=200) posted = DateTimeField(default=datetime.datetime.utcnow) tags = ListField(StringField(max_length=50)) meta = {'allow_inheritance': True} class TextPost(BlogPost): content = StringField(required=True) class LinkPost(BlogPost): url = StringField(required=True) # Create a text-based post >>> post1 = TextPost(title='Using MongoEngine', content='See the tutorial') >>> post1.tags = ['mongodb', 'mongoengine'] >>> post1.save() # Create a link-based post >>> post2 = LinkPost(title='MongoEngine Docs', url='hmarr.com/mongoengine') >>> post2.tags = ['mongoengine', 'documentation'] >>> post2.save() # Iterate over all posts using the BlogPost superclass >>> for post in BlogPost.objects: ... print('===', post.title, '===') ... if isinstance(post, TextPost): ... print(post.content) ... elif isinstance(post, LinkPost): ... print('Link:', post.url) ... # Count all blog posts and its subtypes >>> BlogPost.objects.count() 2 >>> TextPost.objects.count() 1 >>> LinkPost.objects.count() 1 # Count tagged posts >>> BlogPost.objects(tags='mongoengine').count() 2 >>> BlogPost.objects(tags='mongodb').count() 1 Tests ===== To run the test suite, ensure you are running a local instance of MongoDB on the standard port and have ``pytest`` installed. Then, run ``python setup.py test`` or simply ``pytest``. To run the test suite on every supported Python and PyMongo version, you can use ``tox``. You'll need to make sure you have each supported Python version installed in your environment and then: .. code-block:: shell # Install tox $ python -m pip install tox # Run the test suites $ tox If you wish to run a subset of tests, use the pytest convention: .. code-block:: shell # Run all the tests in a particular test file $ pytest tests/fields/test_fields.py # Run only particular test class in that file $ pytest tests/fields/test_fields.py::TestField Community ========= - `MongoEngine Users mailing list `_ - `MongoEngine Developers mailing list `_ Contributing ============ We welcome contributions! See the `Contribution guidelines `_ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/README.rst0000644000175100001730000001247614400345475015215 0ustar00runnerdocker=========== MongoEngine =========== :Info: MongoEngine is an ORM-like layer on top of PyMongo. :Repository: https://github.com/MongoEngine/mongoengine :Author: Harry Marr (http://github.com/hmarr) :Maintainer: Stefan Wójcik (http://github.com/wojcikstefan) .. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master :target: https://travis-ci.org/MongoEngine/mongoengine .. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master :target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black .. image:: https://pepy.tech/badge/mongoengine/month :target: https://pepy.tech/project/mongoengine .. image:: https://img.shields.io/pypi/v/mongoengine.svg :target: https://pypi.python.org/pypi/mongoengine About ===== MongoEngine is a Python Object-Document Mapper for working with MongoDB. Documentation is available at https://mongoengine-odm.readthedocs.io - there is currently a `tutorial `_, a `user guide `_, and an `API reference `_. Supported MongoDB Versions ========================== MongoEngine is currently tested against MongoDB v3.6, v4.0, v4.4 and v5.0. Future versions should be supported as well, but aren't actively tested at the moment. Make sure to open an issue or submit a pull request if you experience any problems with a more recent MongoDB versions. Installation ============ We recommend the use of `virtualenv `_ and of `pip `_. You can then use ``python -m pip install -U mongoengine``. You may also have `setuptools `_ and thus you can use ``easy_install -U mongoengine``. Another option is `pipenv `_. You can then use ``pipenv install mongoengine`` to both create the virtual environment and install the package. Otherwise, you can download the source from `GitHub `_ and run ``python setup.py install``. The support for Python2 was dropped with MongoEngine 0.20.0 Dependencies ============ All of the dependencies can easily be installed via `python -m pip `_. At the very least, you'll need these two packages to use MongoEngine: - pymongo>=3.4 If you utilize a ``DateTimeField``, you might also use a more flexible date parser: - dateutil>=2.1.0 If you need to use an ``ImageField`` or ``ImageGridFsProxy``: - Pillow>=2.0.0 If you need to use signals: - blinker>=1.3 Examples ======== Some simple examples of what MongoEngine code looks like: .. code :: python from mongoengine import * connect('mydb') class BlogPost(Document): title = StringField(required=True, max_length=200) posted = DateTimeField(default=datetime.datetime.utcnow) tags = ListField(StringField(max_length=50)) meta = {'allow_inheritance': True} class TextPost(BlogPost): content = StringField(required=True) class LinkPost(BlogPost): url = StringField(required=True) # Create a text-based post >>> post1 = TextPost(title='Using MongoEngine', content='See the tutorial') >>> post1.tags = ['mongodb', 'mongoengine'] >>> post1.save() # Create a link-based post >>> post2 = LinkPost(title='MongoEngine Docs', url='hmarr.com/mongoengine') >>> post2.tags = ['mongoengine', 'documentation'] >>> post2.save() # Iterate over all posts using the BlogPost superclass >>> for post in BlogPost.objects: ... print('===', post.title, '===') ... if isinstance(post, TextPost): ... print(post.content) ... elif isinstance(post, LinkPost): ... print('Link:', post.url) ... # Count all blog posts and its subtypes >>> BlogPost.objects.count() 2 >>> TextPost.objects.count() 1 >>> LinkPost.objects.count() 1 # Count tagged posts >>> BlogPost.objects(tags='mongoengine').count() 2 >>> BlogPost.objects(tags='mongodb').count() 1 Tests ===== To run the test suite, ensure you are running a local instance of MongoDB on the standard port and have ``pytest`` installed. Then, run ``python setup.py test`` or simply ``pytest``. To run the test suite on every supported Python and PyMongo version, you can use ``tox``. You'll need to make sure you have each supported Python version installed in your environment and then: .. code-block:: shell # Install tox $ python -m pip install tox # Run the test suites $ tox If you wish to run a subset of tests, use the pytest convention: .. code-block:: shell # Run all the tests in a particular test file $ pytest tests/fields/test_fields.py # Run only particular test class in that file $ pytest tests/fields/test_fields.py::TestField Community ========= - `MongoEngine Users mailing list `_ - `MongoEngine Developers mailing list `_ Contributing ============ We welcome contributions! See the `Contribution guidelines `_ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9112809 mongoengine-0.27.0/docs/0000755000175100001730000000000014400345501014432 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/Makefile0000644000175100001730000000641614400345475016113 0ustar00runnerdocker# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" install-deps: -pip install -r requirements.txt clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. Check $(BUILDDIR)/html/index.html" html-readthedocs: $(SPHINXBUILD) -T -E -b readthedocs $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MongoEngine.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MongoEngine.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/apireference.rst0000644000175100001730000000770614400345475017640 0ustar00runnerdocker============= API Reference ============= Connecting ========== .. autofunction:: mongoengine.connect .. autofunction:: mongoengine.register_connection Documents ========= .. autoclass:: mongoengine.Document :members: :inherited-members: .. attribute:: objects A :class:`~mongoengine.queryset.QuerySet` object that is created lazily on access. .. autoclass:: mongoengine.EmbeddedDocument :members: :inherited-members: .. autoclass:: mongoengine.DynamicDocument :members: :inherited-members: .. autoclass:: mongoengine.DynamicEmbeddedDocument :members: :inherited-members: .. autoclass:: mongoengine.document.MapReduceDocument :members: .. autoclass:: mongoengine.ValidationError :members: .. autoclass:: mongoengine.FieldDoesNotExist Context Managers ================ .. autoclass:: mongoengine.context_managers.switch_db .. autoclass:: mongoengine.context_managers.switch_collection .. autoclass:: mongoengine.context_managers.no_dereference .. autoclass:: mongoengine.context_managers.query_counter Querying ======== .. automodule:: mongoengine.queryset :synopsis: Queryset level operations .. autoclass:: mongoengine.queryset.QuerySet :members: :inherited-members: .. automethod:: QuerySet.__call__ .. autoclass:: mongoengine.queryset.QuerySetNoCache :members: .. automethod:: mongoengine.queryset.QuerySetNoCache.__call__ .. autofunction:: mongoengine.queryset.queryset_manager Fields ====== .. autoclass:: mongoengine.base.fields.BaseField .. autoclass:: mongoengine.fields.StringField .. autoclass:: mongoengine.fields.URLField .. autoclass:: mongoengine.fields.EmailField .. autoclass:: mongoengine.fields.EnumField .. autoclass:: mongoengine.fields.IntField .. autoclass:: mongoengine.fields.LongField .. autoclass:: mongoengine.fields.FloatField .. autoclass:: mongoengine.fields.DecimalField .. autoclass:: mongoengine.fields.BooleanField .. autoclass:: mongoengine.fields.DateTimeField .. autoclass:: mongoengine.fields.ComplexDateTimeField .. autoclass:: mongoengine.fields.EmbeddedDocumentField .. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField .. autoclass:: mongoengine.fields.DynamicField .. autoclass:: mongoengine.fields.ListField .. autoclass:: mongoengine.fields.EmbeddedDocumentListField .. autoclass:: mongoengine.fields.SortedListField .. autoclass:: mongoengine.fields.DictField .. autoclass:: mongoengine.fields.MapField .. autoclass:: mongoengine.fields.ReferenceField .. autoclass:: mongoengine.fields.LazyReferenceField .. autoclass:: mongoengine.fields.GenericReferenceField .. autoclass:: mongoengine.fields.GenericLazyReferenceField .. autoclass:: mongoengine.fields.CachedReferenceField .. autoclass:: mongoengine.fields.BinaryField .. autoclass:: mongoengine.fields.FileField .. autoclass:: mongoengine.fields.ImageField .. autoclass:: mongoengine.fields.SequenceField .. autoclass:: mongoengine.fields.ObjectIdField .. autoclass:: mongoengine.fields.UUIDField .. autoclass:: mongoengine.fields.GeoPointField .. autoclass:: mongoengine.fields.PointField .. autoclass:: mongoengine.fields.LineStringField .. autoclass:: mongoengine.fields.PolygonField .. autoclass:: mongoengine.fields.MultiPointField .. autoclass:: mongoengine.fields.MultiLineStringField .. autoclass:: mongoengine.fields.MultiPolygonField .. autoclass:: mongoengine.fields.GridFSError .. autoclass:: mongoengine.fields.GridFSProxy .. autoclass:: mongoengine.fields.ImageGridFsProxy .. autoclass:: mongoengine.fields.ImproperlyConfigured Embedded Document Querying ========================== .. versionadded:: 0.9 Additional queries for Embedded Documents are available when using the :class:`~mongoengine.EmbeddedDocumentListField` to store a list of embedded documents. A list of embedded documents is returned as a special list with the following methods: .. autoclass:: mongoengine.base.datastructures.EmbeddedDocumentList :members: Misc ==== .. autofunction:: mongoengine.common._import_class ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/changelog.rst0000644000175100001730000016355314400345475017142 0ustar00runnerdocker ========= Changelog ========= Development =========== - (Fill this out as you fix issues and develop your features). Changes in 0.27.0 ================= - Update uuidRepresentation warnings with "unspecified" as the future default (instead of 'standard' previously advertised) #2739 - Added `mongo_client_class` optional parameter to connect() to allow to use an alternative mongo client than pymongo.MongoClient. Typically to support mock mongo libraries like mongomock, montydb, mongita #2729 - BREAKING CHANGE: connecting MongoEngine with mongomock should now use the new `mongo_client_class` For more info, check https://docs.mongoengine.org/guide/mongomock.html - Fix DictField that always gets marked as changed #2606 - fix for Queryset.none() that has no effect on update/aggregate / first #2669 Changes in 0.26.0 ================= - BREAKING CHANGE: Improved the performance of :meth:`~mongoengine.Document.save()` by removing the call to :meth:`~mongoengine.Document.ensure_indexes` unless ``meta['auto_create_index_on_save']`` is set to True. With the default settings, Document indexes will still be created on the fly, during the first usage of the collection (query, insert, etc), they will just not be re-created whenever .save() is called. - Added meta ``auto_create_index_on_save`` so you can enable index creation on :meth:`~mongoengine.Document.save()` (as it was < 0.26.0). - BREAKING CHANGE: remove deprecated method ``ensure_index`` (replaced by ``create_index`` long time ago). - Addition of Decimal128Field: :class:`~mongoengine.fields.Decimal128Field` for accurate representation of Decimals (much better than the legacy field DecimalField). Although it could work to switch an existing DecimalField to Decimal128Field without applying a migration script, it is not recommended to do so (DecimalField uses float/str to store the value, Decimal128Field uses Decimal128). - BREAKING CHANGE: When using ListField(EnumField) or DictField(EnumField), the values weren't always cast into the Enum (#2531) - BREAKING CHANGE (bugfix) Querying ObjectIdField or ComplexDateTimeField with None no longer raise a ValidationError (#2681) - Allow updating a field that has an operator name e.g. "type" with .update(set__type="foo"). It was raising an error previously. #2595 Changes in 0.25.0 ================= - Support MONGODB-AWS authentication mechanism (with `authmechanismproperties`) #2507 - Bug Fix - distinct query doesn't obey the ``no_dereference()``. #2663 - Add tests against Mongo 5.0 in pipeline - Drop support for Python 3.6 (EOL) - Bug fix support for PyMongo>=4 to fix "pymongo.errors.InvalidOperation: Cannot use MongoClient after close" errors. #2627 Changes in 0.24.2 ================= - Bug fix regarding uuidRepresentation that was case sensitive #2650 Changes in 0.24.1 ================= - Allow pymongo<5.0 to be pulled - Don't use deprecated property for emptiness check in queryset base #2633 Changes in 0.24.0 ================= - EnumField improvements: now ``choices`` limits the values of an enum to allow - Fix bug that prevented instance queryset from using custom queryset_class #2589 - Fix deepcopy of EmbeddedDocument #2202 - Introduce a base exception class for MongoEngine exceptions (MongoEngineException). Note that this doesn't concern the pymongo errors #2515 - Fix error when using precision=0 with DecimalField #2535 - Add support for regex and whole word text search query #2568 - Add support for update aggregation pipeline #2578 - BREAKING CHANGE: Updates to support pymongo 4.0. Where possible deprecated functionality has been migrated, but additional care should be taken when migrating to pymongo 4.0 as existing code may have been using deprecated features which have now been removed #2614. For the pymongo migration guide see: https://pymongo.readthedocs.io/en/stable/migrate-to-pymongo4.html. In addition to the changes in the migration guide, the following is a high level overview of the changes made to MongoEngine when using pymongo 4.0: - limited support of geohaystack indexes has been removed - ``QuerySet.map_reduce`` has been migrated from ``Collection.map_reduce`` and ``Collection.inline_map_reduce`` to use ``db.command({mapReduce: ..., ...})`` and support between the two may need additional verification. - UUIDs are encoded with the ``pythonLegacy`` encoding by default instead of the newer and cross platform ``standard`` encoding. Existing UUIDs will need to be migrated before changing the encoding, and this should be done explicitly by the user rather than switching to a new default by MongoEngine. This default will change at a later date, but to allow specifying and then migrating to the new format a default ``json_options`` has been provided. - ``Queryset.count`` has been using ``Collection.count_documents`` and transparently falling back to ``Collection.count`` when using features that are not supported by ``Collection.count_documents``. ``Collection.count`` has been removed and no automatic fallback is possible. The migration guide documents the extended functionality which is no longer supported. Rewrite the unsupported queries or fetch the whole result set and perform the count locally. - Pymongo 4 removed db.authenticate(), on which we were relying for authenticating with username/password. The migration involved switching to providing credentials to MongoClient BUT in case the authSource isn't provided, db.authenticate used to default to authSource=current-database and MongoClient defaults to authSource="admin". Long story short, if you observe authentication issue after migrating, make sure you provide the authSource explicitly. (see #2626) Changes in 0.23.1 ================= - Bug fix: ignore LazyReferenceFields when clearing _changed_fields #2484 - Improve connection doc #2481 Changes in 0.23.0 ================= - Bugfix: manually setting SequenceField in DynamicDocument doesn't increment the counter #2471 - Add MongoDB 4.2 and 4.4 to CI - Add support for allowDiskUse on querysets #2468 Changes in 0.22.1 ================= - Declare that Py3.5 is not supported in package metadata #2449 - Moved CI from Travis to Github-Actions Changes in 0.22.0 ================= - Fix LazyReferenceField dereferencing in embedded documents #2426 - Fix regarding the recent use of Cursor.__spec in .count() that was interfering with mongomock #2425 - Drop support for Python 3.5 by introducing f-strings in the codebase Changes in 0.21.0 ================= - Bug fix in DynamicDocument which is not parsing known fields in constructor like Document do #2412 - When using pymongo >= 3.7, make use of Collection.count_documents instead of Collection.count and Cursor.count that got deprecated in pymongo >= 3.7. This should have a negative impact on performance of count see Issue #2219 - Fix a bug that made the queryset drop the read_preference after clone(). - Remove Py3.5 from CI as it reached EOL and add Python 3.9 - Fix some issues related with db_field/field conflict in constructor #2414 - BREAKING CHANGE: Fix the behavior of Doc.objects.limit(0) which should return all documents (similar to mongodb) #2311 - Bug fix in ListField when updating the first item, it was saving the whole list, instead of just replacing the first item (as usually done when updating 1 item of the list) #2392 - Add EnumField: ``mongoengine.fields.EnumField`` - Refactoring - Remove useless code related to Document.__only_fields and Queryset.only_fields - Fix query transformation regarding special operators #2365 - Bug Fix: Document.save() fails when shard_key is not _id #2154 Changes in 0.20.0 ================= - ATTENTION: Drop support for Python2 - Add Mongo 4.0 to Travis - Fix error when setting a string as a ComplexDateTimeField #2253 - Bump development Status classifier to Production/Stable #2232 - Improve Queryset.get to avoid confusing MultipleObjectsReturned message in case multiple match are found #630 - Fixed a bug causing inaccurate query results, while combining ``__raw__`` and regular filters for the same field #2264 - Add support for the `elemMatch` projection operator in .fields() (e.g BlogPost.objects.fields(elemMatch__comments="test")) #2267 - DictField validate failed without default connection (bug introduced in 0.19.0) #2239 - Remove methods that were deprecated years ago: - name parameter in Field constructor e.g `StringField(name="...")`, was replaced by db_field - Queryset.slave_okay() was deprecated since pymongo3 - dropDups was dropped with MongoDB3 - ``Queryset._ensure_indexes`` and ``Queryset.ensure_indexes``, the right method to use is ``Document.ensure_indexes`` - Added pre-commit for development/CI #2212 - Renamed requirements-lint.txt to requirements-dev.txt #2212 - Support for setting ReadConcern #2255 Changes in 0.19.1 ================= - Tests require Pillow < 7.0.0 as it dropped Python2 support - DEPRECATION: The interface of ``QuerySet.aggregate`` method was changed, it no longer takes an unpacked list of pipeline steps (*pipeline) but simply takes the pipeline list just like ``pymongo.Collection.aggregate`` does. #2079 Changes in 0.19.0 ================= - BREAKING CHANGE: ``class_check`` and ``read_preference`` keyword arguments are no longer available when filtering a ``QuerySet``. #2112 - Instead of ``Doc.objects(foo=bar, read_preference=...)`` use ``Doc.objects(foo=bar).read_preference(...)``. - Instead of ``Doc.objects(foo=bar, class_check=False)`` use ``Doc.objects(foo=bar).clear_cls_query(...)``. - This change also renames the private ``QuerySet._initial_query`` attribute to ``_cls_query``. - BREAKING CHANGE: Removed the deprecated ``format`` param from ``QuerySet.explain``. #2113 - BREAKING CHANGE: Renamed ``MongoEngineConnectionError`` to ``ConnectionFailure``. #2111 - If you catch/use ``MongoEngineConnectionError`` in your code, you'll have to rename it. - BREAKING CHANGE: Positional arguments when instantiating a document are no longer supported. #2103 - From now on keyword arguments (e.g. ``Doc(field_name=value)``) are required. - BREAKING CHANGE: A ``LazyReferenceField`` is now stored in the ``_data`` field of its parent as a ``DBRef``, ``Document``, or ``EmbeddedDocument`` (``ObjectId`` is no longer allowed). #2182 - DEPRECATION: ``Q.empty`` & ``QNode.empty`` are marked as deprecated and will be removed in a next version of MongoEngine. #2210 - Added ability to check if Q or QNode are empty by parsing them to bool. - Instead of ``Q(name="John").empty`` use ``not Q(name="John")``. - Fix updating/modifying/deleting/reloading a document that's sharded by a field with ``db_field`` specified. #2125 - Only set no_cursor_timeout when requested (fixes an incompatibility with MongoDB 4.2) #2148 - ``ListField`` now accepts an optional ``max_length`` parameter. #2110 - Improve error message related to InvalidDocumentError #2180 - Added BulkWriteError to replace NotUniqueError which was misleading in bulk write insert #2152 - Added ability to compare Q and Q operations #2204 - Added ability to use a db alias on query_counter #2194 - Added ability to specify collations for querysets with ``Doc.objects.collation`` #2024 - Fix updates of a list field by negative index #2094 - Switch from nosetest to pytest as test runner #2114 - The codebase is now formatted using ``black``. #2109 - Documentation improvements: - Documented how `pymongo.monitoring` can be used to log all queries issued by MongoEngine to the driver. Changes in 0.18.2 ================= - Replace deprecated PyMongo v2.x methods with their v3.x equivalents in the ``SequenceField``. #2097 - Various code clarity and documentation improvements. Changes in 0.18.1 ================= - Fix a bug introduced in 0.18.0 which was causing ``Document.save`` to update all the fields instead of updating only the modified fields. This bug only occurred when using a custom PK. #2082 - Add Python 3.7 to Travis CI. #2058 Changes in 0.18.0 ================= - Drop support for EOL'd MongoDB v2.6, v3.0, and v3.2. - MongoEngine now requires PyMongo >= v3.4. Travis CI now tests against MongoDB v3.4 – v3.6 and PyMongo v3.4 – v3.6. #2017 #2066 - Improve performance by avoiding a call to ``to_mongo`` in ``Document.save``. #2049 - Connection/disconnection improvements: - Expose ``mongoengine.connection.disconnect`` and ``mongoengine.connection.disconnect_all``. - Fix disconnecting. #566 #1599 #605 #607 #1213 #565 - Improve documentation of ``connect``/``disconnect``. - Fix issue when using multiple connections to the same mongo with different credentials. #2047 - ``connect`` fails immediately when db name contains invalid characters. #2031 #1718 - Fix the default write concern of ``Document.save`` that was overwriting the connection write concern. #568 - Fix querying on ``List(EmbeddedDocument)`` subclasses fields. #1961 #1492 - Fix querying on ``(Generic)EmbeddedDocument`` subclasses fields. #475 - Fix ``QuerySet.aggregate`` so that it takes limit and skip value into account. #2029 - Generate unique indices for ``SortedListField`` and ``EmbeddedDocumentListFields``. #2020 - BREAKING CHANGE: Changed the behavior of a custom field validator (i.e ``validation`` parameter of a ``Field``). It is now expected to raise a ``ValidationError`` instead of returning ``True``/``False``. #2050 - BREAKING CHANGES (associated with connection/disconnection fixes): - Calling ``connect`` 2 times with the same alias and different parameter will raise an error (should call ``disconnect`` first). - ``disconnect`` now clears ``mongoengine.connection._connection_settings``. - ``disconnect`` now clears the cached attribute ``Document._collection``. - BREAKING CHANGE: ``EmbeddedDocument.save`` & ``.reload`` no longer exist. #1552 Changes in 0.17.0 ================= - POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (``_cls``, ``_id``) when using ``QuerySet.as_pymongo``. #1976 - Document a BREAKING CHANGE introduced in 0.15.3 and not reported at that time. #1995 - DEPRECATION: ``EmbeddedDocument.save`` & ``.reload`` are marked as deprecated and will be removed in a next version of MongoEngine. #1552 - Fix ``QuerySet.only`` working improperly after using ``QuerySet.count`` of the same instance of a ``QuerySet``. - Fix ``batch_size`` that was not copied when cloning a ``QuerySet`` object. #2011 - Fix ``InvalidStringData`` error when using ``modify`` on a ``BinaryField``. #1127 - Fix test suite and CI to support MongoDB v3.4. #1445 - Fix reference fields querying the database on each access if value contains orphan DBRefs. Changes in 0.16.3 ================= - Fix ``$push`` with the ``$position`` operator not working with lists in embedded documents. #1965 Changes in 0.16.2 ================= - Fix ``Document.save`` that fails when called with ``write_concern=None`` (regression of 0.16.1). #1958 Changes in 0.16.1 ================= - Fix ``_cls`` that is not set properly in the ``Document`` constructor (regression). #1950 - Fix a bug in the ``_delta`` method - update of a ``ListField`` depends on an unrelated dynamic field update. #1733 - Remove PyMongo's deprecated ``Collection.save`` method and use ``Collection.insert_one`` instead. #1899 Changes in 0.16.0 ================= - POTENTIAL BREAKING CHANGES: - ``EmbeddedDocumentField`` will no longer accept references to Document classes in its constructor. #1661 - Get rid of the ``basecls`` parameter from the ``DictField`` constructor (dead code). #1876 - Default value of the ``ComplexDateTime`` field is now ``None`` (and no longer the current datetime). #1368 - Fix an unhashable ``TypeError`` when referencing a ``Document`` with a compound key in an ``EmbeddedDocument``. #1685 - Fix a bug where an ``EmbeddedDocument`` with the same id as its parent would not be tracked for changes. #1768 - Fix the fact that a bulk ``QuerySet.insert`` was not setting primary keys of inserted document instances. #1919 - Fix a bug when referencing an abstract class in a ``ReferenceField``. #1920 - Allow modifications to the document made in ``pre_save_post_validation`` to be taken into account. #1202 - Replace MongoDB v2.4 tests in Travis CI with MongoDB v3.2. #1903 - Fix side effects of using ``QuerySet.no_dereference`` on other documents. #1677 - Fix ``TypeError`` when using lazy Django translation objects as translated choices. #1879 - Improve Python 2-3 codebase compatibility. #1889 - Fix support for changing the default value of the ``ComplexDateTime`` field. #1368 - Improve error message in case an ``EmbeddedDocumentListField`` receives an ``EmbeddedDocument`` instance instead of a list. #1877 - Fix the ``inc`` and ``dec`` operators for the ``DecimalField``. #1517 #1320 - Ignore ``killcursors`` queries in ``query_counter`` context manager. #1869 - Fix the fact that ``query_counter`` was modifying the initial profiling level in case it was != 0. #1870 - Repair the ``no_sub_classes`` context manager + fix the fact that it was swallowing exceptions. #1865 - Fix index creation error that was swallowed by ``hasattr`` under Python 2. #1688 - ``QuerySet.limit`` function behaviour: Passing 0 as parameter will return all the documents in the cursor. #1611 - Bulk insert updates the IDs of the input documents instances. #1919 - Fix a harmless bug related to ``GenericReferenceField`` where modifications in the generic-referenced document were tracked in the parent. #1934 - Improve validation of the ``BinaryField``. #273 - Implement lazy regex compiling in Field classes to improve ``import mongoengine`` performance. #1806 - Update ``GridFSProxy.__str__`` so that it would always print both the filename and grid_id. #710 - Add ``__repr__`` to ``Q`` and ``QCombination`` classes. #1843 - Fix bug in the ``BaseList.__iter__`` operator (was occuring when modifying a BaseList while iterating over it). #1676 - Add a ``DateField``. #513 - Various improvements to the documentation. - Various code quality improvements. Changes in 0.15.3 ================= - ``Queryset.update/update_one`` methods now return an ``UpdateResult`` when ``full_result=True`` is provided and no longer a dict. #1491 - Improve ``LazyReferenceField`` and ``GenericLazyReferenceField`` with nested fields. #1704 - Fix the subfield resolve error in ``generic_emdedded_document`` query. #1651 #1652 - Use each modifier only with ``$position``. #1673 #1675 - Fix validation errors in the ``GenericEmbeddedDocumentField``. #1067 - Update cached fields when a ``fields`` argument is given. #1712 - Add a ``db`` parameter to ``register_connection`` for compatibility with ``connect``. - Use PyMongo v3.x's ``insert_one`` and ``insert_many`` in ``Document.insert``. #1491 - Use PyMongo v3.x's ``update_one`` and ``update_many`` in ``Document.update`` and ``QuerySet.update``. #1491 - Fix how ``reload(fields)`` affects changed fields. #1371 - Fix a bug where the read-only access to the database fails when trying to create indexes. #1338 Changes in 0.15.0 ================= - Add ``LazyReferenceField`` and ``GenericLazyReferenceField``. #1230 Changes in 0.14.1 ================= - Remove ``SemiStrictDict`` and start using a regular dict for ``BaseDocument._data``. #1630 - Add support for the ``$position`` param in the ``$push`` operator. #1566 - Fix ``DateTimeField`` interpreting an empty string as today. #1533 - Add a missing ``__ne__`` method to the ``GridFSProxy`` class. #1632 - Fix ``BaseQuerySet._fields_to_db_fields``. #1553 Changes in 0.14.0 ================= - BREAKING CHANGE: Remove the ``coerce_types`` param from ``QuerySet.as_pymongo``. #1549 - POTENTIAL BREAKING CHANGE: Make ``EmbeddedDocument`` not hashable by default. #1528 - Improve code quality. #1531, #1540, #1541, #1547 Changes in 0.13.0 ================= - POTENTIAL BREAKING CHANGE: Added Unicode support to the ``EmailField``, see docs/upgrade.rst for details. Changes in 0.12.0 ================= - POTENTIAL BREAKING CHANGE: Fix ``limit``/``skip``/``hint``/``batch_size`` chaining. #1476 - POTENTIAL BREAKING CHANGE: Change a public ``QuerySet.clone_into`` method to a private ``QuerySet._clone_into``. #1476 - Fix the way ``Document.objects.create`` works with duplicate IDs. #1485 - Fix connecting to a replica set with PyMongo 2.x. #1436 - Fix using sets in field choices. #1481 - Fix deleting items from a ``ListField``. #1318 - Fix an obscure error message when filtering by ``field__in=non_iterable``. #1237 - Fix behavior of a ``dec`` update operator. #1450 - Add a ``rename`` update operator. #1454 - Add validation for the ``db_field`` parameter. #1448 - Fix the error message displayed when querying an ``EmbeddedDocumentField`` by an invalid value. #1440 - Fix the error message displayed when validating Unicode URLs. #1486 - Raise an error when trying to save an abstract document. #1449 Changes in 0.11.0 ================= - BREAKING CHANGE: Rename ``ConnectionError`` to ``MongoEngineConnectionError`` since the former is a built-in exception name in Python v3.x. #1428 - BREAKING CHANGE: Drop Python v2.6 support. #1428 - BREAKING CHANGE: ``from mongoengine.base import ErrorClass`` won't work anymore for any error from ``mongoengine.errors`` (e.g. ``ValidationError``). Use ``from mongoengine.errors import ErrorClass instead``. #1428 - BREAKING CHANGE: Accessing a broken reference will raise a ``DoesNotExist`` error. In the past it used to return ``None``. #1334 - Fix absent rounding for the ``DecimalField`` when ``force_string`` is set. #1103 Changes in 0.10.8 ================= - Add support for ``QuerySet.batch_size``. (#1426) - Fix a query set iteration within an iteration. #1427 - Fix an issue where specifying a MongoDB URI host would override more information than it should. #1421 - Add an ability to filter the ``GenericReferenceField`` by an ``ObjectId`` and a ``DBRef``. #1425 - Fix cascading deletes for models with a custom primary key field. #1247 - Add ability to specify an authentication mechanism (e.g. X.509). #1333 - Add support for falsy primary keys (e.g. ``doc.pk = 0``). #1354 - Fix ``QuerySet.sum/average`` for fields w/ an explicit ``db_field``. #1417 - Fix filtering by ``embedded_doc=None``. #1422 - Add support for ``Cursor.comment``. #1420 - Fix ``doc.get__display`` methods. #1419 - Fix the ``__repr__`` method of the ``StrictDict`` #1424 - Add a deprecation warning for Python v2.6. Changes in 0.10.7 ================= - Drop Python 3.2 support #1390 - Fix a bug where a dynamic doc has an index inside a dict field. #1278 - Fix: ``ListField`` minus index assignment does not work. #1128 - Fix cascade delete mixing among collections. #1224 - Add ``signal_kwargs`` argument to ``Document.save``, ``Document.delete`` and ``BaseQuerySet.insert`` to be passed to signals calls. #1206 - Raise ``OperationError`` when trying to do a ``drop_collection`` on document with no collection set. - Fix a bug where a count on ``ListField`` of ``EmbeddedDocumentField`` fails. #1187 - Fix ``LongField`` values stored as int32 in Python 3. #1253 - ``MapField`` now handles unicode keys correctly. #1267 - ``ListField`` now handles negative indicies correctly. #1270 - Fix an ``AttributeError`` when initializing an ``EmbeddedDocument`` with positional args. #681 - Fix a ``no_cursor_timeout`` error with PyMongo v3.x. #1304 - Replace map-reduce based ``QuerySet.sum/average`` with aggregation-based implementations. #1336 - Fix support for ``__`` to escape field names that match operators' names in ``update``. #1351 - Fix ``BaseDocument._mark_as_changed``. #1369 - Add support for pickling ``QuerySet`` instances. #1397 - Fix connecting to a list of hosts. #1389 - Fix a bug where accessing broken references wouldn't raise a ``DoesNotExist`` error. #1334 - Fix not being able to specify ``use_db_field=False`` on ``ListField(EmbeddedDocumentField)`` instances. #1218 - Improvements to the dictionary field's docs. #1383 Changes in 0.10.6 ================= - Add support for mocking MongoEngine based on mongomock. #1151 - Fix not being able to run tests on Windows. #1153 - Allow creation of sparse compound indexes. #1114 Changes in 0.10.5 ================= - Fix for reloading of strict with special fields. #1156 Changes in 0.10.4 ================= - ``SaveConditionError`` is now importable from the top level package. #1165 - Add a ``QuerySet.upsert_one`` method. #1157 Changes in 0.10.3 ================= - Fix ``read_preference`` (it had chaining issues with PyMongo v2.x and it didn't work at all with PyMongo v3.x). #1042 Changes in 0.10.2 ================= - Allow shard key to point to a field in an embedded document. #551 - Allow arbirary metadata in fields. #1129 - ReferenceFields now support abstract document types. #837 Changes in 0.10.1 ================= - Fix infinite recursion with cascade delete rules under specific conditions. #1046 - Fix ``CachedReferenceField`` bug when loading cached docs as ``DBRef`` but failing to save them. #1047 - Fix ignored chained options. #842 - ``Document.save``'s ``save_condition`` error raises a ``SaveConditionError`` exception. #1070 - Fix ``Document.reload`` for the ``DynamicDocument``. #1050 - ``StrictDict`` & ``SemiStrictDict`` are shadowed at init time. #1105 - Fix ``ListField`` negative index assignment not working. #1119 - Remove code that marks a field as changed when the field has a default value but does not exist in the database. #1126 - Remove test dependencies (nose and rednose) from install dependencies. #1079 - Recursively build a query when using the ``elemMatch`` operator. #1130 - Fix instance back references for lists of embedded documents. #1131 Changes in 0.10.0 ================= - Django support was removed and will be available as a separate extension. #958 - Allow to load undeclared field with meta attribute 'strict': False #957 - Support for PyMongo 3+ #946 - Removed get_or_create() deprecated since 0.8.0. #300 - Improve Document._created status when switch collection and db #1020 - Queryset update doesn't go through field validation #453 - Added support for specifying authentication source as option ``authSource`` in URI. #967 - Fixed mark_as_changed to handle higher/lower level fields changed. #927 - ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 - Support += and *= for ListField #595 - Use sets for populating dbrefs to dereference - Fixed unpickled documents replacing the global field's list. #888 - Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 - Don't send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769) - Fix for updating sorting in SortedListField. #978 - Added __ support to escape field name in fields lookup keywords that match operators names #949 - Fix for issue where FileField deletion did not free space in GridFS. - No_dereference() not respected on embedded docs containing reference. #517 - Document save raise an exception if save_condition fails #1005 - Fixes some internal _id handling issue. #961 - Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652 - Capped collection multiple of 256. #1011 - Added ``BaseQuerySet.aggregate_sum`` and ``BaseQuerySet.aggregate_average`` methods. - Fix for delete with write_concern {'w': 0}. #1008 - Allow dynamic lookup for more than two parts. #882 - Added support for min_distance on geo queries. #831 - Allow to add custom metadata to fields #705 Changes in 0.9.0 ================ - Update FileField when creating a new file #714 - Added ``EmbeddedDocumentListField`` for Lists of Embedded Documents. #826 - ComplexDateTimeField should fall back to None when null=True #864 - Request Support for $min, $max Field update operators #863 - ``BaseDict`` does not follow ``setdefault`` #866 - Add support for $type operator # 766 - Fix tests for pymongo 2.8+ #877 - No module named 'django.utils.importlib' (Django dev) #872 - Field Choices Now Accept Subclasses of Documents - Ensure Indexes before Each Save #812 - Generate Unique Indices for Lists of EmbeddedDocuments #358 - Sparse fields #515 - write_concern not in params of Collection#remove #801 - Better BaseDocument equality check when not saved #798 - OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771 - with_limit_and_skip for count should default like in pymongo #759 - Fix storing value of precision attribute in DecimalField #787 - Set attribute to None does not work (at least for fields with default values) #734 - Querying by a field defined in a subclass raises InvalidQueryError #744 - Add Support For MongoDB 2.6.X's maxTimeMS #778 - abstract shouldn't be inherited in EmbeddedDocument # 789 - Allow specifying the '_cls' as a field for indexes #397 - Stop ensure_indexes running on a secondaries unless connection is through mongos #746 - Not overriding default values when loading a subset of fields #399 - Saving document doesn't create new fields in existing collection #620 - Added ``Queryset.aggregate`` wrapper to aggregation framework #703 - Added support to show original model fields on to_json calls instead of db_field #697 - Added Queryset.search_text to Text indexes searchs #700 - Fixed tests for Django 1.7 #696 - Follow ReferenceFields in EmbeddedDocuments with select_related #690 - Added preliminary support for text indexes #680 - Added ``elemMatch`` operator as well - ``match`` is too obscure #653 - Added support for progressive JPEG #486 #548 - Allow strings to be used in index creation #675 - Fixed EmbeddedDoc weakref proxy issue #592 - Fixed nested reference field distinct error #583 - Fixed change tracking on nested MapFields #539 - Dynamic fields in embedded documents now visible to queryset.only() / qs.exclude() #425 #507 - Add authentication_source option to register_connection #178 #464 #573 #580 #590 - Implemented equality between Documents and DBRefs #597 - Fixed ReferenceField inside nested ListFields dereferencing problem #368 - Added the ability to reload specific document fields #100 - Added db_alias support and fixes for custom map/reduce output #586 - post_save signal now has access to delta information about field changes #594 #589 - Don't query with $orderby for qs.get() #600 - Fix id shard key save issue #636 - Fixes issue with recursive embedded document errors #557 - Fix clear_changed_fields() clearing unsaved documents bug #602 - Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x. - Removing support for Python < 2.6.6 - Fixed $maxDistance location for geoJSON $near queries with MongoDB 2.6+ #664 - QuerySet.modify() and Document.modify() methods to provide find_and_modify() like behaviour #677 #773 - Added support for the using() method on a queryset #676 - PYPY support #673 - Connection pooling #674 - Avoid to open all documents from cursors in an if stmt #655 - Ability to clear the ordering #657 - Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError #626 - Slots - memory improvements #625 - Fixed incorrectly split a query key when it ends with "_" #619 - Geo docs updates #613 - Workaround a dateutil bug #608 - Conditional save for atomic-style operations #511 - Allow dynamic dictionary-style field access #559 - Increase email field length to accommodate new TLDs #726 - index_cls is ignored when deciding to set _cls as index prefix #733 - Make 'db' argument to connection optional #737 - Allow atomic update for the entire ``DictField`` #742 - Added MultiPointField, MultiLineField, MultiPolygonField - Fix multiple connections aliases being rewritten #748 - Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791 - Make ``in_bulk()`` respect ``no_dereference()`` #775 - Handle None from model __str__; Fixes #753 #754 - _get_changed_fields fix for embedded documents with id field. #925 Changes in 0.8.7 ================ - Calling reload on deleted / nonexistent documents raises DoesNotExist (#538) - Stop ensure_indexes running on a secondaries (#555) - Fix circular import issue with django auth (#531) (#545) Changes in 0.8.6 ================ - Fix django auth import (#531) Changes in 0.8.5 ================ - Fix multi level nested fields getting marked as changed (#523) - Django 1.6 login fix (#522) (#527) - Django 1.6 session fix (#509) - EmbeddedDocument._instance is now set when setting the attribute (#506) - Fixed EmbeddedDocument with ReferenceField equality issue (#502) - Fixed GenericReferenceField serialization order (#499) - Fixed count and none bug (#498) - Fixed bug with .only() and DictField with digit keys (#496) - Added user_permissions to Django User object (#491, #492) - Fix updating Geo Location fields (#488) - Fix handling invalid dict field value (#485) - Added app_label to MongoUser (#484) - Use defaults when host and port are passed as None (#483) - Fixed distinct casting issue with ListField of EmbeddedDocuments (#470) - Fixed Django 1.6 sessions (#454, #480) Changes in 0.8.4 ================ - Remove database name necessity in uri connection schema (#452) - Fixed "$pull" semantics for nested ListFields (#447) - Allow fields to be named the same as query operators (#445) - Updated field filter logic - can now exclude subclass fields (#443) - Fixed dereference issue with embedded listfield referencefields (#439) - Fixed slice when using inheritance causing fields to be excluded (#437) - Fixed ._get_db() attribute after a Document.switch_db() (#441) - Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449) - Handle dynamic fieldnames that look like digits (#434) - Added get_user_document and improve mongo_auth module (#423) - Added str representation of GridFSProxy (#424) - Update transform to handle docs erroneously passed to unset (#416) - Fixed indexing - turn off _cls (#414) - Fixed dereference threading issue in ComplexField.__get__ (#412) - Fixed QuerySetNoCache.count() caching (#410) - Don't follow references in _get_changed_fields (#422, #417) - Allow args and kwargs to be passed through to_json (#420) Changes in 0.8.3 ================ - Fixed EmbeddedDocuments with ``id`` also storing ``_id`` (#402) - Added get_proxy_object helper to filefields (#391) - Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) - Fixed sum and average mapreduce dot notation support (#375, #376, #393) - Fixed as_pymongo to return the id (#386) - Document.select_related() now respects ``db_alias`` (#377) - Reload uses shard_key if applicable (#384) - Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) - Fixed pickling dynamic documents ``_dynamic_fields`` (#387) - Fixed ListField setslice and delslice dirty tracking (#390) - Added Django 1.5 PY3 support (#392) - Added match ($elemMatch) support for EmbeddedDocuments (#379) - Fixed weakref being valid after reload (#374) - Fixed queryset.get() respecting no_dereference (#373) - Added full_result kwarg to update (#380) Changes in 0.8.2 ================ - Added compare_indexes helper (#361) - Fixed cascading saves which weren't turned off as planned (#291) - Fixed Datastructures so instances are a Document or EmbeddedDocument (#363) - Improved cascading saves write performance (#361) - Fixed ambiguity and differing behaviour regarding field defaults (#349) - ImageFields now include PIL error messages if invalid error (#353) - Added lock when calling doc.Delete() for when signals have no sender (#350) - Reload forces read preference to be PRIMARY (#355) - Querysets are now lest restrictive when querying duplicate fields (#332, #333) - FileField now honouring db_alias (#341) - Removed customised __set__ change tracking in ComplexBaseField (#344) - Removed unused var in _get_changed_fields (#347) - Added pre_save_post_validation signal (#345) - DateTimeField now auto converts valid datetime isostrings into dates (#343) - DateTimeField now uses dateutil for parsing if available (#343) - Fixed Doc.objects(read_preference=X) not setting read preference (#352) - Django session ttl index expiry fixed (#329) - Fixed pickle.loads (#342) - Documentation fixes Changes in 0.8.1 ================ - Fixed Python 2.6 django auth importlib issue (#326) - Fixed pickle unsaved document regression (#327) Changes in 0.8.0 ================ - Fixed querying ReferenceField custom_id (#317) - Fixed pickle issues with collections (#316) - Added ``get_next_value`` preview for SequenceFields (#319) - Added no_sub_classes context manager and queryset helper (#312) - Querysets now utilises a local cache - Changed __len__ behaviour in the queryset (#247, #311) - Fixed querying string versions of ObjectIds issue with ReferenceField (#307) - Added $setOnInsert support for upserts (#308) - Upserts now possible with just query parameters (#309) - Upserting is the only way to ensure docs are saved correctly (#306) - Fixed register_delete_rule inheritance issue - Fix cloning of sliced querysets (#303) - Fixed update_one write concern (#302) - Updated minimum requirement for pymongo to 2.5 - Add support for new geojson fields, indexes and queries (#299) - If values cant be compared mark as changed (#287) - Ensure as_pymongo() and to_json honour only() and exclude() (#293) - Document serialization uses field order to ensure a strict order is set (#296) - DecimalField now stores as float not string (#289) - UUIDField now stores as a binary by default (#292) - Added Custom User Model for Django 1.5 (#285) - Cascading saves now default to off (#291) - ReferenceField now store ObjectId's by default rather than DBRef (#290) - Added ImageField support for inline replacements (#86) - Added SequenceField.set_next_value(value) helper (#159) - Updated .only() behaviour - now like exclude it is chainable (#202) - Added with_limit_and_skip support to count() (#235) - Objects queryset manager now inherited (#256) - Updated connection to use MongoClient (#262, #274) - Fixed db_alias and inherited Documents (#143) - Documentation update for document errors (#124) - Deprecated ``get_or_create`` (#35) - Updated inheritable objects created by upsert now contain _cls (#118) - Added support for creating documents with embedded documents in a single operation (#6) - Added to_json and from_json to Document (#1) - Added to_json and from_json to QuerySet (#131) - Updated index creation now tied to Document class (#102) - Added none() to queryset (#127) - Updated SequenceFields to allow post processing of the calculated counter value (#141) - Added clean method to documents for pre validation data cleaning (#60) - Added support setting for read prefrence at a query level (#157) - Added _instance to EmbeddedDocuments pointing to the parent (#139) - Inheritance is off by default (#122) - Remove _types and just use _cls for inheritance (#148) - Only allow QNode instances to be passed as query objects (#199) - Dynamic fields are now validated on save (#153) (#154) - Added support for multiple slices and made slicing chainable. (#170) (#190) (#191) - Fixed GridFSProxy __getattr__ behaviour (#196) - Fix Django timezone support (#151) - Simplified Q objects, removed QueryTreeTransformerVisitor (#98) (#171) - FileFields now copyable (#198) - Querysets now return clones and are no longer edit in place (#56) - Added support for $maxDistance (#179) - Uses getlasterror to test created on updated saves (#163) - Fixed inheritance and unique index creation (#140) - Fixed reverse delete rule with inheritance (#197) - Fixed validation for GenericReferences which haven't been dereferenced - Added switch_db context manager (#106) - Added switch_db method to document instances (#106) - Added no_dereference context manager (#82) (#61) - Added switch_collection context manager (#220) - Added switch_collection method to document instances (#220) - Added support for compound primary keys (#149) (#121) - Fixed overriding objects with custom manager (#58) - Added no_dereference method for querysets (#82) (#61) - Undefined data should not override instance methods (#49) - Added Django Group and Permission (#142) - Added Doc class and pk to Validation messages (#69) - Fixed Documents deleted via a queryset don't call any signals (#105) - Added the "get_decoded" method to the MongoSession class (#216) - Fixed invalid choices error bubbling (#214) - Updated Save so it calls $set and $unset in a single operation (#211) - Fixed inner queryset looping (#204) Changes in 0.7.10 ================= - Fix UnicodeEncodeError for dbref (#278) - Allow construction using positional parameters (#268) - Updated EmailField length to support long domains (#243) - Added 64-bit integer support (#251) - Added Django sessions TTL support (#224) - Fixed issue with numerical keys in MapField(EmbeddedDocumentField()) (#240) - Fixed clearing _changed_fields for complex nested embedded documents (#237, #239, #242) - Added "id" back to _data dictionary (#255) - Only mark a field as changed if the value has changed (#258) - Explicitly check for Document instances when dereferencing (#261) - Fixed order_by chaining issue (#265) - Added dereference support for tuples (#250) - Resolve field name to db field name when using distinct(#260, #264, #269) - Added kwargs to doc.save to help interop with django (#223, #270) - Fixed cloning querysets in PY3 - Int fields no longer unset in save when changed to 0 (#272) - Fixed ReferenceField query chaining bug fixed (#254) Changes in 0.7.9 ================ - Better fix handling for old style _types - Embedded SequenceFields follow collection naming convention Changes in 0.7.8 ================ - Fix sequence fields in embedded documents (#166) - Fix query chaining with .order_by() (#176) - Added optional encoding and collection config for Django sessions (#180, #181, #183) - Fixed EmailField so can add extra validation (#173, #174, #187) - Fixed bulk inserts can now handle custom pk's (#192) - Added as_pymongo method to return raw or cast results from pymongo (#193) Changes in 0.7.7 ================ - Fix handling for old style _types Changes in 0.7.6 ================ - Unicode fix for repr (#133) - Allow updates with match operators (#144) - Updated URLField - now can have a override the regex (#136) - Allow Django AuthenticationBackends to work with Django user (hmarr/mongoengine#573) - Fixed reload issue with ReferenceField where dbref=False (#138) Changes in 0.7.5 ================ - ReferenceFields with dbref=False use ObjectId instead of strings (#134) See ticket for upgrade notes (#134) Changes in 0.7.4 ================ - Fixed index inheritance issues - firmed up testcases (#123) (#125) Changes in 0.7.3 ================ - Reverted EmbeddedDocuments meta handling - now can turn off inheritance (#119) Changes in 0.7.2 ================ - Update index spec generation so its not destructive (#113) Changes in 0.7.1 ================ - Fixed index spec inheritance (#111) Changes in 0.7.0 ================ - Updated queryset.delete so you can use with skip / limit (#107) - Updated index creation allows kwargs to be passed through refs (#104) - Fixed Q object merge edge case (#109) - Fixed reloading on sharded documents (hmarr/mongoengine#569) - Added NotUniqueError for duplicate keys (#62) - Added custom collection / sequence naming for SequenceFields (#92) - Fixed UnboundLocalError in composite index with pk field (#88) - Updated ReferenceField's to optionally store ObjectId strings this will become the default in 0.8 (#89) - Added FutureWarning - save will default to ``cascade=False`` in 0.8 - Added example of indexing embedded document fields (#75) - Fixed ImageField resizing when forcing size (#80) - Add flexibility for fields handling bad data (#78) - Embedded Documents no longer handle meta definitions - Use weakref proxies in base lists / dicts (#74) - Improved queryset filtering (hmarr/mongoengine#554) - Fixed Dynamic Documents and Embedded Documents (hmarr/mongoengine#561) - Fixed abstract classes and shard keys (#64) - Fixed Python 2.5 support - Added Python 3 support (thanks to Laine Heron) Changes in 0.6.20 ================= - Added support for distinct and db_alias (#59) - Improved support for chained querysets when constraining the same fields (hmarr/mongoengine#554) - Fixed BinaryField lookup re (#48) Changes in 0.6.19 ================= - Added Binary support to UUID (#47) - Fixed MapField lookup for fields without declared lookups (#46) - Fixed BinaryField python value issue (#48) - Fixed SequenceField non numeric value lookup (#41) - Fixed queryset manager issue (#52) - Fixed FileField comparision (hmarr/mongoengine#547) Changes in 0.6.18 ================= - Fixed recursion loading bug in _get_changed_fields Changes in 0.6.17 ================= - Fixed issue with custom queryset manager expecting explict variable names Changes in 0.6.16 ================= - Fixed issue where db_alias wasn't inherited Changes in 0.6.15 ================= - Updated validation error messages - Added support for null / zero / false values in item_frequencies - Fixed cascade save edge case - Fixed geo index creation through reference fields - Added support for args / kwargs when using @queryset_manager - Deref list custom id fix Changes in 0.6.14 ================= - Fixed error dict with nested validation - Fixed Int/Float fields and not equals None - Exclude tests from installation - Allow tuples for index meta - Fixed use of str in instance checks - Fixed unicode support in transform update - Added support for add_to_set and each Changes in 0.6.13 ================= - Fixed EmbeddedDocument db_field validation issue - Fixed StringField unicode issue - Fixes __repr__ modifying the cursor Changes in 0.6.12 ================= - Fixes scalar lookups for primary_key - Fixes error with _delta handling DBRefs Changes in 0.6.11 ================= - Fixed inconsistency handling None values field attrs - Fixed map_field embedded db_field issue - Fixed .save() _delta issue with DbRefs - Fixed Django TestCase - Added cmp to Embedded Document - Added PULL reverse_delete_rule - Fixed CASCADE delete bug - Fixed db_field data load error - Fixed recursive save with FileField Changes in 0.6.10 ================= - Fixed basedict / baselist to return super(..) - Promoted BaseDynamicField to DynamicField Changes in 0.6.9 ================ - Fixed sparse indexes on inherited docs - Removed FileField auto deletion, needs more work maybe 0.7 Changes in 0.6.8 ================ - Fixed FileField losing reference when no default set - Removed possible race condition from FileField (grid_file) - Added assignment to save, can now do: ``b = MyDoc(**kwargs).save()`` - Added support for pull operations on nested EmbeddedDocuments - Added support for choices with GenericReferenceFields - Added support for choices with GenericEmbeddedDocumentFields - Fixed Django 1.4 sessions first save data loss - FileField now automatically delete files on .delete() - Fix for GenericReference to_mongo method - Fixed connection regression - Updated Django User document, now allows inheritance Changes in 0.6.7 ================ - Fixed indexing on '_id' or 'pk' or 'id' - Invalid data from the DB now raises a InvalidDocumentError - Cleaned up the Validation Error - docs and code - Added meta ``auto_create_index`` so you can disable index creation - Added write concern options to inserts - Fixed typo in meta for index options - Bug fix Read preference now passed correctly - Added support for File like objects for GridFS - Fix for #473 - Dereferencing abstracts Changes in 0.6.6 ================ - Django 1.4 fixed (finally) - Added tests for Django Changes in 0.6.5 ================ - More Django updates Changes in 0.6.4 ================ - Refactored connection / fixed replicasetconnection - Bug fix for unknown connection alias error message - Sessions support Django 1.3 and Django 1.4 - Minor fix for ReferenceField Changes in 0.6.3 ================ - Updated sessions for Django 1.4 - Bug fix for updates where listfields contain embedded documents - Bug fix for collection naming and mixins Changes in 0.6.2 ================ - Updated documentation for ReplicaSet connections - Hack round _types issue with SERVER-5247 - querying other arrays may also cause problems. Changes in 0.6.1 ================ - Fix for replicaSet connections Changes in 0.6 ============== - Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7 - Added support for covered indexes when inheritance is off - No longer always upsert on save for items with a '_id' - Error raised if update doesn't have an operation - DeReferencing is now thread safe - Errors raised if trying to perform a join in a query - Updates can now take __raw__ queries - Added custom 2D index declarations - Added replicaSet connection support - Updated deprecated imports from pymongo (safe for pymongo 2.2) - Added uri support for connections - Added scalar for efficiently returning partial data values (aliased to values_list) - Fixed limit skip bug - Improved Inheritance / Mixin - Added sharding support - Added pymongo 2.1 support - Fixed Abstract documents can now declare indexes - Added db_alias support to individual documents - Fixed GridFS documents can now be pickled - Added Now raises an InvalidDocumentError when declaring multiple fields with the same db_field - Added InvalidQueryError when calling with_id with a filter - Added support for DBRefs in distinct() - Fixed issue saving False booleans - Fixed issue with dynamic documents deltas - Added Reverse Delete Rule support to ListFields - MapFields aren't supported - Added customisable cascade kwarg options - Fixed Handle None values for non-required fields - Removed Document._get_subclasses() - no longer required - Fixed bug requiring subclasses when not actually needed - Fixed deletion of dynamic data - Added support for the $elementMatch operator - Added reverse option to SortedListFields - Fixed dereferencing - multi directional list dereferencing - Fixed issue creating indexes with recursive embedded documents - Fixed recursive lookup in _unique_with_indexes - Fixed passing ComplexField defaults to constructor for ReferenceFields - Fixed validation of DictField Int keys - Added optional cascade saving - Fixed dereferencing - max_depth now taken into account - Fixed document mutation saving issue - Fixed positional operator when replacing embedded documents - Added Non-Django Style choices back (you can have either) - Fixed __repr__ of a sliced queryset - Added recursive validation error of documents / complex fields - Fixed breaking during queryset iteration - Added pre and post bulk-insert signals - Added ImageField - requires PIL - Fixed Reference Fields can be None in get_or_create / queries - Fixed accessing pk on an embedded document - Fixed calling a queryset after drop_collection now recreates the collection - Add field name to validation exception messages - Added UUID field - Improved efficiency of .get() - Updated ComplexFields so if required they won't accept empty lists / dicts - Added spec file for rpm-based distributions - Fixed ListField so it doesnt accept strings - Added DynamicDocument and EmbeddedDynamicDocument classes for expando schemas Changes in v0.5.2 ================= - A Robust Circular reference bugfix Changes in v0.5.1 ================= - Fixed simple circular reference bug Changes in v0.5 =============== - Added InvalidDocumentError - so Document core methods can't be overwritten - Added GenericEmbeddedDocument - so you can embed any type of embeddable document - Added within_polygon support - for those with mongodb 1.9 - Updated sum / average to use map_reduce as db.eval doesn't work in sharded environments - Added where() - filter to allowing users to specify query expressions as Javascript - Added SequenceField - for creating sequential counters - Added update() convenience method to a document - Added cascading saves - so changes to Referenced documents are saved on .save() - Added select_related() support - Added support for the positional operator - Updated geo index checking to be recursive and check in embedded documents - Updated default collection naming convention - Added Document Mixin support - Fixed queryet __repr__ mid iteration - Added hint() support, so can tell Mongo the proper index to use for the query - Fixed issue with inconsistent setting of _cls breaking inherited referencing - Added help_text and verbose_name to fields to help with some form libs - Updated item_frequencies to handle embedded document lookups - Added delta tracking now only sets / unsets explicitly changed fields - Fixed saving so sets updated values rather than overwrites - Added ComplexDateTimeField - Handles datetimes correctly with microseconds - Added ComplexBaseField - for improved flexibility and performance - Added get_FIELD_display() method for easy choice field displaying - Added queryset.slave_okay(enabled) method - Updated queryset.timeout(enabled) and queryset.snapshot(enabled) to be chainable - Added insert method for bulk inserts - Added blinker signal support - Added query_counter context manager for tests - Added map_reduce method item_frequencies and set as default (as db.eval doesn't work in sharded environments) - Added inline_map_reduce option to map_reduce - Updated connection exception so it provides more info on the cause. - Added searching multiple levels deep in ``DictField`` - Added ``DictField`` entries containing strings to use matching operators - Added ``MapField``, similar to ``DictField`` - Added Abstract Base Classes - Added Custom Objects Managers - Added sliced subfields updating - Added ``NotRegistered`` exception if dereferencing ``Document`` not in the registry - Added a write concern for ``save``, ``update``, ``update_one`` and ``get_or_create`` - Added slicing / subarray fetching controls - Fixed various unique index and other index issues - Fixed threaded connection issues - Added spherical geospatial query operators - Updated queryset to handle latest version of pymongo map_reduce now requires an output. - Added ``Document`` __hash__, __ne__ for pickling - Added ``FileField`` optional size arg for read method - Fixed ``FileField`` seek and tell methods for reading files - Added ``QuerySet.clone`` to support copying querysets - Fixed item_frequencies when using name thats the same as a native js function - Added reverse delete rules - Fixed issue with unset operation - Fixed Q-object bug - Added ``QuerySet.all_fields`` resets previous .only() and .exclude() - Added ``QuerySet.exclude`` - Added django style choices - Fixed order and filter issue - Added ``QuerySet.only`` subfield support - Added creation_counter to ``BaseField`` allowing fields to be sorted in the way the user has specified them - Fixed various errors - Added many tests Changes in v0.4 =============== - Added ``GridFSStorage`` Django storage backend - Added ``FileField`` for GridFS support - New Q-object implementation, which is no longer based on Javascript - Added ``SortedListField`` - Added ``EmailField`` - Added ``GeoPointField`` - Added ``exact`` and ``iexact`` match operators to ``QuerySet`` - Added ``get_document_or_404`` and ``get_list_or_404`` Django shortcuts - Added new query operators for Geo queries - Added ``not`` query operator - Added new update operators: ``pop`` and ``add_to_set`` - Added ``__raw__`` query parameter - Added support for custom querysets - Fixed document inheritance primary key issue - Added support for querying by array element position - Base class can now be defined for ``DictField`` - Fixed MRO error that occured on document inheritance - Added ``QuerySet.distinct``, ``QuerySet.create``, ``QuerySet.snapshot``, ``QuerySet.timeout`` and ``QuerySet.all`` - Subsequent calls to ``connect()`` now work - Introduced ``min_length`` for ``StringField`` - Fixed multi-process connection issue - Other minor fixes Changes in v0.3 =============== - Added MapReduce support - Added ``contains``, ``startswith`` and ``endswith`` query operators (and case-insensitive versions that are prefixed with 'i') - Deprecated fields' ``name`` parameter, replaced with ``db_field`` - Added ``QuerySet.only`` for only retrieving specific fields - Added ``QuerySet.in_bulk()`` for bulk querying using ids - ``QuerySet``\ s now have a ``rewind()`` method, which is called automatically when the iterator is exhausted, allowing ``QuerySet``\ s to be reused - Added ``DictField`` - Added ``URLField`` - Added ``DecimalField`` - Added ``BinaryField`` - Added ``GenericReferenceField`` - Added ``get()`` and ``get_or_create()`` methods to ``QuerySet`` - ``ReferenceField``\ s may now reference the document they are defined on (recursive references) and documents that have not yet been defined - ``Document`` objects may now be compared for equality (equal if _ids are equal and documents are of same type) - ``QuerySet`` update methods now have an ``upsert`` parameter - Added field name substitution for Javascript code (allows the user to use the Python names for fields in JS, which are later substituted for the real field names) - ``Q`` objects now support regex querying - Fixed bug where referenced documents within lists weren't properly dereferenced - ``ReferenceField``\ s may now be queried using their _id - Fixed bug where ``EmbeddedDocuments`` couldn't be non-polymorphic - ``queryset_manager`` functions now accept two arguments -- the document class as the first and the queryset as the second - Fixed bug where ``QuerySet.exec_js`` ignored ``Q`` objects - Other minor fixes Changes in v0.2.2 ================= - Fixed bug that prevented indexes from being used on ``ListField``\ s - ``Document.filter()`` added as an alias to ``Document.__call__()`` - ``validate()`` may now be used on ``EmbeddedDocument``\ s Changes in v0.2.1 ================= - Added a MongoEngine backend for Django sessions - Added ``force_insert`` to ``Document.save()`` - Improved querying syntax for ``ListField`` and ``EmbeddedDocumentField`` - Added support for user-defined primary keys (``_id`` in MongoDB) Changes in v0.2 =============== - Added ``Q`` class for building advanced queries - Added ``QuerySet`` methods for atomic updates to documents - Fields may now specify ``unique=True`` to enforce uniqueness across a collection - Added option for default document ordering - Fixed bug in index definitions Changes in v0.1.3 ================= - Added Django authentication backend - Added ``Document.meta`` support for indexes, which are ensured just before querying takes place - A few minor bugfixes Changes in v0.1.2 ================= - Query values may be processed before before being used in queries - Made connections lazy - Fixed bug in Document dictionary-style access - Added ``BooleanField`` - Added ``Document.reload()`` method Changes in v0.1.1 ================= - Documents may now use capped collections ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9112809 mongoengine-0.27.0/docs/code/0000755000175100001730000000000014400345501015344 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/code/tumblelog.py0000644000175100001730000000326414400345475017727 0ustar00runnerdockerfrom mongoengine import * connect("tumblelog") class Comment(EmbeddedDocument): content = StringField() name = StringField(max_length=120) class User(Document): email = StringField(required=True) first_name = StringField(max_length=50) last_name = StringField(max_length=50) class Post(Document): title = StringField(max_length=120, required=True) author = ReferenceField(User) tags = ListField(StringField(max_length=30)) comments = ListField(EmbeddedDocumentField(Comment)) # bugfix meta = {"allow_inheritance": True} class TextPost(Post): content = StringField() class ImagePost(Post): image_path = StringField() class LinkPost(Post): link_url = StringField() Post.drop_collection() john = User(email="jdoe@example.com", first_name="John", last_name="Doe") john.save() post1 = TextPost(title="Fun with MongoEngine", author=john) post1.content = "Took a look at MongoEngine today, looks pretty cool." post1.tags = ["mongodb", "mongoengine"] post1.save() post2 = LinkPost(title="MongoEngine Documentation", author=john) post2.link_url = "http://tractiondigital.com/labs/mongoengine/docs" post2.tags = ["mongoengine"] post2.save() print("ALL POSTS") print() for post in Post.objects: print(post.title) # print '=' * post.title.count() print("=" * 20) if isinstance(post, TextPost): print(post.content) if isinstance(post, LinkPost): print("Link:", post.link_url) print() print() print("POSTS TAGGED 'MONGODB'") print() for post in Post.objects(tags="mongodb"): print(post.title) print() num_posts = Post.objects(tags="mongodb").count() print('Found %d posts with tag "mongodb"' % num_posts) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/conf.py0000644000175100001730000001512714400345475015751 0ustar00runnerdocker# # MongoEngine documentation build configuration file, created by # sphinx-quickstart on Sun Nov 22 18:14:13 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import sphinx_rtd_theme import mongoengine # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "readthedocs_ext.readthedocs"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8' # The master toctree document. master_doc = "index" # General information about the project. project = "MongoEngine" copyright = "2009, MongoEngine Authors" # noqa: A001 # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = mongoengine.get_version() # The full version, including alpha/beta/rc tags. release = mongoengine.get_version() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. # unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {"canonical_url": "http://docs.mongoengine.org/en/latest/"} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { "index": ["globaltoc.html", "searchbox.html"], "**": ["localtoc.html", "relations.html", "searchbox.html"], } # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_use_modindex = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = "MongoEnginedoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). latex_paper_size = "a4" # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "MongoEngine.tex", "MongoEngine Documentation", "Ross Lawley", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_use_modindex = True autoclass_content = "both" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/django.rst0000644000175100001730000000116214400345475016440 0ustar00runnerdocker============== Django Support ============== .. note:: Django support has been split from the main MongoEngine repository. The *legacy* Django extension may be found bundled with the 0.9 release of MongoEngine. Help Wanted! ------------ The MongoEngine team is looking for help contributing and maintaining a new Django extension for MongoEngine! If you have Django experience and would like to help contribute to the project, please get in touch on the `mailing list `_ or by simply contributing on `GitHub `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/faq.rst0000644000175100001730000000076114400345475015751 0ustar00runnerdocker========================== Frequently Asked Questions ========================== Does MongoEngine support asynchronous drivers (Motor, TxMongo)? --------------------------------------------------------------- No, MongoEngine is exclusively based on PyMongo and isn't designed to support other driver. If this is a requirement for your project, check the alternative: `uMongo`_ and `MotorEngine`_. .. _uMongo: https://umongo.readthedocs.io/ .. _MotorEngine: https://motorengine.readthedocs.io/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9112809 mongoengine-0.27.0/docs/guide/0000755000175100001730000000000014400345501015527 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/connecting.rst0000644000175100001730000002062714400345475020431 0ustar00runnerdocker.. _guide-connecting: ===================== Connecting to MongoDB ===================== Connections in MongoEngine are registered globally and are identified with aliases. If no ``alias`` is provided during the connection, it will use "default" as alias. To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect` function. The first argument is the name of the database to connect to:: from mongoengine import connect connect('project1') By default, MongoEngine assumes that the :program:`mongod` instance is running on **localhost** on port **27017**. If MongoDB is running elsewhere, you need to provide details on how to connect. There are two ways of doing this. Using a connection string in URI format (**this is the preferred method**) or individual attributes provided as keyword arguments. Connect with URI string ======================= When using a connection string in URI format you should specify the connection details as the :attr:`host` to :func:`~mongoengine.connect`. In a web application context for instance, the URI is typically read from the config file:: connect(host="mongodb://127.0.0.1:27017/my_db") If the database requires authentication, you can specify it in the URI. As each database can have its own users configured, you need to tell MongoDB where to look for the user you are working with, that's what the ``?authSource=admin`` bit of the MongoDB connection string is for:: # Connects to 'my_db' database by authenticating # with given credentials against the 'admin' database (by default as authSource isn't provided) connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db") # Equivalent to previous connection but explicitly states that # it should use admin as the authentication source database connect(host="mongodb://my_user:my_password@hostname:port/my_db?authSource=admin") # Connects to 'my_db' database by authenticating # with given credentials against that same database connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=my_db") The URI string can also be used to configure advanced parameters like ssl, replicaSet, etc. For more information or example about URI string, you can refer to the `official doc `_:: connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=admin&ssl=true&replicaSet=globaldb") .. note:: URI containing SRV records (e.g "mongodb+srv://server.example.com/") can be used as well Connect with keyword attributes =============================== The second option for specifying the connection details is to provide the information as keyword attributes to :func:`~mongoengine.connect`:: connect('my_db', host='127.0.0.1', port=27017) If the database requires authentication, :attr:`username`, :attr:`password` and :attr:`authentication_source` arguments should be provided:: connect('my_db', username='my_user', password='my_password', authentication_source='admin') The set of attributes that :func:`~mongoengine.connect` recognizes includes but is not limited to: :attr:`host`, :attr:`port`, :attr:`read_preference`, :attr:`username`, :attr:`password`, :attr:`authentication_source`, :attr:`authentication_mechanism`, :attr:`replicaset`, :attr:`tls`, etc. Most of the parameters accepted by `pymongo.MongoClient `_ can be used with :func:`~mongoengine.connect` and will simply be forwarded when instantiating the `pymongo.MongoClient`. .. note:: Database, username and password from URI string overrides corresponding parameters in :func:`~mongoengine.connect`, this should obviously be avoided: :: connect( db='test', username='user', password='12345', host='mongodb://admin:qwerty@localhost/production' ) will establish connection to ``production`` database using ``admin`` username and ``qwerty`` password. .. note:: Calling :func:`~mongoengine.connect` without argument will establish a connection to the "test" database by default Read Preferences ================ As stated above, Read preferences are supported through the connection but also via individual queries by passing the read_preference :: from pymongo import ReadPreference Bar.objects().read_preference(ReadPreference.PRIMARY) Bar.objects(read_preference=ReadPreference.PRIMARY) Multiple Databases ================== To use multiple databases you can use :func:`~mongoengine.connect` and provide an `alias` name for the connection - if no `alias` is provided then "default" is used. In the background this uses :func:`~mongoengine.register_connection` to store the data and you can register all aliases up front if required. Documents defined in different database --------------------------------------- Individual documents can be attached to different databases by providing a `db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects to point across databases and collections. Below is an example schema, using 3 different databases to store data:: connect(alias='user-db-alias', db='user-db') connect(alias='book-db-alias', db='book-db') connect(alias='users-books-db-alias', db='users-books-db') class User(Document): name = StringField() meta = {'db_alias': 'user-db-alias'} class Book(Document): name = StringField() meta = {'db_alias': 'book-db-alias'} class AuthorBooks(Document): author = ReferenceField(User) book = ReferenceField(Book) meta = {'db_alias': 'users-books-db-alias'} Disconnecting an existing connection ------------------------------------ The function :func:`~mongoengine.disconnect` can be used to disconnect a particular connection. This can be used to change a connection globally:: from mongoengine import connect, disconnect connect('a_db', alias='db1') class User(Document): name = StringField() meta = {'db_alias': 'db1'} disconnect(alias='db1') connect('another_db', alias='db1') .. note:: Calling :func:`~mongoengine.disconnect` without argument will disconnect the "default" connection .. note:: Since connections gets registered globally, it is important to use the `disconnect` function from MongoEngine and not the `disconnect()` method of an existing connection (pymongo.MongoClient) .. note:: :class:`~mongoengine.Document` are caching the pymongo collection. using `disconnect` ensures that it gets cleaned as well Context Managers ================ Sometimes you may want to switch the database or collection to query against. For example, archiving older data into a separate database for performance reasons or writing functions that dynamically choose collections to write a document to. Switch Database --------------- The :class:`~mongoengine.context_managers.switch_db` context manager allows you to change the database alias for a given class allowing quick and easy access to the same User document across databases:: from mongoengine.context_managers import switch_db class User(Document): name = StringField() meta = {'db_alias': 'user-db'} with switch_db(User, 'archive-user-db') as User: User(name='Ross').save() # Saves the 'archive-user-db' .. note:: :func:`~mongoengine.context_managers.switch_db` when used on a class that allow inheritance will change the database alias for instances of a given class only - instances of subclasses will still use the default database. Switch Collection ----------------- The :func:`~mongoengine.context_managers.switch_collection` context manager allows you to change the collection for a given class allowing quick and easy access to the same Group document across collection:: from mongoengine.context_managers import switch_collection class Group(Document): name = StringField() Group(name='test').save() # Saves in the default db with switch_collection(Group, 'group2000') as Group: Group(name='hello Group 2000 collection!').save() # Saves in group2000 collection .. note:: Make sure any aliases have been registered with :func:`~mongoengine.register_connection` or :func:`~mongoengine.connect` before using the context manager. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/defining-documents.rst0000644000175100001730000007724114400345475022070 0ustar00runnerdocker================== Defining documents ================== In MongoDB, a **document** is roughly equivalent to a **row** in an RDBMS. When working with relational databases, rows are stored in **tables**, which have a strict **schema** that the rows follow. MongoDB stores documents in **collections** rather than tables --- the principal difference is that no schema is enforced at a database level. Defining a document's schema ============================ MongoEngine allows you to define schemata for documents as this helps to reduce coding errors, and allows for utility methods to be defined on fields which may be present. To define a schema for a document, create a class that inherits from :class:`~mongoengine.Document`. Fields are specified by adding **field objects** as class attributes to the document class:: from mongoengine import * import datetime class Page(Document): title = StringField(max_length=200, required=True) date_modified = DateTimeField(default=datetime.datetime.utcnow) As BSON (the binary format for storing data in mongodb) is order dependent, documents are serialized based on their field order. .. _dynamic-document-schemas: Dynamic document schemas ======================== One of the benefits of MongoDB is dynamic schemas for a collection, whilst data should be planned and organised (after all explicit is better than implicit!) there are scenarios where having dynamic / expando style documents is desirable. :class:`~mongoengine.DynamicDocument` documents work in the same way as :class:`~mongoengine.Document` but any data / attributes set to them will also be saved :: from mongoengine import * class Page(DynamicDocument): title = StringField(max_length=200, required=True) # Create a new page and add tags >>> page = Page(title='Using MongoEngine') >>> page.tags = ['mongodb', 'mongoengine'] >>> page.save() >>> Page.objects(tags='mongoengine').count() >>> 1 .. note:: There is one caveat on Dynamic Documents: fields cannot start with `_` Dynamic fields are stored in creation order *after* any declared fields. Fields ====== By default, fields are not required. To make a field mandatory, set the :attr:`required` keyword argument of a field to ``True``. Fields also may have validation constraints available (such as :attr:`max_length` in the example above). Fields may also take default values, which will be used if a value is not provided. Default values may optionally be a callable, which will be called to retrieve the value (such as in the above example). The field types available are as follows: * :class:`~mongoengine.fields.BinaryField` * :class:`~mongoengine.fields.BooleanField` * :class:`~mongoengine.fields.ComplexDateTimeField` * :class:`~mongoengine.fields.DateTimeField` * :class:`~mongoengine.fields.DecimalField` * :class:`~mongoengine.fields.DictField` * :class:`~mongoengine.fields.DynamicField` * :class:`~mongoengine.fields.EmailField` * :class:`~mongoengine.fields.EmbeddedDocumentField` * :class:`~mongoengine.fields.EmbeddedDocumentListField` * :class:`~mongoengine.fields.EnumField` * :class:`~mongoengine.fields.FileField` * :class:`~mongoengine.fields.FloatField` * :class:`~mongoengine.fields.GenericEmbeddedDocumentField` * :class:`~mongoengine.fields.GenericReferenceField` * :class:`~mongoengine.fields.GenericLazyReferenceField` * :class:`~mongoengine.fields.GeoPointField` * :class:`~mongoengine.fields.ImageField` * :class:`~mongoengine.fields.IntField` * :class:`~mongoengine.fields.ListField` * :class:`~mongoengine.fields.LongField` * :class:`~mongoengine.fields.MapField` * :class:`~mongoengine.fields.ObjectIdField` * :class:`~mongoengine.fields.ReferenceField` * :class:`~mongoengine.fields.LazyReferenceField` * :class:`~mongoengine.fields.SequenceField` * :class:`~mongoengine.fields.SortedListField` * :class:`~mongoengine.fields.StringField` * :class:`~mongoengine.fields.URLField` * :class:`~mongoengine.fields.UUIDField` * :class:`~mongoengine.fields.PointField` * :class:`~mongoengine.fields.LineStringField` * :class:`~mongoengine.fields.PolygonField` * :class:`~mongoengine.fields.MultiPointField` * :class:`~mongoengine.fields.MultiLineStringField` * :class:`~mongoengine.fields.MultiPolygonField` Field arguments --------------- Each field type can be customized by keyword arguments. The following keyword arguments can be set on all fields: :attr:`db_field` (Default: None) The MongoDB field name. If set, operations in MongoDB will be performed with this value instead of the class attribute. This allows you to use a different attribute than the name of the field used in MongoDB. :: from mongoengine import * class Page(Document): page_number = IntField(db_field="pageNumber") # Create a Page and save it Page(page_number=1).save() # How 'pageNumber' is stored in MongoDB Page.objects.as_pymongo() # [{'_id': ObjectId('629dfc45ee4cc407b1586b1f'), 'pageNumber': 1}] # Retrieve the object page: Page = Page.objects.first() print(page.page_number) # prints 1 print(page.pageNumber) # raises AttributeError .. note:: If set, use the name of the attribute when defining indexes in the :attr:`meta` dictionary rather than the :attr:`db_field` otherwise, :class:`~mongoengine.LookUpError` will be raised. :attr:`required` (Default: False) If set to True and the field is not set on the document instance, a :class:`~mongoengine.ValidationError` will be raised when the document is validated. :attr:`default` (Default: None) A value to use when no value is set for this field. The definition of default parameters follow `the general rules on Python `__, which means that some care should be taken when dealing with default mutable objects (like in :class:`~mongoengine.fields.ListField` or :class:`~mongoengine.fields.DictField`):: class ExampleFirst(Document): # Default an empty list values = ListField(IntField(), default=list) class ExampleSecond(Document): # Default a set of values values = ListField(IntField(), default=lambda: [1,2,3]) class ExampleDangerous(Document): # This can make an .append call to add values to the default (and all the following objects), # instead to just an object values = ListField(IntField(), default=[1,2,3]) .. note:: Unsetting a field with a default value will revert back to the default. :attr:`unique` (Default: False) When True, no documents in the collection will have the same value for this field. :attr:`unique_with` (Default: None) A field name (or list of field names) that when taken together with this field, will not have two documents in the collection with the same value. :attr:`primary_key` (Default: False) When True, use this field as a primary key for the collection. `DictField` and `EmbeddedDocuments` both support being the primary key for a document. .. note:: If set, this field is also accessible through the `pk` field. :attr:`choices` (Default: None) An iterable (e.g. list, tuple or set) of choices to which the value of this field should be limited. Can either be nested tuples of value (stored in mongo) and a human readable key :: SIZE = (('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'Extra Large'), ('XXL', 'Extra Extra Large')) class Shirt(Document): size = StringField(max_length=3, choices=SIZE) Or a flat iterable just containing values :: SIZE = ('S', 'M', 'L', 'XL', 'XXL') class Shirt(Document): size = StringField(max_length=3, choices=SIZE) :attr:`validation` (Optional) A callable to validate the value of the field. The callable takes the value as parameter and should raise a ValidationError if validation fails e.g :: def _not_empty(val): if not val: raise ValidationError('value can not be empty') class Person(Document): name = StringField(validation=_not_empty) :attr:`**kwargs` (Optional) You can supply additional metadata as arbitrary additional keyword arguments. You can not override existing attributes, however. Common choices include `help_text` and `verbose_name`, commonly used by form and widget libraries. List fields ----------- MongoDB allows storing lists of items. To add a list of items to a :class:`~mongoengine.Document`, use the :class:`~mongoengine.fields.ListField` field type. :class:`~mongoengine.fields.ListField` takes another field object as its first argument, which specifies which type elements may be stored within the list:: class Page(Document): tags = ListField(StringField(max_length=50)) Embedded documents ------------------ MongoDB has the ability to embed documents within other documents. Schemata may be defined for these embedded documents, just as they may be for regular documents. To create an embedded document, just define a document as usual, but inherit from :class:`~mongoengine.EmbeddedDocument` rather than :class:`~mongoengine.Document`:: class Comment(EmbeddedDocument): content = StringField() To embed the document within another document, use the :class:`~mongoengine.fields.EmbeddedDocumentField` field type, providing the embedded document class as the first argument:: class Page(Document): comments = ListField(EmbeddedDocumentField(Comment)) comment1 = Comment(content='Good work!') comment2 = Comment(content='Nice article!') page = Page(comments=[comment1, comment2]) Embedded documents can also leverage the flexibility of :ref:`dynamic-document-schemas:` by inheriting :class:`~mongoengine.DynamicEmbeddedDocument`. Dictionary Fields ----------------- Often, an embedded document may be used instead of a dictionary – generally embedded documents are recommended as dictionaries don’t support validation or custom field types. However, sometimes you will not know the structure of what you want to store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate:: class SurveyResponse(Document): date = DateTimeField() user = ReferenceField(User) answers = DictField() survey_response = SurveyResponse(date=datetime.utcnow(), user=request.user) response_form = ResponseForm(request.POST) survey_response.answers = response_form.cleaned_data() survey_response.save() Dictionaries can store complex data, other dictionaries, lists, references to other objects, so are the most flexible field type available. Reference fields ---------------- References may be stored to other documents in the database using the :class:`~mongoengine.fields.ReferenceField`. Pass in another document class as the first argument to the constructor, then simply assign document objects to the field:: class User(Document): name = StringField() class Page(Document): content = StringField() author = ReferenceField(User) john = User(name="John Smith") john.save() post = Page(content="Test Page") post.author = john post.save() The :class:`User` object is automatically turned into a reference behind the scenes, and dereferenced when the :class:`Page` object is retrieved. To add a :class:`~mongoengine.fields.ReferenceField` that references the document being defined, use the string ``'self'`` in place of the document class as the argument to :class:`~mongoengine.fields.ReferenceField`'s constructor. To reference a document that has not yet been defined, use the name of the undefined document as the constructor's argument:: class Employee(Document): name = StringField() boss = ReferenceField('self') profile_page = ReferenceField('ProfilePage') class ProfilePage(Document): content = StringField() .. _many-to-many-with-listfields: Many to Many with ListFields ''''''''''''''''''''''''''' If you are implementing a many to many relationship via a list of references, then the references are stored as DBRefs and to query you need to pass an instance of the object to the query:: class User(Document): name = StringField() class Page(Document): content = StringField() authors = ListField(ReferenceField(User)) bob = User(name="Bob Jones").save() john = User(name="John Smith").save() Page(content="Test Page", authors=[bob, john]).save() Page(content="Another Page", authors=[john]).save() # Find all pages Bob authored Page.objects(authors__in=[bob]) # Find all pages that both Bob and John have authored Page.objects(authors__all=[bob, john]) # Remove Bob from the authors for a page. Page.objects(id='...').update_one(pull__authors=bob) # Add John to the authors for a page. Page.objects(id='...').update_one(push__authors=john) Dealing with deletion of referred documents ''''''''''''''''''''''''''''''''''''''''''' By default, MongoDB doesn't check the integrity of your data, so deleting documents that other documents still hold references to will lead to consistency issues. Mongoengine's :class:`ReferenceField` adds some functionality to safeguard against these kinds of database integrity problems, providing each reference with a delete rule specification. A delete rule is specified by supplying the :attr:`reverse_delete_rule` attributes on the :class:`ReferenceField` definition, like this:: class ProfilePage(Document): employee = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE) The declaration in this example means that when an :class:`Employee` object is removed, the :class:`ProfilePage` that references that employee is removed as well. If a whole batch of employees is removed, all profile pages that are linked are removed as well. Its value can take any of the following constants: :const:`mongoengine.DO_NOTHING` This is the default and won't do anything. Deletes are fast, but may cause database inconsistency or dangling references. :const:`mongoengine.DENY` Deletion is denied if there still exist references to the object being deleted. :const:`mongoengine.NULLIFY` Any object's fields still referring to the object being deleted are set to None (using MongoDB's "unset" operation), effectively nullifying the relationship. :const:`mongoengine.CASCADE` Any object containing fields that are referring to the object being deleted are deleted first. :const:`mongoengine.PULL` Removes the reference to the object (using MongoDB's "pull" operation) from any object's fields of :class:`~mongoengine.fields.ListField` (:class:`~mongoengine.fields.ReferenceField`). .. warning:: A safety note on setting up these delete rules! Since the delete rules are not recorded on the database level by MongoDB itself, but instead at runtime, in-memory, by the MongoEngine module, it is of the upmost importance that the module that declares the relationship is loaded **BEFORE** the delete is invoked. If, for example, the :class:`Employee` object lives in the :mod:`payroll` app, and the :class:`ProfilePage` in the :mod:`people` app, it is extremely important that the :mod:`people` app is loaded before any employee is removed, because otherwise, MongoEngine could never know this relationship exists. In Django, be sure to put all apps that have such delete rule declarations in their :file:`models.py` in the :const:`INSTALLED_APPS` tuple. Generic reference fields '''''''''''''''''''''''' A second kind of reference field also exists, :class:`~mongoengine.fields.GenericReferenceField`. This allows you to reference any kind of :class:`~mongoengine.Document`, and hence doesn't take a :class:`~mongoengine.Document` subclass as a constructor argument:: class Link(Document): url = StringField() class Post(Document): title = StringField() class Bookmark(Document): bookmark_object = GenericReferenceField() link = Link(url='http://hmarr.com/mongoengine/') link.save() post = Post(title='Using MongoEngine') post.save() Bookmark(bookmark_object=link).save() Bookmark(bookmark_object=post).save() .. note:: Using :class:`~mongoengine.fields.GenericReferenceField`\ s is slightly less efficient than the standard :class:`~mongoengine.fields.ReferenceField`\ s, so if you will only be referencing one document type, prefer the standard :class:`~mongoengine.fields.ReferenceField`. Uniqueness constraints ---------------------- MongoEngine allows you to specify that a field should be unique across a collection by providing ``unique=True`` to a :class:`~mongoengine.fields.Field`\ 's constructor. If you try to save a document that has the same value for a unique field as a document that is already in the database, a :class:`~mongoengine.NotUniqueError` will be raised. You may also specify multi-field uniqueness constraints by using :attr:`unique_with`, which may be either a single field name, or a list or tuple of field names:: class User(Document): username = StringField(unique=True) first_name = StringField() last_name = StringField(unique_with='first_name') Document collections ==================== Document classes that inherit **directly** from :class:`~mongoengine.Document` will have their own **collection** in the database. The name of the collection is by default the name of the class converted to snake_case (e.g if your Document class is named `CompanyUser`, the corresponding collection would be `company_user`). If you need to change the name of the collection (e.g. to use MongoEngine with an existing database), then create a class dictionary attribute called :attr:`meta` on your document, and set :attr:`collection` to the name of the collection that you want your document class to use:: class Page(Document): title = StringField(max_length=200, required=True) meta = {'collection': 'cmsPage'} Capped collections ------------------ A :class:`~mongoengine.Document` may use a **Capped Collection** by specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta` dictionary. :attr:`max_documents` is the maximum number of documents that is allowed to be stored in the collection, and :attr:`max_size` is the maximum size of the collection in bytes. :attr:`max_size` is rounded up to the next multiple of 256 by MongoDB internally and mongoengine before. Use also a multiple of 256 to avoid confusions. If :attr:`max_size` is not specified and :attr:`max_documents` is, :attr:`max_size` defaults to 10485760 bytes (10MB). The following example shows a :class:`Log` document that will be limited to 1000 entries and 2MB of disk space:: class Log(Document): ip_address = StringField() meta = {'max_documents': 1000, 'max_size': 2000000} .. defining-indexes_ Indexes ======= You can specify indexes on collections to make querying faster. This is done by creating a list of index specifications called :attr:`indexes` in the :attr:`~mongoengine.Document.meta` dictionary, where an index specification may either be a single field name, a tuple containing multiple field names, or a dictionary containing a full index definition. A direction may be specified on fields by prefixing the field name with a **+** (for ascending) or a **-** sign (for descending). Note that direction only matters on compound indexes. Text indexes may be specified by prefixing the field name with a **$**. Hashed indexes may be specified by prefixing the field name with a **#**:: class Page(Document): category = IntField() title = StringField() rating = StringField() created = DateTimeField() meta = { 'indexes': [ 'title', # single-field index '$title', # text index '#title', # hashed index ('title', '-rating'), # compound index ('category', '_cls'), # compound index { 'fields': ['created'], 'expireAfterSeconds': 3600 # ttl index } ] } If a dictionary is passed then additional options become available. Valid options include, but are not limited to: :attr:`fields` (Default: None) The fields to index. Specified in the same format as described above. :attr:`cls` (Default: True) If you have polymorphic models that inherit and have :attr:`allow_inheritance` turned on, you can configure whether the index should have the :attr:`_cls` field added automatically to the start of the index. :attr:`sparse` (Default: False) Whether the index should be sparse. :attr:`unique` (Default: False) Whether the index should be unique. :attr:`expireAfterSeconds` (Optional) Allows you to automatically expire data from a collection by setting the time in seconds to expire the a field. :attr:`name` (Optional) Allows you to specify a name for the index :attr:`collation` (Optional) Allows to create case insensitive indexes (MongoDB v3.4+ only) .. note:: Additional options are forwarded as **kwargs to pymongo's create_index method. Inheritance adds extra fields indices see: :ref:`document-inheritance`. Global index default options ---------------------------- There are a few top level defaults for all indexes that can be set:: class Page(Document): title = StringField() rating = StringField() meta = { 'index_opts': {}, 'index_background': True, 'index_cls': False, 'auto_create_index': True, 'auto_create_index_on_save': False, } :attr:`index_opts` (Optional) Set any default index options - see the `full options list `_ :attr:`index_background` (Optional) Set the default value for if an index should be indexed in the background :attr:`index_cls` (Optional) A way to turn off a specific index for _cls. :attr:`auto_create_index` (Optional) When this is True (default), MongoEngine will ensure that the correct indexes exist in MongoDB when the Document is first used. This can be disabled in systems where indexes are managed separately. Disabling this will improve performance. :attr:`auto_create_index_on_save` (Optional) When this is True, MongoEngine will ensure that the correct indexes exist in MongoDB each time :meth:`~mongoengine.document.Document.save` is run. Enabling this will degrade performance. The default is False. This option was added in version 0.25. Compound Indexes and Indexing sub documents ------------------------------------------- Compound indexes can be created by adding the Embedded field or dictionary field name to the index definition. Sometimes its more efficient to index parts of Embedded / dictionary fields, in this case use 'dot' notation to identify the value to index eg: `rank.title` .. _geospatial-indexes: Geospatial indexes ------------------ The best geo index for mongodb is the new "2dsphere", which has an improved spherical model and provides better performance and more options when querying. The following fields will explicitly add a "2dsphere" index: - :class:`~mongoengine.fields.PointField` - :class:`~mongoengine.fields.LineStringField` - :class:`~mongoengine.fields.PolygonField` - :class:`~mongoengine.fields.MultiPointField` - :class:`~mongoengine.fields.MultiLineStringField` - :class:`~mongoengine.fields.MultiPolygonField` As "2dsphere" indexes can be part of a compound index, you may not want the automatic index but would prefer a compound index. In this example we turn off auto indexing and explicitly declare a compound index on ``location`` and ``datetime``:: class Log(Document): location = PointField(auto_index=False) datetime = DateTimeField() meta = { 'indexes': [[("location", "2dsphere"), ("datetime", 1)]] } Pre MongoDB 2.4 Geo ''''''''''''''''''' .. note:: For MongoDB < 2.4 this is still current, however the new 2dsphere index is a big improvement over the previous 2D model - so upgrading is advised. Geospatial indexes will be automatically created for all :class:`~mongoengine.fields.GeoPointField`\ s It is also possible to explicitly define geospatial indexes. This is useful if you need to define a geospatial index on a subfield of a :class:`~mongoengine.fields.DictField` or a custom field that contains a point. To create a geospatial index you must prefix the field with the ***** sign. :: class Place(Document): location = DictField() meta = { 'indexes': [ '*location.point', ], } Time To Live (TTL) indexes -------------------------- A special index type that allows you to automatically expire data from a collection after a given period. See the official `ttl `_ documentation for more information. A common usecase might be session data:: class Session(Document): created = DateTimeField(default=datetime.utcnow) meta = { 'indexes': [ {'fields': ['created'], 'expireAfterSeconds': 3600} ] } .. warning:: TTL indexes happen on the MongoDB server and not in the application code, therefore no signals will be fired on document deletion. If you need signals to be fired on deletion, then you must handle the deletion of Documents in your application code. Comparing Indexes ----------------- Use :func:`mongoengine.Document.compare_indexes` to compare actual indexes in the database to those that your document definitions define. This is useful for maintenance purposes and ensuring you have the correct indexes for your schema. Ordering ======== A default ordering can be specified for your :class:`~mongoengine.queryset.QuerySet` using the :attr:`ordering` attribute of :attr:`~mongoengine.Document.meta`. Ordering will be applied when the :class:`~mongoengine.queryset.QuerySet` is created, and can be overridden by subsequent calls to :meth:`~mongoengine.queryset.QuerySet.order_by`. :: from datetime import datetime class BlogPost(Document): title = StringField() published_date = DateTimeField() meta = { 'ordering': ['-published_date'] } blog_post_1 = BlogPost(title="Blog Post #1") blog_post_1.published_date = datetime(2010, 1, 5, 0, 0 ,0) blog_post_2 = BlogPost(title="Blog Post #2") blog_post_2.published_date = datetime(2010, 1, 6, 0, 0 ,0) blog_post_3 = BlogPost(title="Blog Post #3") blog_post_3.published_date = datetime(2010, 1, 7, 0, 0 ,0) blog_post_1.save() blog_post_2.save() blog_post_3.save() # get the "first" BlogPost using default ordering # from BlogPost.meta.ordering latest_post = BlogPost.objects.first() assert latest_post.title == "Blog Post #3" # override default ordering, order BlogPosts by "published_date" first_post = BlogPost.objects.order_by("+published_date").first() assert first_post.title == "Blog Post #1" Shard keys ========== If your collection is sharded by multiple keys, then you can improve shard routing (and thus the performance of your application) by specifying the shard key, using the :attr:`shard_key` attribute of :attr:`~mongoengine.Document.meta`. The shard key should be defined as a tuple. This ensures that the full shard key is sent with the query when calling methods such as :meth:`~mongoengine.document.Document.save`, :meth:`~mongoengine.document.Document.update`, :meth:`~mongoengine.document.Document.modify`, or :meth:`~mongoengine.document.Document.delete` on an existing :class:`~mongoengine.Document` instance:: class LogEntry(Document): machine = StringField() app = StringField() timestamp = DateTimeField() data = StringField() meta = { 'shard_key': ('machine', 'timestamp'), 'indexes': ('machine', 'timestamp'), } .. _document-inheritance: Document inheritance ==================== To create a specialised type of a :class:`~mongoengine.Document` you have defined, you may subclass it and add any extra fields or methods you may need. As this new class is not a direct subclass of :class:`~mongoengine.Document`, it will not be stored in its own collection; it will use the same collection as its superclass uses. This allows for more convenient and efficient retrieval of related documents -- all you need do is set :attr:`allow_inheritance` to True in the :attr:`meta` data for a document.:: # Stored in a collection named 'page' class Page(Document): title = StringField(max_length=200, required=True) meta = {'allow_inheritance': True} # Also stored in the collection named 'page' class DatedPage(Page): date = DateTimeField() .. note:: From 0.8 onwards :attr:`allow_inheritance` defaults to False, meaning you must set it to True to use inheritance. Setting :attr:`allow_inheritance` to True should also be used in :class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it When it comes to querying using :attr:`.objects()`, querying `Page.objects()` will query both `Page` and `DatedPage` whereas querying `DatedPage` will only query the `DatedPage` documents. Behind the scenes, MongoEngine deals with inheritance by adding a :attr:`_cls` attribute that contains the class name in every documents. When a document is loaded, MongoEngine checks it's :attr:`_cls` attribute and use that class to construct the instance.:: Page(title='a funky title').save() DatedPage(title='another title', date=datetime.utcnow()).save() print(Page.objects().count()) # 2 print(DatedPage.objects().count()) # 1 # print documents in their native form # we remove 'id' to avoid polluting the output with unnecessary detail qs = Page.objects.exclude('id').as_pymongo() print(list(qs)) # [ # {'_cls': u 'Page', 'title': 'a funky title'}, # {'_cls': u 'Page.DatedPage', 'title': u 'another title', 'date': datetime.datetime(2019, 12, 13, 20, 16, 59, 993000)} # ] Working with existing data -------------------------- As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and easily get working with existing data. Just define the document to match the expected schema in your database :: # Will work with data in an existing collection named 'cmsPage' class Page(Document): title = StringField(max_length=200, required=True) meta = { 'collection': 'cmsPage' } If you have wildly varying schemas then using a :class:`~mongoengine.DynamicDocument` might be more appropriate, instead of defining all possible field types. If you use :class:`~mongoengine.Document` and the database contains data that isn't defined then that data will be stored in the `document._data` dictionary. Abstract classes ================ If you want to add some extra functionality to a group of Document classes but you don't need or want the overhead of inheritance you can use the :attr:`abstract` attribute of :attr:`~mongoengine.Document.meta`. This won't turn on :ref:`document-inheritance` but will allow you to keep your code DRY:: class BaseDocument(Document): meta = { 'abstract': True, } def check_permissions(self): ... class User(BaseDocument): ... Now the User class will have access to the inherited `check_permissions` method and won't store any of the extra `_cls` information. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/document-instances.rst0000644000175100001730000000706714400345475022110 0ustar00runnerdocker=================== Documents instances =================== To create a new document object, create an instance of the relevant document class, providing values for its fields as constructor keyword arguments. You may provide values for any of the fields on the document:: >>> page = Page(title="Test Page") >>> page.title 'Test Page' You may also assign values to the document's fields using standard object attribute syntax:: >>> page.title = "Example Page" >>> page.title 'Example Page' Saving and deleting documents ============================= MongoEngine tracks changes to documents to provide efficient saving. To save the document to the database, call the :meth:`~mongoengine.Document.save` method. If the document does not exist in the database, it will be created. If it does already exist, then any changes will be updated atomically. For example:: >>> page = Page(title="Test Page") >>> page.save() # Performs an insert >>> page.title = "My Page" >>> page.save() # Performs an atomic set on the title field. .. note:: Changes to documents are tracked and on the whole perform ``set`` operations. * ``list_field.push(0)`` --- *sets* the resulting list * ``del(list_field)`` --- *unsets* whole list With lists its preferable to use ``Doc.update(push__list_field=0)`` as this stops the whole list being updated --- stopping any race conditions. .. seealso:: :ref:`guide-atomic-updates` Cascading Saves --------------- If your document contains :class:`~mongoengine.fields.ReferenceField` or :class:`~mongoengine.fields.GenericReferenceField` objects, then by default the :meth:`~mongoengine.Document.save` method will not save any changes to those objects. If you want all references to be saved also, noting each save is a separate query, then passing :attr:`cascade` as True to the save method will cascade any saves. Deleting documents ------------------ To delete a document, call the :meth:`~mongoengine.Document.delete` method. Note that this will only work if the document exists in the database and has a valid :attr:`id`. Document IDs ============ Each document in the database has a unique id. This may be accessed through the :attr:`id` attribute on :class:`~mongoengine.Document` objects. Usually, the id will be generated automatically by the database server when the object is save, meaning that you may only access the :attr:`id` field once a document has been saved:: >>> page = Page(title="Test Page") >>> page.id >>> page.save() >>> page.id ObjectId('123456789abcdef000000000') Alternatively, you may define one of your own fields to be the document's "primary key" by providing ``primary_key=True`` as a keyword argument to a field's constructor. Under the hood, MongoEngine will use this field as the :attr:`id`; in fact :attr:`id` is actually aliased to your primary key field so you may still use :attr:`id` to access the primary key if you want:: >>> class User(Document): ... email = StringField(primary_key=True) ... name = StringField() ... >>> bob = User(email='bob@example.com', name='Bob') >>> bob.save() >>> bob.id == bob.email == 'bob@example.com' True You can also access the document's "primary key" using the :attr:`pk` field, it's an alias to :attr:`id`:: >>> page = Page(title="Another Test Page") >>> page.save() >>> page.id == page.pk True .. note:: If you define your own primary key field, the field implicitly becomes required, so a :class:`~mongoengine.ValidationError` will be thrown if you don't provide it. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/gridfs.rst0000644000175100001730000000614414400345475017556 0ustar00runnerdocker====== GridFS ====== Writing ------- GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field object. This field acts as a file-like object and provides a couple of different ways of inserting and retrieving data. Arbitrary metadata such as content type can also be stored alongside the files. The object returned when accessing a FileField is a proxy to `Pymongo's GridFS `_ In the following example, a document is created to store details about animals, including a photo:: class Animal(Document): genus = StringField() family = StringField() photo = FileField() marmot = Animal(genus='Marmota', family='Sciuridae') with open('marmot.jpg', 'rb') as fd: marmot.photo.put(fd, content_type = 'image/jpeg') marmot.save() Retrieval --------- So using the :class:`~mongoengine.fields.FileField` is just like using any other field. The file can also be retrieved just as easily:: marmot = Animal.objects(genus='Marmota').first() photo = marmot.photo.read() content_type = marmot.photo.content_type .. note:: If you need to read() the content of a file multiple times, you'll need to "rewind" the file-like object using `seek`:: marmot = Animal.objects(genus='Marmota').first() content1 = marmot.photo.read() assert content1 != "" content2 = marmot.photo.read() # will be empty assert content2 == "" marmot.photo.seek(0) # rewind the file by setting the current position of the cursor in the file to 0 content3 = marmot.photo.read() assert content3 == content1 Streaming --------- Streaming data into a :class:`~mongoengine.fields.FileField` is achieved in a slightly different manner. First, a new file must be created by calling the :func:`new_file` method. Data can then be written using :func:`write`:: marmot.photo.new_file() marmot.photo.write('some_image_data') marmot.photo.write('some_more_image_data') marmot.photo.close() marmot.save() Deletion -------- Deleting stored files is achieved with the :func:`delete` method:: marmot.photo.delete() # Deletes the GridFS document marmot.save() # Saves the GridFS reference (being None) contained in the marmot instance .. warning:: The FileField in a Document actually only stores the ID of a file in a separate GridFS collection. This means that deleting a document with a defined FileField does not actually delete the file. You must be careful to delete any files in a Document as above before deleting the Document itself. Replacing files --------------- Files can be replaced with the :func:`replace` method. This works just like the :func:`put` method so even metadata can (and should) be replaced:: another_marmot = open('another_marmot.png', 'rb') marmot.photo.replace(another_marmot, content_type='image/png') # Replaces the GridFS document marmot.save() # Replaces the GridFS reference contained in marmot instance ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/index.rst0000644000175100001730000000036714400345475017410 0ustar00runnerdocker========== User Guide ========== .. toctree:: :maxdepth: 2 installing connecting defining-documents document-instances querying validation gridfs signals text-indexes migration logging-monitoring mongomock ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/installing.rst0000644000175100001730000000173714400345475020447 0ustar00runnerdocker====================== Installing MongoEngine ====================== To use MongoEngine, you will need to download `MongoDB `_ and ensure it is running in an accessible location. You will also need `PyMongo `_ to use MongoEngine, but if you install MongoEngine using setuptools, then the dependencies will be handled for you. MongoEngine is available on PyPI, so you can use :program:`pip`: .. code-block:: console $ python -m pip install mongoengine Alternatively, if you don't have setuptools installed, `download it from PyPi `_ and run .. code-block:: console $ python setup.py install To use the bleeding-edge version of MongoEngine, you can get the source from `GitHub `_ and install it as above: .. code-block:: console $ git clone git://github.com/mongoengine/mongoengine $ cd mongoengine $ python setup.py install ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/logging-monitoring.rst0000644000175100001730000000564214400345475022113 0ustar00runnerdocker================== Logging/Monitoring ================== It is possible to use `pymongo.monitoring `_ to monitor the driver events (e.g: queries, connections, etc). This can be handy if you want to monitor the queries issued by MongoEngine to the driver. To use `pymongo.monitoring` with MongoEngine, you need to make sure that you are registering the listeners **before** establishing the database connection (i.e calling `connect`): The following snippet provides a basic logging of all command events: .. code-block:: python import logging from pymongo import monitoring from mongoengine import * log = logging.getLogger() log.setLevel(logging.DEBUG) logging.basicConfig(level=logging.DEBUG) class CommandLogger(monitoring.CommandListener): def started(self, event): log.debug("Command {0.command_name} with request id " "{0.request_id} started on server " "{0.connection_id}".format(event)) def succeeded(self, event): log.debug("Command {0.command_name} with request id " "{0.request_id} on server {0.connection_id} " "succeeded in {0.duration_micros} " "microseconds".format(event)) def failed(self, event): log.debug("Command {0.command_name} with request id " "{0.request_id} on server {0.connection_id} " "failed in {0.duration_micros} " "microseconds".format(event)) monitoring.register(CommandLogger()) class Jedi(Document): name = StringField() connect() log.info('GO!') log.info('Saving an item through MongoEngine...') Jedi(name='Obi-Wan Kenobii').save() log.info('Querying through MongoEngine...') obiwan = Jedi.objects.first() log.info('Updating through MongoEngine...') obiwan.name = 'Obi-Wan Kenobi' obiwan.save() Executing this prints the following output:: INFO:root:GO! INFO:root:Saving an item through MongoEngine... DEBUG:root:Command insert with request id 1681692777 started on server ('localhost', 27017) DEBUG:root:Command insert with request id 1681692777 on server ('localhost', 27017) succeeded in 562 microseconds INFO:root:Querying through MongoEngine... DEBUG:root:Command find with request id 1714636915 started on server ('localhost', 27017) DEBUG:root:Command find with request id 1714636915 on server ('localhost', 27017) succeeded in 341 microseconds INFO:root:Updating through MongoEngine... DEBUG:root:Command update with request id 1957747793 started on server ('localhost', 27017) DEBUG:root:Command update with request id 1957747793 on server ('localhost', 27017) succeeded in 455 microseconds More details can of course be obtained by checking the `event` argument from the `CommandListener`. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/migration.rst0000644000175100001730000002644214400345475020274 0ustar00runnerdocker=================== Documents migration =================== The structure of your documents and their associated mongoengine schemas are likely to change over the lifetime of an application. This section provides guidance and recommendations on how to deal with migrations. Due to the very flexible nature of mongodb, migrations of models aren't trivial and for people that know about `alembic` for `sqlalchemy`, there is unfortunately no equivalent library that will manage the migration in an automatic fashion for mongoengine. Example 1: Addition of a field ============================== Let's start by taking a simple example of a model change and review the different option you have to deal with the migration. Let's assume we start with the following schema and save an instance: .. code-block:: python class User(Document): name = StringField() User(name="John Doe").save() # print the objects as they exist in mongodb print(User.objects().as_pymongo()) # [{u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John Doe'}] On the next version of your application, let's now assume that a new field `enabled` gets added to the existing ``User`` model with a `default=True`. Thus you simply update the ``User`` class to the following: .. code-block:: python class User(Document): name = StringField(required=True) enabled = BooleanField(default=True) Without applying any migration, we now reload an object from the database into the ``User`` class and checks its `enabled` attribute: .. code-block:: python assert User.objects.count() == 1 user = User.objects().first() assert user.enabled is True assert User.objects(enabled=True).count() == 0 # uh? assert User.objects(enabled=False).count() == 0 # uh? # this is consistent with what we have in the database # in fact, 'enabled' does not exist print(User.objects().as_pymongo().first()) # {u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John'} assert User.objects(enabled=None).count() == 1 As you can see, even if the document wasn't updated, mongoengine applies the default value seamlessly when it loads the pymongo dict into a ``User`` instance. At first sight it looks like you don't need to migrate the existing documents when adding new fields but this actually leads to inconsistencies when it comes to querying. In fact, when querying, mongoengine isn't trying to account for the default value of the new field and so if you don't actually migrate the existing documents, you are taking a risk that querying/updating will be missing relevant record. When adding fields/modifying default values, you can use any of the following to do the migration as a standalone script: .. code-block:: python # Use mongoengine to set a default value for a given field User.objects().update(enabled=True) # or use pymongo user_coll = User._get_collection() user_coll.update_many({}, {'$set': {'enabled': True}}) Example 2: Inheritance change ============================= Let's consider the following example: .. code-block:: python class Human(Document): name = StringField() meta = {"allow_inheritance": True} class Jedi(Human): dark_side = BooleanField() light_saber_color = StringField() Jedi(name="Darth Vader", dark_side=True, light_saber_color="red").save() Jedi(name="Obi Wan Kenobi", dark_side=False, light_saber_color="blue").save() assert Human.objects.count() == 2 assert Jedi.objects.count() == 2 # Let's check how these documents got stored in mongodb print(Jedi.objects.as_pymongo()) # [ # {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'}, # {'_id': ObjectId('5fac4ac4f61d7fb06046e0fa'), '_cls': 'Human.Jedi', 'name': 'Obi Wan Kenobi', 'dark_side': False, 'light_saber_color': 'blue'} # ] As you can observe, when you use inheritance, MongoEngine stores a field named '_cls' behind the scene to keep track of the Document class. Let's now take the scenario that you want to refactor the inheritance schema and: - Have the Jedi's with dark_side=True/False become GoodJedi's/DarkSith - get rid of the 'dark_side' field move to the following schemas: .. code-block:: python # unchanged class Human(Document): name = StringField() meta = {"allow_inheritance": True} # attribute 'dark_side' removed class GoodJedi(Human): light_saber_color = StringField() # new class class BadSith(Human): light_saber_color = StringField() MongoEngine doesn't know about the change or how to map them with the existing data so if you don't apply any migration, you will observe a strange behavior, as if the collection was suddenly empty. .. code-block:: python # As a reminder, the documents that we inserted # have the _cls field = 'Human.Jedi' # Following has no match # because the query that is used behind the scene is # filtering on {'_cls': 'Human.GoodJedi'} assert GoodJedi.objects().count() == 0 # Following has also no match # because it is filtering on {'_cls': {'$in': ('Human', 'Human.GoodJedi', 'Human.BadSith')}} # which has no match assert Human.objects.count() == 0 assert Human.objects.first() is None # If we bypass MongoEngine and make use of underlying driver (PyMongo) # we can see that the documents are there humans_coll = Human._get_collection() assert humans_coll.count_documents({}) == 2 # print first document print(humans_coll.find_one()) # {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'} As you can see, first obvious problem is that we need to modify '_cls' values based on existing values of 'dark_side' documents. .. code-block:: python humans_coll = Human._get_collection() old_class = 'Human.Jedi' good_jedi_class = 'Human.GoodJedi' bad_sith_class = 'Human.BadSith' humans_coll.update_many({'_cls': old_class, 'dark_side': False}, {'$set': {'_cls': good_jedi_class}}) humans_coll.update_many({'_cls': old_class, 'dark_side': True}, {'$set': {'_cls': bad_sith_class}}) Let's now check if querying improved in MongoEngine: .. code-block:: python assert GoodJedi.objects().count() == 1 # Hoorah! assert BadSith.objects().count() == 1 # Hoorah! assert Human.objects.count() == 2 # Hoorah! # let's now check that documents load correctly jedi = GoodJedi.objects().first() # raises FieldDoesNotExist: The fields "{'dark_side'}" do not exist on the document "Human.GoodJedi" In fact we only took care of renaming the _cls values but we havn't removed the 'dark_side' fields which does not exist anymore on the GoodJedi's and BadSith's models. Let's remove the field from the collections: .. code-block:: python humans_coll = Human._get_collection() humans_coll.update_many({}, {'$unset': {'dark_side': 1}}) .. note:: We did this migration in 2 different steps for the sake of example but it could have been combined with the migration of the _cls fields: :: humans_coll.update_many( {'_cls': old_class, 'dark_side': False}, { '$set': {'_cls': good_jedi_class}, '$unset': {'dark_side': 1} } ) And verify that the documents now load correctly: .. code-block:: python jedi = GoodJedi.objects().first() assert jedi.name == "Obi Wan Kenobi" sith = BadSith.objects().first() assert sith.name == "Darth Vader" An other way of dealing with this migration is to iterate over the documents and update/replace them one by one. This is way slower but it is often useful for complex migrations of Document models. .. code-block:: python for doc in humans_coll.find(): if doc['_cls'] == 'Human.Jedi': doc['_cls'] = 'Human.BadSith' if doc['dark_side'] else 'Human.GoodJedi' doc.pop('dark_side') humans_coll.replace_one({'_id': doc['_id']}, doc) .. warning:: Be aware of this `flaw `_ if you modify documents while iterating Example 4: Index removal ======================== If you remove an index from your Document class, or remove an indexed Field from your Document class, you'll need to manually drop the corresponding index. MongoEngine will not do that for you. The way to deal with this case is to identify the name of the index to drop with `index_information()`, and then drop it with `drop_index()` Let's for instance assume that you start with the following Document class .. code-block:: python class User(Document): name = StringField(index=True) meta = {"indexes": ["name"]} User(name="John Doe").save() As soon as you start interacting with the Document collection (when `.save()` is called in this case), it would create the following indexes: .. code-block:: python print(User._get_collection().index_information()) # { # '_id_': {'key': [('_id', 1)], 'v': 2}, # 'name_1': {'background': False, 'key': [('name', 1)], 'v': 2}, # } Thus: '_id' which is the default index and 'name_1' which is our custom index. If you would remove the 'name' field or its index, you would have to call: .. code-block:: python User._get_collection().drop_index('name_1') .. note:: When adding new fields or new indexes, MongoEngine will take care of creating them (unless `auto_create_index` is disabled) :: Recommendations =============== - Write migration scripts whenever you do changes to the model schemas - Using :class:`~mongoengine.DynamicDocument` or ``meta = {"strict": False}`` may help to avoid some migrations or to have the 2 versions of your application to co-exist. - Write post-processing checks to verify that migrations script worked. See below Post-processing checks ====================== The following recipe can be used to sanity check a Document collection after you applied migration. It does not make any assumption on what was migrated, it will fetch 1000 objects randomly and run some quick checks on the documents to make sure the document looks OK. As it is, it will fail on the first occurrence of an error but this is something that can be adapted based on your needs. .. code-block:: python def get_random_oids(collection, sample_size): pipeline = [{"$project": {'_id': 1}}, {"$sample": {"size": sample_size}}] return [s['_id'] for s in collection.aggregate(pipeline)] def get_random_documents(DocCls, sample_size): doc_collection = DocCls._get_collection() random_oids = get_random_oids(doc_collection, sample_size) return DocCls.objects(id__in=random_oids) def check_documents(DocCls, sample_size): for doc in get_random_documents(DocCls, sample_size): # general validation (types and values) doc.validate() # load all subfields, # this may trigger additional queries if you have ReferenceFields # so it may be slow for field in doc._fields: try: getattr(doc, field) except Exception: LOG.warning(f"Could not load field {field} in Document {doc.id}") raise check_documents(Human, sample_size=1000) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/mongomock.rst0000644000175100001730000000304114400345475020262 0ustar00runnerdocker========================= Use mongomock for testing ========================= Although we recommend running your tests against a regular MongoDB server, it is sometimes useful to plug MongoEngine to alternative implementations (mongomock, montydb, mongita, etc). `mongomock `_ is historically the one suggested for MongoEngine and is a package to do just what the name implies, mocking a mongo database. To use with mongoengine, simply specify mongomock when connecting with mongoengine: .. code-block:: python import mongomock connect('mongoenginetest', host='mongodb://localhost', mongo_client_class=mongomock.MongoClient) conn = get_connection() or with an alias: .. code-block:: python connect('mongoenginetest', host='mongodb://localhost', mongo_client_class=mongomock.MongoClient, alias='testdb') conn = get_connection('testdb') Example of test file: --------------------- .. code-block:: python import unittest from mongoengine import connect, disconnect class Person(Document): name = StringField() class TestPerson(unittest.TestCase): @classmethod def setUpClass(cls): connect('mongoenginetest', host='mongodb://localhost', mongo_client_class=mongomock.MongoClient) @classmethod def tearDownClass(cls): disconnect() def test_thing(self): pers = Person(name='John') pers.save() fresh_pers = Person.objects().first() assert fresh_pers.name == 'John' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/querying.rst0000644000175100001730000007463014400345475020150 0ustar00runnerdocker===================== Querying the database ===================== :class:`~mongoengine.Document` classes have an :attr:`objects` attribute, which is used for accessing the objects in the database associated with the class. The :attr:`objects` attribute is actually a :class:`~mongoengine.queryset.QuerySetManager`, which creates and returns a new :class:`~mongoengine.queryset.QuerySet` object on access. The :class:`~mongoengine.queryset.QuerySet` object may be iterated over to fetch documents from the database:: # Prints out the names of all the users in the database for user in User.objects: print user.name .. note:: As of MongoEngine 0.8 the querysets utilise a local cache. So iterating it multiple times will only cause a single query. If this is not the desired behaviour you can call :class:`~mongoengine.QuerySet.no_cache` (version **0.8.3+**) to return a non-caching queryset. Filtering queries ================= The query may be filtered by calling the :class:`~mongoengine.queryset.QuerySet` object with field lookup keyword arguments. The keys in the keyword arguments correspond to fields on the :class:`~mongoengine.Document` you are querying:: # This will return a QuerySet that will only iterate over users whose # 'country' field is set to 'uk' uk_users = User.objects(country='uk') Fields on embedded documents may also be referred to using field lookup syntax by using a double-underscore in place of the dot in object attribute access syntax:: # This will return a QuerySet that will only iterate over pages that have # been written by a user whose 'country' field is set to 'uk' uk_pages = Page.objects(author__country='uk') .. note:: (version **0.9.1+**) if your field name is like mongodb operator name (for example type, lte, lt...) and you want to place it at the end of lookup keyword mongoengine automatically prepend $ to it. To avoid this use __ at the end of your lookup keyword. For example if your field name is ``type`` and you want to query by this field you must use ``.objects(user__type__="admin")`` instead of ``.objects(user__type="admin")`` Query operators =============== Operators other than equality may also be used in queries --- just attach the operator name to a key with a double-underscore:: # Only find users whose age is 18 or less young_users = Users.objects(age__lte=18) Available operators are as follows: * ``ne`` -- not equal to * ``lt`` -- less than * ``lte`` -- less than or equal to * ``gt`` -- greater than * ``gte`` -- greater than or equal to * ``not`` -- negate a standard check, may be used before other operators (e.g. ``Q(age__not__mod=(5, 0))``) * ``in`` -- value is in list (a list of values should be provided) * ``nin`` -- value is not in list (a list of values should be provided) * ``mod`` -- ``value % x == y``, where ``x`` and ``y`` are two provided values * ``all`` -- every item in list of values provided is in array * ``size`` -- the size of the array is * ``exists`` -- value for field exists String queries -------------- The following operators are available as shortcuts to querying with regular expressions: * ``exact`` -- string field exactly matches value * ``iexact`` -- string field exactly matches value (case insensitive) * ``contains`` -- string field contains value * ``icontains`` -- string field contains value (case insensitive) * ``startswith`` -- string field starts with value * ``istartswith`` -- string field starts with value (case insensitive) * ``endswith`` -- string field ends with value * ``iendswith`` -- string field ends with value (case insensitive) * ``wholeword`` -- string field contains whole word * ``iwholeword`` -- string field contains whole word (case insensitive) * ``regex`` -- string field match by regex * ``iregex`` -- string field match by regex (case insensitive) * ``match`` -- performs an $elemMatch so you can match an entire document within an array Geo queries ----------- There are a few special operators for performing geographical queries. The following were added in MongoEngine 0.8 for :class:`~mongoengine.fields.PointField`, :class:`~mongoengine.fields.LineStringField` and :class:`~mongoengine.fields.PolygonField`: * ``geo_within`` -- check if a geometry is within a polygon. For ease of use it accepts either a geojson geometry or just the polygon coordinates eg:: loc.objects(point__geo_within=[[[40, 5], [40, 6], [41, 6], [40, 5]]]) loc.objects(point__geo_within={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}) * ``geo_within_box`` -- simplified geo_within searching with a box eg:: loc.objects(point__geo_within_box=[(-125.0, 35.0), (-100.0, 40.0)]) loc.objects(point__geo_within_box=[, ]) * ``geo_within_polygon`` -- simplified geo_within searching within a simple polygon eg:: loc.objects(point__geo_within_polygon=[[40, 5], [40, 6], [41, 6], [40, 5]]) loc.objects(point__geo_within_polygon=[ [ , ] , [ , ] , [ , ] ]) * ``geo_within_center`` -- simplified geo_within the flat circle radius of a point eg:: loc.objects(point__geo_within_center=[(-125.0, 35.0), 1]) loc.objects(point__geo_within_center=[ [ , ] , ]) * ``geo_within_sphere`` -- simplified geo_within the spherical circle radius of a point eg:: loc.objects(point__geo_within_sphere=[(-125.0, 35.0), 1]) loc.objects(point__geo_within_sphere=[ [ , ] , ]) * ``geo_intersects`` -- selects all locations that intersect with a geometry eg:: # Inferred from provided points lists: loc.objects(poly__geo_intersects=[40, 6]) loc.objects(poly__geo_intersects=[[40, 5], [40, 6]]) loc.objects(poly__geo_intersects=[[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]) # With geoJson style objects loc.objects(poly__geo_intersects={"type": "Point", "coordinates": [40, 6]}) loc.objects(poly__geo_intersects={"type": "LineString", "coordinates": [[40, 5], [40, 6]]}) loc.objects(poly__geo_intersects={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}) * ``near`` -- find all the locations near a given point:: loc.objects(point__near=[40, 5]) loc.objects(point__near={"type": "Point", "coordinates": [40, 5]}) You can also set the maximum and/or the minimum distance in meters as well:: loc.objects(point__near=[40, 5], point__max_distance=1000) loc.objects(point__near=[40, 5], point__min_distance=100) The older 2D indexes are still supported with the :class:`~mongoengine.fields.GeoPointField`: * ``within_distance`` -- provide a list containing a point and a maximum distance (e.g. [(41.342, -87.653), 5]) * ``within_spherical_distance`` -- same as above but using the spherical geo model (e.g. [(41.342, -87.653), 5/earth_radius]) * ``near`` -- order the documents by how close they are to a given point * ``near_sphere`` -- Same as above but using the spherical geo model * ``within_box`` -- filter documents to those within a given bounding box (e.g. [(35.0, -125.0), (40.0, -100.0)]) * ``within_polygon`` -- filter documents to those within a given polygon (e.g. [(41.91,-87.69), (41.92,-87.68), (41.91,-87.65), (41.89,-87.65)]). .. note:: Requires Mongo Server 2.0 * ``max_distance`` -- can be added to your location queries to set a maximum distance. * ``min_distance`` -- can be added to your location queries to set a minimum distance. Querying lists -------------- On most fields, this syntax will look up documents where the field specified matches the given value exactly, but when the field refers to a :class:`~mongoengine.fields.ListField`, a single item may be provided, in which case lists that contain that item will be matched:: class Page(Document): tags = ListField(StringField()) # This will match all pages that have the word 'coding' as an item in the # 'tags' list Page.objects(tags='coding') It is possible to query by position in a list by using a numerical value as a query operator. So if you wanted to find all pages whose first tag was ``db``, you could use the following query:: Page.objects(tags__0='db') The string queries operators can be used as well for querying a list field, e.g.:: Page.objects(tags__iexact='db') If you only want to fetch part of a list eg: you want to paginate a list, then the `slice` operator is required:: # comments - skip 5, limit 10 Page.objects.fields(slice__comments=[5, 10]) For updating documents, if you don't know the position in a list, you can use the $ positional operator :: Post.objects(comments__by="joe").update(**{'inc__comments__$__votes': 1}) However, this doesn't map well to the syntax so you can also use a capital S instead :: Post.objects(comments__by="joe").update(inc__comments__S__votes=1) .. note:: Due to :program:`Mongo`, currently the $ operator only applies to the first matched item in the query. Raw queries ----------- It is possible to provide a raw :mod:`PyMongo` query as a query parameter, which will be integrated directly into the query. This is done using the ``__raw__`` keyword argument:: Page.objects(__raw__={'tags': 'coding'}) Similarly, a raw update can be provided to the :meth:`~mongoengine.queryset.QuerySet.update` method:: Page.objects(tags='coding').update(__raw__={'$set': {'tags': 'coding'}}) And the two can also be combined:: Page.objects(__raw__={'tags': 'coding'}).update(__raw__={'$set': {'tags': 'coding'}}) Update with Aggregation Pipeline -------------------------------- It is possible to provide a raw :mod:`PyMongo` aggregation update parameter, which will be integrated directly into the update. This is done by using ``__raw__`` keyword argument to the update method and provide the pipeline as a list `Update with Aggregation Pipeline `_ :: # 'tags' field is set to 'coding is fun' Page.objects(tags='coding').update(__raw__=[ {"$set": {"tags": {"$concat": ["$tags", "is fun"]}}} ], ) .. versionadded:: 0.23.2 Sorting/Ordering results ======================== It is possible to order the results by 1 or more keys using :meth:`~mongoengine.queryset.QuerySet.order_by`. The order may be specified by prepending each of the keys by "+" or "-". Ascending order is assumed if there's no prefix.:: # Order by ascending date blogs = BlogPost.objects().order_by('date') # equivalent to .order_by('+date') # Order by ascending date first, then descending title blogs = BlogPost.objects().order_by('+date', '-title') Limiting and skipping results ============================= Just as with traditional ORMs, you may limit the number of results returned or skip a number or results in you query. :meth:`~mongoengine.queryset.QuerySet.limit` and :meth:`~mongoengine.queryset.QuerySet.skip` methods are available on :class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax is preferred for achieving this:: # Only the first 5 people users = User.objects[:5] # All except for the first 5 people users = User.objects[5:] # 5 users, starting from the 11th user found users = User.objects[10:15] You may also index the query to retrieve a single result. If an item at that index does not exists, an :class:`IndexError` will be raised. A shortcut for retrieving the first result and returning :attr:`None` if no result exists is provided (:meth:`~mongoengine.queryset.QuerySet.first`):: >>> # Make sure there are no users >>> User.drop_collection() >>> User.objects[0] IndexError: list index out of range >>> User.objects.first() == None True >>> User(name='Test User').save() >>> User.objects[0] == User.objects.first() True Retrieving unique results ------------------------- To retrieve a result that should be unique in the collection, use :meth:`~mongoengine.queryset.QuerySet.get`. This will raise :class:`~mongoengine.queryset.DoesNotExist` if no document matches the query, and :class:`~mongoengine.queryset.MultipleObjectsReturned` if more than one document matched the query. These exceptions are merged into your document definitions eg: `MyDoc.DoesNotExist` A variation of this method, get_or_create() existed, but it was unsafe. It could not be made safe, because there are no transactions in mongoDB. Other approaches should be investigated, to ensure you don't accidentally duplicate data when using something similar to this method. Therefore it was deprecated in 0.8 and removed in 0.10. Default Document queries ======================== By default, the objects :attr:`~Document.objects` attribute on a document returns a :class:`~mongoengine.queryset.QuerySet` that doesn't filter the collection -- it returns all objects. This may be changed by defining a method on a document that modifies a queryset. The method should accept two arguments -- :attr:`doc_cls` and :attr:`queryset`. The first argument is the :class:`~mongoengine.Document` class that the method is defined on (in this sense, the method is more like a :func:`classmethod` than a regular method), and the second argument is the initial queryset. The method needs to be decorated with :func:`~mongoengine.queryset.queryset_manager` in order for it to be recognised. :: class BlogPost(Document): title = StringField() date = DateTimeField() @queryset_manager def objects(doc_cls, queryset): # This may actually also be done by defining a default ordering for # the document, but this illustrates the use of manager methods return queryset.order_by('-date') You don't need to call your method :attr:`objects` -- you may define as many custom manager methods as you like:: class BlogPost(Document): title = StringField() published = BooleanField() @queryset_manager def live_posts(doc_cls, queryset): return queryset.filter(published=True) BlogPost(title='test1', published=False).save() BlogPost(title='test2', published=True).save() assert len(BlogPost.objects) == 2 assert len(BlogPost.live_posts()) == 1 Custom QuerySets ================ Should you want to add custom methods for interacting with or filtering documents, extending the :class:`~mongoengine.queryset.QuerySet` class may be the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on a document, set ``queryset_class`` to the custom class in a :class:`~mongoengine.Document`'s ``meta`` dictionary:: class AwesomerQuerySet(QuerySet): def get_awesome(self): return self.filter(awesome=True) class Page(Document): meta = {'queryset_class': AwesomerQuerySet} # To call: Page.objects.get_awesome() .. versionadded:: 0.4 Aggregation =========== MongoDB provides some aggregation methods out of the box, but there are not as many as you typically get with an RDBMS. MongoEngine provides a wrapper around the built-in methods and provides some of its own, which are implemented as Javascript code that is executed on the database server. Counting results ---------------- Just as with limiting and skipping results, there is a method on a :class:`~mongoengine.queryset.QuerySet` object -- :meth:`~mongoengine.queryset.QuerySet.count`:: num_users = User.objects.count() You could technically use ``len(User.objects)`` to get the same result, but it would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`. When you execute a server-side count query, you let MongoDB do the heavy lifting and you receive a single integer over the wire. Meanwhile, ``len()`` retrieves all the results, places them in a local cache, and finally counts them. If we compare the performance of the two operations, ``len()`` is much slower than :meth:`~mongoengine.queryset.QuerySet.count`. Further aggregation ------------------- You may sum over the values of a specific field on documents using :meth:`~mongoengine.queryset.QuerySet.sum`:: yearly_expense = Employee.objects.sum('salary') .. note:: If the field isn't present on a document, that document will be ignored from the sum. To get the average (mean) of a field on a collection of documents, use :meth:`~mongoengine.queryset.QuerySet.average`:: mean_age = User.objects.average('age') As MongoDB provides native lists, MongoEngine provides a helper method to get a dictionary of the frequencies of items in lists across an entire collection -- :meth:`~mongoengine.queryset.QuerySet.item_frequencies`. An example of its use would be generating "tag-clouds":: class Article(Document): tag = ListField(StringField()) # After adding some tagged articles... tag_freqs = Article.objects.item_frequencies('tag', normalize=True) from operator import itemgetter top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10] MongoDB aggregation API ----------------------- If you need to run aggregation pipelines, MongoEngine provides an entry point to `Pymongo's aggregation framework `_ through :meth:`~mongoengine.queryset.QuerySet.aggregate`. Check out Pymongo's documentation for the syntax and pipeline. An example of its use would be:: class Person(Document): name = StringField() Person(name='John').save() Person(name='Bob').save() pipeline = [ {"$sort" : {"name" : -1}}, {"$project": {"_id": 0, "name": {"$toUpper": "$name"}}} ] data = Person.objects().aggregate(pipeline) assert data == [{'name': 'BOB'}, {'name': 'JOHN'}] Query efficiency and performance ================================ There are a couple of methods to improve efficiency when querying, reducing the information returned by the query or efficient dereferencing . Retrieving a subset of fields ----------------------------- Sometimes a subset of fields on a :class:`~mongoengine.Document` is required, and for efficiency only these should be retrieved from the database. This issue is especially important for MongoDB, as fields may often be extremely large (e.g. a :class:`~mongoengine.fields.ListField` of :class:`~mongoengine.EmbeddedDocument`\ s, which represent the comments on a blog post. To select only a subset of fields, use :meth:`~mongoengine.queryset.QuerySet.only`, specifying the fields you want to retrieve as its arguments. Note that if fields that are not downloaded are accessed, their default value (or :attr:`None` if no default value is provided) will be given:: >>> class Film(Document): ... title = StringField() ... year = IntField() ... rating = IntField(default=3) ... >>> Film(title='The Shawshank Redemption', year=1994, rating=5).save() >>> f = Film.objects.only('title').first() >>> f.title 'The Shawshank Redemption' >>> f.year # None >>> f.rating # default value 3 .. note:: The :meth:`~mongoengine.queryset.QuerySet.exclude` is the opposite of :meth:`~mongoengine.queryset.QuerySet.only` if you want to exclude a field. If you later need the missing fields, just call :meth:`~mongoengine.Document.reload` on your document. Getting related data -------------------- When iterating the results of :class:`~mongoengine.fields.ListField` or :class:`~mongoengine.fields.DictField` we automatically dereference any :class:`~pymongo.dbref.DBRef` objects as efficiently as possible, reducing the number the queries to mongo. There are times when that efficiency is not enough, documents that have :class:`~mongoengine.fields.ReferenceField` objects or :class:`~mongoengine.fields.GenericReferenceField` objects at the top level are expensive as the number of queries to MongoDB can quickly rise. To limit the number of queries use :func:`~mongoengine.queryset.QuerySet.select_related` which converts the QuerySet to a list and dereferences as efficiently as possible. By default :func:`~mongoengine.queryset.QuerySet.select_related` only dereferences any references to the depth of 1 level. If you have more complicated documents and want to dereference more of the object at once then increasing the :attr:`max_depth` will dereference more levels of the document. Turning off dereferencing ------------------------- Sometimes for performance reasons you don't want to automatically dereference data. To turn off dereferencing of the results of a query use :func:`~mongoengine.queryset.QuerySet.no_dereference` on the queryset like so:: post = Post.objects.no_dereference().first() assert(isinstance(post.author, DBRef)) You can also turn off all dereferencing for a fixed period by using the :class:`~mongoengine.context_managers.no_dereference` context manager:: with no_dereference(Post) as Post: post = Post.objects.first() assert(isinstance(post.author, DBRef)) # Outside the context manager dereferencing occurs. assert(isinstance(post.author, User)) Advanced queries ================ Sometimes calling a :class:`~mongoengine.queryset.QuerySet` object with keyword arguments can't fully express the query you want to use -- for example if you need to combine a number of constraints using *and* and *or*. This is made possible in MongoEngine through the :class:`~mongoengine.queryset.Q` class. A :class:`~mongoengine.queryset.Q` object represents part of a query, and can be initialised using the same keyword-argument syntax you use to query documents. To build a complex query, you may combine :class:`~mongoengine.queryset.Q` objects using the ``&`` (and) and ``|`` (or) operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the first positional argument to :attr:`Document.objects` when you filter it by calling it with keyword arguments:: from mongoengine.queryset.visitor import Q # Get published posts Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now())) # Get top posts Post.objects((Q(featured=True) & Q(hits__gte=1000)) | Q(hits__gte=5000)) .. warning:: You have to use bitwise operators. You cannot use ``or``, ``and`` to combine queries as ``Q(a=a) or Q(b=b)`` is not the same as ``Q(a=a) | Q(b=b)``. As ``Q(a=a)`` equates to true ``Q(a=a) or Q(b=b)`` is the same as ``Q(a=a)``. .. _guide-atomic-updates: Atomic updates ============== Documents may be updated atomically by using the :meth:`~mongoengine.queryset.QuerySet.update_one`, :meth:`~mongoengine.queryset.QuerySet.update` and :meth:`~mongoengine.queryset.QuerySet.modify` methods on a :class:`~mongoengine.queryset.QuerySet` or :meth:`~mongoengine.Document.modify` and :meth:`~mongoengine.Document.save` (with :attr:`save_condition` argument) on a :class:`~mongoengine.Document`. There are several different "modifiers" that you may use with these methods: * ``set`` -- set a particular value * ``set_on_insert`` -- set only if this is new document `need to add upsert=True`_ * ``unset`` -- delete a particular value (since MongoDB v1.3) * ``max`` -- update only if value is bigger * ``min`` -- update only if value is smaller * ``inc`` -- increment a value by a given amount * ``dec`` -- decrement a value by a given amount * ``push`` -- append a value to a list * ``push_all`` -- append several values to a list * ``pop`` -- remove the first or last element of a list `depending on the value`_ * ``pull`` -- remove a value from a list * ``pull_all`` -- remove several values from a list * ``add_to_set`` -- add value to a list only if its not in the list already * ``rename`` -- rename the key name .. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/ The syntax for atomic updates is similar to the querying syntax, but the modifier comes before the field, not after it:: >>> post = BlogPost(title='Test', page_views=0, tags=['database']) >>> post.save() >>> BlogPost.objects(id=post.id).update_one(inc__page_views=1) >>> post.reload() # the document has been changed, so we need to reload it >>> post.page_views 1 >>> BlogPost.objects(id=post.id).update_one(set__title='Example Post') >>> post.reload() >>> post.title 'Example Post' >>> BlogPost.objects(id=post.id).update_one(push__tags='nosql') >>> post.reload() >>> post.tags ['database', 'nosql'] .. note:: If no modifier operator is specified the default will be ``$set``. So the following sentences are identical:: >>> BlogPost.objects(id=post.id).update(title='Example Post') >>> BlogPost.objects(id=post.id).update(set__title='Example Post') .. note:: In version 0.5 the :meth:`~mongoengine.Document.save` runs atomic updates on changed documents by tracking changes to that document. The positional operator allows you to update list items without knowing the index position, therefore making the update a single atomic operation. As we cannot use the `$` syntax in keyword arguments it has been mapped to `S`:: >>> post = BlogPost(title='Test', page_views=0, tags=['database', 'mongo']) >>> post.save() >>> BlogPost.objects(id=post.id, tags='mongo').update(set__tags__S='mongodb') >>> post.reload() >>> post.tags ['database', 'mongodb'] From MongoDB version 2.6, push operator supports $position value which allows to push values with index:: >>> post = BlogPost(title="Test", tags=["mongo"]) >>> post.save() >>> post.update(push__tags__0=["database", "code"]) >>> post.reload() >>> post.tags ['database', 'code', 'mongo'] .. note:: Currently only top level lists are handled, future versions of mongodb / pymongo plan to support nested positional operators. See `The $ positional operator `_. Server-side javascript execution ================================ Javascript functions may be written and sent to the server for execution. The result of this is the return value of the Javascript function. This functionality is accessed through the :meth:`~mongoengine.queryset.QuerySet.exec_js` method on :meth:`~mongoengine.queryset.QuerySet` objects. Pass in a string containing a Javascript function as the first argument. The remaining positional arguments are names of fields that will be passed into you Javascript function as its arguments. This allows functions to be written that may be executed on any field in a collection (e.g. the :meth:`~mongoengine.queryset.QuerySet.sum` method, which accepts the name of the field to sum over as its argument). Note that field names passed in in this manner are automatically translated to the names used on the database (set using the :attr:`name` keyword argument to a field constructor). Keyword arguments to :meth:`~mongoengine.queryset.QuerySet.exec_js` are combined into an object called :attr:`options`, which is available in the Javascript function. This may be used for defining specific parameters for your function. Some variables are made available in the scope of the Javascript function: * ``collection`` -- the name of the collection that corresponds to the :class:`~mongoengine.Document` class that is being used; this should be used to get the :class:`Collection` object from :attr:`db` in Javascript code * ``query`` -- the query that has been generated by the :class:`~mongoengine.queryset.QuerySet` object; this may be passed into the :meth:`find` method on a :class:`Collection` object in the Javascript function * ``options`` -- an object containing the keyword arguments passed into :meth:`~mongoengine.queryset.QuerySet.exec_js` The following example demonstrates the intended usage of :meth:`~mongoengine.queryset.QuerySet.exec_js` by defining a function that sums over a field on a document (this functionality is already available through :meth:`~mongoengine.queryset.QuerySet.sum` but is shown here for sake of example):: def sum_field(document, field_name, include_negatives=True): code = """ function(sumField) { var total = 0.0; db[collection].find(query).forEach(function(doc) { var val = doc[sumField]; if (val >= 0.0 || options.includeNegatives) { total += val; } }); return total; } """ options = {'includeNegatives': include_negatives} return document.objects.exec_js(code, field_name, **options) As fields in MongoEngine may use different names in the database (set using the :attr:`db_field` keyword argument to a :class:`Field` constructor), a mechanism exists for replacing MongoEngine field names with the database field names in Javascript code. When accessing a field on a collection object, use square-bracket notation, and prefix the MongoEngine field name with a tilde. The field name that follows the tilde will be translated to the name used in the database. Note that when referring to fields on embedded documents, the name of the :class:`~mongoengine.fields.EmbeddedDocumentField`, followed by a dot, should be used before the name of the field on the embedded document. The following example shows how the substitutions are made:: class Comment(EmbeddedDocument): content = StringField(db_field='body') class BlogPost(Document): title = StringField(db_field='doctitle') comments = ListField(EmbeddedDocumentField(Comment), name='cs') # Returns a list of dictionaries. Each dictionary contains a value named # "document", which corresponds to the "title" field on a BlogPost, and # "comment", which corresponds to an individual comment. The substitutions # made are shown in the comments. BlogPost.objects.exec_js(""" function() { var comments = []; db[collection].find(query).forEach(function(doc) { // doc[~comments] -> doc["cs"] var docComments = doc[~comments]; for (var i = 0; i < docComments.length; i++) { // doc[~comments][i] -> doc["cs"][i] var comment = doc[~comments][i]; comments.push({ // doc[~title] -> doc["doctitle"] 'document': doc[~title], // comment[~comments.content] -> comment["body"] 'comment': comment[~comments.content] }); } }); return comments; } """) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/signals.rst0000644000175100001730000001177314400345475017744 0ustar00runnerdocker.. _signals: ======= Signals ======= .. versionadded:: 0.5 .. note:: Signal support is provided by the excellent `blinker`_ library. If you wish to enable signal support this library must be installed, though it is not required for MongoEngine to function. Overview -------- Signals are found within the `mongoengine.signals` module. Unless specified signals receive no additional arguments beyond the `sender` class and `document` instance. Post-signals are only called if there were no exceptions raised during the processing of their related function. Available signals include: `pre_init` Called during the creation of a new :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` instance, after the constructor arguments have been collected but before any additional processing has been done to them. (I.e. assignment of default values.) Handlers for this signal are passed the dictionary of arguments using the `values` keyword argument and may modify this dictionary prior to returning. `post_init` Called after all processing of a new :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` instance has been completed. `pre_save` Called within :meth:`~mongoengine.Document.save` prior to performing any actions. `pre_save_post_validation` Called within :meth:`~mongoengine.Document.save` after validation has taken place but before saving. `post_save` Called within :meth:`~mongoengine.Document.save` after most actions (validation, insert/update, and cascades, but not clearing dirty flags) have completed successfully. Passed the additional boolean keyword argument `created` to indicate if the save was an insert or an update. `pre_delete` Called within :meth:`~mongoengine.Document.delete` prior to attempting the delete operation. `post_delete` Called within :meth:`~mongoengine.Document.delete` upon successful deletion of the record. `pre_bulk_insert` Called after validation of the documents to insert, but prior to any data being written. In this case, the `document` argument is replaced by a `documents` argument representing the list of documents being inserted. `post_bulk_insert` Called after a successful bulk insert operation. As per `pre_bulk_insert`, the `document` argument is omitted and replaced with a `documents` argument. An additional boolean argument, `loaded`, identifies the contents of `documents` as either :class:`~mongoengine.Document` instances when `True` or simply a list of primary key values for the inserted records if `False`. Attaching Events ---------------- After writing a handler function like the following:: import logging from datetime import datetime from mongoengine import * from mongoengine import signals def update_modified(sender, document): document.modified = datetime.utcnow() You attach the event handler to your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` subclass:: class Record(Document): modified = DateTimeField() signals.pre_save.connect(update_modified) While this is not the most elaborate document model, it does demonstrate the concepts involved. As a more complete demonstration you can also define your handlers within your subclass:: class Author(Document): name = StringField() @classmethod def pre_save(cls, sender, document, **kwargs): logging.debug("Pre Save: %s" % document.name) @classmethod def post_save(cls, sender, document, **kwargs): logging.debug("Post Save: %s" % document.name) if 'created' in kwargs: if kwargs['created']: logging.debug("Created") else: logging.debug("Updated") signals.pre_save.connect(Author.pre_save, sender=Author) signals.post_save.connect(Author.post_save, sender=Author) .. warning:: Note that EmbeddedDocument only supports pre/post_init signals. pre/post_save, etc should be attached to Document's class only. Attaching pre_save to an EmbeddedDocument is ignored silently. Finally, you can also use this small decorator to quickly create a number of signals and attach them to your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` subclasses as class decorators:: def handler(event): """Signal decorator to allow use of callback functions as class decorators.""" def decorator(fn): def apply(cls): event.connect(fn, sender=cls) return cls fn.apply = apply return fn return decorator Using the first example of updating a modification time the code is now much cleaner looking while still allowing manual execution of the callback:: @handler(signals.pre_save) def update_modified(sender, document): document.modified = datetime.utcnow() @update_modified.apply class Record(Document): modified = DateTimeField() .. _blinker: http://pypi.python.org/pypi/blinker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/text-indexes.rst0000644000175100001730000000224014400345475020712 0ustar00runnerdocker=========== Text Search =========== After MongoDB 2.4 version, supports search documents by text indexes. Defining a Document with text index =================================== Use the *$* prefix to set a text index, Look the declaration:: class News(Document): title = StringField() content = StringField() is_active = BooleanField() meta = {'indexes': [ {'fields': ['$title', "$content"], 'default_language': 'english', 'weights': {'title': 10, 'content': 2} } ]} Querying ======== Saving a document:: News(title="Using mongodb text search", content="Testing text search").save() News(title="MongoEngine 0.9 released", content="Various improvements").save() Next, start a text search using :attr:`QuerySet.search_text` method:: document = News.objects.search_text('testing').first() document.title # may be: "Using mongodb text search" document = News.objects.search_text('released').first() document.title # may be: "MongoEngine 0.9 released" Ordering by text score ====================== :: objects = News.objects.search_text('mongo').order_by('$text_score') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/guide/validation.rst0000644000175100001730000001063114400345475020426 0ustar00runnerdocker==================== Document Validation ==================== By design, MongoEngine strictly validates the documents right before they are inserted in MongoDB and makes sure they are consistent with the fields defined in your models. MongoEngine makes the assumption that the documents that exists in the DB are compliant with the schema. This means that Mongoengine will not validate a document when an object is loaded from the DB into an instance of your model but this operation may fail under some circumstances (e.g. if there is a field in the document fetched from the database that is not defined in your model). Built-in validation =================== Mongoengine provides different fields that encapsulate the corresponding validation out of the box. Validation runs when calling `.validate()` or `.save()` .. code-block:: python from mongoengine import Document, EmailField, IntField class User(Document): email = EmailField() age = IntField(min_value=0, max_value=99) user = User(email='invalid@', age=24) user.validate() # raises ValidationError (Invalid email address: ['email']) user.save() # raises ValidationError (Invalid email address: ['email']) user2 = User(email='john.doe@garbage.com', age=1000) user2.save() # raises ValidationError (Integer value is too large: ['age']) Custom validation ================= The following feature can be used to customize the validation: * Field `validation` parameter .. code-block:: python def not_john_doe(name): if name == 'John Doe': raise ValidationError("John Doe is not a valid name") class Person(Document): full_name = StringField(validation=not_john_doe) Person(full_name='Billy Doe').save() Person(full_name='John Doe').save() # raises ValidationError (John Doe is not a valid name) * Document `clean` method This method is called as part of :meth:`~mongoengine.document.Document.save` and should be used to provide custom model validation and/or to modify some of the field values prior to validation. For instance, you could use it to automatically provide a value for a field, or to do validation that requires access to more than a single field. .. code-block:: python class Essay(Document): status = StringField(choices=('Published', 'Draft'), required=True) pub_date = DateTimeField() def clean(self): # Validate that only published essays have a `pub_date` if self.status == 'Draft' and self.pub_date is not None: raise ValidationError('Draft entries should not have a publication date.') # Set the pub_date for published items if not set. if self.status == 'Published' and self.pub_date is None: self.pub_date = datetime.now() .. note:: Cleaning is only called if validation is turned on and when calling :meth:`~mongoengine.Document.save`. * Adding custom Field classes We recommend as much as possible to use fields provided by MongoEngine. However, it is also possible to subclass a Field and encapsulate some validation by overriding the `validate` method .. code-block:: python class AgeField(IntField): def validate(self, value): super(AgeField, self).validate(value) # let IntField.validate run first if value == 60: self.error('60 is not allowed') class Person(Document): age = AgeField(min_value=0, max_value=99) Person(age=20).save() # passes Person(age=1000).save() # raises ValidationError (Integer value is too large: ['age']) Person(age=60).save() # raises ValidationError (Person:None) (60 is not allowed: ['age']) .. note:: When overriding `validate`, use `self.error("your-custom-error")` instead of raising ValidationError explicitly, it will provide a better context with the error message Skipping validation ==================== Although discouraged as it allows to violate fields constraints, if for some reason you need to disable the validation and cleaning of a document when you call :meth:`~mongoengine.document.Document.save`, you can use `.save(validate=False)`. .. code-block:: python class Person(Document): age = IntField(max_value=100) Person(age=1000).save() # raises ValidationError (Integer value is too large) Person(age=1000).save(validate=False) person = Person.objects.first() assert person.age == 1000 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/index.rst0000644000175100001730000000465014400345475016312 0ustar00runnerdocker============================== MongoEngine User Documentation ============================== **MongoEngine** is an Object-Document Mapper, written in Python for working with MongoDB. To install it, simply run .. code-block:: console $ python -m pip install -U mongoengine :doc:`tutorial` A quick tutorial building a tumblelog to get you up and running with MongoEngine. :doc:`guide/index` The Full guide to MongoEngine --- from modeling documents to storing files, from querying for data to firing signals and *everything* between. :doc:`apireference` The complete API documentation --- the innards of documents, querysets and fields. :doc:`upgrade` How to upgrade MongoEngine. :doc:`faq` Frequently Asked Questions :doc:`django` Using MongoEngine and Django MongoDB and driver support -------------------------- MongoEngine is based on the PyMongo driver and tested against multiple versions of MongoDB. For further details, please refer to the `readme `_. Community --------- To get help with using MongoEngine, use the `MongoEngine Users mailing list `_ or the ever popular `stackoverflow `_. Contributing ------------ **Yes please!** We are always looking for contributions, additions and improvements. The source is available on `GitHub `_ and contributions are always encouraged. Contributions can be as simple as minor tweaks to this documentation, the website or the core. To contribute, fork the project on `GitHub `_ and send a pull request. Changes ------- See the :doc:`changelog` for a full list of changes to MongoEngine and :doc:`upgrade` for upgrade information. .. note:: Always read and test the `upgrade `_ documentation before putting updates live in production **;)** Offline Reading --------------- Download the docs in `pdf `_ or `epub `_ formats for offline reading. .. toctree:: :maxdepth: 1 :numbered: :hidden: tutorial guide/index apireference changelog upgrade faq django Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/requirements.txt0000644000175100001730000000014014400345475017723 0ustar00runnerdockerSphinx==3.3.0 sphinx-rtd-theme==0.5.0 readthedocs-sphinx-ext==2.1.1 docutils==0.17.1 Jinja2<3.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/docs/tutorial.rst0000644000175100001730000003111014400345475017035 0ustar00runnerdocker======== Tutorial ======== This tutorial introduces **MongoEngine** by means of example --- we will walk through how to create a simple **Tumblelog** application. A tumblelog is a blog that supports mixed media content, including text, images, links, video, audio, etc. For simplicity's sake, we'll stick to text, image, and link entries. As the purpose of this tutorial is to introduce MongoEngine, we'll focus on the data-modelling side of the application, leaving out a user interface. Getting started =============== Before we start, make sure that a copy of MongoDB is running in an accessible location --- running it locally will be easier, but if that is not an option then it may be run on a remote server. If you haven't installed MongoEngine, simply use pip to install it like so:: $ python -m pip install mongoengine Before we can start using MongoEngine, we need to tell it how to connect to our instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect` function. If running locally, the only argument we need to provide is the name of the MongoDB database to use:: from mongoengine import * connect('tumblelog') There are lots of options for connecting to MongoDB, for more information about them see the :ref:`guide-connecting` guide. Defining our documents ====================== MongoDB is *schemaless*, which means that no schema is enforced by the database --- we may add and remove fields however we want and MongoDB won't complain. This makes life a lot easier in many regards, especially when there is a change to the data model. However, defining schemas for our documents can help to iron out bugs involving incorrect types or missing fields, and also allow us to define utility methods on our documents in the same way that traditional :abbr:`ORMs (Object-Relational Mappers)` do. In our Tumblelog application we need to store several different types of information. We will need to have a collection of **users**, so that we may link posts to an individual. We also need to store our different types of **posts** (eg: text, image and link) in the database. To aid navigation of our Tumblelog, posts may have **tags** associated with them, so that the list of posts shown to the user may be limited to posts that have been assigned a specific tag. Finally, it would be nice if **comments** could be added to posts. We'll start with **users**, as the other document models are slightly more involved. Users ----- Just as if we were using a relational database with an ORM, we need to define which fields a :class:`User` may have, and what types of data they might store:: class User(Document): email = StringField(required=True) first_name = StringField(max_length=50) last_name = StringField(max_length=50) This looks similar to how the structure of a table would be defined in a regular ORM. The key difference is that this schema will never be passed on to MongoDB --- this will only be enforced at the application level, making future changes easy to manage. Also, the User documents will be stored in a MongoDB *collection* rather than a table. Posts, Comments and Tags ------------------------ Now we'll think about how to store the rest of the information. If we were using a relational database, we would most likely have a table of **posts**, a table of **comments** and a table of **tags**. To associate the comments with individual posts, we would put a column in the comments table that contained a foreign key to the posts table. We'd also need a link table to provide the many-to-many relationship between posts and tags. Then we'd need to address the problem of storing the specialised post-types (text, image and link). There are several ways we can achieve this, but each of them have their problems --- none of them stand out as particularly intuitive solutions. Posts ^^^^^ Happily MongoDB *isn't* a relational database, so we're not going to do it that way. As it turns out, we can use MongoDB's schemaless nature to provide us with a much nicer solution. We will store all of the posts in *one collection* and each post type will only store the fields it needs. If we later want to add video posts, we don't have to modify the collection at all, we just *start using* the new fields we need to support video posts. This fits with the Object-Oriented principle of *inheritance* nicely. We can think of :class:`Post` as a base class, and :class:`TextPost`, :class:`ImagePost` and :class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports this kind of modeling out of the box --- all you need do is turn on inheritance by setting :attr:`allow_inheritance` to True in the :attr:`meta`:: class Post(Document): title = StringField(max_length=120, required=True) author = ReferenceField(User) meta = {'allow_inheritance': True} class TextPost(Post): content = StringField() class ImagePost(Post): image_path = StringField() class LinkPost(Post): link_url = StringField() We are storing a reference to the author of the posts using a :class:`~mongoengine.fields.ReferenceField` object. These are similar to foreign key fields in traditional ORMs, and are automatically translated into references when they are saved, and dereferenced when they are loaded. Tags ^^^^ Now that we have our Post models figured out, how will we attach tags to them? MongoDB allows us to store lists of items natively, so rather than having a link table, we can just store a list of tags in each post. So, for both efficiency and simplicity's sake, we'll store the tags as strings directly within the post, rather than storing references to tags in a separate collection. Especially as tags are generally very short (often even shorter than a document's id), this denormalization won't impact the size of the database very strongly. Let's take a look at the code of our modified :class:`Post` class:: class Post(Document): title = StringField(max_length=120, required=True) author = ReferenceField(User) tags = ListField(StringField(max_length=30)) The :class:`~mongoengine.fields.ListField` object that is used to define a Post's tags takes a field object as its first argument --- this means that you can have lists of any type of field (including lists). .. note:: We don't need to modify the specialized post types as they all inherit from :class:`Post`. Comments ^^^^^^^^ A comment is typically associated with *one* post. In a relational database, to display a post with its comments, we would have to retrieve the post from the database and then query the database again for the comments associated with the post. This works, but there is no real reason to be storing the comments separately from their associated posts, other than to work around the relational model. Using MongoDB we can store the comments as a list of *embedded documents* directly on a post document. An embedded document should be treated no differently than a regular document; it just doesn't have its own collection in the database. Using MongoEngine, we can define the structure of embedded documents, along with utility methods, in exactly the same way we do with regular documents:: class Comment(EmbeddedDocument): content = StringField() name = StringField(max_length=120) We can then store a list of comment documents in our post document:: class Post(Document): title = StringField(max_length=120, required=True) author = ReferenceField(User) tags = ListField(StringField(max_length=30)) comments = ListField(EmbeddedDocumentField(Comment)) Handling deletions of references ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :class:`~mongoengine.fields.ReferenceField` object takes a keyword `reverse_delete_rule` for handling deletion rules if the reference is deleted. To delete all the posts if a user is deleted set the rule:: class Post(Document): title = StringField(max_length=120, required=True) author = ReferenceField(User, reverse_delete_rule=CASCADE) tags = ListField(StringField(max_length=30)) comments = ListField(EmbeddedDocumentField(Comment)) See :class:`~mongoengine.fields.ReferenceField` for more information. .. note:: MapFields and DictFields currently don't support automatic handling of deleted references Adding data to our Tumblelog ============================ Now that we've defined how our documents will be structured, let's start adding some documents to the database. Firstly, we'll need to create a :class:`User` object:: ross = User(email='ross@example.com', first_name='Ross', last_name='Lawley').save() .. note:: We could have also defined our user using attribute syntax:: ross = User(email='ross@example.com') ross.first_name = 'Ross' ross.last_name = 'Lawley' ross.save() Assign another user to a variable called ``john``, just like we did above with ``ross``. Now that we've got our users in the database, let's add a couple of posts:: post1 = TextPost(title='Fun with MongoEngine', author=john) post1.content = 'Took a look at MongoEngine today, looks pretty cool.' post1.tags = ['mongodb', 'mongoengine'] post1.save() post2 = LinkPost(title='MongoEngine Documentation', author=ross) post2.link_url = 'http://docs.mongoengine.com/' post2.tags = ['mongoengine'] post2.save() .. note:: If you change a field on an object that has already been saved and then call :meth:`save` again, the document will be updated. Accessing our data ================== So now we've got a couple of posts in our database, how do we display them? Each document class (i.e. any class that inherits either directly or indirectly from :class:`~mongoengine.Document`) has an :attr:`objects` attribute, which is used to access the documents in the database collection associated with that class. So let's see how we can get our posts' titles:: for post in Post.objects: print(post.title) Retrieving type-specific information ------------------------------------ This will print the titles of our posts, one on each line. But what if we want to access the type-specific data (link_url, content, etc.)? One way is simply to use the :attr:`objects` attribute of a subclass of :class:`Post`:: for post in TextPost.objects: print(post.content) Using TextPost's :attr:`objects` attribute only returns documents that were created using :class:`TextPost`. Actually, there is a more general rule here: the :attr:`objects` attribute of any subclass of :class:`~mongoengine.Document` only looks for documents that were created using that subclass or one of its subclasses. So how would we display all of our posts, showing only the information that corresponds to each post's specific type? There is a better way than just using each of the subclasses individually. When we used :class:`Post`'s :attr:`objects` attribute earlier, the objects being returned weren't actually instances of :class:`Post` --- they were instances of the subclass of :class:`Post` that matches the post's type. Let's look at how this works in practice:: for post in Post.objects: print(post.title) print('=' * len(post.title)) if isinstance(post, TextPost): print(post.content) if isinstance(post, LinkPost): print('Link: {}'.format(post.link_url)) This would print the title of each post, followed by the content if it was a text post, and "Link: " if it was a link post. Searching our posts by tag -------------------------- The :attr:`objects` attribute of a :class:`~mongoengine.Document` is actually a :class:`~mongoengine.queryset.QuerySet` object. This lazily queries the database only when you need the data. It may also be filtered to narrow down your query. Let's adjust our query so that only posts with the tag "mongodb" are returned:: for post in Post.objects(tags='mongodb'): print(post.title) There are also methods available on :class:`~mongoengine.queryset.QuerySet` objects that allow different results to be returned, for example, calling :meth:`first` on the :attr:`objects` attribute will return a single document, the first matched by the query you provide. Aggregation functions may also be used on :class:`~mongoengine.queryset.QuerySet` objects:: num_posts = Post.objects(tags='mongodb').count() print('Found {} posts with tag "mongodb"'.format(num_posts)) Learning more about MongoEngine ------------------------------- If you got this far you've made a great start, so well done! The next step on your MongoEngine journey is the `full user guide `_, where you can learn in-depth about how to use MongoEngine and MongoDB. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/mongoengine/0000755000175100001730000000000014400345501016007 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/__init__.py0000644000175100001730000000216214400345475020133 0ustar00runnerdocker# Import submodules so that we can expose their __all__ from mongoengine import ( connection, document, errors, fields, queryset, signals, ) # Import everything from each submodule so that it can be accessed via # mongoengine, e.g. instead of `from mongoengine.connection import connect`, # users can simply use `from mongoengine import connect`, or even # `from mongoengine import *` and then `connect('testdb')`. from mongoengine.connection import * # noqa: F401 from mongoengine.document import * # noqa: F401 from mongoengine.errors import * # noqa: F401 from mongoengine.fields import * # noqa: F401 from mongoengine.queryset import * # noqa: F401 from mongoengine.signals import * # noqa: F401 __all__ = ( list(document.__all__) + list(fields.__all__) + list(connection.__all__) + list(queryset.__all__) + list(signals.__all__) + list(errors.__all__) ) VERSION = (0, 27, 0) def get_version(): """Return the VERSION as a string. For example, if `VERSION == (0, 10, 7)`, return '0.10.7'. """ return ".".join(map(str, VERSION)) __version__ = get_version() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/mongoengine/base/0000755000175100001730000000000014400345501016721 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/__init__.py0000644000175100001730000000176414400345475021054 0ustar00runnerdocker# Base module is split into several files for convenience. Files inside of # this module should import from a specific submodule (e.g. # `from mongoengine.base.document import BaseDocument`), but all of the # other modules should import directly from the top-level module (e.g. # `from mongoengine.base import BaseDocument`). This approach is cleaner and # also helps with cyclical import errors. from mongoengine.base.common import * from mongoengine.base.datastructures import * from mongoengine.base.document import * from mongoengine.base.fields import * from mongoengine.base.metaclasses import * __all__ = ( # common "UPDATE_OPERATORS", "_document_registry", "get_document", # datastructures "BaseDict", "BaseList", "EmbeddedDocumentList", "LazyReference", # document "BaseDocument", # fields "BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField", # metaclasses "DocumentMetaclass", "TopLevelDocumentMetaclass", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/common.py0000644000175100001730000000300214400345475020570 0ustar00runnerdockerfrom mongoengine.errors import NotRegistered __all__ = ("UPDATE_OPERATORS", "get_document", "_document_registry") UPDATE_OPERATORS = { "set", "unset", "inc", "dec", "mul", "pop", "push", "push_all", "pull", "pull_all", "add_to_set", "set_on_insert", "min", "max", "rename", } _document_registry = {} def get_document(name): """Get a registered Document class by name.""" doc = _document_registry.get(name, None) if not doc: # Possible old style name single_end = name.split(".")[-1] compound_end = ".%s" % single_end possible_match = [ k for k in _document_registry if k.endswith(compound_end) or k == single_end ] if len(possible_match) == 1: doc = _document_registry.get(possible_match.pop(), None) if not doc: raise NotRegistered( """ `%s` has not been registered in the document registry. Importing the document class automatically registers it, has it been imported? """.strip() % name ) return doc def _get_documents_by_db(connection_alias, default_connection_alias): """Get all registered Documents class attached to a given database""" def get_doc_alias(doc_cls): return doc_cls._meta.get("db_alias", default_connection_alias) return [ doc_cls for doc_cls in _document_registry.values() if get_doc_alias(doc_cls) == connection_alias ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/datastructures.py0000644000175100001730000003664714400345475022402 0ustar00runnerdockerimport weakref from bson import DBRef from mongoengine.common import _import_class from mongoengine.errors import DoesNotExist, MultipleObjectsReturned __all__ = ( "BaseDict", "StrictDict", "BaseList", "EmbeddedDocumentList", "LazyReference", ) def mark_as_changed_wrapper(parent_method): """Decorator that ensures _mark_as_changed method gets called.""" def wrapper(self, *args, **kwargs): # Can't use super() in the decorator. result = parent_method(self, *args, **kwargs) self._mark_as_changed() return result return wrapper def mark_key_as_changed_wrapper(parent_method): """Decorator that ensures _mark_as_changed method gets called with the key argument""" def wrapper(self, key, *args, **kwargs): # Can't use super() in the decorator. if not args or key not in self or self[key] != args[0]: self._mark_as_changed(key) return parent_method(self, key, *args, **kwargs) return wrapper class BaseDict(dict): """A special dict so we can watch any changes.""" _dereferenced = False _instance = None _name = None def __init__(self, dict_items, instance, name): BaseDocument = _import_class("BaseDocument") if isinstance(instance, BaseDocument): self._instance = weakref.proxy(instance) self._name = name super().__init__(dict_items) def get(self, key, default=None): # get does not use __getitem__ by default so we must override it as well try: return self.__getitem__(key) except KeyError: return default def __getitem__(self, key): value = super().__getitem__(key) EmbeddedDocument = _import_class("EmbeddedDocument") if isinstance(value, EmbeddedDocument) and value._instance is None: value._instance = self._instance elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, None, f"{self._name}.{key}") super().__setitem__(key, value) value._instance = self._instance elif isinstance(value, list) and not isinstance(value, BaseList): value = BaseList(value, None, f"{self._name}.{key}") super().__setitem__(key, value) value._instance = self._instance return value def __getstate__(self): self.instance = None self._dereferenced = False return self def __setstate__(self, state): self = state return self __setitem__ = mark_key_as_changed_wrapper(dict.__setitem__) __delattr__ = mark_key_as_changed_wrapper(dict.__delattr__) __delitem__ = mark_key_as_changed_wrapper(dict.__delitem__) pop = mark_as_changed_wrapper(dict.pop) clear = mark_as_changed_wrapper(dict.clear) update = mark_as_changed_wrapper(dict.update) popitem = mark_as_changed_wrapper(dict.popitem) setdefault = mark_as_changed_wrapper(dict.setdefault) def _mark_as_changed(self, key=None): if hasattr(self._instance, "_mark_as_changed"): if key: self._instance._mark_as_changed(f"{self._name}.{key}") else: self._instance._mark_as_changed(self._name) class BaseList(list): """A special list so we can watch any changes.""" _dereferenced = False _instance = None _name = None def __init__(self, list_items, instance, name): BaseDocument = _import_class("BaseDocument") if isinstance(instance, BaseDocument): self._instance = weakref.proxy(instance) self._name = name super().__init__(list_items) def __getitem__(self, key): # change index to positive value because MongoDB does not support negative one if isinstance(key, int) and key < 0: key = len(self) + key value = super().__getitem__(key) if isinstance(key, slice): # When receiving a slice operator, we don't convert the structure and bind # to parent's instance. This is buggy for now but would require more work to be handled properly return value EmbeddedDocument = _import_class("EmbeddedDocument") if isinstance(value, EmbeddedDocument) and value._instance is None: value._instance = self._instance elif isinstance(value, dict) and not isinstance(value, BaseDict): # Replace dict by BaseDict value = BaseDict(value, None, f"{self._name}.{key}") super().__setitem__(key, value) value._instance = self._instance elif isinstance(value, list) and not isinstance(value, BaseList): # Replace list by BaseList value = BaseList(value, None, f"{self._name}.{key}") super().__setitem__(key, value) value._instance = self._instance return value def __iter__(self): yield from super().__iter__() def __getstate__(self): self.instance = None self._dereferenced = False return self def __setstate__(self, state): self = state return self def __setitem__(self, key, value): changed_key = key if isinstance(key, slice): # In case of slice, we don't bother to identify the exact elements being updated # instead, we simply marks the whole list as changed changed_key = None result = super().__setitem__(key, value) self._mark_as_changed(changed_key) return result append = mark_as_changed_wrapper(list.append) extend = mark_as_changed_wrapper(list.extend) insert = mark_as_changed_wrapper(list.insert) pop = mark_as_changed_wrapper(list.pop) remove = mark_as_changed_wrapper(list.remove) reverse = mark_as_changed_wrapper(list.reverse) sort = mark_as_changed_wrapper(list.sort) __delitem__ = mark_as_changed_wrapper(list.__delitem__) __iadd__ = mark_as_changed_wrapper(list.__iadd__) __imul__ = mark_as_changed_wrapper(list.__imul__) def _mark_as_changed(self, key=None): if hasattr(self._instance, "_mark_as_changed"): if key is not None: self._instance._mark_as_changed(f"{self._name}.{key % len(self)}") else: self._instance._mark_as_changed(self._name) class EmbeddedDocumentList(BaseList): def __init__(self, list_items, instance, name): super().__init__(list_items, instance, name) self._instance = instance @classmethod def __match_all(cls, embedded_doc, kwargs): """Return True if a given embedded doc matches all the filter kwargs. If it doesn't return False. """ for key, expected_value in kwargs.items(): doc_val = getattr(embedded_doc, key) if doc_val != expected_value and str(doc_val) != expected_value: return False return True @classmethod def __only_matches(cls, embedded_docs, kwargs): """Return embedded docs that match the filter kwargs.""" if not kwargs: return embedded_docs return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)] def filter(self, **kwargs): """ Filters the list by only including embedded documents with the given keyword arguments. This method only supports simple comparison (e.g. .filter(name='John Doe')) and does not support operators like __gte, __lte, __icontains like queryset.filter does :param kwargs: The keyword arguments corresponding to the fields to filter on. *Multiple arguments are treated as if they are ANDed together.* :return: A new ``EmbeddedDocumentList`` containing the matching embedded documents. Raises ``AttributeError`` if a given keyword is not a valid field for the embedded document class. """ values = self.__only_matches(self, kwargs) return EmbeddedDocumentList(values, self._instance, self._name) def exclude(self, **kwargs): """ Filters the list by excluding embedded documents with the given keyword arguments. :param kwargs: The keyword arguments corresponding to the fields to exclude on. *Multiple arguments are treated as if they are ANDed together.* :return: A new ``EmbeddedDocumentList`` containing the non-matching embedded documents. Raises ``AttributeError`` if a given keyword is not a valid field for the embedded document class. """ exclude = self.__only_matches(self, kwargs) values = [item for item in self if item not in exclude] return EmbeddedDocumentList(values, self._instance, self._name) def count(self): """ The number of embedded documents in the list. :return: The length of the list, equivalent to the result of ``len()``. """ return len(self) def get(self, **kwargs): """ Retrieves an embedded document determined by the given keyword arguments. :param kwargs: The keyword arguments corresponding to the fields to search on. *Multiple arguments are treated as if they are ANDed together.* :return: The embedded document matched by the given keyword arguments. Raises ``DoesNotExist`` if the arguments used to query an embedded document returns no results. ``MultipleObjectsReturned`` if more than one result is returned. """ values = self.__only_matches(self, kwargs) if len(values) == 0: raise DoesNotExist("%s matching query does not exist." % self._name) elif len(values) > 1: raise MultipleObjectsReturned( "%d items returned, instead of 1" % len(values) ) return values[0] def first(self): """Return the first embedded document in the list, or ``None`` if empty. """ if len(self) > 0: return self[0] def create(self, **values): """ Creates a new instance of the EmbeddedDocument and appends it to this EmbeddedDocumentList. .. note:: the instance of the EmbeddedDocument is not automatically saved to the database. You still need to call .save() on the parent Document. :param values: A dictionary of values for the embedded document. :return: The new embedded document instance. """ name = self._name EmbeddedClass = self._instance._fields[name].field.document_type_obj self._instance[self._name].append(EmbeddedClass(**values)) return self._instance[self._name][-1] def save(self, *args, **kwargs): """ Saves the ancestor document. :param args: Arguments passed up to the ancestor Document's save method. :param kwargs: Keyword arguments passed up to the ancestor Document's save method. """ self._instance.save(*args, **kwargs) def delete(self): """ Deletes the embedded documents from the database. .. note:: The embedded document changes are not automatically saved to the database after calling this method. :return: The number of entries deleted. """ values = list(self) for item in values: self._instance[self._name].remove(item) return len(values) def update(self, **update): """ Updates the embedded documents with the given replacement values. This function does not support mongoDB update operators such as ``inc__``. .. note:: The embedded document changes are not automatically saved to the database after calling this method. :param update: A dictionary of update values to apply to each embedded document. :return: The number of entries updated. """ if len(update) == 0: return 0 values = list(self) for item in values: for k, v in update.items(): setattr(item, k, v) return len(values) class StrictDict: __slots__ = () _special_fields = {"get", "pop", "iteritems", "items", "keys", "create"} _classes = {} def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) def __getitem__(self, key): key = "_reserved_" + key if key in self._special_fields else key try: return getattr(self, key) except AttributeError: raise KeyError(key) def __setitem__(self, key, value): key = "_reserved_" + key if key in self._special_fields else key return setattr(self, key, value) def __contains__(self, key): return hasattr(self, key) def get(self, key, default=None): try: return self[key] except KeyError: return default def pop(self, key, default=None): v = self.get(key, default) try: delattr(self, key) except AttributeError: pass return v def iteritems(self): for key in self: yield key, self[key] def items(self): return [(k, self[k]) for k in iter(self)] def iterkeys(self): return iter(self) def keys(self): return list(iter(self)) def __iter__(self): return (key for key in self.__slots__ if hasattr(self, key)) def __len__(self): return len(list(self.items())) def __eq__(self, other): return list(self.items()) == list(other.items()) def __ne__(self, other): return not (self == other) @classmethod def create(cls, allowed_keys): allowed_keys_tuple = tuple( ("_reserved_" + k if k in cls._special_fields else k) for k in allowed_keys ) allowed_keys = frozenset(allowed_keys_tuple) if allowed_keys not in cls._classes: class SpecificStrictDict(cls): __slots__ = allowed_keys_tuple def __repr__(self): return "{%s}" % ", ".join( f'"{k!s}": {v!r}' for k, v in self.items() ) cls._classes[allowed_keys] = SpecificStrictDict return cls._classes[allowed_keys] class LazyReference(DBRef): __slots__ = ("_cached_doc", "passthrough", "document_type") def fetch(self, force=False): if not self._cached_doc or force: self._cached_doc = self.document_type.objects.get(pk=self.pk) if not self._cached_doc: raise DoesNotExist("Trying to dereference unknown document %s" % (self)) return self._cached_doc @property def pk(self): return self.id def __init__(self, document_type, pk, cached_doc=None, passthrough=False): self.document_type = document_type self._cached_doc = cached_doc self.passthrough = passthrough super().__init__(self.document_type._get_collection_name(), pk) def __getitem__(self, name): if not self.passthrough: raise KeyError() document = self.fetch() return document[name] def __getattr__(self, name): if not object.__getattribute__(self, "passthrough"): raise AttributeError() document = self.fetch() try: return document[name] except KeyError: raise AttributeError() def __repr__(self): return f"" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/document.py0000644000175100001730000013166514400345475021137 0ustar00runnerdockerimport copy import numbers import warnings from functools import partial import pymongo from bson import SON, DBRef, ObjectId, json_util from mongoengine import signals from mongoengine.base.common import get_document from mongoengine.base.datastructures import ( BaseDict, BaseList, EmbeddedDocumentList, LazyReference, StrictDict, ) from mongoengine.base.fields import ComplexBaseField from mongoengine.common import _import_class from mongoengine.errors import ( FieldDoesNotExist, InvalidDocumentError, LookUpError, OperationError, ValidationError, ) from mongoengine.pymongo_support import LEGACY_JSON_OPTIONS __all__ = ("BaseDocument", "NON_FIELD_ERRORS") NON_FIELD_ERRORS = "__all__" try: GEOHAYSTACK = pymongo.GEOHAYSTACK except AttributeError: GEOHAYSTACK = None class BaseDocument: # TODO simplify how `_changed_fields` is used. # Currently, handling of `_changed_fields` seems unnecessarily convoluted: # 1. `BaseDocument` defines `_changed_fields` in its `__slots__`, yet it's # not setting it to `[]` (or any other value) in `__init__`. # 2. `EmbeddedDocument` sets `_changed_fields` to `[]` it its overloaded # `__init__`. # 3. `Document` does NOT set `_changed_fields` upon initialization. The # field is primarily set via `_from_son` or `_clear_changed_fields`, # though there are also other methods that manipulate it. # 4. The codebase is littered with `hasattr` calls for `_changed_fields`. __slots__ = ( "_changed_fields", "_initialised", "_created", "_data", "_dynamic_fields", "_auto_id_field", "_db_field_map", "__weakref__", ) _dynamic = False _dynamic_lock = True STRICT = False def __init__(self, *args, **values): """ Initialise a document or an embedded document. :param values: A dictionary of keys and values for the document. It may contain additional reserved keywords, e.g. "__auto_convert". :param __auto_convert: If True, supplied values will be converted to Python-type values via each field's `to_python` method. :param _created: Indicates whether this is a brand new document or whether it's already been persisted before. Defaults to true. """ self._initialised = False self._created = True if args: raise TypeError( "Instantiating a document with positional arguments is not " "supported. Please use `field_name=value` keyword arguments." ) __auto_convert = values.pop("__auto_convert", True) _created = values.pop("_created", True) signals.pre_init.send(self.__class__, document=self, values=values) # Check if there are undefined fields supplied to the constructor, # if so raise an Exception. if not self._dynamic and (self._meta.get("strict", True) or _created): _undefined_fields = set(values.keys()) - set( list(self._fields.keys()) + ["id", "pk", "_cls", "_text_score"] ) if _undefined_fields: msg = f'The fields "{_undefined_fields}" do not exist on the document "{self._class_name}"' raise FieldDoesNotExist(msg) if self.STRICT and not self._dynamic: self._data = StrictDict.create(allowed_keys=self._fields_ordered)() else: self._data = {} self._dynamic_fields = SON() # Assign default values for fields # not set in the constructor for field_name in self._fields: if field_name in values: continue value = getattr(self, field_name, None) setattr(self, field_name, value) if "_cls" not in values: self._cls = self._class_name # Set actual values dynamic_data = {} FileField = _import_class("FileField") for key, value in values.items(): field = self._fields.get(key) if field or key in ("id", "pk", "_cls"): if __auto_convert and value is not None: if field and not isinstance(field, FileField): value = field.to_python(value) setattr(self, key, value) else: if self._dynamic: dynamic_data[key] = value else: # For strict Document self._data[key] = value # Set any get__display methods self.__set_field_display() if self._dynamic: self._dynamic_lock = False for key, value in dynamic_data.items(): setattr(self, key, value) # Flag initialised self._initialised = True self._created = _created signals.post_init.send(self.__class__, document=self) def __delattr__(self, *args, **kwargs): """Handle deletions of fields""" field_name = args[0] if field_name in self._fields: default = self._fields[field_name].default if callable(default): default = default() setattr(self, field_name, default) else: super().__delattr__(*args, **kwargs) def __setattr__(self, name, value): # Handle dynamic data only if an initialised dynamic document if self._dynamic and not self._dynamic_lock: if name not in self._fields_ordered and not name.startswith("_"): DynamicField = _import_class("DynamicField") field = DynamicField(db_field=name, null=True) field.name = name self._dynamic_fields[name] = field self._fields_ordered += (name,) if not name.startswith("_"): value = self.__expand_dynamic_values(name, value) # Handle marking data as changed if name in self._dynamic_fields: self._data[name] = value if hasattr(self, "_changed_fields"): self._mark_as_changed(name) try: self__created = self._created except AttributeError: self__created = True if ( self._is_document and not self__created and name in self._meta.get("shard_key", tuple()) and self._data.get(name) != value ): msg = "Shard Keys are immutable. Tried to update %s" % name raise OperationError(msg) try: self__initialised = self._initialised except AttributeError: self__initialised = False # Check if the user has created a new instance of a class if ( self._is_document and self__initialised and self__created and name == self._meta.get("id_field") ): super().__setattr__("_created", False) super().__setattr__(name, value) def __getstate__(self): data = {} for k in ( "_changed_fields", "_initialised", "_created", "_dynamic_fields", "_fields_ordered", ): if hasattr(self, k): data[k] = getattr(self, k) data["_data"] = self.to_mongo() return data def __setstate__(self, data): if isinstance(data["_data"], SON): data["_data"] = self.__class__._from_son(data["_data"])._data for k in ( "_changed_fields", "_initialised", "_created", "_data", "_dynamic_fields", ): if k in data: setattr(self, k, data[k]) if "_fields_ordered" in data: if self._dynamic: self._fields_ordered = data["_fields_ordered"] else: _super_fields_ordered = type(self)._fields_ordered self._fields_ordered = _super_fields_ordered dynamic_fields = data.get("_dynamic_fields") or SON() for k in dynamic_fields.keys(): setattr(self, k, data["_data"].get(k)) def __iter__(self): return iter(self._fields_ordered) def __getitem__(self, name): """Dictionary-style field access, return a field's value if present.""" try: if name in self._fields_ordered: return getattr(self, name) except AttributeError: pass raise KeyError(name) def __setitem__(self, name, value): """Dictionary-style field access, set a field's value.""" # Ensure that the field exists before settings its value if not self._dynamic and name not in self._fields: raise KeyError(name) return setattr(self, name, value) def __contains__(self, name): try: val = getattr(self, name) return val is not None except AttributeError: return False def __len__(self): return len(self._data) def __repr__(self): try: u = self.__str__() except (UnicodeEncodeError, UnicodeDecodeError): u = "[Bad Unicode data]" repr_type = str if u is None else type(u) return repr_type(f"<{self.__class__.__name__}: {u}>") def __str__(self): # TODO this could be simpler? if hasattr(self, "__unicode__"): return self.__unicode__() return "%s object" % self.__class__.__name__ def __eq__(self, other): if ( isinstance(other, self.__class__) and hasattr(other, "id") and other.id is not None ): return self.id == other.id if isinstance(other, DBRef): return ( self._get_collection_name() == other.collection and self.id == other.id ) if self.id is None: return self is other return False def __ne__(self, other): return not self.__eq__(other) def clean(self): """ Hook for doing document level data cleaning (usually validation or assignment) before validation is run. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field defined by NON_FIELD_ERRORS. """ pass def get_text_score(self): """ Get text score from text query """ if "_text_score" not in self._data: raise InvalidDocumentError( "This document is not originally built from a text query" ) return self._data["_text_score"] def to_mongo(self, use_db_field=True, fields=None): """ Return as SON data ready for use with MongoDB. """ fields = fields or [] data = SON() data["_id"] = None data["_cls"] = self._class_name # only root fields ['test1.a', 'test2'] => ['test1', 'test2'] root_fields = {f.split(".")[0] for f in fields} for field_name in self: if root_fields and field_name not in root_fields: continue value = self._data.get(field_name, None) field = self._fields.get(field_name) if field is None and self._dynamic: field = self._dynamic_fields.get(field_name) if value is not None: f_inputs = field.to_mongo.__code__.co_varnames ex_vars = {} if fields and "fields" in f_inputs: key = "%s." % field_name embedded_fields = [ i.replace(key, "") for i in fields if i.startswith(key) ] ex_vars["fields"] = embedded_fields if "use_db_field" in f_inputs: ex_vars["use_db_field"] = use_db_field value = field.to_mongo(value, **ex_vars) # Handle self generating fields if value is None and field._auto_gen: value = field.generate() self._data[field_name] = value if value is not None or field.null: if use_db_field: data[field.db_field] = value else: data[field.name] = value # Only add _cls if allow_inheritance is True if not self._meta.get("allow_inheritance"): data.pop("_cls") return data def validate(self, clean=True): """Ensure that all fields' values are valid and that required fields are present. Raises :class:`ValidationError` if any of the fields' values are found to be invalid. """ # Ensure that each field is matched to a valid value errors = {} if clean: try: self.clean() except ValidationError as error: errors[NON_FIELD_ERRORS] = error # Get a list of tuples of field names and their current values fields = [ ( self._fields.get(name, self._dynamic_fields.get(name)), self._data.get(name), ) for name in self._fields_ordered ] EmbeddedDocumentField = _import_class("EmbeddedDocumentField") GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField") for field, value in fields: if value is not None: try: if isinstance( field, (EmbeddedDocumentField, GenericEmbeddedDocumentField) ): field._validate(value, clean=clean) else: field._validate(value) except ValidationError as error: errors[field.name] = error.errors or error except (ValueError, AttributeError, AssertionError) as error: errors[field.name] = error elif field.required and not getattr(field, "_auto_gen", False): errors[field.name] = ValidationError( "Field is required", field_name=field.name ) if errors: pk = "None" if hasattr(self, "pk"): pk = self.pk elif self._instance and hasattr(self._instance, "pk"): pk = self._instance.pk message = f"ValidationError ({self._class_name}:{pk}) " raise ValidationError(message, errors=errors) def to_json(self, *args, **kwargs): """Convert this document to JSON. :param use_db_field: Serialize field names as they appear in MongoDB (as opposed to attribute names on this document). Defaults to True. """ use_db_field = kwargs.pop("use_db_field", True) if "json_options" not in kwargs: warnings.warn( "No 'json_options' are specified! Falling back to " "LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. " "For use with other MongoDB drivers specify the UUID " "representation to use. This will be changed to " "uuid_representation=UNSPECIFIED in a future release.", DeprecationWarning, ) kwargs["json_options"] = LEGACY_JSON_OPTIONS return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) @classmethod def from_json(cls, json_data, created=False, **kwargs): """Converts json data to a Document instance :param str json_data: The json data to load into the Document :param bool created: Boolean defining whether to consider the newly instantiated document as brand new or as persisted already: * If True, consider the document as brand new, no matter what data it's loaded with (i.e. even if an ID is loaded). * If False and an ID is NOT provided, consider the document as brand new. * If False and an ID is provided, assume that the object has already been persisted (this has an impact on the subsequent call to .save()). * Defaults to ``False``. """ # TODO should `created` default to False? If the object already exists # in the DB, you would likely retrieve it from MongoDB itself through # a query, not load it from JSON data. if "json_options" not in kwargs: warnings.warn( "No 'json_options' are specified! Falling back to " "LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. " "For use with other MongoDB drivers specify the UUID " "representation to use. This will be changed to " "uuid_representation=UNSPECIFIED in a future release.", DeprecationWarning, ) kwargs["json_options"] = LEGACY_JSON_OPTIONS return cls._from_son(json_util.loads(json_data, **kwargs), created=created) def __expand_dynamic_values(self, name, value): """Expand any dynamic values to their correct types / values.""" if not isinstance(value, (dict, list, tuple)): return value # If the value is a dict with '_cls' in it, turn it into a document is_dict = isinstance(value, dict) if is_dict and "_cls" in value: cls = get_document(value["_cls"]) return cls(**value) if is_dict: value = {k: self.__expand_dynamic_values(k, v) for k, v in value.items()} else: value = [self.__expand_dynamic_values(name, v) for v in value] # Convert lists / values so we can watch for any changes on them EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField") if isinstance(value, (list, tuple)) and not isinstance(value, BaseList): if issubclass(type(self), EmbeddedDocumentListField): value = EmbeddedDocumentList(value, self, name) else: value = BaseList(value, self, name) elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, self, name) return value def _mark_as_changed(self, key): """Mark a key as explicitly changed by the user.""" if not hasattr(self, "_changed_fields"): return if "." in key: key, rest = key.split(".", 1) key = self._db_field_map.get(key, key) key = f"{key}.{rest}" else: key = self._db_field_map.get(key, key) if key not in self._changed_fields: levels, idx = key.split("."), 1 while idx <= len(levels): if ".".join(levels[:idx]) in self._changed_fields: break idx += 1 else: self._changed_fields.append(key) # remove lower level changed fields level = ".".join(levels[:idx]) + "." remove = self._changed_fields.remove for field in self._changed_fields[:]: if field.startswith(level): remove(field) def _clear_changed_fields(self): """Using _get_changed_fields iterate and remove any fields that are marked as changed. """ ReferenceField = _import_class("ReferenceField") GenericReferenceField = _import_class("GenericReferenceField") for changed in self._get_changed_fields(): parts = changed.split(".") data = self for part in parts: if isinstance(data, list): try: data = data[int(part)] except IndexError: data = None elif isinstance(data, dict): data = data.get(part, None) else: field_name = data._reverse_db_field_map.get(part, part) data = getattr(data, field_name, None) if not isinstance(data, LazyReference) and hasattr( data, "_changed_fields" ): if getattr(data, "_is_document", False): continue data._changed_fields = [] elif isinstance(data, (list, tuple, dict)): if hasattr(data, "field") and isinstance( data.field, (ReferenceField, GenericReferenceField) ): continue BaseDocument._nestable_types_clear_changed_fields(data) self._changed_fields = [] @staticmethod def _nestable_types_clear_changed_fields(data): """Inspect nested data for changed fields :param data: data to inspect for changes """ Document = _import_class("Document") # Loop list / dict fields as they contain documents # Determine the iterator to use if not hasattr(data, "items"): iterator = enumerate(data) else: iterator = data.items() for _index_or_key, value in iterator: if hasattr(value, "_get_changed_fields") and not isinstance( value, Document ): # don't follow references value._clear_changed_fields() elif isinstance(value, (list, tuple, dict)): BaseDocument._nestable_types_clear_changed_fields(value) @staticmethod def _nestable_types_changed_fields(changed_fields, base_key, data): """Inspect nested data for changed fields :param changed_fields: Previously collected changed fields :param base_key: The base key that must be used to prepend changes to this data :param data: data to inspect for changes """ # Loop list / dict fields as they contain documents # Determine the iterator to use if not hasattr(data, "items"): iterator = enumerate(data) else: iterator = data.items() for index_or_key, value in iterator: item_key = f"{base_key}{index_or_key}." # don't check anything lower if this key is already marked # as changed. if item_key[:-1] in changed_fields: continue if hasattr(value, "_get_changed_fields"): changed = value._get_changed_fields() changed_fields += [f"{item_key}{k}" for k in changed if k] elif isinstance(value, (list, tuple, dict)): BaseDocument._nestable_types_changed_fields( changed_fields, item_key, value ) def _get_changed_fields(self): """Return a list of all fields that have explicitly been changed.""" EmbeddedDocument = _import_class("EmbeddedDocument") LazyReferenceField = _import_class("LazyReferenceField") ReferenceField = _import_class("ReferenceField") GenericLazyReferenceField = _import_class("GenericLazyReferenceField") GenericReferenceField = _import_class("GenericReferenceField") SortedListField = _import_class("SortedListField") changed_fields = [] changed_fields += getattr(self, "_changed_fields", []) for field_name in self._fields_ordered: db_field_name = self._db_field_map.get(field_name, field_name) key = "%s." % db_field_name data = self._data.get(field_name, None) field = self._fields.get(field_name) if db_field_name in changed_fields: # Whole field already marked as changed, no need to go further continue if isinstance(field, ReferenceField): # Don't follow referenced documents continue if isinstance(data, EmbeddedDocument): # Find all embedded fields that have been changed changed = data._get_changed_fields() changed_fields += [f"{key}{k}" for k in changed if k] elif isinstance(data, (list, tuple, dict)): if hasattr(field, "field") and isinstance( field.field, ( LazyReferenceField, ReferenceField, GenericLazyReferenceField, GenericReferenceField, ), ): continue elif isinstance(field, SortedListField) and field._ordering: # if ordering is affected whole list is changed if any(field._ordering in d._changed_fields for d in data): changed_fields.append(db_field_name) continue self._nestable_types_changed_fields(changed_fields, key, data) return changed_fields def _delta(self): """Returns the delta (set, unset) of the changes for a document. Gets any values that have been explicitly changed. """ # Handles cases where not loaded from_son but has _id doc = self.to_mongo() set_fields = self._get_changed_fields() unset_data = {} if hasattr(self, "_changed_fields"): set_data = {} # Fetch each set item from its path for path in set_fields: parts = path.split(".") d = doc new_path = [] for p in parts: if isinstance(d, (ObjectId, DBRef)): # Don't dig in the references break elif isinstance(d, list) and p.isdigit(): # An item of a list (identified by its index) is updated d = d[int(p)] elif hasattr(d, "get"): # dict-like (dict, embedded document) d = d.get(p) new_path.append(p) path = ".".join(new_path) set_data[path] = d else: set_data = doc if "_id" in set_data: del set_data["_id"] # Determine if any changed items were actually unset. for path, value in list(set_data.items()): if value or isinstance( value, (numbers.Number, bool) ): # Account for 0 and True that are truthy continue parts = path.split(".") if self._dynamic and len(parts) and parts[0] in self._dynamic_fields: del set_data[path] unset_data[path] = 1 continue # If we've set a value that ain't the default value don't unset it. default = None if path in self._fields: default = self._fields[path].default else: # Perform a full lookup for lists / embedded lookups d = self db_field_name = parts.pop() for p in parts: if isinstance(d, list) and p.isdigit(): d = d[int(p)] elif hasattr(d, "__getattribute__") and not isinstance(d, dict): real_path = d._reverse_db_field_map.get(p, p) d = getattr(d, real_path) else: d = d.get(p) if hasattr(d, "_fields"): field_name = d._reverse_db_field_map.get( db_field_name, db_field_name ) if field_name in d._fields: default = d._fields.get(field_name).default else: default = None if default is not None: default = default() if callable(default) else default if value != default: continue del set_data[path] unset_data[path] = 1 return set_data, unset_data @classmethod def _get_collection_name(cls): """Return the collection name for this class. None for abstract class. """ return cls._meta.get("collection", None) @classmethod def _from_son(cls, son, _auto_dereference=True, created=False): """Create an instance of a Document (subclass) from a PyMongo SON (dict)""" if son and not isinstance(son, dict): raise ValueError( "The source SON object needs to be of type 'dict' but a '%s' was found" % type(son) ) # Get the class name from the document, falling back to the given # class if unavailable class_name = son.get("_cls", cls._class_name) # Convert SON to a data dict, making sure each key is a string and # corresponds to the right db field. # This is needed as _from_son is currently called both from BaseDocument.__init__ # and from EmbeddedDocumentField.to_python data = {} for key, value in son.items(): key = str(key) key = cls._db_field_map.get(key, key) data[key] = value # Return correct subclass for document type if class_name != cls._class_name: cls = get_document(class_name) errors_dict = {} fields = cls._fields if not _auto_dereference: fields = copy.deepcopy(fields) for field_name, field in fields.items(): field._auto_dereference = _auto_dereference if field.db_field in data: value = data[field.db_field] try: data[field_name] = ( value if value is None else field.to_python(value) ) if field_name != field.db_field: del data[field.db_field] except (AttributeError, ValueError) as e: errors_dict[field_name] = e if errors_dict: errors = "\n".join([f"Field '{k}' - {v}" for k, v in errors_dict.items()]) msg = "Invalid data to create a `{}` instance.\n{}".format( cls._class_name, errors, ) raise InvalidDocumentError(msg) # In STRICT documents, remove any keys that aren't in cls._fields if cls.STRICT: data = {k: v for k, v in data.items() if k in cls._fields} obj = cls(__auto_convert=False, _created=created, **data) obj._changed_fields = [] if not _auto_dereference: obj._fields = fields return obj @classmethod def _build_index_specs(cls, meta_indexes): """Generate and merge the full index specs.""" geo_indices = cls._geo_indices() unique_indices = cls._unique_with_indexes() index_specs = [cls._build_index_spec(spec) for spec in meta_indexes] def merge_index_specs(index_specs, indices): """Helper method for merging index specs.""" if not indices: return index_specs # Create a map of index fields to index spec. We're converting # the fields from a list to a tuple so that it's hashable. spec_fields = {tuple(index["fields"]): index for index in index_specs} # For each new index, if there's an existing index with the same # fields list, update the existing spec with all data from the # new spec. for new_index in indices: candidate = spec_fields.get(tuple(new_index["fields"])) if candidate is None: index_specs.append(new_index) else: candidate.update(new_index) return index_specs # Merge geo indexes and unique_with indexes into the meta index specs. index_specs = merge_index_specs(index_specs, geo_indices) index_specs = merge_index_specs(index_specs, unique_indices) return index_specs @classmethod def _build_index_spec(cls, spec): """Build a PyMongo index spec from a MongoEngine index spec.""" if isinstance(spec, str): spec = {"fields": [spec]} elif isinstance(spec, (list, tuple)): spec = {"fields": list(spec)} elif isinstance(spec, dict): spec = dict(spec) index_list = [] direction = None # Check to see if we need to include _cls allow_inheritance = cls._meta.get("allow_inheritance") include_cls = ( allow_inheritance and not spec.get("sparse", False) and spec.get("cls", True) and "_cls" not in spec["fields"] ) # 733: don't include cls if index_cls is False unless there is an explicit cls with the index include_cls = include_cls and ( spec.get("cls", False) or cls._meta.get("index_cls", True) ) if "cls" in spec: spec.pop("cls") for key in spec["fields"]: # If inherited spec continue if isinstance(key, (list, tuple)): continue # ASCENDING from + # DESCENDING from - # TEXT from $ # HASHED from # # GEOSPHERE from ( # GEOHAYSTACK from ) # GEO2D from * direction = pymongo.ASCENDING if key.startswith("-"): direction = pymongo.DESCENDING elif key.startswith("$"): direction = pymongo.TEXT elif key.startswith("#"): direction = pymongo.HASHED elif key.startswith("("): direction = pymongo.GEOSPHERE elif key.startswith(")"): try: direction = pymongo.GEOHAYSTACK except AttributeError: raise NotImplementedError elif key.startswith("*"): direction = pymongo.GEO2D if key.startswith(("+", "-", "*", "$", "#", "(", ")")): key = key[1:] # Use real field name, do it manually because we need field # objects for the next part (list field checking) parts = key.split(".") if parts in (["pk"], ["id"], ["_id"]): key = "_id" else: fields = cls._lookup_field(parts) parts = [] for field in fields: try: if field != "_id": field = field.db_field except AttributeError: pass parts.append(field) key = ".".join(parts) index_list.append((key, direction)) # Don't add cls to a geo index if ( include_cls and direction not in (pymongo.GEO2D, pymongo.GEOSPHERE) and (GEOHAYSTACK is None or direction != GEOHAYSTACK) ): index_list.insert(0, ("_cls", 1)) if index_list: spec["fields"] = index_list return spec @classmethod def _unique_with_indexes(cls, namespace=""): """Find unique indexes in the document schema and return them.""" unique_indexes = [] for field_name, field in cls._fields.items(): sparse = field.sparse # Generate a list of indexes needed by uniqueness constraints if field.unique: unique_fields = [field.db_field] # Add any unique_with fields to the back of the index spec if field.unique_with: if isinstance(field.unique_with, str): field.unique_with = [field.unique_with] # Convert unique_with field names to real field names unique_with = [] for other_name in field.unique_with: parts = other_name.split(".") # Lookup real name parts = cls._lookup_field(parts) name_parts = [part.db_field for part in parts] unique_with.append(".".join(name_parts)) # Unique field should be required parts[-1].required = True sparse = not sparse and parts[-1].name not in cls.__dict__ unique_fields += unique_with # Add the new index to the list fields = [(f"{namespace}{f}", pymongo.ASCENDING) for f in unique_fields] index = {"fields": fields, "unique": True, "sparse": sparse} unique_indexes.append(index) if field.__class__.__name__ in { "EmbeddedDocumentListField", "ListField", "SortedListField", }: field = field.field # Grab any embedded document field unique indexes if ( field.__class__.__name__ == "EmbeddedDocumentField" and field.document_type != cls ): field_namespace = "%s." % field_name doc_cls = field.document_type unique_indexes += doc_cls._unique_with_indexes(field_namespace) return unique_indexes @classmethod def _geo_indices(cls, inspected=None, parent_field=None): inspected = inspected or [] geo_indices = [] inspected.append(cls) geo_field_type_names = ( "EmbeddedDocumentField", "GeoPointField", "PointField", "LineStringField", "PolygonField", ) geo_field_types = tuple(_import_class(field) for field in geo_field_type_names) for field in cls._fields.values(): if not isinstance(field, geo_field_types): continue if hasattr(field, "document_type"): field_cls = field.document_type if field_cls in inspected: continue if hasattr(field_cls, "_geo_indices"): geo_indices += field_cls._geo_indices( inspected, parent_field=field.db_field ) elif field._geo_index: field_name = field.db_field if parent_field: field_name = f"{parent_field}.{field_name}" geo_indices.append({"fields": [(field_name, field._geo_index)]}) return geo_indices @classmethod def _lookup_field(cls, parts): """Given the path to a given field, return a list containing the Field object associated with that field and all of its parent Field objects. Args: parts (str, list, or tuple) - path to the field. Should be a string for simple fields existing on this document or a list of strings for a field that exists deeper in embedded documents. Returns: A list of Field instances for fields that were found or strings for sub-fields that weren't. Example: >>> user._lookup_field('name') [] >>> user._lookup_field('roles') [] >>> user._lookup_field(['roles', 'role']) [, ] >>> user._lookup_field('doesnt_exist') raises LookUpError >>> user._lookup_field(['roles', 'doesnt_exist']) [, 'doesnt_exist'] """ # TODO this method is WAY too complicated. Simplify it. # TODO don't think returning a string for embedded non-existent fields is desired ListField = _import_class("ListField") DynamicField = _import_class("DynamicField") if not isinstance(parts, (list, tuple)): parts = [parts] fields = [] field = None for field_name in parts: # Handle ListField indexing: if field_name.isdigit() and isinstance(field, ListField): fields.append(field_name) continue # Look up first field from the document if field is None: if field_name == "pk": # Deal with "primary key" alias field_name = cls._meta["id_field"] if field_name in cls._fields: field = cls._fields[field_name] elif cls._dynamic: field = DynamicField(db_field=field_name) elif cls._meta.get("allow_inheritance") or cls._meta.get( "abstract", False ): # 744: in case the field is defined in a subclass for subcls in cls.__subclasses__(): try: field = subcls._lookup_field([field_name])[0] except LookUpError: continue if field is not None: break else: raise LookUpError('Cannot resolve field "%s"' % field_name) else: raise LookUpError('Cannot resolve field "%s"' % field_name) else: ReferenceField = _import_class("ReferenceField") GenericReferenceField = _import_class("GenericReferenceField") # If previous field was a reference, throw an error (we # cannot look up fields that are on references). if isinstance(field, (ReferenceField, GenericReferenceField)): raise LookUpError( "Cannot perform join in mongoDB: %s" % "__".join(parts) ) # If the parent field has a "field" attribute which has a # lookup_member method, call it to find the field # corresponding to this iteration. if hasattr(getattr(field, "field", None), "lookup_member"): new_field = field.field.lookup_member(field_name) # If the parent field is a DynamicField or if it's part of # a DynamicDocument, mark current field as a DynamicField # with db_name equal to the field name. elif cls._dynamic and ( isinstance(field, DynamicField) or getattr(getattr(field, "document_type", None), "_dynamic", None) ): new_field = DynamicField(db_field=field_name) # Else, try to use the parent field's lookup_member method # to find the subfield. elif hasattr(field, "lookup_member"): new_field = field.lookup_member(field_name) # Raise a LookUpError if all the other conditions failed. else: raise LookUpError( "Cannot resolve subfield or operator {} " "on the field {}".format(field_name, field.name) ) # If current field still wasn't found and the parent field # is a ComplexBaseField, add the name current field name and # move on. if not new_field and isinstance(field, ComplexBaseField): fields.append(field_name) continue elif not new_field: raise LookUpError('Cannot resolve field "%s"' % field_name) field = new_field # update field to the new field type fields.append(field) return fields @classmethod def _translate_field_name(cls, field, sep="."): """Translate a field attribute name to a database field name.""" parts = field.split(sep) parts = [f.db_field for f in cls._lookup_field(parts)] return ".".join(parts) def __set_field_display(self): """For each field that specifies choices, create a get__display method. """ fields_with_choices = [(n, f) for n, f in self._fields.items() if f.choices] for attr_name, field in fields_with_choices: setattr( self, "get_%s_display" % attr_name, partial(self.__get_field_display, field=field), ) def __get_field_display(self, field): """Return the display value for a choice field""" value = getattr(self, field.name) if field.choices and isinstance(field.choices[0], (list, tuple)): if value is None: return None sep = getattr(field, "display_sep", " ") values = ( value if field.__class__.__name__ in ("ListField", "SortedListField") else [value] ) return sep.join( [str(dict(field.choices).get(val, val)) for val in values or []] ) return value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/fields.py0000644000175100001730000006404714400345475020566 0ustar00runnerdockerimport operator import weakref import pymongo from bson import SON, DBRef, ObjectId from mongoengine.base.common import UPDATE_OPERATORS from mongoengine.base.datastructures import ( BaseDict, BaseList, EmbeddedDocumentList, ) from mongoengine.common import _import_class from mongoengine.errors import DeprecatedError, ValidationError __all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField") class BaseField: """A base class for fields in a MongoDB document. Instances of this class may be added to subclasses of `Document` to define a document's schema. """ name = None # set in TopLevelDocumentMetaclass _geo_index = False _auto_gen = False # Call `generate` to generate a value _auto_dereference = True # These track each time a Field instance is created. Used to retain order. # The auto_creation_counter is used for fields that MongoEngine implicitly # creates, creation_counter is used for all user-specified fields. creation_counter = 0 auto_creation_counter = -1 def __init__( self, db_field=None, required=False, default=None, unique=False, unique_with=None, primary_key=False, validation=None, choices=None, null=False, sparse=False, **kwargs, ): """ :param db_field: The database field to store this field in (defaults to the name of the field) :param required: If the field is required. Whether it has to have a value or not. Defaults to False. :param default: (optional) The default value for this field if no value has been set, if the value is set to None or has been unset. It can be a callable. :param unique: Is the field value unique or not (Creates an index). Defaults to False. :param unique_with: (optional) The other field this field should be unique with (Creates an index). :param primary_key: Mark this field as the primary key ((Creates an index)). Defaults to False. :param validation: (optional) A callable to validate the value of the field. The callable takes the value as parameter and should raise a ValidationError if validation fails :param choices: (optional) The valid choices :param null: (optional) If the field value can be null when a default exist. If not set, the default value will be used in case a field with a default value is set to None. Defaults to False. :param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False` means that uniqueness won't be enforced for `None` values (Creates an index). Defaults to False. :param **kwargs: (optional) Arbitrary indirection-free metadata for this field can be supplied as additional keyword arguments and accessed as attributes of the field. Must not conflict with any existing attributes. Common metadata includes `verbose_name` and `help_text`. """ self.db_field = db_field if not primary_key else "_id" self.required = required or primary_key self.default = default self.unique = bool(unique or unique_with) self.unique_with = unique_with self.primary_key = primary_key self.validation = validation self.choices = choices self.null = null self.sparse = sparse self._owner_document = None # Make sure db_field is a string (if it's explicitly defined). if self.db_field is not None and not isinstance(self.db_field, str): raise TypeError("db_field should be a string.") # Make sure db_field doesn't contain any forbidden characters. if isinstance(self.db_field, str) and ( "." in self.db_field or "\0" in self.db_field or self.db_field.startswith("$") ): raise ValueError( 'field names cannot contain dots (".") or null characters ' '("\\0"), and they must not start with a dollar sign ("$").' ) # Detect and report conflicts between metadata and base properties. conflicts = set(dir(self)) & set(kwargs) if conflicts: raise TypeError( "%s already has attribute(s): %s" % (self.__class__.__name__, ", ".join(conflicts)) ) # Assign metadata to the instance # This efficient method is available because no __slots__ are defined. self.__dict__.update(kwargs) # Adjust the appropriate creation counter, and save our local copy. if self.db_field == "_id": self.creation_counter = BaseField.auto_creation_counter BaseField.auto_creation_counter -= 1 else: self.creation_counter = BaseField.creation_counter BaseField.creation_counter += 1 def __get__(self, instance, owner): """Descriptor for retrieving a value from a field in a document.""" if instance is None: # Document class being used rather than a document object return self # Get value from document instance if available return instance._data.get(self.name) def __set__(self, instance, value): """Descriptor for assigning a value to a field in a document.""" # If setting to None and there is a default value provided for this # field, then set the value to the default value. if value is None: if self.null: value = None elif self.default is not None: value = self.default if callable(value): value = value() if instance._initialised: try: value_has_changed = ( self.name not in instance._data or instance._data[self.name] != value ) if value_has_changed: instance._mark_as_changed(self.name) except Exception: # Some values can't be compared and throw an error when we # attempt to do so (e.g. tz-naive and tz-aware datetimes). # Mark the field as changed in such cases. instance._mark_as_changed(self.name) EmbeddedDocument = _import_class("EmbeddedDocument") if isinstance(value, EmbeddedDocument): value._instance = weakref.proxy(instance) elif isinstance(value, (list, tuple)): for v in value: if isinstance(v, EmbeddedDocument): v._instance = weakref.proxy(instance) instance._data[self.name] = value def error(self, message="", errors=None, field_name=None): """Raise a ValidationError.""" field_name = field_name if field_name else self.name raise ValidationError(message, errors=errors, field_name=field_name) def to_python(self, value): """Convert a MongoDB-compatible type to a Python type.""" return value def to_mongo(self, value): """Convert a Python type to a MongoDB-compatible type.""" return self.to_python(value) def _to_mongo_safe_call(self, value, use_db_field=True, fields=None): """Helper method to call to_mongo with proper inputs.""" f_inputs = self.to_mongo.__code__.co_varnames ex_vars = {} if "fields" in f_inputs: ex_vars["fields"] = fields if "use_db_field" in f_inputs: ex_vars["use_db_field"] = use_db_field return self.to_mongo(value, **ex_vars) def prepare_query_value(self, op, value): """Prepare a value that is being used in a query for PyMongo.""" if op in UPDATE_OPERATORS: self.validate(value) return value def validate(self, value, clean=True): """Perform validation on a value.""" pass def _validate_choices(self, value): Document = _import_class("Document") EmbeddedDocument = _import_class("EmbeddedDocument") choice_list = self.choices if isinstance(next(iter(choice_list)), (list, tuple)): # next(iter) is useful for sets choice_list = [k for k, _ in choice_list] # Choices which are other types of Documents if isinstance(value, (Document, EmbeddedDocument)): if not any(isinstance(value, c) for c in choice_list): self.error("Value must be an instance of %s" % (choice_list)) # Choices which are types other than Documents else: values = value if isinstance(value, (list, tuple)) else [value] if len(set(values) - set(choice_list)): self.error("Value must be one of %s" % str(choice_list)) def _validate(self, value, **kwargs): # Check the Choices Constraint if self.choices: self._validate_choices(value) # check validation argument if self.validation is not None: if callable(self.validation): try: # breaking change of 0.18 # Get rid of True/False-type return for the validation method # in favor of having validation raising a ValidationError ret = self.validation(value) if ret is not None: raise DeprecatedError( "validation argument for `%s` must not return anything, " "it should raise a ValidationError if validation fails" % self.name ) except ValidationError as ex: self.error(str(ex)) else: raise ValueError( 'validation argument for `"%s"` must be a ' "callable." % self.name ) self.validate(value, **kwargs) @property def owner_document(self): return self._owner_document def _set_owner_document(self, owner_document): self._owner_document = owner_document @owner_document.setter def owner_document(self, owner_document): self._set_owner_document(owner_document) class ComplexBaseField(BaseField): """Handles complex fields, such as lists / dictionaries. Allows for nesting of embedded documents inside complex types. Handles the lazy dereferencing of a queryset by lazily dereferencing all items in a list / dict rather than one at a time. """ def __init__(self, field=None, **kwargs): self.field = field super().__init__(**kwargs) @staticmethod def _lazy_load_refs(instance, name, ref_values, *, max_depth): _dereference = _import_class("DeReference")() documents = _dereference( ref_values, max_depth=max_depth, instance=instance, name=name, ) return documents def __set__(self, instance, value): # Some fields e.g EnumField are converted upon __set__ # So it is fair to mimic the same behavior when using e.g ListField(EnumField) EnumField = _import_class("EnumField") if self.field and isinstance(self.field, EnumField): if isinstance(value, (list, tuple)): value = [self.field.to_python(sub_val) for sub_val in value] elif isinstance(value, dict): value = {key: self.field.to_python(sub) for key, sub in value.items()} return super().__set__(instance, value) def __get__(self, instance, owner): """Descriptor to automatically dereference references.""" if instance is None: # Document class being used rather than a document object return self ReferenceField = _import_class("ReferenceField") GenericReferenceField = _import_class("GenericReferenceField") EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField") auto_dereference = instance._fields[self.name]._auto_dereference dereference = auto_dereference and ( self.field is None or isinstance(self.field, (GenericReferenceField, ReferenceField)) ) if ( instance._initialised and dereference and instance._data.get(self.name) and not getattr(instance._data[self.name], "_dereferenced", False) ): ref_values = instance._data.get(self.name) instance._data[self.name] = self._lazy_load_refs( ref_values=ref_values, instance=instance, name=self.name, max_depth=1 ) if hasattr(instance._data[self.name], "_dereferenced"): instance._data[self.name]._dereferenced = True value = super().__get__(instance, owner) # Convert lists / values so we can watch for any changes on them if isinstance(value, (list, tuple)): if issubclass(type(self), EmbeddedDocumentListField) and not isinstance( value, EmbeddedDocumentList ): value = EmbeddedDocumentList(value, instance, self.name) elif not isinstance(value, BaseList): value = BaseList(value, instance, self.name) instance._data[self.name] = value elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, instance, self.name) instance._data[self.name] = value if ( auto_dereference and instance._initialised and isinstance(value, (BaseList, BaseDict)) and not value._dereferenced ): value = self._lazy_load_refs( ref_values=value, instance=instance, name=self.name, max_depth=1 ) value._dereferenced = True instance._data[self.name] = value return value def to_python(self, value): """Convert a MongoDB-compatible type to a Python type.""" if isinstance(value, str): return value if hasattr(value, "to_python"): return value.to_python() BaseDocument = _import_class("BaseDocument") if isinstance(value, BaseDocument): # Something is wrong, return the value as it is return value is_list = False if not hasattr(value, "items"): try: is_list = True value = {idx: v for idx, v in enumerate(value)} except TypeError: # Not iterable return the value return value if self.field: self.field._auto_dereference = self._auto_dereference value_dict = { key: self.field.to_python(item) for key, item in value.items() } else: Document = _import_class("Document") value_dict = {} for k, v in value.items(): if isinstance(v, Document): # We need the id from the saved object to create the DBRef if v.pk is None: self.error( "You can only reference documents once they" " have been saved to the database" ) collection = v._get_collection_name() value_dict[k] = DBRef(collection, v.pk) elif hasattr(v, "to_python"): value_dict[k] = v.to_python() else: value_dict[k] = self.to_python(v) if is_list: # Convert back to a list return [ v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0)) ] return value_dict def to_mongo(self, value, use_db_field=True, fields=None): """Convert a Python type to a MongoDB-compatible type.""" Document = _import_class("Document") EmbeddedDocument = _import_class("EmbeddedDocument") GenericReferenceField = _import_class("GenericReferenceField") if isinstance(value, str): return value if hasattr(value, "to_mongo"): if isinstance(value, Document): return GenericReferenceField().to_mongo(value) cls = value.__class__ val = value.to_mongo(use_db_field, fields) # If it's a document that is not inherited add _cls if isinstance(value, EmbeddedDocument): val["_cls"] = cls.__name__ return val is_list = False if not hasattr(value, "items"): try: is_list = True value = {k: v for k, v in enumerate(value)} except TypeError: # Not iterable return the value return value if self.field: value_dict = { key: self.field._to_mongo_safe_call(item, use_db_field, fields) for key, item in value.items() } else: value_dict = {} for k, v in value.items(): if isinstance(v, Document): # We need the id from the saved object to create the DBRef if v.pk is None: self.error( "You can only reference documents once they" " have been saved to the database" ) # If it's a document that is not inheritable it won't have # any _cls data so make it a generic reference allows # us to dereference meta = getattr(v, "_meta", {}) allow_inheritance = meta.get("allow_inheritance") if not allow_inheritance: value_dict[k] = GenericReferenceField().to_mongo(v) else: collection = v._get_collection_name() value_dict[k] = DBRef(collection, v.pk) elif hasattr(v, "to_mongo"): cls = v.__class__ val = v.to_mongo(use_db_field, fields) # If it's a document that is not inherited add _cls if isinstance(v, (Document, EmbeddedDocument)): val["_cls"] = cls.__name__ value_dict[k] = val else: value_dict[k] = self.to_mongo(v, use_db_field, fields) if is_list: # Convert back to a list return [ v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0)) ] return value_dict def validate(self, value): """If field is provided ensure the value is valid.""" errors = {} if self.field: if hasattr(value, "items"): sequence = value.items() else: sequence = enumerate(value) for k, v in sequence: try: self.field._validate(v) except ValidationError as error: errors[k] = error.errors or error except (ValueError, AssertionError) as error: errors[k] = error if errors: field_class = self.field.__class__.__name__ self.error(f"Invalid {field_class} item ({value})", errors=errors) # Don't allow empty values if required if self.required and not value: self.error("Field is required and cannot be empty") def prepare_query_value(self, op, value): return self.to_mongo(value) def lookup_member(self, member_name): if self.field: return self.field.lookup_member(member_name) return None def _set_owner_document(self, owner_document): if self.field: self.field.owner_document = owner_document self._owner_document = owner_document class ObjectIdField(BaseField): """A field wrapper around MongoDB's ObjectIds.""" def to_python(self, value): try: if not isinstance(value, ObjectId): value = ObjectId(value) except Exception: pass return value def to_mongo(self, value): if isinstance(value, ObjectId): return value try: return ObjectId(str(value)) except Exception as e: self.error(str(e)) def prepare_query_value(self, op, value): if value is None: return value return self.to_mongo(value) def validate(self, value): try: ObjectId(str(value)) except Exception: self.error("Invalid ObjectID") class GeoJsonBaseField(BaseField): """A geo json field storing a geojson style object.""" _geo_index = pymongo.GEOSPHERE _type = "GeoBase" def __init__(self, auto_index=True, *args, **kwargs): """ :param bool auto_index: Automatically create a '2dsphere' index.\ Defaults to `True`. """ self._name = "%sField" % self._type if not auto_index: self._geo_index = False super().__init__(*args, **kwargs) def validate(self, value): """Validate the GeoJson object based on its type.""" if isinstance(value, dict): if set(value.keys()) == {"type", "coordinates"}: if value["type"] != self._type: self.error(f'{self._name} type must be "{self._type}"') return self.validate(value["coordinates"]) else: self.error( "%s can only accept a valid GeoJson dictionary" " or lists of (x, y)" % self._name ) return elif not isinstance(value, (list, tuple)): self.error("%s can only accept lists of [x, y]" % self._name) return validate = getattr(self, "_validate_%s" % self._type.lower()) error = validate(value) if error: self.error(error) def _validate_polygon(self, value, top_level=True): if not isinstance(value, (list, tuple)): return "Polygons must contain list of linestrings" # Quick and dirty validator try: value[0][0][0] except (TypeError, IndexError): return "Invalid Polygon must contain at least one valid linestring" errors = [] for val in value: error = self._validate_linestring(val, False) if not error and val[0] != val[-1]: error = "LineStrings must start and end at the same point" if error and error not in errors: errors.append(error) if errors: if top_level: return "Invalid Polygon:\n%s" % ", ".join(errors) else: return "%s" % ", ".join(errors) def _validate_linestring(self, value, top_level=True): """Validate a linestring.""" if not isinstance(value, (list, tuple)): return "LineStrings must contain list of coordinate pairs" # Quick and dirty validator try: value[0][0] except (TypeError, IndexError): return "Invalid LineString must contain at least one valid point" errors = [] for val in value: error = self._validate_point(val) if error and error not in errors: errors.append(error) if errors: if top_level: return "Invalid LineString:\n%s" % ", ".join(errors) else: return "%s" % ", ".join(errors) def _validate_point(self, value): """Validate each set of coords""" if not isinstance(value, (list, tuple)): return "Points must be a list of coordinate pairs" elif not len(value) == 2: return "Value (%s) must be a two-dimensional point" % repr(value) elif not isinstance(value[0], (float, int)) or not isinstance( value[1], (float, int) ): return "Both values (%s) in point must be float or int" % repr(value) def _validate_multipoint(self, value): if not isinstance(value, (list, tuple)): return "MultiPoint must be a list of Point" # Quick and dirty validator try: value[0][0] except (TypeError, IndexError): return "Invalid MultiPoint must contain at least one valid point" errors = [] for point in value: error = self._validate_point(point) if error and error not in errors: errors.append(error) if errors: return "%s" % ", ".join(errors) def _validate_multilinestring(self, value, top_level=True): if not isinstance(value, (list, tuple)): return "MultiLineString must be a list of LineString" # Quick and dirty validator try: value[0][0][0] except (TypeError, IndexError): return "Invalid MultiLineString must contain at least one valid linestring" errors = [] for linestring in value: error = self._validate_linestring(linestring, False) if error and error not in errors: errors.append(error) if errors: if top_level: return "Invalid MultiLineString:\n%s" % ", ".join(errors) else: return "%s" % ", ".join(errors) def _validate_multipolygon(self, value): if not isinstance(value, (list, tuple)): return "MultiPolygon must be a list of Polygon" # Quick and dirty validator try: value[0][0][0][0] except (TypeError, IndexError): return "Invalid MultiPolygon must contain at least one valid Polygon" errors = [] for polygon in value: error = self._validate_polygon(polygon, False) if error and error not in errors: errors.append(error) if errors: return "Invalid MultiPolygon:\n%s" % ", ".join(errors) def to_mongo(self, value): if isinstance(value, dict): return value return SON([("type", self._type), ("coordinates", value)]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/metaclasses.py0000644000175100001730000004331614400345475021620 0ustar00runnerdockerimport itertools import warnings from mongoengine.base.common import _document_registry from mongoengine.base.fields import ( BaseField, ComplexBaseField, ObjectIdField, ) from mongoengine.common import _import_class from mongoengine.errors import InvalidDocumentError from mongoengine.queryset import ( DO_NOTHING, DoesNotExist, MultipleObjectsReturned, QuerySetManager, ) __all__ = ("DocumentMetaclass", "TopLevelDocumentMetaclass") class DocumentMetaclass(type): """Metaclass for all documents.""" # TODO lower complexity of this method def __new__(mcs, name, bases, attrs): flattened_bases = mcs._get_bases(bases) super_new = super().__new__ # If a base class just call super metaclass = attrs.get("my_metaclass") if metaclass and issubclass(metaclass, DocumentMetaclass): return super_new(mcs, name, bases, attrs) attrs["_is_document"] = attrs.get("_is_document", False) attrs["_cached_reference_fields"] = [] # EmbeddedDocuments could have meta data for inheritance if "meta" in attrs: attrs["_meta"] = attrs.pop("meta") # EmbeddedDocuments should inherit meta data if "_meta" not in attrs: meta = MetaDict() for base in flattened_bases[::-1]: # Add any mixin metadata from plain objects if hasattr(base, "meta"): meta.merge(base.meta) elif hasattr(base, "_meta"): meta.merge(base._meta) attrs["_meta"] = meta attrs["_meta"][ "abstract" ] = False # 789: EmbeddedDocument shouldn't inherit abstract # If allow_inheritance is True, add a "_cls" string field to the attrs if attrs["_meta"].get("allow_inheritance"): StringField = _import_class("StringField") attrs["_cls"] = StringField() # Handle document Fields # Merge all fields from subclasses doc_fields = {} for base in flattened_bases[::-1]: if hasattr(base, "_fields"): doc_fields.update(base._fields) # Standard object mixin - merge in any Fields if not hasattr(base, "_meta"): base_fields = {} for attr_name, attr_value in base.__dict__.items(): if not isinstance(attr_value, BaseField): continue attr_value.name = attr_name if not attr_value.db_field: attr_value.db_field = attr_name base_fields[attr_name] = attr_value doc_fields.update(base_fields) # Discover any document fields field_names = {} for attr_name, attr_value in attrs.items(): if not isinstance(attr_value, BaseField): continue attr_value.name = attr_name if not attr_value.db_field: attr_value.db_field = attr_name doc_fields[attr_name] = attr_value # Count names to ensure no db_field redefinitions field_names[attr_value.db_field] = ( field_names.get(attr_value.db_field, 0) + 1 ) # Ensure no duplicate db_fields duplicate_db_fields = [k for k, v in field_names.items() if v > 1] if duplicate_db_fields: msg = "Multiple db_fields defined for: %s " % ", ".join(duplicate_db_fields) raise InvalidDocumentError(msg) # Set _fields and db_field maps attrs["_fields"] = doc_fields attrs["_db_field_map"] = { k: getattr(v, "db_field", k) for k, v in doc_fields.items() } attrs["_reverse_db_field_map"] = { v: k for k, v in attrs["_db_field_map"].items() } attrs["_fields_ordered"] = tuple( i[1] for i in sorted((v.creation_counter, v.name) for v in doc_fields.values()) ) # # Set document hierarchy # superclasses = () class_name = [name] for base in flattened_bases: if not getattr(base, "_is_base_cls", True) and not getattr( base, "_meta", {} ).get("abstract", True): # Collate hierarchy for _cls and _subclasses class_name.append(base.__name__) if hasattr(base, "_meta"): # Warn if allow_inheritance isn't set and prevent # inheritance of classes where inheritance is set to False allow_inheritance = base._meta.get("allow_inheritance") if not allow_inheritance and not base._meta.get("abstract"): raise ValueError( "Document %s may not be subclassed. " 'To enable inheritance, use the "allow_inheritance" meta attribute.' % base.__name__ ) # Get superclasses from last base superclass document_bases = [b for b in flattened_bases if hasattr(b, "_class_name")] if document_bases: superclasses = document_bases[0]._superclasses superclasses += (document_bases[0]._class_name,) _cls = ".".join(reversed(class_name)) attrs["_class_name"] = _cls attrs["_superclasses"] = superclasses attrs["_subclasses"] = (_cls,) attrs["_types"] = attrs["_subclasses"] # TODO depreciate _types # Create the new_class new_class = super_new(mcs, name, bases, attrs) # Set _subclasses for base in document_bases: if _cls not in base._subclasses: base._subclasses += (_cls,) base._types = base._subclasses # TODO depreciate _types ( Document, EmbeddedDocument, DictField, CachedReferenceField, ) = mcs._import_classes() if issubclass(new_class, Document): new_class._collection = None # Add class to the _document_registry _document_registry[new_class._class_name] = new_class # Handle delete rules for field in new_class._fields.values(): f = field if f.owner_document is None: f.owner_document = new_class delete_rule = getattr(f, "reverse_delete_rule", DO_NOTHING) if isinstance(f, CachedReferenceField): if issubclass(new_class, EmbeddedDocument): raise InvalidDocumentError( "CachedReferenceFields is not allowed in EmbeddedDocuments" ) if f.auto_sync: f.start_listener() f.document_type._cached_reference_fields.append(f) if isinstance(f, ComplexBaseField) and hasattr(f, "field"): delete_rule = getattr(f.field, "reverse_delete_rule", DO_NOTHING) if isinstance(f, DictField) and delete_rule != DO_NOTHING: msg = ( "Reverse delete rules are not supported " "for %s (field: %s)" % (field.__class__.__name__, field.name) ) raise InvalidDocumentError(msg) f = field.field if delete_rule != DO_NOTHING: if issubclass(new_class, EmbeddedDocument): msg = ( "Reverse delete rules are not supported for " "EmbeddedDocuments (field: %s)" % field.name ) raise InvalidDocumentError(msg) f.document_type.register_delete_rule(new_class, field.name, delete_rule) if ( field.name and hasattr(Document, field.name) and EmbeddedDocument not in new_class.mro() ): msg = "%s is a document method and not a valid field name" % field.name raise InvalidDocumentError(msg) return new_class @classmethod def _get_bases(mcs, bases): if isinstance(bases, BasesTuple): return bases seen = [] bases = mcs.__get_bases(bases) unique_bases = (b for b in bases if not (b in seen or seen.append(b))) return BasesTuple(unique_bases) @classmethod def __get_bases(mcs, bases): for base in bases: if base is object: continue yield base yield from mcs.__get_bases(base.__bases__) @classmethod def _import_classes(mcs): Document = _import_class("Document") EmbeddedDocument = _import_class("EmbeddedDocument") DictField = _import_class("DictField") CachedReferenceField = _import_class("CachedReferenceField") return Document, EmbeddedDocument, DictField, CachedReferenceField class TopLevelDocumentMetaclass(DocumentMetaclass): """Metaclass for top-level documents (i.e. documents that have their own collection in the database. """ def __new__(mcs, name, bases, attrs): flattened_bases = mcs._get_bases(bases) super_new = super().__new__ # Set default _meta data if base class, otherwise get user defined meta if attrs.get("my_metaclass") == TopLevelDocumentMetaclass: # defaults attrs["_meta"] = { "abstract": True, "max_documents": None, "max_size": None, "ordering": [], # default ordering applied at runtime "indexes": [], # indexes to be ensured at runtime "id_field": None, "index_background": False, "index_opts": None, "delete_rules": None, # allow_inheritance can be True, False, and None. True means # "allow inheritance", False means "don't allow inheritance", # None means "do whatever your parent does, or don't allow # inheritance if you're a top-level class". "allow_inheritance": None, } attrs["_is_base_cls"] = True attrs["_meta"].update(attrs.get("meta", {})) else: attrs["_meta"] = attrs.get("meta", {}) # Explicitly set abstract to false unless set attrs["_meta"]["abstract"] = attrs["_meta"].get("abstract", False) attrs["_is_base_cls"] = False # Set flag marking as document class - as opposed to an object mixin attrs["_is_document"] = True # Ensure queryset_class is inherited if "objects" in attrs: manager = attrs["objects"] if hasattr(manager, "queryset_class"): attrs["_meta"]["queryset_class"] = manager.queryset_class # Clean up top level meta if "meta" in attrs: del attrs["meta"] # Find the parent document class parent_doc_cls = [ b for b in flattened_bases if b.__class__ == TopLevelDocumentMetaclass ] parent_doc_cls = None if not parent_doc_cls else parent_doc_cls[0] # Prevent classes setting collection different to their parents # If parent wasn't an abstract class if ( parent_doc_cls and "collection" in attrs.get("_meta", {}) and not parent_doc_cls._meta.get("abstract", True) ): msg = "Trying to set a collection on a subclass (%s)" % name warnings.warn(msg, SyntaxWarning) del attrs["_meta"]["collection"] # Ensure abstract documents have abstract bases if attrs.get("_is_base_cls") or attrs["_meta"].get("abstract"): if parent_doc_cls and not parent_doc_cls._meta.get("abstract", False): msg = "Abstract document cannot have non-abstract base" raise ValueError(msg) return super_new(mcs, name, bases, attrs) # Merge base class metas. # Uses a special MetaDict that handles various merging rules meta = MetaDict() for base in flattened_bases[::-1]: # Add any mixin metadata from plain objects if hasattr(base, "meta"): meta.merge(base.meta) elif hasattr(base, "_meta"): meta.merge(base._meta) # Set collection in the meta if its callable if getattr(base, "_is_document", False) and not base._meta.get("abstract"): collection = meta.get("collection", None) if callable(collection): meta["collection"] = collection(base) meta.merge(attrs.get("_meta", {})) # Top level meta # Only simple classes (i.e. direct subclasses of Document) may set # allow_inheritance to False. If the base Document allows inheritance, # none of its subclasses can override allow_inheritance to False. simple_class = all( b._meta.get("abstract") for b in flattened_bases if hasattr(b, "_meta") ) if ( not simple_class and meta["allow_inheritance"] is False and not meta["abstract"] ): raise ValueError( "Only direct subclasses of Document may set " '"allow_inheritance" to False' ) # Set default collection name if "collection" not in meta: meta["collection"] = ( "".join("_%s" % c if c.isupper() else c for c in name) .strip("_") .lower() ) attrs["_meta"] = meta # Call super and get the new class new_class = super_new(mcs, name, bases, attrs) meta = new_class._meta # Set index specifications meta["index_specs"] = new_class._build_index_specs(meta["indexes"]) # If collection is a callable - call it and set the value collection = meta.get("collection") if callable(collection): new_class._meta["collection"] = collection(new_class) # Provide a default queryset unless exists or one has been set if "objects" not in dir(new_class): new_class.objects = QuerySetManager() # Validate the fields and set primary key if needed for field_name, field in new_class._fields.items(): if field.primary_key: # Ensure only one primary key is set current_pk = new_class._meta.get("id_field") if current_pk and current_pk != field_name: raise ValueError("Cannot override primary key field") # Set primary key if not current_pk: new_class._meta["id_field"] = field_name new_class.id = field # If the document doesn't explicitly define a primary key field, create # one. Make it an ObjectIdField and give it a non-clashing name ("id" # by default, but can be different if that one's taken). if not new_class._meta.get("id_field"): id_name, id_db_name = mcs.get_auto_id_names(new_class) new_class._meta["id_field"] = id_name new_class._fields[id_name] = ObjectIdField(db_field=id_db_name) new_class._fields[id_name].name = id_name new_class.id = new_class._fields[id_name] new_class._db_field_map[id_name] = id_db_name new_class._reverse_db_field_map[id_db_name] = id_name # Prepend the ID field to _fields_ordered (so that it's *always* # the first field). new_class._fields_ordered = (id_name,) + new_class._fields_ordered # Merge in exceptions with parent hierarchy. exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned) module = attrs.get("__module__") for exc in exceptions_to_merge: name = exc.__name__ parents = tuple( getattr(base, name) for base in flattened_bases if hasattr(base, name) ) or (exc,) # Create a new exception and set it as an attribute on the new # class. exception = type(name, parents, {"__module__": module}) setattr(new_class, name, exception) return new_class @classmethod def get_auto_id_names(mcs, new_class): """Find a name for the automatic ID field for the given new class. Return a two-element tuple where the first item is the field name (i.e. the attribute name on the object) and the second element is the DB field name (i.e. the name of the key stored in MongoDB). Defaults to ('id', '_id'), or generates a non-clashing name in the form of ('auto_id_X', '_auto_id_X') if the default name is already taken. """ id_name, id_db_name = ("id", "_id") existing_fields = {field_name for field_name in new_class._fields} existing_db_fields = {v.db_field for v in new_class._fields.values()} if id_name not in existing_fields and id_db_name not in existing_db_fields: return id_name, id_db_name id_basename, id_db_basename, i = ("auto_id", "_auto_id", 0) for i in itertools.count(): id_name = f"{id_basename}_{i}" id_db_name = f"{id_db_basename}_{i}" if id_name not in existing_fields and id_db_name not in existing_db_fields: return id_name, id_db_name class MetaDict(dict): """Custom dictionary for meta classes. Handles the merging of set indexes """ _merge_options = ("indexes",) def merge(self, new_options): for k, v in new_options.items(): if k in self._merge_options: self[k] = self.get(k, []) + v else: self[k] = v class BasesTuple(tuple): """Special class to handle introspection of bases tuple in __new__""" pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/base/utils.py0000644000175100001730000000115114400345475020443 0ustar00runnerdockerimport re class LazyRegexCompiler: """Descriptor to allow lazy compilation of regex""" def __init__(self, pattern, flags=0): self._pattern = pattern self._flags = flags self._compiled_regex = None @property def compiled_regex(self): if self._compiled_regex is None: self._compiled_regex = re.compile(self._pattern, self._flags) return self._compiled_regex def __get__(self, instance, owner): return self.compiled_regex def __set__(self, instance, value): raise AttributeError("Can not set attribute LazyRegexCompiler") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/common.py0000644000175100001730000000377214400345475017674 0ustar00runnerdocker_class_registry_cache = {} _field_list_cache = [] def _import_class(cls_name): """Cache mechanism for imports. Due to complications of circular imports mongoengine needs to do lots of inline imports in functions. This is inefficient as classes are imported repeated throughout the mongoengine code. This is compounded by some recursive functions requiring inline imports. :mod:`mongoengine.common` provides a single point to import all these classes. Circular imports aren't an issue as it dynamically imports the class when first needed. Subsequent calls to the :func:`~mongoengine.common._import_class` can then directly retrieve the class from the :data:`mongoengine.common._class_registry_cache`. """ if cls_name in _class_registry_cache: return _class_registry_cache.get(cls_name) doc_classes = ( "Document", "DynamicEmbeddedDocument", "EmbeddedDocument", "MapReduceDocument", ) # Field Classes if not _field_list_cache: from mongoengine.fields import __all__ as fields _field_list_cache.extend(fields) from mongoengine.base.fields import __all__ as fields _field_list_cache.extend(fields) field_classes = _field_list_cache deref_classes = ("DeReference",) if cls_name == "BaseDocument": from mongoengine.base import document as module import_classes = ["BaseDocument"] elif cls_name in doc_classes: from mongoengine import document as module import_classes = doc_classes elif cls_name in field_classes: from mongoengine import fields as module import_classes = field_classes elif cls_name in deref_classes: from mongoengine import dereference as module import_classes = deref_classes else: raise ValueError("No import set for: %s" % cls_name) for cls in import_classes: _class_registry_cache[cls] = getattr(module, cls) return _class_registry_cache.get(cls_name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/connection.py0000644000175100001730000004036314400345475020540 0ustar00runnerdockerimport warnings from pymongo import MongoClient, ReadPreference, uri_parser from pymongo.database import _check_name from mongoengine.pymongo_support import PYMONGO_VERSION __all__ = [ "DEFAULT_CONNECTION_NAME", "DEFAULT_DATABASE_NAME", "ConnectionFailure", "connect", "disconnect", "disconnect_all", "get_connection", "get_db", "register_connection", ] DEFAULT_CONNECTION_NAME = "default" DEFAULT_DATABASE_NAME = "test" DEFAULT_HOST = "localhost" DEFAULT_PORT = 27017 _connection_settings = {} _connections = {} _dbs = {} READ_PREFERENCE = ReadPreference.PRIMARY class ConnectionFailure(Exception): """Error raised when the database connection can't be established or when a connection with a requested alias can't be retrieved. """ pass def _check_db_name(name): """Check if a database name is valid. This functionality is copied from pymongo Database class constructor. """ if not isinstance(name, str): raise TypeError("name must be an instance of %s" % str) elif name != "$external": _check_name(name) def _get_connection_settings( db=None, name=None, host=None, port=None, read_preference=READ_PREFERENCE, username=None, password=None, authentication_source=None, authentication_mechanism=None, authmechanismproperties=None, **kwargs, ): """Get the connection settings as a dict :param db: the name of the database to use, for compatibility with connect :param name: the name of the specific database to use :param host: the host name of the: program: `mongod` instance to connect to :param port: the port that the: program: `mongod` instance is running on :param read_preference: The read preference for the collection :param username: username to authenticate with :param password: password to authenticate with :param authentication_source: database to authenticate against :param authentication_mechanism: database authentication mechanisms. By default, use SCRAM-SHA-1 with MongoDB 3.0 and later, MONGODB-CR (MongoDB Challenge Response protocol) for older servers. :param mongo_client_class: using alternative connection client other than pymongo.MongoClient, e.g. mongomock, montydb, that provides pymongo alike interface but not necessarily for connecting to a real mongo instance. :param kwargs: ad-hoc parameters to be passed into the pymongo driver, for example maxpoolsize, tz_aware, etc. See the documentation for pymongo's `MongoClient` for a full list. """ conn_settings = { "name": name or db or DEFAULT_DATABASE_NAME, "host": host or DEFAULT_HOST, "port": port or DEFAULT_PORT, "read_preference": read_preference, "username": username, "password": password, "authentication_source": authentication_source, "authentication_mechanism": authentication_mechanism, "authmechanismproperties": authmechanismproperties, } _check_db_name(conn_settings["name"]) conn_host = conn_settings["host"] # Host can be a list or a string, so if string, force to a list. if isinstance(conn_host, str): conn_host = [conn_host] resolved_hosts = [] for entity in conn_host: # Reject old mongomock integration # To be removed in a few versions after 0.27.0 if entity.startswith("mongomock://") or kwargs.get("is_mock"): raise Exception( "Use of mongomock:// URI or 'is_mock' were removed in favor of 'mongo_client_class=mongomock.MongoClient'. " "Check the CHANGELOG for more info" ) # Handle URI style connections, only updating connection params which # were explicitly specified in the URI. if "://" in entity: uri_dict = uri_parser.parse_uri(entity) resolved_hosts.append(entity) database = uri_dict.get("database") if database: conn_settings["name"] = database for param in ("read_preference", "username", "password"): if uri_dict.get(param): conn_settings[param] = uri_dict[param] uri_options = uri_dict["options"] if "replicaset" in uri_options: conn_settings["replicaSet"] = uri_options["replicaset"] if "authsource" in uri_options: conn_settings["authentication_source"] = uri_options["authsource"] if "authmechanism" in uri_options: conn_settings["authentication_mechanism"] = uri_options["authmechanism"] if "readpreference" in uri_options: read_preferences = ( ReadPreference.NEAREST, ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ) # Starting with PyMongo v3.5, the "readpreference" option is # returned as a string (e.g. "secondaryPreferred") and not an # int (e.g. 3). # TODO simplify the code below once we drop support for # PyMongo v3.4. read_pf_mode = uri_options["readpreference"] if isinstance(read_pf_mode, str): read_pf_mode = read_pf_mode.lower() for preference in read_preferences: if ( preference.name.lower() == read_pf_mode or preference.mode == read_pf_mode ): conn_settings["read_preference"] = preference break if "authmechanismproperties" in uri_options: conn_settings["authmechanismproperties"] = uri_options[ "authmechanismproperties" ] else: resolved_hosts.append(entity) conn_settings["host"] = resolved_hosts # Deprecated parameters that should not be passed on kwargs.pop("slaves", None) kwargs.pop("is_slave", None) keys = { key.lower() for key in kwargs.keys() } # pymongo options are case insensitive if "uuidrepresentation" not in keys: warnings.warn( "No uuidRepresentation is specified! Falling back to " "'pythonLegacy' which is the default for pymongo 3.x. " "For compatibility with other MongoDB drivers this should be " "specified as 'standard' or '{java,csharp}Legacy' to work with " "older drivers in those languages. This will be changed to " "'unspecified' in a future release.", DeprecationWarning, ) kwargs["uuidRepresentation"] = "pythonLegacy" conn_settings.update(kwargs) return conn_settings def register_connection( alias, db=None, name=None, host=None, port=None, read_preference=READ_PREFERENCE, username=None, password=None, authentication_source=None, authentication_mechanism=None, authmechanismproperties=None, **kwargs, ): """Register the connection settings. :param alias: the name that will be used to refer to this connection throughout MongoEngine :param db: the name of the database to use, for compatibility with connect :param name: the name of the specific database to use :param host: the host name of the: program: `mongod` instance to connect to :param port: the port that the: program: `mongod` instance is running on :param read_preference: The read preference for the collection :param username: username to authenticate with :param password: password to authenticate with :param authentication_source: database to authenticate against :param authentication_mechanism: database authentication mechanisms. By default, use SCRAM-SHA-1 with MongoDB 3.0 and later, MONGODB-CR (MongoDB Challenge Response protocol) for older servers. :param mongo_client_class: using alternative connection client other than pymongo.MongoClient, e.g. mongomock, montydb, that provides pymongo alike interface but not necessarily for connecting to a real mongo instance. :param kwargs: ad-hoc parameters to be passed into the pymongo driver, for example maxpoolsize, tz_aware, etc. See the documentation for pymongo's `MongoClient` for a full list. """ conn_settings = _get_connection_settings( db=db, name=name, host=host, port=port, read_preference=read_preference, username=username, password=password, authentication_source=authentication_source, authentication_mechanism=authentication_mechanism, authmechanismproperties=authmechanismproperties, **kwargs, ) _connection_settings[alias] = conn_settings def disconnect(alias=DEFAULT_CONNECTION_NAME): """Close the connection with a given alias.""" from mongoengine import Document from mongoengine.base.common import _get_documents_by_db connection = _connections.pop(alias, None) if connection: # MongoEngine may share the same MongoClient across multiple aliases # if connection settings are the same so we only close # the client if we're removing the final reference. # Important to use 'is' instead of '==' because clients connected to the same cluster # will compare equal even with different options if all(connection is not c for c in _connections.values()): connection.close() if alias in _dbs: # Detach all cached collections in Documents for doc_cls in _get_documents_by_db(alias, DEFAULT_CONNECTION_NAME): if issubclass(doc_cls, Document): # Skip EmbeddedDocument doc_cls._disconnect() del _dbs[alias] if alias in _connection_settings: del _connection_settings[alias] def disconnect_all(): """Close all registered database.""" for alias in list(_connections.keys()): disconnect(alias) def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): """Return a connection with a given alias.""" # Connect to the database if not already connected if reconnect: disconnect(alias) # If the requested alias already exists in the _connections list, return # it immediately. if alias in _connections: return _connections[alias] # Validate that the requested alias exists in the _connection_settings. # Raise ConnectionFailure if it doesn't. if alias not in _connection_settings: if alias == DEFAULT_CONNECTION_NAME: msg = "You have not defined a default connection" else: msg = 'Connection with alias "%s" has not been defined' % alias raise ConnectionFailure(msg) def _clean_settings(settings_dict): if PYMONGO_VERSION < (4,): irrelevant_fields_set = { "name", "username", "password", "authentication_source", "authentication_mechanism", "authmechanismproperties", } rename_fields = {} else: irrelevant_fields_set = {"name"} rename_fields = { "authentication_source": "authSource", "authentication_mechanism": "authMechanism", } return { rename_fields.get(k, k): v for k, v in settings_dict.items() if k not in irrelevant_fields_set and v is not None } raw_conn_settings = _connection_settings[alias].copy() # Retrieve a copy of the connection settings associated with the requested # alias and remove the database name and authentication info (we don't # care about them at this point). conn_settings = _clean_settings(raw_conn_settings) # Determine if we should use PyMongo's or mongomock's MongoClient. if "mongo_client_class" in conn_settings: mongo_client_class = conn_settings.pop("mongo_client_class") else: mongo_client_class = MongoClient # Re-use existing connection if one is suitable. existing_connection = _find_existing_connection(raw_conn_settings) if existing_connection: connection = existing_connection else: connection = _create_connection( alias=alias, mongo_client_class=mongo_client_class, **conn_settings ) _connections[alias] = connection return _connections[alias] def _create_connection(alias, mongo_client_class, **connection_settings): """ Create the new connection for this alias. Raise ConnectionFailure if it can't be established. """ try: return mongo_client_class(**connection_settings) except Exception as e: raise ConnectionFailure(f"Cannot connect to database {alias} :\n{e}") def _find_existing_connection(connection_settings): """ Check if an existing connection could be reused Iterate over all of the connection settings and if an existing connection with the same parameters is suitable, return it :param connection_settings: the settings of the new connection :return: An existing connection or None """ connection_settings_bis = ( (db_alias, settings.copy()) for db_alias, settings in _connection_settings.items() ) def _clean_settings(settings_dict): # Only remove the name but it's important to # keep the username/password/authentication_source/authentication_mechanism # to identify if the connection could be shared (cfr https://github.com/MongoEngine/mongoengine/issues/2047) return {k: v for k, v in settings_dict.items() if k != "name"} cleaned_conn_settings = _clean_settings(connection_settings) for db_alias, connection_settings in connection_settings_bis: db_conn_settings = _clean_settings(connection_settings) if cleaned_conn_settings == db_conn_settings and _connections.get(db_alias): return _connections[db_alias] def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False): if reconnect: disconnect(alias) if alias not in _dbs: conn = get_connection(alias) conn_settings = _connection_settings[alias] db = conn[conn_settings["name"]] # Authenticate if necessary if ( PYMONGO_VERSION < (4,) and conn_settings["username"] and ( conn_settings["password"] or conn_settings["authentication_mechanism"] == "MONGODB-X509" ) and conn_settings["authmechanismproperties"] is None ): auth_kwargs = {"source": conn_settings["authentication_source"]} if conn_settings["authentication_mechanism"] is not None: auth_kwargs["mechanism"] = conn_settings["authentication_mechanism"] db.authenticate( conn_settings["username"], conn_settings["password"], **auth_kwargs ) _dbs[alias] = db return _dbs[alias] def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs): """Connect to the database specified by the 'db' argument. Connection settings may be provided here as well if the database is not running on the default port on localhost. If authentication is needed, provide username and password arguments as well. Multiple databases are supported by using aliases. Provide a separate `alias` to connect to a different instance of: program: `mongod`. In order to replace a connection identified by a given alias, you'll need to call ``disconnect`` first See the docstring for `register_connection` for more details about all supported kwargs. """ if alias in _connections: prev_conn_setting = _connection_settings[alias] new_conn_settings = _get_connection_settings(db, **kwargs) if new_conn_settings != prev_conn_setting: err_msg = ( "A different connection with alias `{}` was already " "registered. Use disconnect() first" ).format(alias) raise ConnectionFailure(err_msg) else: register_connection(alias, db, **kwargs) return get_connection(alias) # Support old naming convention _get_connection = get_connection _get_db = get_db ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/context_managers.py0000644000175100001730000002232614400345475021741 0ustar00runnerdockerfrom contextlib import contextmanager from pymongo.read_concern import ReadConcern from pymongo.write_concern import WriteConcern from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.pymongo_support import count_documents __all__ = ( "switch_db", "switch_collection", "no_dereference", "no_sub_classes", "query_counter", "set_write_concern", "set_read_write_concern", ) class switch_db: """switch_db alias context manager. Example :: # Register connections register_connection('default', 'mongoenginetest') register_connection('testdb-1', 'mongoenginetest2') class Group(Document): name = StringField() Group(name='test').save() # Saves in the default db with switch_db(Group, 'testdb-1') as Group: Group(name='hello testdb!').save() # Saves in testdb-1 """ def __init__(self, cls, db_alias): """Construct the switch_db context manager :param cls: the class to change the registered db :param db_alias: the name of the specific database to use """ self.cls = cls self.collection = cls._get_collection() self.db_alias = db_alias self.ori_db_alias = cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME) def __enter__(self): """Change the db_alias and clear the cached collection.""" self.cls._meta["db_alias"] = self.db_alias self.cls._collection = None return self.cls def __exit__(self, t, value, traceback): """Reset the db_alias and collection.""" self.cls._meta["db_alias"] = self.ori_db_alias self.cls._collection = self.collection class switch_collection: """switch_collection alias context manager. Example :: class Group(Document): name = StringField() Group(name='test').save() # Saves in the default db with switch_collection(Group, 'group1') as Group: Group(name='hello testdb!').save() # Saves in group1 collection """ def __init__(self, cls, collection_name): """Construct the switch_collection context manager. :param cls: the class to change the registered db :param collection_name: the name of the collection to use """ self.cls = cls self.ori_collection = cls._get_collection() self.ori_get_collection_name = cls._get_collection_name self.collection_name = collection_name def __enter__(self): """Change the _get_collection_name and clear the cached collection.""" @classmethod def _get_collection_name(cls): return self.collection_name self.cls._get_collection_name = _get_collection_name self.cls._collection = None return self.cls def __exit__(self, t, value, traceback): """Reset the collection.""" self.cls._collection = self.ori_collection self.cls._get_collection_name = self.ori_get_collection_name class no_dereference: """no_dereference context manager. Turns off all dereferencing in Documents for the duration of the context manager:: with no_dereference(Group) as Group: Group.objects.find() """ def __init__(self, cls): """Construct the no_dereference context manager. :param cls: the class to turn dereferencing off on """ self.cls = cls ReferenceField = _import_class("ReferenceField") GenericReferenceField = _import_class("GenericReferenceField") ComplexBaseField = _import_class("ComplexBaseField") self.deref_fields = [ k for k, v in self.cls._fields.items() if isinstance(v, (ReferenceField, GenericReferenceField, ComplexBaseField)) ] def __enter__(self): """Change the objects default and _auto_dereference values.""" for field in self.deref_fields: self.cls._fields[field]._auto_dereference = False return self.cls def __exit__(self, t, value, traceback): """Reset the default and _auto_dereference values.""" for field in self.deref_fields: self.cls._fields[field]._auto_dereference = True return self.cls class no_sub_classes: """no_sub_classes context manager. Only returns instances of this class and no sub (inherited) classes:: with no_sub_classes(Group) as Group: Group.objects.find() """ def __init__(self, cls): """Construct the no_sub_classes context manager. :param cls: the class to turn querying sub classes on """ self.cls = cls self.cls_initial_subclasses = None def __enter__(self): """Change the objects default and _auto_dereference values.""" self.cls_initial_subclasses = self.cls._subclasses self.cls._subclasses = (self.cls._class_name,) return self.cls def __exit__(self, t, value, traceback): """Reset the default and _auto_dereference values.""" self.cls._subclasses = self.cls_initial_subclasses class query_counter: """Query_counter context manager to get the number of queries. This works by updating the `profiling_level` of the database so that all queries get logged, resetting the db.system.profile collection at the beginning of the context and counting the new entries. This was designed for debugging purpose. In fact it is a global counter so queries issued by other threads/processes can interfere with it Usage: .. code-block:: python class User(Document): name = StringField() with query_counter() as q: user = User(name='Bob') assert q == 0 # no query fired yet user.save() assert q == 1 # 1 query was fired, an 'insert' user_bis = User.objects().first() assert q == 2 # a 2nd query was fired, a 'find_one' Be aware that: - Iterating over large amount of documents (>101) makes pymongo issue `getmore` queries to fetch the next batch of documents (https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches) - Some queries are ignored by default by the counter (killcursors, db.system.indexes) """ def __init__(self, alias=DEFAULT_CONNECTION_NAME): self.db = get_db(alias=alias) self.initial_profiling_level = None self._ctx_query_counter = 0 # number of queries issued by the context self._ignored_query = { "ns": {"$ne": "%s.system.indexes" % self.db.name}, "op": {"$ne": "killcursors"}, # MONGODB < 3.2 "command.killCursors": {"$exists": False}, # MONGODB >= 3.2 } def _turn_on_profiling(self): profile_update_res = self.db.command({"profile": 0}) self.initial_profiling_level = profile_update_res["was"] self.db.system.profile.drop() self.db.command({"profile": 2}) def _resets_profiling(self): self.db.command({"profile": self.initial_profiling_level}) def __enter__(self): self._turn_on_profiling() return self def __exit__(self, t, value, traceback): self._resets_profiling() def __eq__(self, value): counter = self._get_count() return value == counter def __ne__(self, value): return not self.__eq__(value) def __lt__(self, value): return self._get_count() < value def __le__(self, value): return self._get_count() <= value def __gt__(self, value): return self._get_count() > value def __ge__(self, value): return self._get_count() >= value def __int__(self): return self._get_count() def __repr__(self): """repr query_counter as the number of queries.""" return "%s" % self._get_count() def _get_count(self): """Get the number of queries by counting the current number of entries in db.system.profile and substracting the queries issued by this context. In fact everytime this is called, 1 query is issued so we need to balance that """ count = ( count_documents(self.db.system.profile, self._ignored_query) - self._ctx_query_counter ) self._ctx_query_counter += ( 1 # Account for the query we just issued to gather the information ) return count @contextmanager def set_write_concern(collection, write_concerns): combined_concerns = dict(collection.write_concern.document.items()) combined_concerns.update(write_concerns) yield collection.with_options(write_concern=WriteConcern(**combined_concerns)) @contextmanager def set_read_write_concern(collection, write_concerns, read_concerns): combined_write_concerns = dict(collection.write_concern.document.items()) if write_concerns is not None: combined_write_concerns.update(write_concerns) combined_read_concerns = dict(collection.read_concern.document.items()) if read_concerns is not None: combined_read_concerns.update(read_concerns) yield collection.with_options( write_concern=WriteConcern(**combined_write_concerns), read_concern=ReadConcern(**combined_read_concerns), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/dereference.py0000644000175100001730000003065414400345475020652 0ustar00runnerdockerfrom bson import SON, DBRef from mongoengine.base import ( BaseDict, BaseList, EmbeddedDocumentList, TopLevelDocumentMetaclass, get_document, ) from mongoengine.base.datastructures import LazyReference from mongoengine.connection import get_db from mongoengine.document import Document, EmbeddedDocument from mongoengine.fields import ( DictField, ListField, MapField, ReferenceField, ) from mongoengine.queryset import QuerySet class DeReference: def __call__(self, items, max_depth=1, instance=None, name=None): """ Cheaply dereferences the items to a set depth. Also handles the conversion of complex data types. :param items: The iterable (dict, list, queryset) to be dereferenced. :param max_depth: The maximum depth to recurse to :param instance: The owning instance used for tracking changes by :class:`~mongoengine.base.ComplexBaseField` :param name: The name of the field, used for tracking changes by :class:`~mongoengine.base.ComplexBaseField` :param get: A boolean determining if being called by __get__ """ if items is None or isinstance(items, str): return items # cheapest way to convert a queryset to a list # list(queryset) uses a count() query to determine length if isinstance(items, QuerySet): items = [i for i in items] self.max_depth = max_depth doc_type = None if instance and isinstance( instance, (Document, EmbeddedDocument, TopLevelDocumentMetaclass) ): doc_type = instance._fields.get(name) while hasattr(doc_type, "field"): doc_type = doc_type.field if isinstance(doc_type, ReferenceField): field = doc_type doc_type = doc_type.document_type is_list = not hasattr(items, "items") if is_list and all(i.__class__ == doc_type for i in items): return items elif not is_list and all( i.__class__ == doc_type for i in items.values() ): return items elif not field.dbref: # We must turn the ObjectIds into DBRefs # Recursively dig into the sub items of a list/dict # to turn the ObjectIds into DBRefs def _get_items_from_list(items): new_items = [] for v in items: value = v if isinstance(v, dict): value = _get_items_from_dict(v) elif isinstance(v, list): value = _get_items_from_list(v) elif not isinstance(v, (DBRef, Document)): value = field.to_python(v) new_items.append(value) return new_items def _get_items_from_dict(items): new_items = {} for k, v in items.items(): value = v if isinstance(v, list): value = _get_items_from_list(v) elif isinstance(v, dict): value = _get_items_from_dict(v) elif not isinstance(v, (DBRef, Document)): value = field.to_python(v) new_items[k] = value return new_items if not hasattr(items, "items"): items = _get_items_from_list(items) else: items = _get_items_from_dict(items) self.reference_map = self._find_references(items) self.object_map = self._fetch_objects(doc_type=doc_type) return self._attach_objects(items, 0, instance, name) def _find_references(self, items, depth=0): """ Recursively finds all db references to be dereferenced :param items: The iterable (dict, list, queryset) :param depth: The current depth of recursion """ reference_map = {} if not items or depth >= self.max_depth: return reference_map # Determine the iterator to use if isinstance(items, dict): iterator = items.values() else: iterator = items # Recursively find dbreferences depth += 1 for item in iterator: if isinstance(item, (Document, EmbeddedDocument)): for field_name, field in item._fields.items(): v = item._data.get(field_name, None) if isinstance(v, LazyReference): # LazyReference inherits DBRef but should not be dereferenced here ! continue elif isinstance(v, DBRef): reference_map.setdefault(field.document_type, set()).add(v.id) elif isinstance(v, (dict, SON)) and "_ref" in v: reference_map.setdefault(get_document(v["_cls"]), set()).add( v["_ref"].id ) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: field_cls = getattr( getattr(field, "field", None), "document_type", None ) references = self._find_references(v, depth) for key, refs in references.items(): if isinstance( field_cls, (Document, TopLevelDocumentMetaclass) ): key = field_cls reference_map.setdefault(key, set()).update(refs) elif isinstance(item, LazyReference): # LazyReference inherits DBRef but should not be dereferenced here ! continue elif isinstance(item, DBRef): reference_map.setdefault(item.collection, set()).add(item.id) elif isinstance(item, (dict, SON)) and "_ref" in item: reference_map.setdefault(get_document(item["_cls"]), set()).add( item["_ref"].id ) elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth: references = self._find_references(item, depth - 1) for key, refs in references.items(): reference_map.setdefault(key, set()).update(refs) return reference_map def _fetch_objects(self, doc_type=None): """Fetch all references and convert to their document objects""" object_map = {} for collection, dbrefs in self.reference_map.items(): # we use getattr instead of hasattr because hasattr swallows any exception under python2 # so it could hide nasty things without raising exceptions (cfr bug #1688)) ref_document_cls_exists = getattr(collection, "objects", None) is not None if ref_document_cls_exists: col_name = collection._get_collection_name() refs = [ dbref for dbref in dbrefs if (col_name, dbref) not in object_map ] references = collection.objects.in_bulk(refs) for key, doc in references.items(): object_map[(col_name, key)] = doc else: # Generic reference: use the refs data to convert to document if isinstance(doc_type, (ListField, DictField, MapField)): continue refs = [ dbref for dbref in dbrefs if (collection, dbref) not in object_map ] if doc_type: references = doc_type._get_db()[collection].find( {"_id": {"$in": refs}} ) for ref in references: doc = doc_type._from_son(ref) object_map[(collection, doc.id)] = doc else: references = get_db()[collection].find({"_id": {"$in": refs}}) for ref in references: if "_cls" in ref: doc = get_document(ref["_cls"])._from_son(ref) elif doc_type is None: doc = get_document( "".join(x.capitalize() for x in collection.split("_")) )._from_son(ref) else: doc = doc_type._from_son(ref) object_map[(collection, doc.id)] = doc return object_map def _attach_objects(self, items, depth=0, instance=None, name=None): """ Recursively finds all db references to be dereferenced :param items: The iterable (dict, list, queryset) :param depth: The current depth of recursion :param instance: The owning instance used for tracking changes by :class:`~mongoengine.base.ComplexBaseField` :param name: The name of the field, used for tracking changes by :class:`~mongoengine.base.ComplexBaseField` """ if not items: if isinstance(items, (BaseDict, BaseList)): return items if instance: if isinstance(items, dict): return BaseDict(items, instance, name) else: return BaseList(items, instance, name) if isinstance(items, (dict, SON)): if "_ref" in items: return self.object_map.get( (items["_ref"].collection, items["_ref"].id), items ) elif "_cls" in items: doc = get_document(items["_cls"])._from_son(items) _cls = doc._data.pop("_cls", None) del items["_cls"] doc._data = self._attach_objects(doc._data, depth, doc, None) if _cls is not None: doc._data["_cls"] = _cls return doc if not hasattr(items, "items"): is_list = True list_type = BaseList if isinstance(items, EmbeddedDocumentList): list_type = EmbeddedDocumentList as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] else: is_list = False iterator = items.items() data = {} depth += 1 for k, v in iterator: if is_list: data.append(v) else: data[k] = v if k in self.object_map and not is_list: data[k] = self.object_map[k] elif isinstance(v, (Document, EmbeddedDocument)): for field_name in v._fields: v = data[k]._data.get(field_name, None) if isinstance(v, DBRef): data[k]._data[field_name] = self.object_map.get( (v.collection, v.id), v ) elif isinstance(v, (dict, SON)) and "_ref" in v: data[k]._data[field_name] = self.object_map.get( (v["_ref"].collection, v["_ref"].id), v ) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: item_name = f"{name}.{k}.{field_name}" data[k]._data[field_name] = self._attach_objects( v, depth, instance=instance, name=item_name ) elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: item_name = f"{name}.{k}" if name else name data[k] = self._attach_objects( v, depth - 1, instance=instance, name=item_name ) elif isinstance(v, DBRef) and hasattr(v, "id"): data[k] = self.object_map.get((v.collection, v.id), v) if instance and name: if is_list: return tuple(data) if as_tuple else list_type(data, instance, name) return BaseDict(data, instance, name) depth += 1 return data ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/document.py0000644000175100001730000012475614400345475020230 0ustar00runnerdockerimport re import pymongo from bson.dbref import DBRef from pymongo.read_preferences import ReadPreference from mongoengine import signals from mongoengine.base import ( BaseDict, BaseDocument, BaseList, DocumentMetaclass, EmbeddedDocumentList, TopLevelDocumentMetaclass, get_document, ) from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.context_managers import ( set_write_concern, switch_collection, switch_db, ) from mongoengine.errors import ( InvalidDocumentError, InvalidQueryError, SaveConditionError, ) from mongoengine.pymongo_support import list_collection_names from mongoengine.queryset import ( NotUniqueError, OperationError, QuerySet, transform, ) __all__ = ( "Document", "EmbeddedDocument", "DynamicDocument", "DynamicEmbeddedDocument", "OperationError", "InvalidCollectionError", "NotUniqueError", "MapReduceDocument", ) def includes_cls(fields): """Helper function used for ensuring and comparing indexes.""" first_field = None if len(fields): if isinstance(fields[0], str): first_field = fields[0] elif isinstance(fields[0], (list, tuple)) and len(fields[0]): first_field = fields[0][0] return first_field == "_cls" class InvalidCollectionError(Exception): pass class EmbeddedDocument(BaseDocument, metaclass=DocumentMetaclass): r"""A :class:`~mongoengine.Document` that isn't stored in its own collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as fields on :class:`~mongoengine.Document`\ s through the :class:`~mongoengine.EmbeddedDocumentField` field type. A :class:`~mongoengine.EmbeddedDocument` subclass may be itself subclassed, to create a specialised version of the embedded document that will be stored in the same collection. To facilitate this behaviour a `_cls` field is added to documents (hidden though the MongoEngine interface). To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the :attr:`meta` dictionary. """ __slots__ = ("_instance",) # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = DocumentMetaclass # A generic embedded document doesn't have any immutable properties # that describe it uniquely, hence it shouldn't be hashable. You can # define your own __hash__ method on a subclass if you need your # embedded documents to be hashable. __hash__ = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._instance = None self._changed_fields = [] def __eq__(self, other): if isinstance(other, self.__class__): return self._data == other._data return False def __ne__(self, other): return not self.__eq__(other) def __getstate__(self): data = super().__getstate__() data["_instance"] = None return data def __setstate__(self, state): super().__setstate__(state) self._instance = state["_instance"] def to_mongo(self, *args, **kwargs): data = super().to_mongo(*args, **kwargs) # remove _id from the SON if it's in it and it's None if "_id" in data and data["_id"] is None: del data["_id"] return data class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass): """The base class used for defining the structure and properties of collections of documents stored in MongoDB. Inherit from this class, and add fields as class attributes to define a document's structure. Individual documents may then be created by making instances of the :class:`~mongoengine.Document` subclass. By default, the MongoDB collection used to store documents created using a :class:`~mongoengine.Document` subclass will be the name of the subclass converted to snake_case. A different collection may be specified by providing :attr:`collection` to the :attr:`meta` dictionary in the class definition. A :class:`~mongoengine.Document` subclass may be itself subclassed, to create a specialised version of the document that will be stored in the same collection. To facilitate this behaviour a `_cls` field is added to documents (hidden though the MongoEngine interface). To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the :attr:`meta` dictionary. A :class:`~mongoengine.Document` may use a **Capped Collection** by specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta` dictionary. :attr:`max_documents` is the maximum number of documents that is allowed to be stored in the collection, and :attr:`max_size` is the maximum size of the collection in bytes. :attr:`max_size` is rounded up to the next multiple of 256 by MongoDB internally and mongoengine before. Use also a multiple of 256 to avoid confusions. If :attr:`max_size` is not specified and :attr:`max_documents` is, :attr:`max_size` defaults to 10485760 bytes (10MB). Indexes may be created by specifying :attr:`indexes` in the :attr:`meta` dictionary. The value should be a list of field names or tuples of field names. Index direction may be specified by prefixing the field names with a **+** or **-** sign. Automatic index creation can be disabled by specifying :attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to False then indexes will not be created by MongoEngine. This is useful in production systems where index creation is performed as part of a deployment system. By default, _cls will be added to the start of every index (that doesn't contain a list) if allow_inheritance is True. This can be disabled by either setting cls to False on the specific index or by setting index_cls to False on the meta dictionary for the document. By default, any extra attribute existing in stored data but not declared in your model will raise a :class:`~mongoengine.FieldDoesNotExist` error. This can be disabled by setting :attr:`strict` to ``False`` in the :attr:`meta` dictionary. """ # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = TopLevelDocumentMetaclass __slots__ = ("__objects",) @property def pk(self): """Get the primary key.""" if "id_field" not in self._meta: return None return getattr(self, self._meta["id_field"]) @pk.setter def pk(self, value): """Set the primary key.""" return setattr(self, self._meta["id_field"], value) def __hash__(self): """Return the hash based on the PK of this document. If it's new and doesn't have a PK yet, return the default object hash instead. """ if self.pk is None: return super(BaseDocument, self).__hash__() return hash(self.pk) @classmethod def _get_db(cls): """Some Model using other db_alias""" return get_db(cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME)) @classmethod def _disconnect(cls): """Detach the Document class from the (cached) database collection""" cls._collection = None @classmethod def _get_collection(cls): """Return the PyMongo collection corresponding to this document. Upon first call, this method: 1. Initializes a :class:`~pymongo.collection.Collection` corresponding to this document. 2. Creates indexes defined in this document's :attr:`meta` dictionary. This happens only if `auto_create_index` is True. """ if not hasattr(cls, "_collection") or cls._collection is None: # Get the collection, either capped or regular. if cls._meta.get("max_size") or cls._meta.get("max_documents"): cls._collection = cls._get_capped_collection() else: db = cls._get_db() collection_name = cls._get_collection_name() cls._collection = db[collection_name] # Ensure indexes on the collection unless auto_create_index was # set to False. Plus, there is no need to ensure indexes on slave. db = cls._get_db() if cls._meta.get("auto_create_index", True) and db.client.is_primary: cls.ensure_indexes() return cls._collection @classmethod def _get_capped_collection(cls): """Create a new or get an existing capped PyMongo collection.""" db = cls._get_db() collection_name = cls._get_collection_name() # Get max document limit and max byte size from meta. max_size = cls._meta.get("max_size") or 10 * 2**20 # 10MB default max_documents = cls._meta.get("max_documents") # MongoDB will automatically raise the size to make it a multiple of # 256 bytes. We raise it here ourselves to be able to reliably compare # the options below. if max_size % 256: max_size = (max_size // 256 + 1) * 256 # If the collection already exists and has different options # (i.e. isn't capped or has different max/size), raise an error. if collection_name in list_collection_names( db, include_system_collections=True ): collection = db[collection_name] options = collection.options() if options.get("max") != max_documents or options.get("size") != max_size: raise InvalidCollectionError( 'Cannot create collection "{}" as a capped ' "collection as it already exists".format(cls._collection) ) return collection # Create a new capped collection. opts = {"capped": True, "size": max_size} if max_documents: opts["max"] = max_documents return db.create_collection(collection_name, **opts) def to_mongo(self, *args, **kwargs): data = super().to_mongo(*args, **kwargs) # If '_id' is None, try and set it from self._data. If that # doesn't exist either, remove '_id' from the SON completely. if data["_id"] is None: if self._data.get("id") is None: del data["_id"] else: data["_id"] = self._data["id"] return data def modify(self, query=None, **update): """Perform an atomic update of the document in the database and reload the document object using updated version. Returns True if the document has been updated or False if the document in the database doesn't match the query. .. note:: All unsaved changes that have been made to the document are rejected if the method returns True. :param query: the update will be performed only if the document in the database matches the query :param update: Django-style update keyword arguments """ if query is None: query = {} if self.pk is None: raise InvalidDocumentError("The document does not have a primary key.") id_field = self._meta["id_field"] query = query.copy() if isinstance(query, dict) else query.to_query(self) if id_field not in query: query[id_field] = self.pk elif query[id_field] != self.pk: raise InvalidQueryError( "Invalid document modify query: it must modify only this document." ) # Need to add shard key to query, or you get an error query.update(self._object_key) updated = self._qs(**query).modify(new=True, **update) if updated is None: return False for field in self._fields_ordered: setattr(self, field, self._reload(field, updated[field])) self._changed_fields = updated._changed_fields self._created = False return True def save( self, force_insert=False, validate=True, clean=True, write_concern=None, cascade=None, cascade_kwargs=None, _refs=None, save_condition=None, signal_kwargs=None, **kwargs, ): """Save the :class:`~mongoengine.Document` to the database. If the document already exists, it will be updated, otherwise it will be created. Returns the saved object instance. :param force_insert: only try to create a new document, don't allow updates of existing documents. :param validate: validates the document; set to ``False`` to skip. :param clean: call the document clean method, requires `validate` to be True. :param write_concern: Extra keyword arguments are passed down to :meth:`~pymongo.collection.Collection.save` OR :meth:`~pymongo.collection.Collection.insert` which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. :param cascade: Sets the flag for cascading saves. You can set a default by setting "cascade" in the document __meta__ :param cascade_kwargs: (optional) kwargs dictionary to be passed throw to cascading saves. Implies ``cascade=True``. :param _refs: A list of processed references used in cascading saves :param save_condition: only perform save if matching record in db satisfies condition(s) (e.g. version number). Raises :class:`OperationError` if the conditions are not satisfied :param signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. .. versionchanged:: 0.5 In existing documents it only saves changed fields using set / unset. Saves are cascaded and any :class:`~bson.dbref.DBRef` objects that have changes are saved as well. .. versionchanged:: 0.6 Added cascading saves .. versionchanged:: 0.8 Cascade saves are optional and default to False. If you want fine grain control then you can turn off using document meta['cascade'] = True. Also you can pass different kwargs to the cascade save using cascade_kwargs which overwrites the existing kwargs with custom values. .. versionchanged:: 0.26 save() no longer calls :meth:`~mongoengine.Document.ensure_indexes` unless ``meta['auto_create_index_on_save']`` is set to True. """ signal_kwargs = signal_kwargs or {} if self._meta.get("abstract"): raise InvalidDocumentError("Cannot save an abstract document.") signals.pre_save.send(self.__class__, document=self, **signal_kwargs) if validate: self.validate(clean=clean) if write_concern is None: write_concern = {} doc_id = self.to_mongo(fields=[self._meta["id_field"]]) created = "_id" not in doc_id or self._created or force_insert signals.pre_save_post_validation.send( self.__class__, document=self, created=created, **signal_kwargs ) # it might be refreshed by the pre_save_post_validation hook, e.g., for etag generation doc = self.to_mongo() # Initialize the Document's underlying pymongo.Collection (+create indexes) if not already initialized # Important to do this here to avoid that the index creation gets wrapped in the try/except block below # and turned into mongoengine.OperationError if self._collection is None: _ = self._get_collection() elif self._meta.get("auto_create_index_on_save", False): # ensure_indexes is called as part of _get_collection so no need to re-call it again here self.ensure_indexes() try: # Save a new document or update an existing one if created: object_id = self._save_create( doc=doc, force_insert=force_insert, write_concern=write_concern ) else: object_id, created = self._save_update( doc, save_condition, write_concern ) if cascade is None: cascade = self._meta.get("cascade", False) or cascade_kwargs is not None if cascade: kwargs = { "force_insert": force_insert, "validate": validate, "write_concern": write_concern, "cascade": cascade, } if cascade_kwargs: # Allow granular control over cascades kwargs.update(cascade_kwargs) kwargs["_refs"] = _refs self.cascade_save(**kwargs) except pymongo.errors.DuplicateKeyError as err: message = "Tried to save duplicate unique keys (%s)" raise NotUniqueError(message % err) except pymongo.errors.OperationFailure as err: message = "Could not save document (%s)" if re.match("^E1100[01] duplicate key", str(err)): # E11000 - duplicate key error index # E11001 - duplicate key on update message = "Tried to save duplicate unique keys (%s)" raise NotUniqueError(message % err) raise OperationError(message % err) # Make sure we store the PK on this document now that it's saved id_field = self._meta["id_field"] if created or id_field not in self._meta.get("shard_key", []): self[id_field] = self._fields[id_field].to_python(object_id) signals.post_save.send( self.__class__, document=self, created=created, **signal_kwargs ) self._clear_changed_fields() self._created = False return self def _save_create(self, doc, force_insert, write_concern): """Save a new document. Helper method, should only be used inside save(). """ collection = self._get_collection() with set_write_concern(collection, write_concern) as wc_collection: if force_insert: return wc_collection.insert_one(doc).inserted_id # insert_one will provoke UniqueError alongside save does not # therefore, it need to catch and call replace_one. if "_id" in doc: select_dict = {"_id": doc["_id"]} select_dict = self._integrate_shard_key(doc, select_dict) raw_object = wc_collection.find_one_and_replace(select_dict, doc) if raw_object: return doc["_id"] object_id = wc_collection.insert_one(doc).inserted_id return object_id def _get_update_doc(self): """Return a dict containing all the $set and $unset operations that should be sent to MongoDB based on the changes made to this Document. """ updates, removals = self._delta() update_doc = {} if updates: update_doc["$set"] = updates if removals: update_doc["$unset"] = removals return update_doc def _integrate_shard_key(self, doc, select_dict): """Integrates the collection's shard key to the `select_dict`, which will be used for the query. The value from the shard key is taken from the `doc` and finally the select_dict is returned. """ # Need to add shard key to query, or you get an error shard_key = self._meta.get("shard_key", tuple()) for k in shard_key: path = self._lookup_field(k.split(".")) actual_key = [p.db_field for p in path] val = doc for ak in actual_key: val = val[ak] select_dict[".".join(actual_key)] = val return select_dict def _save_update(self, doc, save_condition, write_concern): """Update an existing document. Helper method, should only be used inside save(). """ collection = self._get_collection() object_id = doc["_id"] created = False select_dict = {} if save_condition is not None: select_dict = transform.query(self.__class__, **save_condition) select_dict["_id"] = object_id select_dict = self._integrate_shard_key(doc, select_dict) update_doc = self._get_update_doc() if update_doc: upsert = save_condition is None with set_write_concern(collection, write_concern) as wc_collection: last_error = wc_collection.update_one( select_dict, update_doc, upsert=upsert ).raw_result if not upsert and last_error["n"] == 0: raise SaveConditionError( "Race condition preventing document update detected" ) if last_error is not None: updated_existing = last_error.get("updatedExisting") if updated_existing is False: created = True # !!! This is bad, means we accidentally created a new, # potentially corrupted document. See # https://github.com/MongoEngine/mongoengine/issues/564 return object_id, created def cascade_save(self, **kwargs): """Recursively save any references and generic references on the document. """ _refs = kwargs.get("_refs") or [] ReferenceField = _import_class("ReferenceField") GenericReferenceField = _import_class("GenericReferenceField") for name, cls in self._fields.items(): if not isinstance(cls, (ReferenceField, GenericReferenceField)): continue ref = self._data.get(name) if not ref or isinstance(ref, DBRef): continue if not getattr(ref, "_changed_fields", True): continue ref_id = f"{ref.__class__.__name__},{str(ref._data)}" if ref and ref_id not in _refs: _refs.append(ref_id) kwargs["_refs"] = _refs ref.save(**kwargs) ref._changed_fields = [] @property def _qs(self): """Return the default queryset corresponding to this document.""" if not hasattr(self, "__objects"): queryset_class = self._meta.get("queryset_class", QuerySet) self.__objects = queryset_class(self.__class__, self._get_collection()) return self.__objects @property def _object_key(self): """Return a query dict that can be used to fetch this document. Most of the time the dict is a simple PK lookup, but in case of a sharded collection with a compound shard key, it can contain a more complex query. Note that the dict returned by this method uses MongoEngine field names instead of PyMongo field names (e.g. "pk" instead of "_id", "some__nested__field" instead of "some.nested.field", etc.). """ select_dict = {"pk": self.pk} shard_key = self.__class__._meta.get("shard_key", tuple()) for k in shard_key: val = self field_parts = k.split(".") for part in field_parts: val = getattr(val, part) select_dict["__".join(field_parts)] = val return select_dict def update(self, **kwargs): """Performs an update on the :class:`~mongoengine.Document` A convenience wrapper to :meth:`~mongoengine.QuerySet.update`. Raises :class:`OperationError` if called on an object that has not yet been saved. """ if self.pk is None: if kwargs.get("upsert", False): query = self.to_mongo() if "_cls" in query: del query["_cls"] return self._qs.filter(**query).update_one(**kwargs) else: raise OperationError("attempt to update a document not yet saved") # Need to add shard key to query, or you get an error return self._qs.filter(**self._object_key).update_one(**kwargs) def delete(self, signal_kwargs=None, **write_concern): """Delete the :class:`~mongoengine.Document` from the database. This will only take effect if the document has been previously saved. :param signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., w: 2, fsync: True)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. """ signal_kwargs = signal_kwargs or {} signals.pre_delete.send(self.__class__, document=self, **signal_kwargs) # Delete FileFields separately FileField = _import_class("FileField") for name, field in self._fields.items(): if isinstance(field, FileField): getattr(self, name).delete() try: self._qs.filter(**self._object_key).delete( write_concern=write_concern, _from_doc_delete=True ) except pymongo.errors.OperationFailure as err: message = "Could not delete document (%s)" % err.args raise OperationError(message) signals.post_delete.send(self.__class__, document=self, **signal_kwargs) def switch_db(self, db_alias, keep_created=True): """ Temporarily switch the database for a document instance. Only really useful for archiving off data and calling `save()`:: user = User.objects.get(id=user_id) user.switch_db('archive-db') user.save() :param str db_alias: The database alias to use for saving the document :param bool keep_created: keep self._created value after switching db, else is reset to True .. seealso:: Use :class:`~mongoengine.context_managers.switch_collection` if you need to read from another collection """ with switch_db(self.__class__, db_alias) as cls: collection = cls._get_collection() db = cls._get_db() self._get_collection = lambda: collection self._get_db = lambda: db self._collection = collection self._created = True if not keep_created else self._created self.__objects = self._qs self.__objects._collection_obj = collection return self def switch_collection(self, collection_name, keep_created=True): """ Temporarily switch the collection for a document instance. Only really useful for archiving off data and calling `save()`:: user = User.objects.get(id=user_id) user.switch_collection('old-users') user.save() :param str collection_name: The database alias to use for saving the document :param bool keep_created: keep self._created value after switching collection, else is reset to True .. seealso:: Use :class:`~mongoengine.context_managers.switch_db` if you need to read from another database """ with switch_collection(self.__class__, collection_name) as cls: collection = cls._get_collection() self._get_collection = lambda: collection self._collection = collection self._created = True if not keep_created else self._created self.__objects = self._qs self.__objects._collection_obj = collection return self def select_related(self, max_depth=1): """Handles dereferencing of :class:`~bson.dbref.DBRef` objects to a maximum depth in order to cut down the number queries to mongodb. """ DeReference = _import_class("DeReference") DeReference()([self], max_depth + 1) return self def reload(self, *fields, **kwargs): """Reloads all attributes from the database. :param fields: (optional) args list of fields to reload :param max_depth: (optional) depth of dereferencing to follow """ max_depth = 1 if fields and isinstance(fields[0], int): max_depth = fields[0] fields = fields[1:] elif "max_depth" in kwargs: max_depth = kwargs["max_depth"] if self.pk is None: raise self.DoesNotExist("Document does not exist") obj = ( self._qs.read_preference(ReadPreference.PRIMARY) .filter(**self._object_key) .only(*fields) .limit(1) .select_related(max_depth=max_depth) ) if obj: obj = obj[0] else: raise self.DoesNotExist("Document does not exist") for field in obj._data: if not fields or field in fields: try: setattr(self, field, self._reload(field, obj[field])) except (KeyError, AttributeError): try: # If field is a special field, e.g. items is stored as _reserved_items, # a KeyError is thrown. So try to retrieve the field from _data setattr(self, field, self._reload(field, obj._data.get(field))) except KeyError: # If field is removed from the database while the object # is in memory, a reload would cause a KeyError # i.e. obj.update(unset__field=1) followed by obj.reload() delattr(self, field) self._changed_fields = ( list(set(self._changed_fields) - set(fields)) if fields else obj._changed_fields ) self._created = False return self def _reload(self, key, value): """Used by :meth:`~mongoengine.Document.reload` to ensure the correct instance is linked to self. """ if isinstance(value, BaseDict): value = [(k, self._reload(k, v)) for k, v in value.items()] value = BaseDict(value, self, key) elif isinstance(value, EmbeddedDocumentList): value = [self._reload(key, v) for v in value] value = EmbeddedDocumentList(value, self, key) elif isinstance(value, BaseList): value = [self._reload(key, v) for v in value] value = BaseList(value, self, key) elif isinstance(value, (EmbeddedDocument, DynamicEmbeddedDocument)): value._instance = None value._changed_fields = [] return value def to_dbref(self): """Returns an instance of :class:`~bson.dbref.DBRef` useful in `__raw__` queries.""" if self.pk is None: msg = "Only saved documents can have a valid dbref" raise OperationError(msg) return DBRef(self.__class__._get_collection_name(), self.pk) @classmethod def register_delete_rule(cls, document_cls, field_name, rule): """This method registers the delete rules to apply when removing this object. """ classes = [ get_document(class_name) for class_name in cls._subclasses if class_name != cls.__name__ ] + [cls] documents = [ get_document(class_name) for class_name in document_cls._subclasses if class_name != document_cls.__name__ ] + [document_cls] for klass in classes: for document_cls in documents: delete_rules = klass._meta.get("delete_rules") or {} delete_rules[(document_cls, field_name)] = rule klass._meta["delete_rules"] = delete_rules @classmethod def drop_collection(cls): """Drops the entire collection associated with this :class:`~mongoengine.Document` type from the database. Raises :class:`OperationError` if the document has no collection set (i.g. if it is `abstract`) """ coll_name = cls._get_collection_name() if not coll_name: raise OperationError( "Document %s has no collection defined (is it abstract ?)" % cls ) cls._collection = None db = cls._get_db() db.drop_collection(coll_name) @classmethod def create_index(cls, keys, background=False, **kwargs): """Creates the given indexes if required. :param keys: a single index key or a list of index keys (to construct a multi-field index); keys may be prefixed with a **+** or a **-** to determine the index ordering :param background: Allows index creation in the background """ index_spec = cls._build_index_spec(keys) index_spec = index_spec.copy() fields = index_spec.pop("fields") index_spec["background"] = background index_spec.update(kwargs) return cls._get_collection().create_index(fields, **index_spec) @classmethod def ensure_indexes(cls): """Checks the document meta data and ensures all the indexes exist. Global defaults can be set in the meta - see :doc:`guide/defining-documents` By default, this will get called automatically upon first interaction with the Document collection (query, save, etc) so unless you disabled `auto_create_index`, you shouldn't have to call this manually. This also gets called upon every call to Document.save if `auto_create_index_on_save` is set to True If called multiple times, MongoDB will not re-recreate indexes if they exist already .. note:: You can disable automatic index creation by setting `auto_create_index` to False in the documents meta data """ background = cls._meta.get("index_background", False) index_opts = cls._meta.get("index_opts") or {} index_cls = cls._meta.get("index_cls", True) collection = cls._get_collection() # determine if an index which we are creating includes # _cls as its first field; if so, we can avoid creating # an extra index on _cls, as mongodb will use the existing # index to service queries against _cls cls_indexed = False # Ensure document-defined indexes are created if cls._meta["index_specs"]: index_spec = cls._meta["index_specs"] for spec in index_spec: spec = spec.copy() fields = spec.pop("fields") cls_indexed = cls_indexed or includes_cls(fields) opts = index_opts.copy() opts.update(spec) # we shouldn't pass 'cls' to the collection.ensureIndex options # because of https://jira.mongodb.org/browse/SERVER-769 if "cls" in opts: del opts["cls"] collection.create_index(fields, background=background, **opts) # If _cls is being used (for polymorphism), it needs an index, # only if another index doesn't begin with _cls if index_cls and not cls_indexed and cls._meta.get("allow_inheritance"): # we shouldn't pass 'cls' to the collection.ensureIndex options # because of https://jira.mongodb.org/browse/SERVER-769 if "cls" in index_opts: del index_opts["cls"] collection.create_index("_cls", background=background, **index_opts) @classmethod def list_indexes(cls): """Lists all indexes that should be created for the Document collection. It includes all the indexes from super- and sub-classes. Note that it will only return the indexes' fields, not the indexes' options """ if cls._meta.get("abstract"): return [] # get all the base classes, subclasses and siblings classes = [] def get_classes(cls): if cls not in classes and isinstance(cls, TopLevelDocumentMetaclass): classes.append(cls) for base_cls in cls.__bases__: if ( isinstance(base_cls, TopLevelDocumentMetaclass) and base_cls != Document and not base_cls._meta.get("abstract") and base_cls._get_collection().full_name == cls._get_collection().full_name and base_cls not in classes ): classes.append(base_cls) get_classes(base_cls) for subclass in cls.__subclasses__(): if ( isinstance(base_cls, TopLevelDocumentMetaclass) and subclass._get_collection().full_name == cls._get_collection().full_name and subclass not in classes ): classes.append(subclass) get_classes(subclass) get_classes(cls) # get the indexes spec for all the gathered classes def get_indexes_spec(cls): indexes = [] if cls._meta["index_specs"]: index_spec = cls._meta["index_specs"] for spec in index_spec: spec = spec.copy() fields = spec.pop("fields") indexes.append(fields) return indexes indexes = [] for klass in classes: for index in get_indexes_spec(klass): if index not in indexes: indexes.append(index) # finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed if [("_id", 1)] not in indexes: indexes.append([("_id", 1)]) if cls._meta.get("index_cls", True) and cls._meta.get("allow_inheritance"): indexes.append([("_cls", 1)]) return indexes @classmethod def compare_indexes(cls): """Compares the indexes defined in MongoEngine with the ones existing in the database. Returns any missing/extra indexes. """ required = cls.list_indexes() existing = [] collection = cls._get_collection() for info in collection.index_information().values(): if "_fts" in info["key"][0]: # Useful for text indexes (but not only) index_type = info["key"][0][1] text_index_fields = info.get("weights").keys() existing.append([(key, index_type) for key in text_index_fields]) else: existing.append(info["key"]) missing = [index for index in required if index not in existing] extra = [index for index in existing if index not in required] # if { _cls: 1 } is missing, make sure it's *really* necessary if [("_cls", 1)] in missing: cls_obsolete = False for index in existing: if includes_cls(index) and index not in extra: cls_obsolete = True break if cls_obsolete: missing.remove([("_cls", 1)]) return {"missing": missing, "extra": extra} class DynamicDocument(Document, metaclass=TopLevelDocumentMetaclass): """A Dynamic Document class allowing flexible, expandable and uncontrolled schemas. As a :class:`~mongoengine.Document` subclass, acts in the same way as an ordinary document but has expanded style properties. Any data passed or set against the :class:`~mongoengine.DynamicDocument` that is not a field is automatically converted into a :class:`~mongoengine.fields.DynamicField` and data can be attributed to that field. .. note:: There is one caveat on Dynamic Documents: undeclared fields cannot start with `_` """ # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = TopLevelDocumentMetaclass _dynamic = True def __delattr__(self, *args, **kwargs): """Delete the attribute by setting to None and allowing _delta to unset it. """ field_name = args[0] if field_name in self._dynamic_fields: setattr(self, field_name, None) self._dynamic_fields[field_name].null = False else: super().__delattr__(*args, **kwargs) class DynamicEmbeddedDocument(EmbeddedDocument, metaclass=DocumentMetaclass): """A Dynamic Embedded Document class allowing flexible, expandable and uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more information about dynamic documents. """ # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = DocumentMetaclass _dynamic = True def __delattr__(self, *args, **kwargs): """Delete the attribute by setting to None and allowing _delta to unset it. """ field_name = args[0] if field_name in self._fields: default = self._fields[field_name].default if callable(default): default = default() setattr(self, field_name, default) else: setattr(self, field_name, None) class MapReduceDocument: """A document returned from a map/reduce query. :param collection: An instance of :class:`~pymongo.Collection` :param key: Document/result key, often an instance of :class:`~bson.objectid.ObjectId`. If supplied as an ``ObjectId`` found in the given ``collection``, the object can be accessed via the ``object`` property. :param value: The result(s) for this key. """ def __init__(self, document, collection, key, value): self._document = document self._collection = collection self.key = key self.value = value @property def object(self): """Lazy-load the object referenced by ``self.key``. ``self.key`` should be the ``primary_key``. """ id_field = self._document()._meta["id_field"] id_field_type = type(id_field) if not isinstance(self.key, id_field_type): try: self.key = id_field_type(self.key) except Exception: raise Exception("Could not cast key as %s" % id_field_type.__name__) if not hasattr(self, "_key_object"): self._key_object = self._document.objects.with_id(self.key) return self._key_object return self._key_object ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/errors.py0000644000175100001730000001001014400345475017677 0ustar00runnerdockerfrom collections import defaultdict __all__ = ( "NotRegistered", "InvalidDocumentError", "LookUpError", "DoesNotExist", "MultipleObjectsReturned", "InvalidQueryError", "OperationError", "NotUniqueError", "BulkWriteError", "FieldDoesNotExist", "ValidationError", "SaveConditionError", "DeprecatedError", ) class MongoEngineException(Exception): pass class NotRegistered(MongoEngineException): pass class InvalidDocumentError(MongoEngineException): pass class LookUpError(AttributeError): pass class DoesNotExist(MongoEngineException): pass class MultipleObjectsReturned(MongoEngineException): pass class InvalidQueryError(MongoEngineException): pass class OperationError(MongoEngineException): pass class NotUniqueError(OperationError): pass class BulkWriteError(OperationError): pass class SaveConditionError(OperationError): pass class FieldDoesNotExist(MongoEngineException): """Raised when trying to set a field not declared in a :class:`~mongoengine.Document` or an :class:`~mongoengine.EmbeddedDocument`. To avoid this behavior on data loading, you should set the :attr:`strict` to ``False`` in the :attr:`meta` dictionary. """ class ValidationError(AssertionError): """Validation exception. May represent an error validating a field or a document containing fields with validation errors. :ivar errors: A dictionary of errors for fields within this document or list, or None if the error is for an individual field. """ errors = {} field_name = None _message = None def __init__(self, message="", **kwargs): super().__init__(message) self.errors = kwargs.get("errors", {}) self.field_name = kwargs.get("field_name") self.message = message def __str__(self): return str(self.message) def __repr__(self): return f"{self.__class__.__name__}({self.message},)" def __getattribute__(self, name): message = super().__getattribute__(name) if name == "message": if self.field_name: message = "%s" % message if self.errors: message = f"{message}({self._format_errors()})" return message def _get_message(self): return self._message def _set_message(self, message): self._message = message message = property(_get_message, _set_message) def to_dict(self): """Returns a dictionary of all errors within a document Keys are field names or list indices and values are the validation error messages, or a nested dictionary of errors for an embedded document or list. """ def build_dict(source): errors_dict = {} if isinstance(source, dict): for field_name, error in source.items(): errors_dict[field_name] = build_dict(error) elif isinstance(source, ValidationError) and source.errors: return build_dict(source.errors) else: return str(source) return errors_dict if not self.errors: return {} return build_dict(self.errors) def _format_errors(self): """Returns a string listing all errors within a document""" def generate_key(value, prefix=""): if isinstance(value, list): value = " ".join([generate_key(k) for k in value]) elif isinstance(value, dict): value = " ".join([generate_key(v, k) for k, v in value.items()]) results = f"{prefix}.{value}" if prefix else value return results error_dict = defaultdict(list) for k, v in self.to_dict().items(): error_dict[generate_key(v)].append(k) return " ".join([f"{k}: {v}" for k, v in error_dict.items()]) class DeprecatedError(MongoEngineException): """Raise when a user uses a feature that has been Deprecated""" pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/fields.py0000644000175100001730000026327314400345475017656 0ustar00runnerdockerimport datetime import decimal import inspect import itertools import re import socket import time import uuid from io import BytesIO from operator import itemgetter import gridfs import pymongo from bson import SON, Binary, DBRef, ObjectId from bson.decimal128 import Decimal128, create_decimal128_context from bson.int64 import Int64 from pymongo import ReturnDocument try: import dateutil except ImportError: dateutil = None else: import dateutil.parser from mongoengine.base import ( BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField, LazyReference, ObjectIdField, get_document, ) from mongoengine.base.utils import LazyRegexCompiler from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.document import Document, EmbeddedDocument from mongoengine.errors import ( DoesNotExist, InvalidQueryError, ValidationError, ) from mongoengine.queryset import DO_NOTHING from mongoengine.queryset.base import BaseQuerySet from mongoengine.queryset.transform import STRING_OPERATORS try: from PIL import Image, ImageOps except ImportError: Image = None ImageOps = None __all__ = ( "StringField", "URLField", "EmailField", "IntField", "LongField", "FloatField", "DecimalField", "BooleanField", "DateTimeField", "DateField", "ComplexDateTimeField", "EmbeddedDocumentField", "ObjectIdField", "GenericEmbeddedDocumentField", "DynamicField", "ListField", "SortedListField", "EmbeddedDocumentListField", "DictField", "MapField", "ReferenceField", "CachedReferenceField", "LazyReferenceField", "GenericLazyReferenceField", "GenericReferenceField", "BinaryField", "GridFSError", "GridFSProxy", "FileField", "ImageGridFsProxy", "ImproperlyConfigured", "ImageField", "GeoPointField", "PointField", "LineStringField", "PolygonField", "SequenceField", "UUIDField", "EnumField", "MultiPointField", "MultiLineStringField", "MultiPolygonField", "GeoJsonBaseField", "Decimal128Field", ) RECURSIVE_REFERENCE_CONSTANT = "self" class StringField(BaseField): """A unicode string field.""" def __init__(self, regex=None, max_length=None, min_length=None, **kwargs): """ :param regex: (optional) A string pattern that will be applied during validation :param max_length: (optional) A max length that will be applied during validation :param min_length: (optional) A min length that will be applied during validation :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField` """ self.regex = re.compile(regex) if regex else None self.max_length = max_length self.min_length = min_length super().__init__(**kwargs) def to_python(self, value): if isinstance(value, str): return value try: value = value.decode("utf-8") except Exception: pass return value def validate(self, value): if not isinstance(value, str): self.error("StringField only accepts string values") if self.max_length is not None and len(value) > self.max_length: self.error("String value is too long") if self.min_length is not None and len(value) < self.min_length: self.error("String value is too short") if self.regex is not None and self.regex.match(value) is None: self.error("String value did not match validation regex") def lookup_member(self, member_name): return None def prepare_query_value(self, op, value): if not isinstance(op, str): return value if op in STRING_OPERATORS: case_insensitive = op.startswith("i") op = op.lstrip("i") flags = re.IGNORECASE if case_insensitive else 0 regex = r"%s" if op == "startswith": regex = r"^%s" elif op == "endswith": regex = r"%s$" elif op == "exact": regex = r"^%s$" elif op == "wholeword": regex = r"\b%s\b" elif op == "regex": regex = value if op == "regex": value = re.compile(regex, flags) else: # escape unsafe characters which could lead to a re.error value = re.escape(value) value = re.compile(regex % value, flags) return super().prepare_query_value(op, value) class URLField(StringField): """A field that validates input as an URL.""" _URL_REGEX = LazyRegexCompiler( r"^(?:[a-z0-9\.\-]*)://" # scheme is validated separately r"(?:(?:[A-Z0-9](?:[A-Z0-9-_]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(? self.max_value: self.error("Integer value is too large") def prepare_query_value(self, op, value): if value is None: return value return super().prepare_query_value(op, int(value)) class LongField(IntField): """64-bit integer field. (Equivalent to IntField since the support to Python2 was dropped)""" def to_mongo(self, value): return Int64(value) class FloatField(BaseField): """Floating point number field.""" def __init__(self, min_value=None, max_value=None, **kwargs): """ :param min_value: (optional) A min value that will be applied during validation :param max_value: (optional) A max value that will be applied during validation :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField` """ self.min_value, self.max_value = min_value, max_value super().__init__(**kwargs) def to_python(self, value): try: value = float(value) except ValueError: pass return value def validate(self, value): if isinstance(value, int): try: value = float(value) except OverflowError: self.error("The value is too large to be converted to float") if not isinstance(value, float): self.error("FloatField only accepts float and integer values") if self.min_value is not None and value < self.min_value: self.error("Float value is too small") if self.max_value is not None and value > self.max_value: self.error("Float value is too large") def prepare_query_value(self, op, value): if value is None: return value return super().prepare_query_value(op, float(value)) class DecimalField(BaseField): """Disclaimer: This field is kept for historical reason but since it converts the values to float, it is not suitable for true decimal storage. Consider using :class:`~mongoengine.fields.Decimal128Field`. Fixed-point decimal number field. Stores the value as a float by default unless `force_string` is used. If using floats, beware of Decimal to float conversion (potential precision loss) """ def __init__( self, min_value=None, max_value=None, force_string=False, precision=2, rounding=decimal.ROUND_HALF_UP, **kwargs, ): """ :param min_value: (optional) A min value that will be applied during validation :param max_value: (optional) A max value that will be applied during validation :param force_string: Store the value as a string (instead of a float). Be aware that this affects query sorting and operation like lte, gte (as string comparison is applied) and some query operator won't work (e.g. inc, dec) :param precision: Number of decimal places to store. :param rounding: The rounding rule from the python decimal library: - decimal.ROUND_CEILING (towards Infinity) - decimal.ROUND_DOWN (towards zero) - decimal.ROUND_FLOOR (towards -Infinity) - decimal.ROUND_HALF_DOWN (to nearest with ties going towards zero) - decimal.ROUND_HALF_EVEN (to nearest with ties going to nearest even integer) - decimal.ROUND_HALF_UP (to nearest with ties going away from zero) - decimal.ROUND_UP (away from zero) - decimal.ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero) Defaults to: ``decimal.ROUND_HALF_UP`` :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField` """ self.min_value = min_value self.max_value = max_value self.force_string = force_string if precision < 0 or not isinstance(precision, int): self.error("precision must be a positive integer") self.precision = precision self.rounding = rounding super().__init__(**kwargs) def to_python(self, value): # Convert to string for python 2.6 before casting to Decimal try: value = decimal.Decimal("%s" % value) except (TypeError, ValueError, decimal.InvalidOperation): return value if self.precision > 0: return value.quantize( decimal.Decimal(".%s" % ("0" * self.precision)), rounding=self.rounding ) else: return value.quantize(decimal.Decimal(), rounding=self.rounding) def to_mongo(self, value): if self.force_string: return str(self.to_python(value)) return float(self.to_python(value)) def validate(self, value): if not isinstance(value, decimal.Decimal): if not isinstance(value, str): value = str(value) try: value = decimal.Decimal(value) except (TypeError, ValueError, decimal.InvalidOperation) as exc: self.error("Could not convert value to decimal: %s" % exc) if self.min_value is not None and value < self.min_value: self.error("Decimal value is too small") if self.max_value is not None and value > self.max_value: self.error("Decimal value is too large") def prepare_query_value(self, op, value): if value is None: return value return super().prepare_query_value(op, self.to_mongo(value)) class BooleanField(BaseField): """Boolean field type.""" def to_python(self, value): try: value = bool(value) except (ValueError, TypeError): pass return value def validate(self, value): if not isinstance(value, bool): self.error("BooleanField only accepts boolean values") class DateTimeField(BaseField): """Datetime field. Uses the python-dateutil library if available alternatively use time.strptime to parse the dates. Note: python-dateutil's parser is fully featured and when installed you can utilise it to convert varying types of date formats into valid python datetime objects. Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow) Note: Microseconds are rounded to the nearest millisecond. Pre UTC microsecond support is effectively broken. Use :class:`~mongoengine.fields.ComplexDateTimeField` if you need accurate microsecond support. """ def validate(self, value): new_value = self.to_mongo(value) if not isinstance(new_value, (datetime.datetime, datetime.date)): self.error('cannot parse date "%s"' % value) def to_mongo(self, value): if value is None: return value if isinstance(value, datetime.datetime): return value if isinstance(value, datetime.date): return datetime.datetime(value.year, value.month, value.day) if callable(value): return value() if isinstance(value, str): return self._parse_datetime(value) else: return None @staticmethod def _parse_datetime(value): # Attempt to parse a datetime from a string value = value.strip() if not value: return None if dateutil: try: return dateutil.parser.parse(value) except (TypeError, ValueError, OverflowError): return None # split usecs, because they are not recognized by strptime. if "." in value: try: value, usecs = value.split(".") usecs = int(usecs) except ValueError: return None else: usecs = 0 kwargs = {"microsecond": usecs} try: # Seconds are optional, so try converting seconds first. return datetime.datetime( *time.strptime(value, "%Y-%m-%d %H:%M:%S")[:6], **kwargs ) except ValueError: try: # Try without seconds. return datetime.datetime( *time.strptime(value, "%Y-%m-%d %H:%M")[:5], **kwargs ) except ValueError: # Try without hour/minutes/seconds. try: return datetime.datetime( *time.strptime(value, "%Y-%m-%d")[:3], **kwargs ) except ValueError: return None def prepare_query_value(self, op, value): return super().prepare_query_value(op, self.to_mongo(value)) class DateField(DateTimeField): def to_mongo(self, value): value = super().to_mongo(value) # drop hours, minutes, seconds if isinstance(value, datetime.datetime): value = datetime.datetime(value.year, value.month, value.day) return value def to_python(self, value): value = super().to_python(value) # convert datetime to date if isinstance(value, datetime.datetime): value = datetime.date(value.year, value.month, value.day) return value class ComplexDateTimeField(StringField): """ ComplexDateTimeField handles microseconds exactly instead of rounding like DateTimeField does. Derives from a StringField so you can do `gte` and `lte` filtering by using lexicographical comparison when filtering / sorting strings. The stored string has the following format: YYYY,MM,DD,HH,MM,SS,NNNNNN Where NNNNNN is the number of microseconds of the represented `datetime`. The `,` as the separator can be easily modified by passing the `separator` keyword when initializing the field. Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow) """ def __init__(self, separator=",", **kwargs): """ :param separator: Allows to customize the separator used for storage (default ``,``) :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField` """ self.separator = separator self.format = separator.join(["%Y", "%m", "%d", "%H", "%M", "%S", "%f"]) super().__init__(**kwargs) def _convert_from_datetime(self, val): """ Convert a `datetime` object to a string representation (which will be stored in MongoDB). This is the reverse function of `_convert_from_string`. >>> a = datetime(2011, 6, 8, 20, 26, 24, 92284) >>> ComplexDateTimeField()._convert_from_datetime(a) '2011,06,08,20,26,24,092284' """ return val.strftime(self.format) def _convert_from_string(self, data): """ Convert a string representation to a `datetime` object (the object you will manipulate). This is the reverse function of `_convert_from_datetime`. >>> a = '2011,06,08,20,26,24,092284' >>> ComplexDateTimeField()._convert_from_string(a) datetime.datetime(2011, 6, 8, 20, 26, 24, 92284) """ values = [int(d) for d in data.split(self.separator)] return datetime.datetime(*values) def __get__(self, instance, owner): if instance is None: return self data = super().__get__(instance, owner) if isinstance(data, datetime.datetime) or data is None: return data return self._convert_from_string(data) def __set__(self, instance, value): super().__set__(instance, value) value = instance._data[self.name] if value is not None: if isinstance(value, datetime.datetime): instance._data[self.name] = self._convert_from_datetime(value) else: instance._data[self.name] = value def validate(self, value): value = self.to_python(value) if not isinstance(value, datetime.datetime): self.error("Only datetime objects may used in a ComplexDateTimeField") def to_python(self, value): original_value = value try: return self._convert_from_string(value) except Exception: return original_value def to_mongo(self, value): value = self.to_python(value) return self._convert_from_datetime(value) def prepare_query_value(self, op, value): if value is None: return value return super().prepare_query_value(op, self._convert_from_datetime(value)) class EmbeddedDocumentField(BaseField): """An embedded document field - with a declared document_type. Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`. """ def __init__(self, document_type, **kwargs): # XXX ValidationError raised outside of the "validate" method. if not ( isinstance(document_type, str) or issubclass(document_type, EmbeddedDocument) ): self.error( "Invalid embedded document class provided to an " "EmbeddedDocumentField" ) self.document_type_obj = document_type super().__init__(**kwargs) @property def document_type(self): if isinstance(self.document_type_obj, str): if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT: resolved_document_type = self.owner_document else: resolved_document_type = get_document(self.document_type_obj) if not issubclass(resolved_document_type, EmbeddedDocument): # Due to the late resolution of the document_type # There is a chance that it won't be an EmbeddedDocument (#1661) self.error( "Invalid embedded document class provided to an " "EmbeddedDocumentField" ) self.document_type_obj = resolved_document_type return self.document_type_obj def to_python(self, value): if not isinstance(value, self.document_type): return self.document_type._from_son( value, _auto_dereference=self._auto_dereference ) return value def to_mongo(self, value, use_db_field=True, fields=None): if not isinstance(value, self.document_type): return value return self.document_type.to_mongo(value, use_db_field, fields) def validate(self, value, clean=True): """Make sure that the document instance is an instance of the EmbeddedDocument subclass provided when the document was defined. """ # Using isinstance also works for subclasses of self.document if not isinstance(value, self.document_type): self.error( "Invalid embedded document instance provided to an " "EmbeddedDocumentField" ) self.document_type.validate(value, clean) def lookup_member(self, member_name): doc_and_subclasses = [self.document_type] + self.document_type.__subclasses__() for doc_type in doc_and_subclasses: field = doc_type._fields.get(member_name) if field: return field def prepare_query_value(self, op, value): if value is not None and not isinstance(value, self.document_type): # Short circuit for special operators, returning them as is if isinstance(value, dict) and all(k.startswith("$") for k in value.keys()): return value try: value = self.document_type._from_son(value) except ValueError: raise InvalidQueryError( "Querying the embedded document '%s' failed, due to an invalid query value" % (self.document_type._class_name,) ) super().prepare_query_value(op, value) return self.to_mongo(value) class GenericEmbeddedDocumentField(BaseField): """A generic embedded document field - allows any :class:`~mongoengine.EmbeddedDocument` to be stored. Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`. .. note :: You can use the choices param to limit the acceptable EmbeddedDocument types """ def prepare_query_value(self, op, value): return super().prepare_query_value(op, self.to_mongo(value)) def to_python(self, value): if isinstance(value, dict): doc_cls = get_document(value["_cls"]) value = doc_cls._from_son(value) return value def validate(self, value, clean=True): if self.choices and isinstance(value, SON): for choice in self.choices: if value["_cls"] == choice._class_name: return True if not isinstance(value, EmbeddedDocument): self.error( "Invalid embedded document instance provided to an " "GenericEmbeddedDocumentField" ) value.validate(clean=clean) def lookup_member(self, member_name): document_choices = self.choices or [] for document_choice in document_choices: doc_and_subclasses = [document_choice] + document_choice.__subclasses__() for doc_type in doc_and_subclasses: field = doc_type._fields.get(member_name) if field: return field def to_mongo(self, document, use_db_field=True, fields=None): if document is None: return None data = document.to_mongo(use_db_field, fields) if "_cls" not in data: data["_cls"] = document._class_name return data class DynamicField(BaseField): """A truly dynamic field type capable of handling different and varying types of data. Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" def to_mongo(self, value, use_db_field=True, fields=None): """Convert a Python type to a MongoDB compatible type.""" if isinstance(value, str): return value if hasattr(value, "to_mongo"): cls = value.__class__ val = value.to_mongo(use_db_field, fields) # If we its a document thats not inherited add _cls if isinstance(value, Document): val = {"_ref": value.to_dbref(), "_cls": cls.__name__} if isinstance(value, EmbeddedDocument): val["_cls"] = cls.__name__ return val if not isinstance(value, (dict, list, tuple)): return value is_list = False if not hasattr(value, "items"): is_list = True value = {k: v for k, v in enumerate(value)} data = {} for k, v in value.items(): data[k] = self.to_mongo(v, use_db_field, fields) value = data if is_list: # Convert back to a list value = [v for k, v in sorted(data.items(), key=itemgetter(0))] return value def to_python(self, value): if isinstance(value, dict) and "_cls" in value: doc_cls = get_document(value["_cls"]) if "_ref" in value: value = doc_cls._get_db().dereference(value["_ref"]) return doc_cls._from_son(value) return super().to_python(value) def lookup_member(self, member_name): return member_name def prepare_query_value(self, op, value): if isinstance(value, str): return StringField().prepare_query_value(op, value) return super().prepare_query_value(op, self.to_mongo(value)) def validate(self, value, clean=True): if hasattr(value, "validate"): value.validate(clean=clean) class ListField(ComplexBaseField): """A list field that wraps a standard field, allowing multiple instances of the field to be used as a list in the database. If using with ReferenceFields see: :ref:`many-to-many-with-listfields` .. note:: Required means it cannot be empty - as the default for ListFields is [] """ def __init__(self, field=None, max_length=None, **kwargs): self.max_length = max_length kwargs.setdefault("default", lambda: []) super().__init__(field=field, **kwargs) def __get__(self, instance, owner): if instance is None: # Document class being used rather than a document object return self value = instance._data.get(self.name) LazyReferenceField = _import_class("LazyReferenceField") GenericLazyReferenceField = _import_class("GenericLazyReferenceField") if ( isinstance(self.field, (LazyReferenceField, GenericLazyReferenceField)) and value ): instance._data[self.name] = [self.field.build_lazyref(x) for x in value] return super().__get__(instance, owner) def validate(self, value): """Make sure that a list of valid fields is being used.""" if not isinstance(value, (list, tuple, BaseQuerySet)): self.error("Only lists and tuples may be used in a list field") # Validate that max_length is not exceeded. # NOTE It's still possible to bypass this enforcement by using $push. # However, if the document is reloaded after $push and then re-saved, # the validation error will be raised. if self.max_length is not None and len(value) > self.max_length: self.error("List is too long") super().validate(value) def prepare_query_value(self, op, value): # Validate that the `set` operator doesn't contain more items than `max_length`. if op == "set" and self.max_length is not None and len(value) > self.max_length: self.error("List is too long") if self.field: # If the value is iterable and it's not a string nor a # BaseDocument, call prepare_query_value for each of its items. if ( op in ("set", "unset", None) and hasattr(value, "__iter__") and not isinstance(value, str) and not isinstance(value, BaseDocument) ): return [self.field.prepare_query_value(op, v) for v in value] return self.field.prepare_query_value(op, value) return super().prepare_query_value(op, value) class EmbeddedDocumentListField(ListField): """A :class:`~mongoengine.ListField` designed specially to hold a list of embedded documents to provide additional query helpers. .. note:: The only valid list values are subclasses of :class:`~mongoengine.EmbeddedDocument`. """ def __init__(self, document_type, **kwargs): """ :param document_type: The type of :class:`~mongoengine.EmbeddedDocument` the list will hold. :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.ListField` """ super().__init__(field=EmbeddedDocumentField(document_type), **kwargs) class SortedListField(ListField): """A ListField that sorts the contents of its list before writing to the database in order to ensure that a sorted list is always retrieved. .. warning:: There is a potential race condition when handling lists. If you set / save the whole list then other processes trying to save the whole list as well could overwrite changes. The safest way to append to a list is to perform a push operation. """ def __init__(self, field, **kwargs): self._ordering = kwargs.pop("ordering", None) self._order_reverse = kwargs.pop("reverse", False) super().__init__(field, **kwargs) def to_mongo(self, value, use_db_field=True, fields=None): value = super().to_mongo(value, use_db_field, fields) if self._ordering is not None: return sorted( value, key=itemgetter(self._ordering), reverse=self._order_reverse ) return sorted(value, reverse=self._order_reverse) def key_not_string(d): """Helper function to recursively determine if any key in a dictionary is not a string. """ for k, v in d.items(): if not isinstance(k, str) or (isinstance(v, dict) and key_not_string(v)): return True def key_starts_with_dollar(d): """Helper function to recursively determine if any key in a dictionary starts with a dollar """ for k, v in d.items(): if (k.startswith("$")) or (isinstance(v, dict) and key_starts_with_dollar(v)): return True class DictField(ComplexBaseField): """A dictionary field that wraps a standard Python dictionary. This is similar to an embedded document, but the structure is not defined. .. note:: Required means it cannot be empty - as the default for DictFields is {} """ def __init__(self, field=None, *args, **kwargs): self._auto_dereference = False kwargs.setdefault("default", lambda: {}) super().__init__(*args, field=field, **kwargs) def validate(self, value): """Make sure that a list of valid fields is being used.""" if not isinstance(value, dict): self.error("Only dictionaries may be used in a DictField") if key_not_string(value): msg = "Invalid dictionary key - documents must have only string keys" self.error(msg) # Following condition applies to MongoDB >= 3.6 # older Mongo has stricter constraints but # it will be rejected upon insertion anyway # Having a validation that depends on the MongoDB version # is not straightforward as the field isn't aware of the connected Mongo if key_starts_with_dollar(value): self.error( 'Invalid dictionary key name - keys may not startswith "$" characters' ) super().validate(value) def lookup_member(self, member_name): return DictField(db_field=member_name) def prepare_query_value(self, op, value): match_operators = [*STRING_OPERATORS] if op in match_operators and isinstance(value, str): return StringField().prepare_query_value(op, value) if hasattr( self.field, "field" ): # Used for instance when using DictField(ListField(IntField())) if op in ("set", "unset") and isinstance(value, dict): return { k: self.field.prepare_query_value(op, v) for k, v in value.items() } return self.field.prepare_query_value(op, value) return super().prepare_query_value(op, value) class MapField(DictField): """A field that maps a name to a specified field type. Similar to a DictField, except the 'value' of each item must match the specified field type. """ def __init__(self, field=None, *args, **kwargs): # XXX ValidationError raised outside the "validate" method. if not isinstance(field, BaseField): self.error("Argument to MapField constructor must be a valid field") super().__init__(field=field, *args, **kwargs) class ReferenceField(BaseField): """A reference to a document that will be automatically dereferenced on access (lazily). Note this means you will get a database I/O access everytime you access this field. This is necessary because the field returns a :class:`~mongoengine.Document` which precise type can depend of the value of the `_cls` field present in the document in database. In short, using this type of field can lead to poor performances (especially if you access this field only to retrieve it `pk` field which is already known before dereference). To solve this you should consider using the :class:`~mongoengine.fields.LazyReferenceField`. Use the `reverse_delete_rule` to handle what should happen if the document the field is referencing is deleted. EmbeddedDocuments, DictFields and MapFields does not support reverse_delete_rule and an `InvalidDocumentError` will be raised if trying to set on one of these Document / Field types. The options are: * DO_NOTHING (0) - don't do anything (default). * NULLIFY (1) - Updates the reference to null. * CASCADE (2) - Deletes the documents associated with the reference. * DENY (3) - Prevent the deletion of the reference object. * PULL (4) - Pull the reference from a :class:`~mongoengine.fields.ListField` of references Alternative syntax for registering delete rules (useful when implementing bi-directional delete rules) .. code-block:: python class Org(Document): owner = ReferenceField('User') class User(Document): org = ReferenceField('Org', reverse_delete_rule=CASCADE) User.register_delete_rule(Org, 'owner', DENY) """ def __init__( self, document_type, dbref=False, reverse_delete_rule=DO_NOTHING, **kwargs ): """Initialises the Reference Field. :param document_type: The type of Document that will be referenced :param dbref: Store the reference as :class:`~pymongo.dbref.DBRef` or as the :class:`~pymongo.objectid.ObjectId`. :param reverse_delete_rule: Determines what to do when the referring object is deleted :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField` .. note :: A reference to an abstract document type is always stored as a :class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`. """ # XXX ValidationError raised outside of the "validate" method. if not isinstance(document_type, str) and not issubclass( document_type, Document ): self.error( "Argument to ReferenceField constructor must be a " "document class or a string" ) self.dbref = dbref self.document_type_obj = document_type self.reverse_delete_rule = reverse_delete_rule super().__init__(**kwargs) @property def document_type(self): if isinstance(self.document_type_obj, str): if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT: self.document_type_obj = self.owner_document else: self.document_type_obj = get_document(self.document_type_obj) return self.document_type_obj @staticmethod def _lazy_load_ref(ref_cls, dbref): dereferenced_son = ref_cls._get_db().dereference(dbref) if dereferenced_son is None: raise DoesNotExist(f"Trying to dereference unknown document {dbref}") return ref_cls._from_son(dereferenced_son) def __get__(self, instance, owner): """Descriptor to allow lazy dereferencing.""" if instance is None: # Document class being used rather than a document object return self # Get value from document instance if available ref_value = instance._data.get(self.name) auto_dereference = instance._fields[self.name]._auto_dereference # Dereference DBRefs if auto_dereference and isinstance(ref_value, DBRef): if hasattr(ref_value, "cls"): # Dereference using the class type specified in the reference cls = get_document(ref_value.cls) else: cls = self.document_type instance._data[self.name] = self._lazy_load_ref(cls, ref_value) return super().__get__(instance, owner) def to_mongo(self, document): if isinstance(document, DBRef): if not self.dbref: return document.id return document if isinstance(document, Document): # We need the id from the saved object to create the DBRef id_ = document.pk # XXX ValidationError raised outside of the "validate" method. if id_ is None: self.error( "You can only reference documents once they have" " been saved to the database" ) # Use the attributes from the document instance, so that they # override the attributes of this field's document type cls = document else: id_ = document cls = self.document_type id_field_name = cls._meta["id_field"] id_field = cls._fields[id_field_name] id_ = id_field.to_mongo(id_) if self.document_type._meta.get("abstract"): collection = cls._get_collection_name() return DBRef(collection, id_, cls=cls._class_name) elif self.dbref: collection = cls._get_collection_name() return DBRef(collection, id_) return id_ def to_python(self, value): """Convert a MongoDB-compatible type to a Python type.""" if not self.dbref and not isinstance( value, (DBRef, Document, EmbeddedDocument) ): collection = self.document_type._get_collection_name() value = DBRef(collection, self.document_type.id.to_python(value)) return value def prepare_query_value(self, op, value): if value is None: return None super().prepare_query_value(op, value) return self.to_mongo(value) def validate(self, value): if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)): self.error( "A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents" ) if isinstance(value, Document) and value.id is None: self.error( "You can only reference documents once they have been " "saved to the database" ) def lookup_member(self, member_name): return self.document_type._fields.get(member_name) class CachedReferenceField(BaseField): """A referencefield with cache fields to purpose pseudo-joins""" def __init__(self, document_type, fields=None, auto_sync=True, **kwargs): """Initialises the Cached Reference Field. :param document_type: The type of Document that will be referenced :param fields: A list of fields to be cached in document :param auto_sync: if True documents are auto updated :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField` """ if fields is None: fields = [] # XXX ValidationError raised outside of the "validate" method. if not isinstance(document_type, str) and not ( inspect.isclass(document_type) and issubclass(document_type, Document) ): self.error( "Argument to CachedReferenceField constructor must be a" " document class or a string" ) self.auto_sync = auto_sync self.document_type_obj = document_type self.fields = fields super().__init__(**kwargs) def start_listener(self): from mongoengine import signals signals.post_save.connect(self.on_document_pre_save, sender=self.document_type) def on_document_pre_save(self, sender, document, created, **kwargs): if created: return None update_kwargs = { f"set__{self.name}__{key}": val for key, val in document._delta()[0].items() if key in self.fields } if update_kwargs: filter_kwargs = {} filter_kwargs[self.name] = document self.owner_document.objects(**filter_kwargs).update(**update_kwargs) def to_python(self, value): if isinstance(value, dict): collection = self.document_type._get_collection_name() value = DBRef(collection, self.document_type.id.to_python(value["_id"])) return self.document_type._from_son( self.document_type._get_db().dereference(value) ) return value @property def document_type(self): if isinstance(self.document_type_obj, str): if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT: self.document_type_obj = self.owner_document else: self.document_type_obj = get_document(self.document_type_obj) return self.document_type_obj @staticmethod def _lazy_load_ref(ref_cls, dbref): dereferenced_son = ref_cls._get_db().dereference(dbref) if dereferenced_son is None: raise DoesNotExist(f"Trying to dereference unknown document {dbref}") return ref_cls._from_son(dereferenced_son) def __get__(self, instance, owner): if instance is None: # Document class being used rather than a document object return self # Get value from document instance if available value = instance._data.get(self.name) auto_dereference = instance._fields[self.name]._auto_dereference # Dereference DBRefs if auto_dereference and isinstance(value, DBRef): instance._data[self.name] = self._lazy_load_ref(self.document_type, value) return super().__get__(instance, owner) def to_mongo(self, document, use_db_field=True, fields=None): id_field_name = self.document_type._meta["id_field"] id_field = self.document_type._fields[id_field_name] # XXX ValidationError raised outside of the "validate" method. if isinstance(document, Document): # We need the id from the saved object to create the DBRef id_ = document.pk if id_ is None: self.error( "You can only reference documents once they have" " been saved to the database" ) else: self.error("Only accept a document object") value = SON((("_id", id_field.to_mongo(id_)),)) if fields: new_fields = [f for f in self.fields if f in fields] else: new_fields = self.fields value.update(dict(document.to_mongo(use_db_field, fields=new_fields))) return value def prepare_query_value(self, op, value): if value is None: return None # XXX ValidationError raised outside of the "validate" method. if isinstance(value, Document): if value.pk is None: self.error( "You can only reference documents once they have" " been saved to the database" ) value_dict = {"_id": value.pk} for field in self.fields: value_dict.update({field: value[field]}) return value_dict raise NotImplementedError def validate(self, value): if not isinstance(value, self.document_type): self.error("A CachedReferenceField only accepts documents") if isinstance(value, Document) and value.id is None: self.error( "You can only reference documents once they have been " "saved to the database" ) def lookup_member(self, member_name): return self.document_type._fields.get(member_name) def sync_all(self): """ Sync all cached fields on demand. Caution: this operation may be slower. """ update_key = "set__%s" % self.name for doc in self.document_type.objects: filter_kwargs = {} filter_kwargs[self.name] = doc update_kwargs = {} update_kwargs[update_key] = doc self.owner_document.objects(**filter_kwargs).update(**update_kwargs) class GenericReferenceField(BaseField): """A reference to *any* :class:`~mongoengine.document.Document` subclass that will be automatically dereferenced on access (lazily). Note this field works the same way as :class:`~mongoengine.document.ReferenceField`, doing database I/O access the first time it is accessed (even if it's to access it ``pk`` or ``id`` field). To solve this you should consider using the :class:`~mongoengine.fields.GenericLazyReferenceField`. .. note :: * Any documents used as a generic reference must be registered in the document registry. Importing the model will automatically register it. * You can use the choices param to limit the acceptable Document types """ def __init__(self, *args, **kwargs): choices = kwargs.pop("choices", None) super().__init__(*args, **kwargs) self.choices = [] # Keep the choices as a list of allowed Document class names if choices: for choice in choices: if isinstance(choice, str): self.choices.append(choice) elif isinstance(choice, type) and issubclass(choice, Document): self.choices.append(choice._class_name) else: # XXX ValidationError raised outside of the "validate" # method. self.error( "Invalid choices provided: must be a list of" "Document subclasses and/or str" ) def _validate_choices(self, value): if isinstance(value, dict): # If the field has not been dereferenced, it is still a dict # of class and DBRef value = value.get("_cls") elif isinstance(value, Document): value = value._class_name super()._validate_choices(value) @staticmethod def _lazy_load_ref(ref_cls, dbref): dereferenced_son = ref_cls._get_db().dereference(dbref) if dereferenced_son is None: raise DoesNotExist(f"Trying to dereference unknown document {dbref}") return ref_cls._from_son(dereferenced_son) def __get__(self, instance, owner): if instance is None: return self value = instance._data.get(self.name) auto_dereference = instance._fields[self.name]._auto_dereference if auto_dereference and isinstance(value, dict): doc_cls = get_document(value["_cls"]) instance._data[self.name] = self._lazy_load_ref(doc_cls, value["_ref"]) return super().__get__(instance, owner) def validate(self, value): if not isinstance(value, (Document, DBRef, dict, SON)): self.error("GenericReferences can only contain documents") if isinstance(value, (dict, SON)): if "_ref" not in value or "_cls" not in value: self.error("GenericReferences can only contain documents") # We need the id from the saved object to create the DBRef elif isinstance(value, Document) and value.id is None: self.error( "You can only reference documents once they have been" " saved to the database" ) def to_mongo(self, document): if document is None: return None if isinstance(document, (dict, SON, ObjectId, DBRef)): return document id_field_name = document.__class__._meta["id_field"] id_field = document.__class__._fields[id_field_name] if isinstance(document, Document): # We need the id from the saved object to create the DBRef id_ = document.id if id_ is None: # XXX ValidationError raised outside of the "validate" method. self.error( "You can only reference documents once they have" " been saved to the database" ) else: id_ = document id_ = id_field.to_mongo(id_) collection = document._get_collection_name() ref = DBRef(collection, id_) return SON((("_cls", document._class_name), ("_ref", ref))) def prepare_query_value(self, op, value): if value is None: return None return self.to_mongo(value) class BinaryField(BaseField): """A binary data field.""" def __init__(self, max_bytes=None, **kwargs): self.max_bytes = max_bytes super().__init__(**kwargs) def __set__(self, instance, value): """Handle bytearrays in python 3.1""" if isinstance(value, bytearray): value = bytes(value) return super().__set__(instance, value) def to_mongo(self, value): return Binary(value) def validate(self, value): if not isinstance(value, (bytes, Binary)): self.error( "BinaryField only accepts instances of " "(%s, %s, Binary)" % (bytes.__name__, Binary.__name__) ) if self.max_bytes is not None and len(value) > self.max_bytes: self.error("Binary value is too long") def prepare_query_value(self, op, value): if value is None: return value return super().prepare_query_value(op, self.to_mongo(value)) class EnumField(BaseField): """Enumeration Field. Values are stored underneath as is, so it will only work with simple types (str, int, etc) that are bson encodable Example usage: .. code-block:: python class Status(Enum): NEW = 'new' ONGOING = 'ongoing' DONE = 'done' class ModelWithEnum(Document): status = EnumField(Status, default=Status.NEW) ModelWithEnum(status='done') ModelWithEnum(status=Status.DONE) Enum fields can be searched using enum or its value: .. code-block:: python ModelWithEnum.objects(status='new').count() ModelWithEnum.objects(status=Status.NEW).count() The values can be restricted to a subset of the enum by using the ``choices`` parameter: .. code-block:: python class ModelWithEnum(Document): status = EnumField(Status, choices=[Status.NEW, Status.DONE]) """ def __init__(self, enum, **kwargs): self._enum_cls = enum if kwargs.get("choices"): invalid_choices = [] for choice in kwargs["choices"]: if not isinstance(choice, enum): invalid_choices.append(choice) if invalid_choices: raise ValueError("Invalid choices: %r" % invalid_choices) else: kwargs["choices"] = list(self._enum_cls) # Implicit validator super().__init__(**kwargs) def validate(self, value): if isinstance(value, self._enum_cls): return super().validate(value) try: self._enum_cls(value) except ValueError: self.error(f"{value} is not a valid {self._enum_cls}") def to_python(self, value): value = super().to_python(value) if not isinstance(value, self._enum_cls): try: return self._enum_cls(value) except ValueError: return value return value def __set__(self, instance, value): return super().__set__(instance, self.to_python(value)) def to_mongo(self, value): if isinstance(value, self._enum_cls): return value.value return value def prepare_query_value(self, op, value): if value is None: return value return super().prepare_query_value(op, self.to_mongo(value)) class GridFSError(Exception): pass class GridFSProxy: """Proxy object to handle writing and reading of files to and from GridFS""" _fs = None def __init__( self, grid_id=None, key=None, instance=None, db_alias=DEFAULT_CONNECTION_NAME, collection_name="fs", ): self.grid_id = grid_id # Store GridFS id for file self.key = key self.instance = instance self.db_alias = db_alias self.collection_name = collection_name self.newfile = None # Used for partial writes self.gridout = None def __getattr__(self, name): attrs = ( "_fs", "grid_id", "key", "instance", "db_alias", "collection_name", "newfile", "gridout", ) if name in attrs: return self.__getattribute__(name) obj = self.get() if hasattr(obj, name): return getattr(obj, name) raise AttributeError def __get__(self, instance, value): return self def __bool__(self): return bool(self.grid_id) def __getstate__(self): self_dict = self.__dict__ self_dict["_fs"] = None return self_dict def __copy__(self): copied = GridFSProxy() copied.__dict__.update(self.__getstate__()) return copied def __deepcopy__(self, memo): return self.__copy__() def __repr__(self): return f"<{self.__class__.__name__}: {self.grid_id}>" def __str__(self): gridout = self.get() filename = gridout.filename if gridout else "" return f"<{self.__class__.__name__}: {filename} ({self.grid_id})>" def __eq__(self, other): if isinstance(other, GridFSProxy): return ( (self.grid_id == other.grid_id) and (self.collection_name == other.collection_name) and (self.db_alias == other.db_alias) ) else: return False def __ne__(self, other): return not self == other @property def fs(self): if not self._fs: self._fs = gridfs.GridFS(get_db(self.db_alias), self.collection_name) return self._fs def get(self, grid_id=None): if grid_id: self.grid_id = grid_id if self.grid_id is None: return None try: if self.gridout is None: self.gridout = self.fs.get(self.grid_id) return self.gridout except Exception: # File has been deleted return None def new_file(self, **kwargs): self.newfile = self.fs.new_file(**kwargs) self.grid_id = self.newfile._id self._mark_as_changed() def put(self, file_obj, **kwargs): if self.grid_id: raise GridFSError( "This document already has a file. Either delete " "it or call replace to overwrite it" ) self.grid_id = self.fs.put(file_obj, **kwargs) self._mark_as_changed() def write(self, string): if self.grid_id: if not self.newfile: raise GridFSError( "This document already has a file. Either " "delete it or call replace to overwrite it" ) else: self.new_file() self.newfile.write(string) def writelines(self, lines): if not self.newfile: self.new_file() self.grid_id = self.newfile._id self.newfile.writelines(lines) def read(self, size=-1): gridout = self.get() if gridout is None: return None else: try: return gridout.read(size) except Exception: return "" def delete(self): # Delete file from GridFS, FileField still remains self.fs.delete(self.grid_id) self.grid_id = None self.gridout = None self._mark_as_changed() def replace(self, file_obj, **kwargs): self.delete() self.put(file_obj, **kwargs) def close(self): if self.newfile: self.newfile.close() def _mark_as_changed(self): """Inform the instance that `self.key` has been changed""" if self.instance: self.instance._mark_as_changed(self.key) class FileField(BaseField): """A GridFS storage field.""" proxy_class = GridFSProxy def __init__( self, db_alias=DEFAULT_CONNECTION_NAME, collection_name="fs", **kwargs ): super().__init__(**kwargs) self.collection_name = collection_name self.db_alias = db_alias def __get__(self, instance, owner): if instance is None: return self # Check if a file already exists for this model grid_file = instance._data.get(self.name) if not isinstance(grid_file, self.proxy_class): grid_file = self.get_proxy_obj(key=self.name, instance=instance) instance._data[self.name] = grid_file if not grid_file.key: grid_file.key = self.name grid_file.instance = instance return grid_file def __set__(self, instance, value): key = self.name if ( hasattr(value, "read") and not isinstance(value, GridFSProxy) ) or isinstance(value, (bytes, str)): # using "FileField() = file/string" notation grid_file = instance._data.get(self.name) # If a file already exists, delete it if grid_file: try: grid_file.delete() except Exception: pass # Create a new proxy object as we don't already have one instance._data[key] = self.get_proxy_obj(key=key, instance=instance) instance._data[key].put(value) else: instance._data[key] = value instance._mark_as_changed(key) def get_proxy_obj(self, key, instance, db_alias=None, collection_name=None): if db_alias is None: db_alias = self.db_alias if collection_name is None: collection_name = self.collection_name return self.proxy_class( key=key, instance=instance, db_alias=db_alias, collection_name=collection_name, ) def to_mongo(self, value): # Store the GridFS file id in MongoDB if isinstance(value, self.proxy_class) and value.grid_id is not None: return value.grid_id return None def to_python(self, value): if value is not None: return self.proxy_class( value, collection_name=self.collection_name, db_alias=self.db_alias ) def validate(self, value): if value.grid_id is not None: if not isinstance(value, self.proxy_class): self.error("FileField only accepts GridFSProxy values") if not isinstance(value.grid_id, ObjectId): self.error("Invalid GridFSProxy value") class ImageGridFsProxy(GridFSProxy): """Proxy for ImageField""" def put(self, file_obj, **kwargs): """ Insert a image in database applying field properties (size, thumbnail_size) """ field = self.instance._fields[self.key] # Handle nested fields if hasattr(field, "field") and isinstance(field.field, FileField): field = field.field try: img = Image.open(file_obj) img_format = img.format except Exception as e: raise ValidationError("Invalid image: %s" % e) # Progressive JPEG # TODO: fixme, at least unused, at worst bad implementation progressive = img.info.get("progressive") or False if ( kwargs.get("progressive") and isinstance(kwargs.get("progressive"), bool) and img_format == "JPEG" ): progressive = True else: progressive = False if field.size and ( img.size[0] > field.size["width"] or img.size[1] > field.size["height"] ): size = field.size if size["force"]: img = ImageOps.fit( img, (size["width"], size["height"]), Image.ANTIALIAS ) else: img.thumbnail((size["width"], size["height"]), Image.ANTIALIAS) thumbnail = None if field.thumbnail_size: size = field.thumbnail_size if size["force"]: thumbnail = ImageOps.fit( img, (size["width"], size["height"]), Image.ANTIALIAS ) else: thumbnail = img.copy() thumbnail.thumbnail((size["width"], size["height"]), Image.ANTIALIAS) if thumbnail: thumb_id = self._put_thumbnail(thumbnail, img_format, progressive) else: thumb_id = None w, h = img.size io = BytesIO() img.save(io, img_format, progressive=progressive) io.seek(0) return super().put( io, width=w, height=h, format=img_format, thumbnail_id=thumb_id, **kwargs ) def delete(self, *args, **kwargs): # deletes thumbnail out = self.get() if out and out.thumbnail_id: self.fs.delete(out.thumbnail_id) return super().delete() def _put_thumbnail(self, thumbnail, format, progressive, **kwargs): w, h = thumbnail.size io = BytesIO() thumbnail.save(io, format, progressive=progressive) io.seek(0) return self.fs.put(io, width=w, height=h, format=format, **kwargs) @property def size(self): """ return a width, height of image """ out = self.get() if out: return out.width, out.height @property def format(self): """ return format of image ex: PNG, JPEG, GIF, etc """ out = self.get() if out: return out.format @property def thumbnail(self): """ return a gridfs.grid_file.GridOut representing a thumbnail of Image """ out = self.get() if out and out.thumbnail_id: return self.fs.get(out.thumbnail_id) def write(self, *args, **kwargs): raise RuntimeError('Please use "put" method instead') def writelines(self, *args, **kwargs): raise RuntimeError('Please use "put" method instead') class ImproperlyConfigured(Exception): pass class ImageField(FileField): """ A Image File storage field. :param size: max size to store images, provided as (width, height, force) if larger, it will be automatically resized (ex: size=(800, 600, True)) :param thumbnail_size: size to generate a thumbnail, provided as (width, height, force) """ proxy_class = ImageGridFsProxy def __init__( self, size=None, thumbnail_size=None, collection_name="images", **kwargs ): if not Image: raise ImproperlyConfigured("PIL library was not found") params_size = ("width", "height", "force") extra_args = {"size": size, "thumbnail_size": thumbnail_size} for att_name, att in extra_args.items(): value = None if isinstance(att, (tuple, list)): value = dict(itertools.zip_longest(params_size, att, fillvalue=None)) setattr(self, att_name, value) super().__init__(collection_name=collection_name, **kwargs) class SequenceField(BaseField): """Provides a sequential counter see: https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectIDs-SequenceNumbers .. note:: Although traditional databases often use increasing sequence numbers for primary keys. In MongoDB, the preferred approach is to use Object IDs instead. The concept is that in a very large cluster of machines, it is easier to create an object ID than have global, uniformly increasing sequence numbers. :param collection_name: Name of the counter collection (default 'mongoengine.counters') :param sequence_name: Name of the sequence in the collection (default 'ClassName.counter') :param value_decorator: Any callable to use as a counter (default int) Use any callable as `value_decorator` to transform calculated counter into any value suitable for your needs, e.g. string or hexadecimal representation of the default integer counter value. .. note:: In case the counter is defined in the abstract document, it will be common to all inherited documents and the default sequence name will be the class name of the abstract document. """ _auto_gen = True COLLECTION_NAME = "mongoengine.counters" VALUE_DECORATOR = int def __init__( self, collection_name=None, db_alias=None, sequence_name=None, value_decorator=None, *args, **kwargs, ): self.collection_name = collection_name or self.COLLECTION_NAME self.db_alias = db_alias or DEFAULT_CONNECTION_NAME self.sequence_name = sequence_name self.value_decorator = ( value_decorator if callable(value_decorator) else self.VALUE_DECORATOR ) super().__init__(*args, **kwargs) def generate(self): """ Generate and Increment the counter """ sequence_name = self.get_sequence_name() sequence_id = f"{sequence_name}.{self.name}" collection = get_db(alias=self.db_alias)[self.collection_name] counter = collection.find_one_and_update( filter={"_id": sequence_id}, update={"$inc": {"next": 1}}, return_document=ReturnDocument.AFTER, upsert=True, ) return self.value_decorator(counter["next"]) def set_next_value(self, value): """Helper method to set the next sequence value""" sequence_name = self.get_sequence_name() sequence_id = f"{sequence_name}.{self.name}" collection = get_db(alias=self.db_alias)[self.collection_name] counter = collection.find_one_and_update( filter={"_id": sequence_id}, update={"$set": {"next": value}}, return_document=ReturnDocument.AFTER, upsert=True, ) return self.value_decorator(counter["next"]) def get_next_value(self): """Helper method to get the next value for previewing. .. warning:: There is no guarantee this will be the next value as it is only fixed on set. """ sequence_name = self.get_sequence_name() sequence_id = f"{sequence_name}.{self.name}" collection = get_db(alias=self.db_alias)[self.collection_name] data = collection.find_one({"_id": sequence_id}) if data: return self.value_decorator(data["next"] + 1) return self.value_decorator(1) def get_sequence_name(self): if self.sequence_name: return self.sequence_name owner = self.owner_document if issubclass(owner, Document) and not owner._meta.get("abstract"): return owner._get_collection_name() else: return ( "".join("_%s" % c if c.isupper() else c for c in owner._class_name) .strip("_") .lower() ) def __get__(self, instance, owner): value = super().__get__(instance, owner) if value is None and instance._initialised: value = self.generate() instance._data[self.name] = value instance._mark_as_changed(self.name) return value def __set__(self, instance, value): if value is None and instance._initialised: value = self.generate() return super().__set__(instance, value) def prepare_query_value(self, op, value): """ This method is overridden in order to convert the query value into to required type. We need to do this in order to be able to successfully compare query values passed as string, the base implementation returns the value as is. """ return self.value_decorator(value) def to_python(self, value): if value is None: value = self.generate() return value class UUIDField(BaseField): """A UUID field.""" _binary = None def __init__(self, binary=True, **kwargs): """ Store UUID data in the database :param binary: if False store as a string. """ self._binary = binary super().__init__(**kwargs) def to_python(self, value): if not self._binary: original_value = value try: if not isinstance(value, str): value = str(value) return uuid.UUID(value) except (ValueError, TypeError, AttributeError): return original_value return value def to_mongo(self, value): if not self._binary: return str(value) elif isinstance(value, str): return uuid.UUID(value) return value def prepare_query_value(self, op, value): if value is None: return None return self.to_mongo(value) def validate(self, value): if not isinstance(value, uuid.UUID): if not isinstance(value, str): value = str(value) try: uuid.UUID(value) except (ValueError, TypeError, AttributeError) as exc: self.error("Could not convert to UUID: %s" % exc) class GeoPointField(BaseField): """A list storing a longitude and latitude coordinate. .. note:: this represents a generic point in a 2D plane and a legacy way of representing a geo point. It admits 2d indexes but not "2dsphere" indexes in MongoDB > 2.4 which are more natural for modeling geospatial points. See :ref:`geospatial-indexes` """ _geo_index = pymongo.GEO2D def validate(self, value): """Make sure that a geo-value is of type (x, y)""" if not isinstance(value, (list, tuple)): self.error("GeoPointField can only accept tuples or lists of (x, y)") if not len(value) == 2: self.error("Value (%s) must be a two-dimensional point" % repr(value)) elif not isinstance(value[0], (float, int)) or not isinstance( value[1], (float, int) ): self.error("Both values (%s) in point must be float or int" % repr(value)) class PointField(GeoJsonBaseField): """A GeoJSON field storing a longitude and latitude coordinate. The data is represented as: .. code-block:: js {'type' : 'Point' , 'coordinates' : [x, y]} You can either pass a dict with the full information or a list to set the value. Requires mongodb >= 2.4 """ _type = "Point" class LineStringField(GeoJsonBaseField): """A GeoJSON field storing a line of longitude and latitude coordinates. The data is represented as: .. code-block:: js {'type' : 'LineString' , 'coordinates' : [[x1, y1], [x2, y2] ... [xn, yn]]} You can either pass a dict with the full information or a list of points. Requires mongodb >= 2.4 """ _type = "LineString" class PolygonField(GeoJsonBaseField): """A GeoJSON field storing a polygon of longitude and latitude coordinates. The data is represented as: .. code-block:: js {'type' : 'Polygon' , 'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]], [[x1, y1], [x1, y1] ... [xn, yn]]} You can either pass a dict with the full information or a list of LineStrings. The first LineString being the outside and the rest being holes. Requires mongodb >= 2.4 """ _type = "Polygon" class MultiPointField(GeoJsonBaseField): """A GeoJSON field storing a list of Points. The data is represented as: .. code-block:: js {'type' : 'MultiPoint' , 'coordinates' : [[x1, y1], [x2, y2]]} You can either pass a dict with the full information or a list to set the value. Requires mongodb >= 2.6 """ _type = "MultiPoint" class MultiLineStringField(GeoJsonBaseField): """A GeoJSON field storing a list of LineStrings. The data is represented as: .. code-block:: js {'type' : 'MultiLineString' , 'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]], [[x1, y1], [x1, y1] ... [xn, yn]]]} You can either pass a dict with the full information or a list of points. Requires mongodb >= 2.6 """ _type = "MultiLineString" class MultiPolygonField(GeoJsonBaseField): """A GeoJSON field storing list of Polygons. The data is represented as: .. code-block:: js {'type' : 'MultiPolygon' , 'coordinates' : [[ [[x1, y1], [x1, y1] ... [xn, yn]], [[x1, y1], [x1, y1] ... [xn, yn]] ], [ [[x1, y1], [x1, y1] ... [xn, yn]], [[x1, y1], [x1, y1] ... [xn, yn]] ] } You can either pass a dict with the full information or a list of Polygons. Requires mongodb >= 2.6 """ _type = "MultiPolygon" class LazyReferenceField(BaseField): """A really lazy reference to a document. Unlike the :class:`~mongoengine.fields.ReferenceField` it will **not** be automatically (lazily) dereferenced on access. Instead, access will return a :class:`~mongoengine.base.LazyReference` class instance, allowing access to `pk` or manual dereference by using ``fetch()`` method. """ def __init__( self, document_type, passthrough=False, dbref=False, reverse_delete_rule=DO_NOTHING, **kwargs, ): """Initialises the Reference Field. :param dbref: Store the reference as :class:`~pymongo.dbref.DBRef` or as the :class:`~pymongo.objectid.ObjectId`.id . :param reverse_delete_rule: Determines what to do when the referring object is deleted :param passthrough: When trying to access unknown fields, the :class:`~mongoengine.base.datastructure.LazyReference` instance will automatically call `fetch()` and try to retrieve the field on the fetched document. Note this only work getting field (not setting or deleting). """ # XXX ValidationError raised outside of the "validate" method. if not isinstance(document_type, str) and not issubclass( document_type, Document ): self.error( "Argument to LazyReferenceField constructor must be a " "document class or a string" ) self.dbref = dbref self.passthrough = passthrough self.document_type_obj = document_type self.reverse_delete_rule = reverse_delete_rule super().__init__(**kwargs) @property def document_type(self): if isinstance(self.document_type_obj, str): if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT: self.document_type_obj = self.owner_document else: self.document_type_obj = get_document(self.document_type_obj) return self.document_type_obj def build_lazyref(self, value): if isinstance(value, LazyReference): if value.passthrough != self.passthrough: value = LazyReference( value.document_type, value.pk, passthrough=self.passthrough ) elif value is not None: if isinstance(value, self.document_type): value = LazyReference( self.document_type, value.pk, passthrough=self.passthrough ) elif isinstance(value, DBRef): value = LazyReference( self.document_type, value.id, passthrough=self.passthrough ) else: # value is the primary key of the referenced document value = LazyReference( self.document_type, value, passthrough=self.passthrough ) return value def __get__(self, instance, owner): """Descriptor to allow lazy dereferencing.""" if instance is None: # Document class being used rather than a document object return self value = self.build_lazyref(instance._data.get(self.name)) if value: instance._data[self.name] = value return super().__get__(instance, owner) def to_mongo(self, value): if isinstance(value, LazyReference): pk = value.pk elif isinstance(value, self.document_type): pk = value.pk elif isinstance(value, DBRef): pk = value.id else: # value is the primary key of the referenced document pk = value id_field_name = self.document_type._meta["id_field"] id_field = self.document_type._fields[id_field_name] pk = id_field.to_mongo(pk) if self.dbref: return DBRef(self.document_type._get_collection_name(), pk) else: return pk def to_python(self, value): """Convert a MongoDB-compatible type to a Python type.""" if not isinstance(value, (DBRef, Document, EmbeddedDocument)): collection = self.document_type._get_collection_name() value = DBRef(collection, self.document_type.id.to_python(value)) value = self.build_lazyref(value) return value def validate(self, value): if isinstance(value, LazyReference): if value.collection != self.document_type._get_collection_name(): self.error("Reference must be on a `%s` document." % self.document_type) pk = value.pk elif isinstance(value, self.document_type): pk = value.pk elif isinstance(value, DBRef): # TODO: check collection ? collection = self.document_type._get_collection_name() if value.collection != collection: self.error("DBRef on bad collection (must be on `%s`)" % collection) pk = value.id else: # value is the primary key of the referenced document id_field_name = self.document_type._meta["id_field"] id_field = getattr(self.document_type, id_field_name) pk = value try: id_field.validate(pk) except ValidationError: self.error( "value should be `{0}` document, LazyReference or DBRef on `{0}` " "or `{0}`'s primary key (i.e. `{1}`)".format( self.document_type.__name__, type(id_field).__name__ ) ) if pk is None: self.error( "You can only reference documents once they have been " "saved to the database" ) def prepare_query_value(self, op, value): if value is None: return None super().prepare_query_value(op, value) return self.to_mongo(value) def lookup_member(self, member_name): return self.document_type._fields.get(member_name) class GenericLazyReferenceField(GenericReferenceField): """A reference to *any* :class:`~mongoengine.document.Document` subclass. Unlike the :class:`~mongoengine.fields.GenericReferenceField` it will **not** be automatically (lazily) dereferenced on access. Instead, access will return a :class:`~mongoengine.base.LazyReference` class instance, allowing access to `pk` or manual dereference by using ``fetch()`` method. .. note :: * Any documents used as a generic reference must be registered in the document registry. Importing the model will automatically register it. * You can use the choices param to limit the acceptable Document types """ def __init__(self, *args, **kwargs): self.passthrough = kwargs.pop("passthrough", False) super().__init__(*args, **kwargs) def _validate_choices(self, value): if isinstance(value, LazyReference): value = value.document_type._class_name super()._validate_choices(value) def build_lazyref(self, value): if isinstance(value, LazyReference): if value.passthrough != self.passthrough: value = LazyReference( value.document_type, value.pk, passthrough=self.passthrough ) elif value is not None: if isinstance(value, (dict, SON)): value = LazyReference( get_document(value["_cls"]), value["_ref"].id, passthrough=self.passthrough, ) elif isinstance(value, Document): value = LazyReference( type(value), value.pk, passthrough=self.passthrough ) return value def __get__(self, instance, owner): if instance is None: return self value = self.build_lazyref(instance._data.get(self.name)) if value: instance._data[self.name] = value return super().__get__(instance, owner) def validate(self, value): if isinstance(value, LazyReference) and value.pk is None: self.error( "You can only reference documents once they have been" " saved to the database" ) return super().validate(value) def to_mongo(self, document): if document is None: return None if isinstance(document, LazyReference): return SON( ( ("_cls", document.document_type._class_name), ( "_ref", DBRef( document.document_type._get_collection_name(), document.pk ), ), ) ) else: return super().to_mongo(document) class Decimal128Field(BaseField): """ 128-bit decimal-based floating-point field capable of emulating decimal rounding with exact precision. This field will expose decimal.Decimal but stores the value as a `bson.Decimal128` behind the scene, this field is intended for monetary data, scientific computations, etc. """ DECIMAL_CONTEXT = create_decimal128_context() def __init__(self, min_value=None, max_value=None, **kwargs): self.min_value = min_value self.max_value = max_value super().__init__(**kwargs) def to_mongo(self, value): if value is None: return None if isinstance(value, Decimal128): return value if not isinstance(value, decimal.Decimal): with decimal.localcontext(self.DECIMAL_CONTEXT) as ctx: value = ctx.create_decimal(value) return Decimal128(value) def to_python(self, value): if value is None: return None return self.to_mongo(value).to_decimal() def validate(self, value): if not isinstance(value, Decimal128): try: value = Decimal128(value) except (TypeError, ValueError, decimal.InvalidOperation) as exc: self.error("Could not convert value to Decimal128: %s" % exc) if self.min_value is not None and value.to_decimal() < self.min_value: self.error("Decimal value is too small") if self.max_value is not None and value.to_decimal() > self.max_value: self.error("Decimal value is too large") def prepare_query_value(self, op, value): return super().prepare_query_value(op, self.to_mongo(value)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/mongodb_support.py0000644000175100001730000000106314400345475021614 0ustar00runnerdocker""" Helper functions, constants, and types to aid with MongoDB version support """ from mongoengine.connection import get_connection # Constant that can be used to compare the version retrieved with # get_mongodb_version() MONGODB_34 = (3, 4) MONGODB_36 = (3, 6) MONGODB_42 = (4, 2) MONGODB_44 = (4, 4) def get_mongodb_version(): """Return the version of the default connected mongoDB (first 2 digits) :return: tuple(int, int) """ version_list = get_connection().server_info()["versionArray"][:2] # e.g: (3, 2) return tuple(version_list) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/pymongo_support.py0000644000175100001730000000532514400345475021664 0ustar00runnerdocker""" Helper functions, constants, and types to aid with PyMongo support. """ import pymongo from bson import binary, json_util from pymongo.errors import OperationFailure PYMONGO_VERSION = tuple(pymongo.version_tuple[:2]) # This will be changed to UuidRepresentation.UNSPECIFIED in a future # (breaking) release. if PYMONGO_VERSION >= (4,): LEGACY_JSON_OPTIONS = json_util.LEGACY_JSON_OPTIONS.with_options( uuid_representation=binary.UuidRepresentation.PYTHON_LEGACY, ) else: LEGACY_JSON_OPTIONS = json_util.DEFAULT_JSON_OPTIONS def count_documents( collection, filter, skip=None, limit=None, hint=None, collation=None ): """Pymongo>3.7 deprecates count in favour of count_documents""" if limit == 0: return 0 # Pymongo raises an OperationFailure if called with limit=0 kwargs = {} if skip is not None: kwargs["skip"] = skip if limit is not None: kwargs["limit"] = limit if hint not in (-1, None): kwargs["hint"] = hint if collation is not None: kwargs["collation"] = collation # count_documents appeared in pymongo 3.7 if PYMONGO_VERSION >= (3, 7): try: return collection.count_documents(filter=filter, **kwargs) except OperationFailure as err: if PYMONGO_VERSION >= (4,): raise # OperationFailure - accounts for some operators that used to work # with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere) # fallback to deprecated Cursor.count # Keeping this should be reevaluated the day pymongo removes .count entirely message = str(err) if not ( "not allowed in this context" in message and ( "$where" in message or "$geoNear" in message or "$near" in message or "$nearSphere" in message ) ): raise cursor = collection.find(filter) for option, option_value in kwargs.items(): cursor_method = getattr(cursor, option) cursor = cursor_method(option_value) with_limit_and_skip = "skip" in kwargs or "limit" in kwargs return cursor.count(with_limit_and_skip=with_limit_and_skip) def list_collection_names(db, include_system_collections=False): """Pymongo>3.7 deprecates collection_names in favour of list_collection_names""" if PYMONGO_VERSION >= (3, 7): collections = db.list_collection_names() else: collections = db.collection_names() if not include_system_collections: collections = [c for c in collections if not c.startswith("system.")] return collections ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/mongoengine/queryset/0000755000175100001730000000000014400345501017670 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/__init__.py0000644000175100001730000000135014400345475022012 0ustar00runnerdockerfrom mongoengine.errors import * from mongoengine.queryset.field_list import * from mongoengine.queryset.manager import * from mongoengine.queryset.queryset import * from mongoengine.queryset.transform import * from mongoengine.queryset.visitor import * # Expose just the public subset of all imported objects and constants. __all__ = ( "QuerySet", "QuerySetNoCache", "Q", "queryset_manager", "QuerySetManager", "QueryFieldList", "DO_NOTHING", "NULLIFY", "CASCADE", "DENY", "PULL", # Errors that might be related to a queryset, mostly here for backward # compatibility "DoesNotExist", "InvalidQueryError", "MultipleObjectsReturned", "NotUniqueError", "OperationError", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/base.py0000644000175100001730000022051714400345475021175 0ustar00runnerdockerimport copy import itertools import re import warnings from collections.abc import Mapping import pymongo import pymongo.errors from bson import SON, json_util from bson.code import Code from pymongo.collection import ReturnDocument from pymongo.common import validate_read_preference from pymongo.read_concern import ReadConcern from mongoengine import signals from mongoengine.base import get_document from mongoengine.common import _import_class from mongoengine.connection import get_db from mongoengine.context_managers import ( set_read_write_concern, set_write_concern, switch_db, ) from mongoengine.errors import ( BulkWriteError, InvalidQueryError, LookUpError, NotUniqueError, OperationError, ) from mongoengine.pymongo_support import ( LEGACY_JSON_OPTIONS, count_documents, ) from mongoengine.queryset import transform from mongoengine.queryset.field_list import QueryFieldList from mongoengine.queryset.visitor import Q, QNode __all__ = ("BaseQuerySet", "DO_NOTHING", "NULLIFY", "CASCADE", "DENY", "PULL") # Delete rules DO_NOTHING = 0 NULLIFY = 1 CASCADE = 2 DENY = 3 PULL = 4 class BaseQuerySet: """A set of results returned from a query. Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as the results. """ __dereference = False _auto_dereference = True def __init__(self, document, collection): self._document = document self._collection_obj = collection self._mongo_query = None self._query_obj = Q() self._cls_query = {} self._where_clause = None self._loaded_fields = QueryFieldList() self._ordering = None self._snapshot = False self._timeout = True self._allow_disk_use = False self._read_preference = None self._read_concern = None self._iter = False self._scalar = [] self._none = False self._as_pymongo = False self._search_text = None # If inheritance is allowed, only return instances and instances of # subclasses of the class being used if document._meta.get("allow_inheritance") is True: if len(self._document._subclasses) == 1: self._cls_query = {"_cls": self._document._subclasses[0]} else: self._cls_query = {"_cls": {"$in": self._document._subclasses}} self._loaded_fields = QueryFieldList(always_include=["_cls"]) self._cursor_obj = None self._limit = None self._skip = None self._hint = -1 # Using -1 as None is a valid value for hint self._collation = None self._batch_size = None self._max_time_ms = None self._comment = None # Hack - As people expect cursor[5:5] to return # an empty result set. It's hard to do that right, though, because the # server uses limit(0) to mean 'no limit'. So we set _empty # in that case and check for it when iterating. We also unset # it anytime we change _limit. Inspired by how it is done in pymongo.Cursor self._empty = False def __call__(self, q_obj=None, **query): """Filter the selected documents by calling the :class:`~mongoengine.queryset.QuerySet` with a query. :param q_obj: a :class:`~mongoengine.queryset.Q` object to be used in the query; the :class:`~mongoengine.queryset.QuerySet` is filtered multiple times with different :class:`~mongoengine.queryset.Q` objects, only the last one will be used. :param query: Django-style query keyword arguments. """ query = Q(**query) if q_obj: # Make sure proper query object is passed. if not isinstance(q_obj, QNode): msg = ( "Not a query object: %s. " "Did you intend to use key=value?" % q_obj ) raise InvalidQueryError(msg) query &= q_obj queryset = self.clone() queryset._query_obj &= query queryset._mongo_query = None queryset._cursor_obj = None return queryset def __getstate__(self): """ Need for pickling queryset See https://github.com/MongoEngine/mongoengine/issues/442 """ obj_dict = self.__dict__.copy() # don't picke collection, instead pickle collection params obj_dict.pop("_collection_obj") # don't pickle cursor obj_dict["_cursor_obj"] = None return obj_dict def __setstate__(self, obj_dict): """ Need for pickling queryset See https://github.com/MongoEngine/mongoengine/issues/442 """ obj_dict["_collection_obj"] = obj_dict["_document"]._get_collection() # update attributes self.__dict__.update(obj_dict) # forse load cursor # self._cursor def __getitem__(self, key): """Return a document instance corresponding to a given index if the key is an integer. If the key is a slice, translate its bounds into a skip and a limit, and return a cloned queryset with that skip/limit applied. For example: >>> User.objects[0] >>> User.objects[1:3] [, ] """ queryset = self.clone() queryset._empty = False # Handle a slice if isinstance(key, slice): queryset._cursor_obj = queryset._cursor[key] queryset._skip, queryset._limit = key.start, key.stop if key.start and key.stop: queryset._limit = key.stop - key.start if queryset._limit == 0: queryset._empty = True # Allow further QuerySet modifications to be performed return queryset # Handle an index elif isinstance(key, int): if queryset._scalar: return queryset._get_scalar( queryset._document._from_son( queryset._cursor[key], _auto_dereference=self._auto_dereference, ) ) if queryset._as_pymongo: return queryset._cursor[key] return queryset._document._from_son( queryset._cursor[key], _auto_dereference=self._auto_dereference, ) raise TypeError("Provide a slice or an integer index") def __iter__(self): raise NotImplementedError def _has_data(self): """Return True if cursor has any data.""" queryset = self.order_by() return False if queryset.first() is None else True def __bool__(self): """Avoid to open all records in an if stmt in Py3.""" return self._has_data() # Core functions def all(self): """Returns a copy of the current QuerySet.""" return self.__call__() def filter(self, *q_objs, **query): """An alias of :meth:`~mongoengine.queryset.QuerySet.__call__`""" return self.__call__(*q_objs, **query) def search_text(self, text, language=None): """ Start a text search, using text indexes. Require: MongoDB server version 2.6+. :param language: The language that determines the list of stop words for the search and the rules for the stemmer and tokenizer. If not specified, the search uses the default language of the index. For supported languages, see `Text Search Languages `. """ queryset = self.clone() if queryset._search_text: raise OperationError("It is not possible to use search_text two times.") query_kwargs = SON({"$search": text}) if language: query_kwargs["$language"] = language queryset._query_obj &= Q(__raw__={"$text": query_kwargs}) queryset._mongo_query = None queryset._cursor_obj = None queryset._search_text = text return queryset def get(self, *q_objs, **query): """Retrieve the the matching object raising :class:`~mongoengine.queryset.MultipleObjectsReturned` or `DocumentName.MultipleObjectsReturned` exception if multiple results and :class:`~mongoengine.queryset.DoesNotExist` or `DocumentName.DoesNotExist` if no results are found. """ queryset = self.clone() queryset = queryset.order_by().limit(2) queryset = queryset.filter(*q_objs, **query) try: result = next(queryset) except StopIteration: msg = "%s matching query does not exist." % queryset._document._class_name raise queryset._document.DoesNotExist(msg) try: # Check if there is another match next(queryset) except StopIteration: return result # If we were able to retrieve the 2nd doc, raise the MultipleObjectsReturned exception. raise queryset._document.MultipleObjectsReturned( "2 or more items returned, instead of 1" ) def create(self, **kwargs): """Create new object. Returns the saved object instance.""" return self._document(**kwargs).save(force_insert=True) def first(self): """Retrieve the first object matching the query.""" queryset = self.clone() if self._none or self._empty: return None try: result = queryset[0] except IndexError: result = None return result def insert( self, doc_or_docs, load_bulk=True, write_concern=None, signal_kwargs=None ): """bulk insert documents :param doc_or_docs: a document or list of documents to be inserted :param load_bulk (optional): If True returns the list of document instances :param write_concern: Extra keyword arguments are passed down to :meth:`~pymongo.collection.Collection.insert` which will be used as options for the resultant ``getLastError`` command. For example, ``insert(..., {w: 2, fsync: True})`` will wait until at least two servers have recorded the write and will force an fsync on each server being written to. :param signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. By default returns document instances, set ``load_bulk`` to False to return just ``ObjectIds`` """ Document = _import_class("Document") if write_concern is None: write_concern = {} docs = doc_or_docs return_one = False if isinstance(docs, Document) or issubclass(docs.__class__, Document): return_one = True docs = [docs] for doc in docs: if not isinstance(doc, self._document): msg = "Some documents inserted aren't instances of %s" % str( self._document ) raise OperationError(msg) if doc.pk and not doc._created: msg = "Some documents have ObjectIds, use doc.update() instead" raise OperationError(msg) signal_kwargs = signal_kwargs or {} signals.pre_bulk_insert.send(self._document, documents=docs, **signal_kwargs) raw = [doc.to_mongo() for doc in docs] with set_write_concern(self._collection, write_concern) as collection: insert_func = collection.insert_many if return_one: raw = raw[0] insert_func = collection.insert_one try: inserted_result = insert_func(raw) ids = ( [inserted_result.inserted_id] if return_one else inserted_result.inserted_ids ) except pymongo.errors.DuplicateKeyError as err: message = "Could not save document (%s)" raise NotUniqueError(message % err) except pymongo.errors.BulkWriteError as err: # inserting documents that already have an _id field will # give huge performance debt or raise message = "Bulk write error: (%s)" raise BulkWriteError(message % err.details) except pymongo.errors.OperationFailure as err: message = "Could not save document (%s)" if re.match("^E1100[01] duplicate key", str(err)): # E11000 - duplicate key error index # E11001 - duplicate key on update message = "Tried to save duplicate unique keys (%s)" raise NotUniqueError(message % err) raise OperationError(message % err) # Apply inserted_ids to documents for doc, doc_id in zip(docs, ids): doc.pk = doc_id if not load_bulk: signals.post_bulk_insert.send( self._document, documents=docs, loaded=False, **signal_kwargs ) return ids[0] if return_one else ids documents = self.in_bulk(ids) results = [documents.get(obj_id) for obj_id in ids] signals.post_bulk_insert.send( self._document, documents=results, loaded=True, **signal_kwargs ) return results[0] if return_one else results def count(self, with_limit_and_skip=False): """Count the selected elements in the query. :param with_limit_and_skip (optional): take any :meth:`limit` or :meth:`skip` that has been applied to this cursor into account when getting the count """ # mimic the fact that setting .limit(0) in pymongo sets no limit # https://docs.mongodb.com/manual/reference/method/cursor.limit/#zero-value if ( self._limit == 0 and with_limit_and_skip is False or self._none or self._empty ): return 0 kwargs = ( {"limit": self._limit, "skip": self._skip} if with_limit_and_skip else {} ) if self._limit == 0: # mimic the fact that historically .limit(0) sets no limit kwargs.pop("limit", None) if self._hint not in (-1, None): kwargs["hint"] = self._hint if self._collation: kwargs["collation"] = self._collation count = count_documents( collection=self._cursor.collection, filter=self._query, **kwargs, ) self._cursor_obj = None return count def delete(self, write_concern=None, _from_doc_delete=False, cascade_refs=None): """Delete the documents matched by the query. :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. :param _from_doc_delete: True when called from document delete therefore signals will have been triggered so don't loop. :returns number of deleted documents """ queryset = self.clone() doc = queryset._document if write_concern is None: write_concern = {} # Handle deletes where skips or limits have been applied or # there is an untriggered delete signal has_delete_signal = signals.signals_available and ( signals.pre_delete.has_receivers_for(doc) or signals.post_delete.has_receivers_for(doc) ) call_document_delete = ( queryset._skip or queryset._limit or has_delete_signal ) and not _from_doc_delete if call_document_delete: cnt = 0 for doc in queryset: doc.delete(**write_concern) cnt += 1 return cnt delete_rules = doc._meta.get("delete_rules") or {} delete_rules = list(delete_rules.items()) # Check for DENY rules before actually deleting/nullifying any other # references for rule_entry, rule in delete_rules: document_cls, field_name = rule_entry if document_cls._meta.get("abstract"): continue if rule == DENY: refs = document_cls.objects(**{field_name + "__in": self}) if refs.limit(1).count() > 0: raise OperationError( "Could not delete document (%s.%s refers to it)" % (document_cls.__name__, field_name) ) # Check all the other rules for rule_entry, rule in delete_rules: document_cls, field_name = rule_entry if document_cls._meta.get("abstract"): continue if rule == CASCADE: cascade_refs = set() if cascade_refs is None else cascade_refs # Handle recursive reference if doc._collection == document_cls._collection: for ref in queryset: cascade_refs.add(ref.id) refs = document_cls.objects( **{field_name + "__in": self, "pk__nin": cascade_refs} ) if refs.count() > 0: refs.delete(write_concern=write_concern, cascade_refs=cascade_refs) elif rule == NULLIFY: document_cls.objects(**{field_name + "__in": self}).update( write_concern=write_concern, **{"unset__%s" % field_name: 1} ) elif rule == PULL: document_cls.objects(**{field_name + "__in": self}).update( write_concern=write_concern, **{"pull_all__%s" % field_name: self} ) with set_write_concern(queryset._collection, write_concern) as collection: result = collection.delete_many(queryset._query) # If we're using an unack'd write concern, we don't really know how # many items have been deleted at this point, hence we only return # the count for ack'd ops. if result.acknowledged: return result.deleted_count def update( self, upsert=False, multi=True, write_concern=None, read_concern=None, full_result=False, **update, ): """Perform an atomic update on the fields matched by the query. :param upsert: insert if document doesn't exist (default ``False``) :param multi: Update multiple documents. :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. :param read_concern: Override the read concern for the operation :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number updated items :param update: Django-style update keyword arguments :returns the number of updated documents (unless ``full_result`` is True) """ if not update and not upsert: raise OperationError("No update parameters, would remove data") if write_concern is None: write_concern = {} if self._none or self._empty: return 0 queryset = self.clone() query = queryset._query if "__raw__" in update and isinstance(update["__raw__"], list): update = [ transform.update(queryset._document, **{"__raw__": u}) for u in update["__raw__"] ] else: update = transform.update(queryset._document, **update) # If doing an atomic upsert on an inheritable class # then ensure we add _cls to the update operation if upsert and "_cls" in query: if "$set" in update: update["$set"]["_cls"] = queryset._document._class_name else: update["$set"] = {"_cls": queryset._document._class_name} try: with set_read_write_concern( queryset._collection, write_concern, read_concern ) as collection: update_func = collection.update_one if multi: update_func = collection.update_many result = update_func(query, update, upsert=upsert) if full_result: return result elif result.raw_result: return result.raw_result["n"] except pymongo.errors.DuplicateKeyError as err: raise NotUniqueError("Update failed (%s)" % err) except pymongo.errors.OperationFailure as err: if str(err) == "multi not coded yet": message = "update() method requires MongoDB 1.1.3+" raise OperationError(message) raise OperationError("Update failed (%s)" % err) def upsert_one(self, write_concern=None, read_concern=None, **update): """Overwrite or add the first document matched by the query. :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. :param read_concern: Override the read concern for the operation :param update: Django-style update keyword arguments :returns the new or overwritten document """ atomic_update = self.update( multi=False, upsert=True, write_concern=write_concern, read_concern=read_concern, full_result=True, **update, ) if atomic_update.raw_result["updatedExisting"]: document = self.get() else: document = self._document.objects.with_id(atomic_update.upserted_id) return document def update_one(self, upsert=False, write_concern=None, full_result=False, **update): """Perform an atomic update on the fields of the first document matched by the query. :param upsert: insert if document doesn't exist (default ``False``) :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant ``getLastError`` command. For example, ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number updated items :param update: Django-style update keyword arguments full_result :returns the number of updated documents (unless ``full_result`` is True) """ return self.update( upsert=upsert, multi=False, write_concern=write_concern, full_result=full_result, **update, ) def modify( self, upsert=False, full_response=False, remove=False, new=False, **update ): """Update and return the updated document. Returns either the document before or after modification based on `new` parameter. If no documents match the query and `upsert` is false, returns ``None``. If upserting and `new` is false, returns ``None``. If the full_response parameter is ``True``, the return value will be the entire response object from the server, including the 'ok' and 'lastErrorObject' fields, rather than just the modified document. This is useful mainly because the 'lastErrorObject' document holds information about the command's execution. :param upsert: insert if document doesn't exist (default ``False``) :param full_response: return the entire response object from the server (default ``False``, not available for PyMongo 3+) :param remove: remove rather than updating (default ``False``) :param new: return updated rather than original document (default ``False``) :param update: Django-style update keyword arguments """ if remove and new: raise OperationError("Conflicting parameters: remove and new") if not update and not upsert and not remove: raise OperationError("No update parameters, must either update or remove") if self._none or self._empty: return None queryset = self.clone() query = queryset._query if not remove: update = transform.update(queryset._document, **update) sort = queryset._ordering try: if full_response: msg = "With PyMongo 3+, it is not possible anymore to get the full response." warnings.warn(msg, DeprecationWarning) if remove: result = queryset._collection.find_one_and_delete( query, sort=sort, **self._cursor_args ) else: if new: return_doc = ReturnDocument.AFTER else: return_doc = ReturnDocument.BEFORE result = queryset._collection.find_one_and_update( query, update, upsert=upsert, sort=sort, return_document=return_doc, **self._cursor_args, ) except pymongo.errors.DuplicateKeyError as err: raise NotUniqueError("Update failed (%s)" % err) except pymongo.errors.OperationFailure as err: raise OperationError("Update failed (%s)" % err) if full_response: if result["value"] is not None: result["value"] = self._document._from_son(result["value"]) else: if result is not None: result = self._document._from_son(result) return result def with_id(self, object_id): """Retrieve the object matching the id provided. Uses `object_id` only and raises InvalidQueryError if a filter has been applied. Returns `None` if no document exists with that id. :param object_id: the value for the id of the document to look up """ queryset = self.clone() if queryset._query_obj: msg = "Cannot use a filter whilst using `with_id`" raise InvalidQueryError(msg) return queryset.filter(pk=object_id).first() def in_bulk(self, object_ids): """Retrieve a set of documents by their ids. :param object_ids: a list or tuple of ObjectId's :rtype: dict of ObjectId's as keys and collection-specific Document subclasses as values. """ doc_map = {} docs = self._collection.find({"_id": {"$in": object_ids}}, **self._cursor_args) if self._scalar: for doc in docs: doc_map[doc["_id"]] = self._get_scalar(self._document._from_son(doc)) elif self._as_pymongo: for doc in docs: doc_map[doc["_id"]] = doc else: for doc in docs: doc_map[doc["_id"]] = self._document._from_son( doc, _auto_dereference=self._auto_dereference, ) return doc_map def none(self): """Returns a queryset that never returns any objects and no query will be executed when accessing the results inspired by django none() https://docs.djangoproject.com/en/dev/ref/models/querysets/#none """ queryset = self.clone() queryset._none = True return queryset def no_sub_classes(self): """Filter for only the instances of this specific document. Do NOT return any inherited documents. """ if self._document._meta.get("allow_inheritance") is True: self._cls_query = {"_cls": self._document._class_name} return self def using(self, alias): """This method is for controlling which database the QuerySet will be evaluated against if you are using more than one database. :param alias: The database alias """ with switch_db(self._document, alias) as cls: collection = cls._get_collection() return self._clone_into(self.__class__(self._document, collection)) def clone(self): """Create a copy of the current queryset.""" return self._clone_into(self.__class__(self._document, self._collection_obj)) def _clone_into(self, new_qs): """Copy all of the relevant properties of this queryset to a new queryset (which has to be an instance of :class:`~mongoengine.queryset.base.BaseQuerySet`). """ if not isinstance(new_qs, BaseQuerySet): raise OperationError( "%s is not a subclass of BaseQuerySet" % new_qs.__name__ ) copy_props = ( "_mongo_query", "_cls_query", "_none", "_query_obj", "_where_clause", "_loaded_fields", "_ordering", "_snapshot", "_timeout", "_allow_disk_use", "_read_preference", "_read_concern", "_iter", "_scalar", "_as_pymongo", "_limit", "_skip", "_empty", "_hint", "_collation", "_auto_dereference", "_search_text", "_max_time_ms", "_comment", "_batch_size", ) for prop in copy_props: val = getattr(self, prop) setattr(new_qs, prop, copy.copy(val)) if self._cursor_obj: new_qs._cursor_obj = self._cursor_obj.clone() return new_qs def select_related(self, max_depth=1): """Handles dereferencing of :class:`~bson.dbref.DBRef` objects or :class:`~bson.object_id.ObjectId` a maximum depth in order to cut down the number queries to mongodb. """ # Make select related work the same for querysets max_depth += 1 queryset = self.clone() return queryset._dereference(queryset, max_depth=max_depth) def limit(self, n): """Limit the number of returned documents to `n`. This may also be achieved using array-slicing syntax (e.g. ``User.objects[:5]``). :param n: the maximum number of objects to return if n is greater than 0. When 0 is passed, returns all the documents in the cursor """ queryset = self.clone() queryset._limit = n queryset._empty = False # cancels the effect of empty # If a cursor object has already been created, apply the limit to it. if queryset._cursor_obj: queryset._cursor_obj.limit(queryset._limit) return queryset def skip(self, n): """Skip `n` documents before returning the results. This may also be achieved using array-slicing syntax (e.g. ``User.objects[5:]``). :param n: the number of objects to skip before returning results """ queryset = self.clone() queryset._skip = n # If a cursor object has already been created, apply the skip to it. if queryset._cursor_obj: queryset._cursor_obj.skip(queryset._skip) return queryset def hint(self, index=None): """Added 'hint' support, telling Mongo the proper index to use for the query. Judicious use of hints can greatly improve query performance. When doing a query on multiple fields (at least one of which is indexed) pass the indexed field as a hint to the query. Hinting will not do anything if the corresponding index does not exist. The last hint applied to this cursor takes precedence over all others. """ queryset = self.clone() queryset._hint = index # If a cursor object has already been created, apply the hint to it. if queryset._cursor_obj: queryset._cursor_obj.hint(queryset._hint) return queryset def collation(self, collation=None): """ Collation allows users to specify language-specific rules for string comparison, such as rules for lettercase and accent marks. :param collation: `~pymongo.collation.Collation` or dict with following fields: { locale: str, caseLevel: bool, caseFirst: str, strength: int, numericOrdering: bool, alternate: str, maxVariable: str, backwards: str } Collation should be added to indexes like in test example """ queryset = self.clone() queryset._collation = collation if queryset._cursor_obj: queryset._cursor_obj.collation(collation) return queryset def batch_size(self, size): """Limit the number of documents returned in a single batch (each batch requires a round trip to the server). See http://api.mongodb.com/python/current/api/pymongo/cursor.html#pymongo.cursor.Cursor.batch_size for details. :param size: desired size of each batch. """ queryset = self.clone() queryset._batch_size = size # If a cursor object has already been created, apply the batch size to it. if queryset._cursor_obj: queryset._cursor_obj.batch_size(queryset._batch_size) return queryset def distinct(self, field): """Return a list of distinct values for a given field. :param field: the field to select distinct values from .. note:: This is a command and won't take ordering or limit into account. """ queryset = self.clone() try: field = self._fields_to_dbfields([field]).pop() except LookUpError: pass raw_values = queryset._cursor.distinct(field) if not self._auto_dereference: return raw_values distinct = self._dereference(raw_values, 1, name=field, instance=self._document) doc_field = self._document._fields.get(field.split(".", 1)[0]) instance = None # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField) EmbeddedDocumentField = _import_class("EmbeddedDocumentField") ListField = _import_class("ListField") GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField") if isinstance(doc_field, ListField): doc_field = getattr(doc_field, "field", doc_field) if isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)): instance = getattr(doc_field, "document_type", None) # handle distinct on subdocuments if "." in field: for field_part in field.split(".")[1:]: # if looping on embedded document, get the document type instance if instance and isinstance( doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField) ): doc_field = instance # now get the subdocument doc_field = getattr(doc_field, field_part, doc_field) # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField) if isinstance(doc_field, ListField): doc_field = getattr(doc_field, "field", doc_field) if isinstance( doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField) ): instance = getattr(doc_field, "document_type", None) if instance and isinstance( doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField) ): distinct = [instance(**doc) for doc in distinct] return distinct def only(self, *fields): """Load only a subset of this document's fields. :: post = BlogPost.objects(...).only('title', 'author.name') .. note :: `only()` is chainable and will perform a union :: So with the following it will fetch both: `title` and `author.name`:: post = BlogPost.objects.only('title').only('author.name') :func:`~mongoengine.queryset.QuerySet.all_fields` will reset any field filters. :param fields: fields to include """ fields = {f: QueryFieldList.ONLY for f in fields} return self.fields(True, **fields) def exclude(self, *fields): """Opposite to .only(), exclude some document's fields. :: post = BlogPost.objects(...).exclude('comments') .. note :: `exclude()` is chainable and will perform a union :: So with the following it will exclude both: `title` and `author.name`:: post = BlogPost.objects.exclude('title').exclude('author.name') :func:`~mongoengine.queryset.QuerySet.all_fields` will reset any field filters. :param fields: fields to exclude """ fields = {f: QueryFieldList.EXCLUDE for f in fields} return self.fields(**fields) def fields(self, _only_called=False, **kwargs): """Manipulate how you load this document's fields. Used by `.only()` and `.exclude()` to manipulate which fields to retrieve. If called directly, use a set of kwargs similar to the MongoDB projection document. For example: Include only a subset of fields: posts = BlogPost.objects(...).fields(author=1, title=1) Exclude a specific field: posts = BlogPost.objects(...).fields(comments=0) To retrieve a subrange or sublist of array elements, support exist for both the `slice` and `elemMatch` projection operator: posts = BlogPost.objects(...).fields(slice__comments=5) posts = BlogPost.objects(...).fields(elemMatch__comments="test") :param kwargs: A set of keyword arguments identifying what to include, exclude, or slice. """ # Check for an operator and transform to mongo-style if there is operators = ["slice", "elemMatch"] cleaned_fields = [] for key, value in kwargs.items(): parts = key.split("__") if parts[0] in operators: op = parts.pop(0) value = {"$" + op: value} key = ".".join(parts) cleaned_fields.append((key, value)) # Sort fields by their values, explicitly excluded fields first, then # explicitly included, and then more complicated operators such as # $slice. def _sort_key(field_tuple): _, value = field_tuple if isinstance(value, int): return value # 0 for exclusion, 1 for inclusion return 2 # so that complex values appear last fields = sorted(cleaned_fields, key=_sort_key) # Clone the queryset, group all fields by their value, convert # each of them to db_fields, and set the queryset's _loaded_fields queryset = self.clone() for value, group in itertools.groupby(fields, lambda x: x[1]): fields = [field for field, value in group] fields = queryset._fields_to_dbfields(fields) queryset._loaded_fields += QueryFieldList( fields, value=value, _only_called=_only_called ) return queryset def all_fields(self): """Include all fields. Reset all previously calls of .only() or .exclude(). :: post = BlogPost.objects.exclude('comments').all_fields() """ queryset = self.clone() queryset._loaded_fields = QueryFieldList( always_include=queryset._loaded_fields.always_include ) return queryset def order_by(self, *keys): """Order the :class:`~mongoengine.queryset.QuerySet` by the given keys. The order may be specified by prepending each of the keys by a "+" or a "-". Ascending order is assumed if there's no prefix. If no keys are passed, existing ordering is cleared instead. :param keys: fields to order the query results by; keys may be prefixed with "+" or a "-" to determine the ordering direction. """ queryset = self.clone() old_ordering = queryset._ordering new_ordering = queryset._get_order_by(keys) if queryset._cursor_obj: # If a cursor object has already been created, apply the sort to it if new_ordering: queryset._cursor_obj.sort(new_ordering) # If we're trying to clear a previous explicit ordering, we need # to clear the cursor entirely (because PyMongo doesn't allow # clearing an existing sort on a cursor). elif old_ordering: queryset._cursor_obj = None queryset._ordering = new_ordering return queryset def clear_cls_query(self): """Clear the default "_cls" query. By default, all queries generated for documents that allow inheritance include an extra "_cls" clause. In most cases this is desirable, but sometimes you might achieve better performance if you clear that default query. Scan the code for `_cls_query` to get more details. """ queryset = self.clone() queryset._cls_query = {} return queryset def comment(self, text): """Add a comment to the query. See https://docs.mongodb.com/manual/reference/method/cursor.comment/#cursor.comment for details. """ return self._chainable_method("comment", text) def explain(self): """Return an explain plan record for the :class:`~mongoengine.queryset.QuerySet` cursor. """ return self._cursor.explain() # DEPRECATED. Has no more impact on PyMongo 3+ def snapshot(self, enabled): """Enable or disable snapshot mode when querying. :param enabled: whether or not snapshot mode is enabled """ msg = "snapshot is deprecated as it has no impact when using PyMongo 3+." warnings.warn(msg, DeprecationWarning) queryset = self.clone() queryset._snapshot = enabled return queryset def allow_disk_use(self, enabled): """Enable or disable the use of temporary files on disk while processing a blocking sort operation. (To store data exceeding the 100 megabyte system memory limit) :param enabled: whether or not temporary files on disk are used """ queryset = self.clone() queryset._allow_disk_use = enabled return queryset def timeout(self, enabled): """Enable or disable the default mongod timeout when querying. (no_cursor_timeout option) :param enabled: whether or not the timeout is used """ queryset = self.clone() queryset._timeout = enabled return queryset def read_preference(self, read_preference): """Change the read_preference when querying. :param read_preference: override ReplicaSetConnection-level preference. """ validate_read_preference("read_preference", read_preference) queryset = self.clone() queryset._read_preference = read_preference queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_preference return queryset def read_concern(self, read_concern): """Change the read_concern when querying. :param read_concern: override ReplicaSetConnection-level preference. """ if read_concern is not None and not isinstance(read_concern, Mapping): raise TypeError(f"{read_concern!r} is not a valid read concern.") queryset = self.clone() queryset._read_concern = ( ReadConcern(**read_concern) if read_concern is not None else None ) queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_concern return queryset def scalar(self, *fields): """Instead of returning Document instances, return either a specific value or a tuple of values in order. Can be used along with :func:`~mongoengine.queryset.QuerySet.no_dereference` to turn off dereferencing. .. note:: This effects all results and can be unset by calling ``scalar`` without arguments. Calls ``only`` automatically. :param fields: One or more fields to return instead of a Document. """ queryset = self.clone() queryset._scalar = list(fields) if fields: queryset = queryset.only(*fields) else: queryset = queryset.all_fields() return queryset def values_list(self, *fields): """An alias for scalar""" return self.scalar(*fields) def as_pymongo(self): """Instead of returning Document instances, return raw values from pymongo. This method is particularly useful if you don't need dereferencing and care primarily about the speed of data retrieval. """ queryset = self.clone() queryset._as_pymongo = True return queryset def max_time_ms(self, ms): """Wait `ms` milliseconds before killing the query on the server :param ms: the number of milliseconds before killing the query on the server """ return self._chainable_method("max_time_ms", ms) # JSON Helpers def to_json(self, *args, **kwargs): """Converts a queryset to JSON""" if "json_options" not in kwargs: warnings.warn( "No 'json_options' are specified! Falling back to " "LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. " "For use with other MongoDB drivers specify the UUID " "representation to use. This will be changed to " "uuid_representation=UNSPECIFIED in a future release.", DeprecationWarning, ) kwargs["json_options"] = LEGACY_JSON_OPTIONS return json_util.dumps(self.as_pymongo(), *args, **kwargs) def from_json(self, json_data): """Converts json data to unsaved objects""" son_data = json_util.loads(json_data) return [self._document._from_son(data) for data in son_data] def aggregate(self, pipeline, *suppl_pipeline, **kwargs): """Perform a aggregate function based in your queryset params :param pipeline: list of aggregation commands,\ see: http://docs.mongodb.org/manual/core/aggregation-pipeline/ :param suppl_pipeline: unpacked list of pipeline (added to support deprecation of the old interface) parameter will be removed shortly :param kwargs: (optional) kwargs dictionary to be passed to pymongo's aggregate call See https://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.aggregate """ using_deprecated_interface = isinstance(pipeline, dict) or bool(suppl_pipeline) user_pipeline = [pipeline] if isinstance(pipeline, dict) else list(pipeline) if using_deprecated_interface: msg = "Calling .aggregate() with un unpacked list (*pipeline) is deprecated, it will soon change and will expect a list (similar to pymongo.Collection.aggregate interface), see documentation" warnings.warn(msg, DeprecationWarning) user_pipeline += suppl_pipeline initial_pipeline = [] if self._none or self._empty: initial_pipeline.append({"$limit": 1}) initial_pipeline.append({"$match": {"$expr": False}}) if self._query: initial_pipeline.append({"$match": self._query}) if self._ordering: initial_pipeline.append({"$sort": dict(self._ordering)}) if self._limit is not None: # As per MongoDB Documentation (https://docs.mongodb.com/manual/reference/operator/aggregation/limit/), # keeping limit stage right after sort stage is more efficient. But this leads to wrong set of documents # for a skip stage that might succeed these. So we need to maintain more documents in memory in such a # case (https://stackoverflow.com/a/24161461). initial_pipeline.append({"$limit": self._limit + (self._skip or 0)}) if self._skip is not None: initial_pipeline.append({"$skip": self._skip}) final_pipeline = initial_pipeline + user_pipeline collection = self._collection if self._read_preference is not None or self._read_concern is not None: collection = self._collection.with_options( read_preference=self._read_preference, read_concern=self._read_concern ) return collection.aggregate(final_pipeline, cursor={}, **kwargs) # JS functionality def map_reduce( self, map_f, reduce_f, output, finalize_f=None, limit=None, scope=None ): """Perform a map/reduce query using the current query spec and ordering. While ``map_reduce`` respects ``QuerySet`` chaining, it must be the last call made, as it does not return a maleable ``QuerySet``. See the :meth:`~mongoengine.tests.QuerySetTest.test_map_reduce` and :meth:`~mongoengine.tests.QuerySetTest.test_map_advanced` tests in ``tests.queryset.QuerySetTest`` for usage examples. :param map_f: map function, as :class:`~bson.code.Code` or string :param reduce_f: reduce function, as :class:`~bson.code.Code` or string :param output: output collection name, if set to 'inline' will return the results inline. This can also be a dictionary containing output options see: http://docs.mongodb.org/manual/reference/command/mapReduce/#dbcmd.mapReduce :param finalize_f: finalize function, an optional function that performs any post-reduction processing. :param scope: values to insert into map/reduce global scope. Optional. :param limit: number of objects from current query to provide to map/reduce method Returns an iterator yielding :class:`~mongoengine.document.MapReduceDocument`. """ queryset = self.clone() MapReduceDocument = _import_class("MapReduceDocument") map_f_scope = {} if isinstance(map_f, Code): map_f_scope = map_f.scope map_f = str(map_f) map_f = Code(queryset._sub_js_fields(map_f), map_f_scope or None) reduce_f_scope = {} if isinstance(reduce_f, Code): reduce_f_scope = reduce_f.scope reduce_f = str(reduce_f) reduce_f_code = queryset._sub_js_fields(reduce_f) reduce_f = Code(reduce_f_code, reduce_f_scope or None) mr_args = {"query": queryset._query} if finalize_f: finalize_f_scope = {} if isinstance(finalize_f, Code): finalize_f_scope = finalize_f.scope finalize_f = str(finalize_f) finalize_f_code = queryset._sub_js_fields(finalize_f) finalize_f = Code(finalize_f_code, finalize_f_scope or None) mr_args["finalize"] = finalize_f if scope: mr_args["scope"] = scope if limit: mr_args["limit"] = limit if output == "inline" and not queryset._ordering: inline = True mr_args["out"] = {"inline": 1} else: inline = False if isinstance(output, str): mr_args["out"] = output elif isinstance(output, dict): ordered_output = [] for part in ("replace", "merge", "reduce"): value = output.get(part) if value: ordered_output.append((part, value)) break else: raise OperationError("actionData not specified for output") db_alias = output.get("db_alias") remaing_args = ["db", "sharded", "nonAtomic"] if db_alias: ordered_output.append(("db", get_db(db_alias).name)) del remaing_args[0] for part in remaing_args: value = output.get(part) if value: ordered_output.append((part, value)) mr_args["out"] = SON(ordered_output) db = queryset._document._get_db() result = db.command( { "mapReduce": queryset._document._get_collection_name(), "map": map_f, "reduce": reduce_f, **mr_args, } ) if inline: docs = result["results"] else: if isinstance(result["result"], str): docs = db[result["result"]].find() else: info = result["result"] docs = db.client[info["db"]][info["collection"]].find() if queryset._ordering: docs = docs.sort(queryset._ordering) for doc in docs: yield MapReduceDocument( queryset._document, queryset._collection, doc["_id"], doc["value"] ) def exec_js(self, code, *fields, **options): """Execute a Javascript function on the server. A list of fields may be provided, which will be translated to their correct names and supplied as the arguments to the function. A few extra variables are added to the function's scope: ``collection``, which is the name of the collection in use; ``query``, which is an object representing the current query; and ``options``, which is an object containing any options specified as keyword arguments. As fields in MongoEngine may use different names in the database (set using the :attr:`db_field` keyword argument to a :class:`Field` constructor), a mechanism exists for replacing MongoEngine field names with the database field names in Javascript code. When accessing a field, use square-bracket notation, and prefix the MongoEngine field name with a tilde (~). :param code: a string of Javascript code to execute :param fields: fields that you will be using in your function, which will be passed in to your function as arguments :param options: options that you want available to the function (accessed in Javascript through the ``options`` object) """ queryset = self.clone() code = queryset._sub_js_fields(code) fields = [queryset._document._translate_field_name(f) for f in fields] collection = queryset._document._get_collection_name() scope = {"collection": collection, "options": options or {}} query = queryset._query if queryset._where_clause: query["$where"] = queryset._where_clause scope["query"] = query code = Code(code, scope=scope) db = queryset._document._get_db() return db.command("eval", code, args=fields).get("retval") def where(self, where_clause): """Filter ``QuerySet`` results with a ``$where`` clause (a Javascript expression). Performs automatic field name substitution like :meth:`mongoengine.queryset.Queryset.exec_js`. .. note:: When using this mode of query, the database will call your function, or evaluate your predicate clause, for each object in the collection. """ queryset = self.clone() where_clause = queryset._sub_js_fields(where_clause) queryset._where_clause = where_clause return queryset def sum(self, field): """Sum over the values of the specified field. :param field: the field to sum over; use dot notation to refer to embedded document fields """ db_field = self._fields_to_dbfields([field]).pop() pipeline = [ {"$match": self._query}, {"$group": {"_id": "sum", "total": {"$sum": "$" + db_field}}}, ] # if we're performing a sum over a list field, we sum up all the # elements in the list, hence we need to $unwind the arrays first ListField = _import_class("ListField") field_parts = field.split(".") field_instances = self._document._lookup_field(field_parts) if isinstance(field_instances[-1], ListField): pipeline.insert(1, {"$unwind": "$" + field}) result = tuple(self._document._get_collection().aggregate(pipeline)) if result: return result[0]["total"] return 0 def average(self, field): """Average over the values of the specified field. :param field: the field to average over; use dot notation to refer to embedded document fields """ db_field = self._fields_to_dbfields([field]).pop() pipeline = [ {"$match": self._query}, {"$group": {"_id": "avg", "total": {"$avg": "$" + db_field}}}, ] # if we're performing an average over a list field, we average out # all the elements in the list, hence we need to $unwind the arrays # first ListField = _import_class("ListField") field_parts = field.split(".") field_instances = self._document._lookup_field(field_parts) if isinstance(field_instances[-1], ListField): pipeline.insert(1, {"$unwind": "$" + field}) result = tuple(self._document._get_collection().aggregate(pipeline)) if result: return result[0]["total"] return 0 def item_frequencies(self, field, normalize=False, map_reduce=True): """Returns a dictionary of all items present in a field across the whole queried set of documents, and their corresponding frequency. This is useful for generating tag clouds, or searching documents. .. note:: Can only do direct simple mappings and cannot map across :class:`~mongoengine.fields.ReferenceField` or :class:`~mongoengine.fields.GenericReferenceField` for more complex counting a manual map reduce call is required. If the field is a :class:`~mongoengine.fields.ListField`, the items within each list will be counted individually. :param field: the field to use :param normalize: normalize the results so they add to 1.0 :param map_reduce: Use map_reduce over exec_js """ if map_reduce: return self._item_frequencies_map_reduce(field, normalize=normalize) return self._item_frequencies_exec_js(field, normalize=normalize) # Iterator helpers def __next__(self): """Wrap the result in a :class:`~mongoengine.Document` object.""" if self._none or self._empty: raise StopIteration raw_doc = next(self._cursor) if self._as_pymongo: return raw_doc doc = self._document._from_son( raw_doc, _auto_dereference=self._auto_dereference, ) if self._scalar: return self._get_scalar(doc) return doc def rewind(self): """Rewind the cursor to its unevaluated state.""" self._iter = False self._cursor.rewind() # Properties @property def _collection(self): """Property that returns the collection object. This allows us to perform operations only if the collection is accessed. """ return self._collection_obj @property def _cursor_args(self): fields_name = "projection" # snapshot is not handled at all by PyMongo 3+ # TODO: evaluate similar possibilities using modifiers if self._snapshot: msg = "The snapshot option is not anymore available with PyMongo 3+" warnings.warn(msg, DeprecationWarning) cursor_args = {} if not self._timeout: cursor_args["no_cursor_timeout"] = True if self._allow_disk_use: cursor_args["allow_disk_use"] = True if self._loaded_fields: cursor_args[fields_name] = self._loaded_fields.as_dict() if self._search_text: if fields_name not in cursor_args: cursor_args[fields_name] = {} cursor_args[fields_name]["_text_score"] = {"$meta": "textScore"} return cursor_args @property def _cursor(self): """Return a PyMongo cursor object corresponding to this queryset.""" # If _cursor_obj already exists, return it immediately. if self._cursor_obj is not None: return self._cursor_obj # Create a new PyMongo cursor. # XXX In PyMongo 3+, we define the read preference on a collection # level, not a cursor level. Thus, we need to get a cloned collection # object using `with_options` first. if self._read_preference is not None or self._read_concern is not None: self._cursor_obj = self._collection.with_options( read_preference=self._read_preference, read_concern=self._read_concern ).find(self._query, **self._cursor_args) else: self._cursor_obj = self._collection.find(self._query, **self._cursor_args) # Apply "where" clauses to cursor if self._where_clause: where_clause = self._sub_js_fields(self._where_clause) self._cursor_obj.where(where_clause) # Apply ordering to the cursor. # XXX self._ordering can be equal to: # * None if we didn't explicitly call order_by on this queryset. # * A list of PyMongo-style sorting tuples. # * An empty list if we explicitly called order_by() without any # arguments. This indicates that we want to clear the default # ordering. if self._ordering: # explicit ordering self._cursor_obj.sort(self._ordering) elif self._ordering is None and self._document._meta["ordering"]: # default ordering order = self._get_order_by(self._document._meta["ordering"]) self._cursor_obj.sort(order) if self._limit is not None: self._cursor_obj.limit(self._limit) if self._skip is not None: self._cursor_obj.skip(self._skip) if self._hint != -1: self._cursor_obj.hint(self._hint) if self._collation is not None: self._cursor_obj.collation(self._collation) if self._batch_size is not None: self._cursor_obj.batch_size(self._batch_size) if self._comment is not None: self._cursor_obj.comment(self._comment) return self._cursor_obj def __deepcopy__(self, memo): """Essential for chained queries with ReferenceFields involved""" return self.clone() @property def _query(self): if self._mongo_query is None: self._mongo_query = self._query_obj.to_query(self._document) if self._cls_query: if "_cls" in self._mongo_query: self._mongo_query = {"$and": [self._cls_query, self._mongo_query]} else: self._mongo_query.update(self._cls_query) return self._mongo_query @property def _dereference(self): if not self.__dereference: self.__dereference = _import_class("DeReference")() return self.__dereference def no_dereference(self): """Turn off any dereferencing for the results of this queryset.""" queryset = self.clone() queryset._auto_dereference = False return queryset # Helper Functions def _item_frequencies_map_reduce(self, field, normalize=False): map_func = """ function() {{ var path = '{{{{~{field}}}}}'.split('.'); var field = this; for (p in path) {{ if (typeof field != 'undefined') field = field[path[p]]; else break; }} if (field && field.constructor == Array) {{ field.forEach(function(item) {{ emit(item, 1); }}); }} else if (typeof field != 'undefined') {{ emit(field, 1); }} else {{ emit(null, 1); }} }} """.format( field=field ) reduce_func = """ function(key, values) { var total = 0; var valuesSize = values.length; for (var i=0; i < valuesSize; i++) { total += parseInt(values[i], 10); } return total; } """ values = self.map_reduce(map_func, reduce_func, "inline") frequencies = {} for f in values: key = f.key if isinstance(key, float): if int(key) == key: key = int(key) frequencies[key] = int(f.value) if normalize: count = sum(frequencies.values()) frequencies = {k: float(v) / count for k, v in frequencies.items()} return frequencies def _item_frequencies_exec_js(self, field, normalize=False): """Uses exec_js to execute""" freq_func = """ function(path) { var path = path.split('.'); var total = 0.0; db[collection].find(query).forEach(function(doc) { var field = doc; for (p in path) { if (field) field = field[path[p]]; else break; } if (field && field.constructor == Array) { total += field.length; } else { total++; } }); var frequencies = {}; var types = {}; var inc = 1.0; db[collection].find(query).forEach(function(doc) { field = doc; for (p in path) { if (field) field = field[path[p]]; else break; } if (field && field.constructor == Array) { field.forEach(function(item) { frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]); }); } else { var item = field; types[item] = item; frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]); } }); return [total, frequencies, types]; } """ total, data, types = self.exec_js(freq_func, field) values = {types.get(k): int(v) for k, v in data.items()} if normalize: values = {k: float(v) / total for k, v in values.items()} frequencies = {} for k, v in values.items(): if isinstance(k, float): if int(k) == k: k = int(k) frequencies[k] = v return frequencies def _fields_to_dbfields(self, fields): """Translate fields' paths to their db equivalents.""" subclasses = [] if self._document._meta["allow_inheritance"]: subclasses = [get_document(x) for x in self._document._subclasses][1:] db_field_paths = [] for field in fields: field_parts = field.split(".") try: field = ".".join( f if isinstance(f, str) else f.db_field for f in self._document._lookup_field(field_parts) ) db_field_paths.append(field) except LookUpError as err: found = False # If a field path wasn't found on the main document, go # through its subclasses and see if it exists on any of them. for subdoc in subclasses: try: subfield = ".".join( f if isinstance(f, str) else f.db_field for f in subdoc._lookup_field(field_parts) ) db_field_paths.append(subfield) found = True break except LookUpError: pass if not found: raise err return db_field_paths def _get_order_by(self, keys): """Given a list of MongoEngine-style sort keys, return a list of sorting tuples that can be applied to a PyMongo cursor. For example: >>> qs._get_order_by(['-last_name', 'first_name']) [('last_name', -1), ('first_name', 1)] """ key_list = [] for key in keys: if not key: continue if key == "$text_score": key_list.append(("_text_score", {"$meta": "textScore"})) continue direction = pymongo.ASCENDING if key[0] == "-": direction = pymongo.DESCENDING if key[0] in ("-", "+"): key = key[1:] key = key.replace("__", ".") try: key = self._document._translate_field_name(key) except Exception: # TODO this exception should be more specific pass key_list.append((key, direction)) return key_list def _get_scalar(self, doc): def lookup(obj, name): chunks = name.split("__") for chunk in chunks: obj = getattr(obj, chunk) return obj data = [lookup(doc, n) for n in self._scalar] if len(data) == 1: return data[0] return tuple(data) def _sub_js_fields(self, code): """When fields are specified with [~fieldname] syntax, where *fieldname* is the Python name of a field, *fieldname* will be substituted for the MongoDB name of the field (specified using the :attr:`name` keyword argument in a field's constructor). """ def field_sub(match): # Extract just the field name, and look up the field objects field_name = match.group(1).split(".") fields = self._document._lookup_field(field_name) # Substitute the correct name for the field into the javascript return '["%s"]' % fields[-1].db_field def field_path_sub(match): # Extract just the field name, and look up the field objects field_name = match.group(1).split(".") fields = self._document._lookup_field(field_name) # Substitute the correct name for the field into the javascript return ".".join([f.db_field for f in fields]) code = re.sub(r"\[\s*~([A-z_][A-z_0-9.]+?)\s*\]", field_sub, code) code = re.sub(r"\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}", field_path_sub, code) return code def _chainable_method(self, method_name, val): """Call a particular method on the PyMongo cursor call a particular chainable method with the provided value. """ queryset = self.clone() # Get an existing cursor object or create a new one cursor = queryset._cursor # Find the requested method on the cursor and call it with the # provided value getattr(cursor, method_name)(val) # Cache the value on the queryset._{method_name} setattr(queryset, "_" + method_name, val) return queryset ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/field_list.py0000644000175100001730000000562414400345475022401 0ustar00runnerdocker__all__ = ("QueryFieldList",) class QueryFieldList: """Object that handles combinations of .only() and .exclude() calls""" ONLY = 1 EXCLUDE = 0 def __init__( self, fields=None, value=ONLY, always_include=None, _only_called=False ): """The QueryFieldList builder :param fields: A list of fields used in `.only()` or `.exclude()` :param value: How to handle the fields; either `ONLY` or `EXCLUDE` :param always_include: Any fields to always_include eg `_cls` :param _only_called: Has `.only()` been called? If so its a set of fields otherwise it performs a union. """ self.value = value self.fields = set(fields or []) self.always_include = set(always_include or []) self._id = None self._only_called = _only_called self.slice = {} def __add__(self, f): if isinstance(f.value, dict): for field in f.fields: self.slice[field] = f.value if not self.fields: self.fields = f.fields elif not self.fields: self.fields = f.fields self.value = f.value self.slice = {} elif self.value is self.ONLY and f.value is self.ONLY: self._clean_slice() if self._only_called: self.fields = self.fields.union(f.fields) else: self.fields = f.fields elif self.value is self.EXCLUDE and f.value is self.EXCLUDE: self.fields = self.fields.union(f.fields) self._clean_slice() elif self.value is self.ONLY and f.value is self.EXCLUDE: self.fields -= f.fields self._clean_slice() elif self.value is self.EXCLUDE and f.value is self.ONLY: self.value = self.ONLY self.fields = f.fields - self.fields self._clean_slice() if "_id" in f.fields: self._id = f.value if self.always_include: if self.value is self.ONLY and self.fields: if sorted(self.slice.keys()) != sorted(self.fields): self.fields = self.fields.union(self.always_include) else: self.fields -= self.always_include if getattr(f, "_only_called", False): self._only_called = True return self def __bool__(self): return bool(self.fields) def as_dict(self): field_list = {field: self.value for field in self.fields} if self.slice: field_list.update(self.slice) if self._id is not None: field_list["_id"] = self._id return field_list def reset(self): self.fields = set() self.slice = {} self.value = self.ONLY def _clean_slice(self): if self.slice: for field in set(self.slice.keys()) - self.fields: del self.slice[field] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/manager.py0000644000175100001730000000425614400345475021675 0ustar00runnerdockerfrom functools import partial from mongoengine.queryset.queryset import QuerySet __all__ = ("queryset_manager", "QuerySetManager") class QuerySetManager: """ The default QuerySet Manager. Custom QuerySet Manager functions can extend this class and users can add extra queryset functionality. Any custom manager methods must accept a :class:`~mongoengine.Document` class as its first argument, and a :class:`~mongoengine.queryset.QuerySet` as its second argument. The method function should return a :class:`~mongoengine.queryset.QuerySet` , probably the same one that was passed in, but modified in some way. """ get_queryset = None default = QuerySet def __init__(self, queryset_func=None): if queryset_func: self.get_queryset = queryset_func def __get__(self, instance, owner): """Descriptor for instantiating a new QuerySet object when Document.objects is accessed. """ if instance is not None: # Document object being used rather than a document class return self # owner is the document that contains the QuerySetManager queryset_class = owner._meta.get("queryset_class", self.default) queryset = queryset_class(owner, owner._get_collection()) if self.get_queryset: arg_count = self.get_queryset.__code__.co_argcount if arg_count == 1: queryset = self.get_queryset(queryset) elif arg_count == 2: queryset = self.get_queryset(owner, queryset) else: queryset = partial(self.get_queryset, owner, queryset) return queryset def queryset_manager(func): """Decorator that allows you to define custom QuerySet managers on :class:`~mongoengine.Document` classes. The manager must be a function that accepts a :class:`~mongoengine.Document` class as its first argument, and a :class:`~mongoengine.queryset.QuerySet` as its second argument. The method function should return a :class:`~mongoengine.queryset.QuerySet`, probably the same one that was passed in, but modified in some way. """ return QuerySetManager(func) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/queryset.py0000644000175100001730000001346014400345475022141 0ustar00runnerdockerfrom mongoengine.errors import OperationError from mongoengine.queryset.base import ( CASCADE, DENY, DO_NOTHING, NULLIFY, PULL, BaseQuerySet, ) __all__ = ( "QuerySet", "QuerySetNoCache", "DO_NOTHING", "NULLIFY", "CASCADE", "DENY", "PULL", ) # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 ITER_CHUNK_SIZE = 100 class QuerySet(BaseQuerySet): """The default queryset, that builds queries and handles a set of results returned from a query. Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as the results. """ _has_more = True _len = None _result_cache = None def __iter__(self): """Iteration utilises a results cache which iterates the cursor in batches of ``ITER_CHUNK_SIZE``. If ``self._has_more`` the cursor hasn't been exhausted so cache then batch. Otherwise iterate the result_cache. """ self._iter = True if self._has_more: return self._iter_results() # iterating over the cache. return iter(self._result_cache) def __len__(self): """Since __len__ is called quite frequently (for example, as part of list(qs)), we populate the result cache and cache the length. """ if self._len is not None: return self._len # Populate the result cache with *all* of the docs in the cursor if self._has_more: list(self._iter_results()) # Cache the length of the complete result cache and return it self._len = len(self._result_cache) return self._len def __repr__(self): """Provide a string representation of the QuerySet""" if self._iter: return ".. queryset mid-iteration .." self._populate_cache() data = self._result_cache[: REPR_OUTPUT_SIZE + 1] if len(data) > REPR_OUTPUT_SIZE: data[-1] = "...(remaining elements truncated)..." return repr(data) def _iter_results(self): """A generator for iterating over the result cache. Also populates the cache if there are more possible results to yield. Raises StopIteration when there are no more results. """ if self._result_cache is None: self._result_cache = [] pos = 0 while True: # For all positions lower than the length of the current result # cache, serve the docs straight from the cache w/o hitting the # database. # XXX it's VERY important to compute the len within the `while` # condition because the result cache might expand mid-iteration # (e.g. if we call len(qs) inside a loop that iterates over the # queryset). Fortunately len(list) is O(1) in Python, so this # doesn't cause performance issues. while pos < len(self._result_cache): yield self._result_cache[pos] pos += 1 # return if we already established there were no more # docs in the db cursor. if not self._has_more: return # Otherwise, populate more of the cache and repeat. if len(self._result_cache) <= pos: self._populate_cache() def _populate_cache(self): """ Populates the result cache with ``ITER_CHUNK_SIZE`` more entries (until the cursor is exhausted). """ if self._result_cache is None: self._result_cache = [] # Skip populating the cache if we already established there are no # more docs to pull from the database. if not self._has_more: return # Pull in ITER_CHUNK_SIZE docs from the database and store them in # the result cache. try: for _ in range(ITER_CHUNK_SIZE): self._result_cache.append(next(self)) except StopIteration: # Getting this exception means there are no more docs in the # db cursor. Set _has_more to False so that we can use that # information in other places. self._has_more = False def count(self, with_limit_and_skip=False): """Count the selected elements in the query. :param with_limit_and_skip (optional): take any :meth:`limit` or :meth:`skip` that has been applied to this cursor into account when getting the count """ if with_limit_and_skip is False: return super().count(with_limit_and_skip) if self._len is None: # cache the length self._len = super().count(with_limit_and_skip) return self._len def no_cache(self): """Convert to a non-caching queryset""" if self._result_cache is not None: raise OperationError("QuerySet already cached") return self._clone_into(QuerySetNoCache(self._document, self._collection)) class QuerySetNoCache(BaseQuerySet): """A non caching QuerySet""" def cache(self): """Convert to a caching queryset""" return self._clone_into(QuerySet(self._document, self._collection)) def __repr__(self): """Provides the string representation of the QuerySet""" if self._iter: return ".. queryset mid-iteration .." data = [] for _ in range(REPR_OUTPUT_SIZE + 1): try: data.append(next(self)) except StopIteration: break if len(data) > REPR_OUTPUT_SIZE: data[-1] = "...(remaining elements truncated)..." self.rewind() return repr(data) def __iter__(self): queryset = self if queryset._iter: queryset = self.clone() queryset.rewind() return queryset ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/transform.py0000644000175100001730000004552414400345475022301 0ustar00runnerdockerfrom collections import defaultdict import pymongo from bson import SON, ObjectId from bson.dbref import DBRef from mongoengine.base import UPDATE_OPERATORS from mongoengine.common import _import_class from mongoengine.errors import InvalidQueryError __all__ = ("query", "update", "STRING_OPERATORS") COMPARISON_OPERATORS = ( "ne", "gt", "gte", "lt", "lte", "in", "nin", "mod", "all", "size", "exists", "not", "elemMatch", "type", ) GEO_OPERATORS = ( "within_distance", "within_spherical_distance", "within_box", "within_polygon", "near", "near_sphere", "max_distance", "min_distance", "geo_within", "geo_within_box", "geo_within_polygon", "geo_within_center", "geo_within_sphere", "geo_intersects", ) STRING_OPERATORS = ( "contains", "icontains", "startswith", "istartswith", "endswith", "iendswith", "exact", "iexact", "regex", "iregex", "wholeword", "iwholeword", ) CUSTOM_OPERATORS = ("match",) MATCH_OPERATORS = ( COMPARISON_OPERATORS + GEO_OPERATORS + STRING_OPERATORS + CUSTOM_OPERATORS ) # TODO make this less complex def query(_doc_cls=None, **kwargs): """Transform a query from Django-style format to Mongo format.""" mongo_query = {} merge_query = defaultdict(list) for key, value in sorted(kwargs.items()): if key == "__raw__": mongo_query.update(value) continue parts = key.rsplit("__") indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()] parts = [part for part in parts if not part.isdigit()] # Check for an operator and transform to mongo-style if there is op = None if len(parts) > 1 and parts[-1] in MATCH_OPERATORS: op = parts.pop() # Allow to escape operator-like field name by __ if len(parts) > 1 and parts[-1] == "": parts.pop() negate = False if len(parts) > 1 and parts[-1] == "not": parts.pop() negate = True if _doc_cls: # Switch field names to proper names [set in Field(name='foo')] try: fields = _doc_cls._lookup_field(parts) except Exception as e: raise InvalidQueryError(e) parts = [] CachedReferenceField = _import_class("CachedReferenceField") GenericReferenceField = _import_class("GenericReferenceField") cleaned_fields = [] for field in fields: append_field = True if isinstance(field, str): parts.append(field) append_field = False # is last and CachedReferenceField elif isinstance(field, CachedReferenceField) and fields[-1] == field: parts.append("%s._id" % field.db_field) else: parts.append(field.db_field) if append_field: cleaned_fields.append(field) # Convert value to proper value field = cleaned_fields[-1] singular_ops = [None, "ne", "gt", "gte", "lt", "lte", "not"] singular_ops += STRING_OPERATORS if op in singular_ops: value = field.prepare_query_value(op, value) if isinstance(field, CachedReferenceField) and value: value = value["_id"] elif op in ("in", "nin", "all", "near") and not isinstance(value, dict): # Raise an error if the in/nin/all/near param is not iterable. value = _prepare_query_for_iterable(field, op, value) # If we're querying a GenericReferenceField, we need to alter the # key depending on the value: # * If the value is a DBRef, the key should be "field_name._ref". # * If the value is an ObjectId, the key should be "field_name._ref.$id". if isinstance(field, GenericReferenceField): if isinstance(value, DBRef): parts[-1] += "._ref" elif isinstance(value, ObjectId): parts[-1] += "._ref.$id" # if op and op not in COMPARISON_OPERATORS: if op: if op in GEO_OPERATORS: value = _geo_operator(field, op, value) elif op in ("match", "elemMatch"): ListField = _import_class("ListField") EmbeddedDocumentField = _import_class("EmbeddedDocumentField") if ( isinstance(value, dict) and isinstance(field, ListField) and isinstance(field.field, EmbeddedDocumentField) ): value = query(field.field.document_type, **value) else: value = field.prepare_query_value(op, value) value = {"$elemMatch": value} elif op in CUSTOM_OPERATORS: NotImplementedError( 'Custom method "%s" has not ' "been implemented" % op ) elif op not in STRING_OPERATORS: value = {"$" + op: value} if negate: value = {"$not": value} for i, part in indices: parts.insert(i, part) key = ".".join(parts) if key not in mongo_query: mongo_query[key] = value else: if isinstance(mongo_query[key], dict) and isinstance(value, dict): mongo_query[key].update(value) # $max/minDistance needs to come last - convert to SON value_dict = mongo_query[key] if ("$maxDistance" in value_dict or "$minDistance" in value_dict) and ( "$near" in value_dict or "$nearSphere" in value_dict ): value_son = SON() for k, v in value_dict.items(): if k == "$maxDistance" or k == "$minDistance": continue value_son[k] = v # Required for MongoDB >= 2.6, may fail when combining # PyMongo 3+ and MongoDB < 2.6 near_embedded = False for near_op in ("$near", "$nearSphere"): if isinstance(value_dict.get(near_op), dict): value_son[near_op] = SON(value_son[near_op]) if "$maxDistance" in value_dict: value_son[near_op]["$maxDistance"] = value_dict[ "$maxDistance" ] if "$minDistance" in value_dict: value_son[near_op]["$minDistance"] = value_dict[ "$minDistance" ] near_embedded = True if not near_embedded: if "$maxDistance" in value_dict: value_son["$maxDistance"] = value_dict["$maxDistance"] if "$minDistance" in value_dict: value_son["$minDistance"] = value_dict["$minDistance"] mongo_query[key] = value_son else: # Store for manually merging later merge_query[key].append(value) # The queryset has been filter in such a way we must manually merge for k, v in merge_query.items(): merge_query[k].append(mongo_query[k]) del mongo_query[k] if isinstance(v, list): value = [{k: val} for val in v] if "$and" in mongo_query.keys(): mongo_query["$and"].extend(value) else: mongo_query["$and"] = value return mongo_query def update(_doc_cls=None, **update): """Transform an update spec from Django-style format to Mongo format. """ mongo_update = {} for key, value in update.items(): if key == "__raw__": mongo_update.update(value) continue parts = key.split("__") # if there is no operator, default to 'set' if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS: parts.insert(0, "set") # Check for an operator and transform to mongo-style if there is op = None if parts[0] in UPDATE_OPERATORS: op = parts.pop(0) # Convert Pythonic names to Mongo equivalents operator_map = { "push_all": "pushAll", "pull_all": "pullAll", "dec": "inc", "add_to_set": "addToSet", "set_on_insert": "setOnInsert", } if op == "dec": # Support decrement by flipping a positive value's sign # and using 'inc' value = -value # If the operator doesn't found from operator map, the op value # will stay unchanged op = operator_map.get(op, op) match = None if len(parts) == 1: # typical update like set__field # but also allows to update a field named like a comparison operator # like set__type = "something" (without clashing with the 'type' operator) pass elif len(parts) > 1: # can be either an embedded field like set__foo__bar # or a comparison operator as in pull__foo__in if parts[-1] in COMPARISON_OPERATORS: match = parts.pop() # e.g. pop 'in' from pull__foo__in # Allow to escape operator-like field name by __ # e.g. in the case of an embedded foo.type field # Doc.objects().update(set__foo__type="bar") # see https://github.com/MongoEngine/mongoengine/pull/1351 if parts[-1] == "": match = parts.pop() # e.g. pop last '__' from set__foo__type__ if _doc_cls: # Switch field names to proper names [set in Field(name='foo')] try: fields = _doc_cls._lookup_field(parts) except Exception as e: raise InvalidQueryError(e) parts = [] cleaned_fields = [] appended_sub_field = False for field in fields: append_field = True if isinstance(field, str): # Convert the S operator to $ if field == "S": field = "$" parts.append(field) append_field = False else: parts.append(field.db_field) if append_field: appended_sub_field = False cleaned_fields.append(field) if hasattr(field, "field"): cleaned_fields.append(field.field) appended_sub_field = True # Convert value to proper value if appended_sub_field: field = cleaned_fields[-2] else: field = cleaned_fields[-1] GeoJsonBaseField = _import_class("GeoJsonBaseField") if isinstance(field, GeoJsonBaseField): value = field.to_mongo(value) if op == "pull": if field.required or value is not None: if match in ("in", "nin") and not isinstance(value, dict): value = _prepare_query_for_iterable(field, op, value) else: value = field.prepare_query_value(op, value) elif op == "push" and isinstance(value, (list, tuple, set)): value = [field.prepare_query_value(op, v) for v in value] elif op in (None, "set", "push"): if field.required or value is not None: value = field.prepare_query_value(op, value) elif op in ("pushAll", "pullAll"): value = [field.prepare_query_value(op, v) for v in value] elif op in ("addToSet", "setOnInsert"): if isinstance(value, (list, tuple, set)): value = [field.prepare_query_value(op, v) for v in value] elif field.required or value is not None: value = field.prepare_query_value(op, value) elif op == "unset": value = 1 elif op == "inc": value = field.prepare_query_value(op, value) if match: match = "$" + match value = {match: value} key = ".".join(parts) if "pull" in op and "." in key: # Dot operators don't work on pull operations # unless they point to a list field # Otherwise it uses nested dict syntax if op == "pullAll": raise InvalidQueryError( "pullAll operations only support a single field depth" ) # Look for the last list field and use dot notation until there field_classes = [c.__class__ for c in cleaned_fields] field_classes.reverse() ListField = _import_class("ListField") EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField") if ListField in field_classes or EmbeddedDocumentListField in field_classes: # Join all fields via dot notation to the last ListField or EmbeddedDocumentListField # Then process as normal if ListField in field_classes: _check_field = ListField else: _check_field = EmbeddedDocumentListField last_listField = len(cleaned_fields) - field_classes.index(_check_field) key = ".".join(parts[:last_listField]) parts = parts[last_listField:] parts.insert(0, key) parts.reverse() for key in parts: value = {key: value} elif op == "addToSet" and isinstance(value, list): value = {key: {"$each": value}} elif op in ("push", "pushAll"): if parts[-1].isdigit(): key = ".".join(parts[0:-1]) position = int(parts[-1]) # $position expects an iterable. If pushing a single value, # wrap it in a list. if not isinstance(value, (set, tuple, list)): value = [value] value = {key: {"$each": value, "$position": position}} else: if op == "pushAll": op = "push" # convert to non-deprecated keyword if not isinstance(value, (set, tuple, list)): value = [value] value = {key: {"$each": value}} else: value = {key: value} else: value = {key: value} key = "$" + op if key not in mongo_update: mongo_update[key] = value elif key in mongo_update and isinstance(mongo_update[key], dict): mongo_update[key].update(value) return mongo_update def _geo_operator(field, op, value): """Helper to return the query for a given geo query.""" if op == "max_distance": value = {"$maxDistance": value} elif op == "min_distance": value = {"$minDistance": value} elif field._geo_index == pymongo.GEO2D: if op == "within_distance": value = {"$within": {"$center": value}} elif op == "within_spherical_distance": value = {"$within": {"$centerSphere": value}} elif op == "within_polygon": value = {"$within": {"$polygon": value}} elif op == "near": value = {"$near": value} elif op == "near_sphere": value = {"$nearSphere": value} elif op == "within_box": value = {"$within": {"$box": value}} else: raise NotImplementedError( 'Geo method "%s" has not been ' "implemented for a GeoPointField" % op ) else: if op == "geo_within": value = {"$geoWithin": _infer_geometry(value)} elif op == "geo_within_box": value = {"$geoWithin": {"$box": value}} elif op == "geo_within_polygon": value = {"$geoWithin": {"$polygon": value}} elif op == "geo_within_center": value = {"$geoWithin": {"$center": value}} elif op == "geo_within_sphere": value = {"$geoWithin": {"$centerSphere": value}} elif op == "geo_intersects": value = {"$geoIntersects": _infer_geometry(value)} elif op == "near": value = {"$near": _infer_geometry(value)} else: raise NotImplementedError( 'Geo method "{}" has not been implemented for a {} '.format( op, field._name ) ) return value def _infer_geometry(value): """Helper method that tries to infer the $geometry shape for a given value. """ if isinstance(value, dict): if "$geometry" in value: return value elif "coordinates" in value and "type" in value: return {"$geometry": value} raise InvalidQueryError( "Invalid $geometry dictionary should have type and coordinates keys" ) elif isinstance(value, (list, set)): # TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon? try: value[0][0][0] return {"$geometry": {"type": "Polygon", "coordinates": value}} except (TypeError, IndexError): pass try: value[0][0] return {"$geometry": {"type": "LineString", "coordinates": value}} except (TypeError, IndexError): pass try: value[0] return {"$geometry": {"type": "Point", "coordinates": value}} except (TypeError, IndexError): pass raise InvalidQueryError( "Invalid $geometry data. Can be either a " "dictionary or (nested) lists of coordinate(s)" ) def _prepare_query_for_iterable(field, op, value): # We need a special check for BaseDocument, because - although it's iterable - using # it as such in the context of this method is most definitely a mistake. BaseDocument = _import_class("BaseDocument") if isinstance(value, BaseDocument): raise TypeError( "When using the `in`, `nin`, `all`, or " "`near`-operators you can't use a " "`Document`, you must wrap your object " "in a list (object -> [object])." ) if not hasattr(value, "__iter__"): raise TypeError( "The `in`, `nin`, `all`, or " "`near`-operators must be applied to an " "iterable (e.g. a list)." ) return [field.prepare_query_value(op, v) for v in value] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/queryset/visitor.py0000644000175100001730000001253214400345475021756 0ustar00runnerdockerimport copy import warnings from mongoengine.errors import InvalidQueryError from mongoengine.queryset import transform __all__ = ("Q", "QNode") def warn_empty_is_deprecated(): msg = "'empty' property is deprecated in favour of using 'not bool(filter)'" warnings.warn(msg, DeprecationWarning, stacklevel=2) class QNodeVisitor: """Base visitor class for visiting Q-object nodes in a query tree.""" def visit_combination(self, combination): """Called by QCombination objects.""" return combination def visit_query(self, query): """Called by (New)Q objects.""" return query class DuplicateQueryConditionsError(InvalidQueryError): pass class SimplificationVisitor(QNodeVisitor): """Simplifies query trees by combining unnecessary 'and' connection nodes into a single Q-object. """ def visit_combination(self, combination): if combination.operation == combination.AND: # The simplification only applies to 'simple' queries if all(isinstance(node, Q) for node in combination.children): queries = [n.query for n in combination.children] try: return Q(**self._query_conjunction(queries)) except DuplicateQueryConditionsError: # Cannot be simplified pass return combination def _query_conjunction(self, queries): """Merges query dicts - effectively &ing them together.""" query_ops = set() combined_query = {} for query in queries: ops = set(query.keys()) # Make sure that the same operation isn't applied more than once # to a single field intersection = ops.intersection(query_ops) if intersection: raise DuplicateQueryConditionsError() query_ops.update(ops) combined_query.update(copy.deepcopy(query)) return combined_query class QueryCompilerVisitor(QNodeVisitor): """Compiles the nodes in a query tree to a PyMongo-compatible query dictionary. """ def __init__(self, document): self.document = document def visit_combination(self, combination): operator = "$and" if combination.operation == combination.OR: operator = "$or" return {operator: combination.children} def visit_query(self, query): return transform.query(self.document, **query.query) class QNode: """Base class for nodes in query trees.""" AND = 0 OR = 1 def to_query(self, document): query = self.accept(SimplificationVisitor()) query = query.accept(QueryCompilerVisitor(document)) return query def accept(self, visitor): raise NotImplementedError def _combine(self, other, operation): """Combine this node with another node into a QCombination object. """ # If the other Q() is empty, ignore it and just use `self`. if not bool(other): return self # Or if this Q is empty, ignore it and just use `other`. if not bool(self): return other return QCombination(operation, [self, other]) @property def empty(self): warn_empty_is_deprecated() return False def __or__(self, other): return self._combine(other, self.OR) def __and__(self, other): return self._combine(other, self.AND) class QCombination(QNode): """Represents the combination of several conditions by a given logical operator. """ def __init__(self, operation, children): self.operation = operation self.children = [] for node in children: # If the child is a combination of the same type, we can merge its # children directly into this combinations children if isinstance(node, QCombination) and node.operation == operation: self.children += node.children else: self.children.append(node) def __repr__(self): op = " & " if self.operation is self.AND else " | " return "(%s)" % op.join([repr(node) for node in self.children]) def __bool__(self): return bool(self.children) def accept(self, visitor): for i in range(len(self.children)): if isinstance(self.children[i], QNode): self.children[i] = self.children[i].accept(visitor) return visitor.visit_combination(self) @property def empty(self): warn_empty_is_deprecated() return not bool(self.children) def __eq__(self, other): return ( self.__class__ == other.__class__ and self.operation == other.operation and self.children == other.children ) class Q(QNode): """A simple query object, used in a query tree to build up more complex query structures. """ def __init__(self, **query): self.query = query def __repr__(self): return "Q(**%s)" % repr(self.query) def __bool__(self): return bool(self.query) def __eq__(self, other): return self.__class__ == other.__class__ and self.query == other.query def accept(self, visitor): return visitor.visit_query(self) @property def empty(self): warn_empty_is_deprecated() return not bool(self.query) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/mongoengine/signals.py0000644000175100001730000000337514400345475020043 0ustar00runnerdocker__all__ = ( "pre_init", "post_init", "pre_save", "pre_save_post_validation", "post_save", "pre_delete", "post_delete", ) signals_available = False try: from blinker import Namespace signals_available = True except ImportError: class Namespace: def signal(self, name, doc=None): return _FakeSignal(name, doc) class _FakeSignal: """If blinker is unavailable, create a fake class with the same interface that allows sending of signals but will fail with an error on anything else. Instead of doing anything on send, it will just ignore the arguments and do nothing instead. """ def __init__(self, name, doc=None): self.name = name self.__doc__ = doc def _fail(self, *args, **kwargs): raise RuntimeError( "signalling support is unavailable " "because the blinker library is " "not installed." ) send = lambda *a, **kw: None # noqa connect = ( disconnect ) = has_receivers_for = receivers_for = temporarily_connected_to = _fail del _fail # the namespace for code signals. If you are not mongoengine code, do # not put signals in here. Create your own namespace instead. _signals = Namespace() pre_init = _signals.signal("pre_init") post_init = _signals.signal("post_init") pre_save = _signals.signal("pre_save") pre_save_post_validation = _signals.signal("pre_save_post_validation") post_save = _signals.signal("post_save") pre_delete = _signals.signal("pre_delete") post_delete = _signals.signal("post_delete") pre_bulk_insert = _signals.signal("pre_bulk_insert") post_bulk_insert = _signals.signal("post_bulk_insert") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/mongoengine.egg-info/0000755000175100001730000000000014400345501017501 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839168.0 mongoengine-0.27.0/mongoengine.egg-info/PKG-INFO0000644000175100001730000001503514400345500020601 0ustar00runnerdockerMetadata-Version: 2.1 Name: mongoengine Version: 0.27.0 Summary: MongoEngine is a Python Object-Document Mapper for working with MongoDB. Home-page: http://mongoengine.org/ Author: Harry Marr Author-email: harry.marr@gmail.com Maintainer: Stefan Wojcik Maintainer-email: wojcikstefan@gmail.com License: MIT Download-URL: https://github.com/MongoEngine/mongoengine/tarball/master Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Database Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.7 License-File: LICENSE License-File: AUTHORS =========== MongoEngine =========== :Info: MongoEngine is an ORM-like layer on top of PyMongo. :Repository: https://github.com/MongoEngine/mongoengine :Author: Harry Marr (http://github.com/hmarr) :Maintainer: Stefan Wójcik (http://github.com/wojcikstefan) .. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master :target: https://travis-ci.org/MongoEngine/mongoengine .. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master :target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black .. image:: https://pepy.tech/badge/mongoengine/month :target: https://pepy.tech/project/mongoengine .. image:: https://img.shields.io/pypi/v/mongoengine.svg :target: https://pypi.python.org/pypi/mongoengine About ===== MongoEngine is a Python Object-Document Mapper for working with MongoDB. Documentation is available at https://mongoengine-odm.readthedocs.io - there is currently a `tutorial `_, a `user guide `_, and an `API reference `_. Supported MongoDB Versions ========================== MongoEngine is currently tested against MongoDB v3.6, v4.0, v4.4 and v5.0. Future versions should be supported as well, but aren't actively tested at the moment. Make sure to open an issue or submit a pull request if you experience any problems with a more recent MongoDB versions. Installation ============ We recommend the use of `virtualenv `_ and of `pip `_. You can then use ``python -m pip install -U mongoengine``. You may also have `setuptools `_ and thus you can use ``easy_install -U mongoengine``. Another option is `pipenv `_. You can then use ``pipenv install mongoengine`` to both create the virtual environment and install the package. Otherwise, you can download the source from `GitHub `_ and run ``python setup.py install``. The support for Python2 was dropped with MongoEngine 0.20.0 Dependencies ============ All of the dependencies can easily be installed via `python -m pip `_. At the very least, you'll need these two packages to use MongoEngine: - pymongo>=3.4 If you utilize a ``DateTimeField``, you might also use a more flexible date parser: - dateutil>=2.1.0 If you need to use an ``ImageField`` or ``ImageGridFsProxy``: - Pillow>=2.0.0 If you need to use signals: - blinker>=1.3 Examples ======== Some simple examples of what MongoEngine code looks like: .. code :: python from mongoengine import * connect('mydb') class BlogPost(Document): title = StringField(required=True, max_length=200) posted = DateTimeField(default=datetime.datetime.utcnow) tags = ListField(StringField(max_length=50)) meta = {'allow_inheritance': True} class TextPost(BlogPost): content = StringField(required=True) class LinkPost(BlogPost): url = StringField(required=True) # Create a text-based post >>> post1 = TextPost(title='Using MongoEngine', content='See the tutorial') >>> post1.tags = ['mongodb', 'mongoengine'] >>> post1.save() # Create a link-based post >>> post2 = LinkPost(title='MongoEngine Docs', url='hmarr.com/mongoengine') >>> post2.tags = ['mongoengine', 'documentation'] >>> post2.save() # Iterate over all posts using the BlogPost superclass >>> for post in BlogPost.objects: ... print('===', post.title, '===') ... if isinstance(post, TextPost): ... print(post.content) ... elif isinstance(post, LinkPost): ... print('Link:', post.url) ... # Count all blog posts and its subtypes >>> BlogPost.objects.count() 2 >>> TextPost.objects.count() 1 >>> LinkPost.objects.count() 1 # Count tagged posts >>> BlogPost.objects(tags='mongoengine').count() 2 >>> BlogPost.objects(tags='mongodb').count() 1 Tests ===== To run the test suite, ensure you are running a local instance of MongoDB on the standard port and have ``pytest`` installed. Then, run ``python setup.py test`` or simply ``pytest``. To run the test suite on every supported Python and PyMongo version, you can use ``tox``. You'll need to make sure you have each supported Python version installed in your environment and then: .. code-block:: shell # Install tox $ python -m pip install tox # Run the test suites $ tox If you wish to run a subset of tests, use the pytest convention: .. code-block:: shell # Run all the tests in a particular test file $ pytest tests/fields/test_fields.py # Run only particular test class in that file $ pytest tests/fields/test_fields.py::TestField Community ========= - `MongoEngine Users mailing list `_ - `MongoEngine Developers mailing list `_ Contributing ============ We welcome contributions! See the `Contribution guidelines `_ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839168.0 mongoengine-0.27.0/mongoengine.egg-info/SOURCES.txt0000644000175100001730000000267714400345500021400 0ustar00runnerdockerAUTHORS LICENSE MANIFEST.in README.rst setup.cfg setup.py docs/Makefile docs/apireference.rst docs/changelog.rst docs/conf.py docs/django.rst docs/faq.rst docs/index.rst docs/requirements.txt docs/tutorial.rst docs/code/tumblelog.py docs/guide/connecting.rst docs/guide/defining-documents.rst docs/guide/document-instances.rst docs/guide/gridfs.rst docs/guide/index.rst docs/guide/installing.rst docs/guide/logging-monitoring.rst docs/guide/migration.rst docs/guide/mongomock.rst docs/guide/querying.rst docs/guide/signals.rst docs/guide/text-indexes.rst docs/guide/validation.rst mongoengine/__init__.py mongoengine/common.py mongoengine/connection.py mongoengine/context_managers.py mongoengine/dereference.py mongoengine/document.py mongoengine/errors.py mongoengine/fields.py mongoengine/mongodb_support.py mongoengine/pymongo_support.py mongoengine/signals.py mongoengine.egg-info/PKG-INFO mongoengine.egg-info/SOURCES.txt mongoengine.egg-info/dependency_links.txt mongoengine.egg-info/requires.txt mongoengine.egg-info/top_level.txt mongoengine/base/__init__.py mongoengine/base/common.py mongoengine/base/datastructures.py mongoengine/base/document.py mongoengine/base/fields.py mongoengine/base/metaclasses.py mongoengine/base/utils.py mongoengine/queryset/__init__.py mongoengine/queryset/base.py mongoengine/queryset/field_list.py mongoengine/queryset/manager.py mongoengine/queryset/queryset.py mongoengine/queryset/transform.py mongoengine/queryset/visitor.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839168.0 mongoengine-0.27.0/mongoengine.egg-info/dependency_links.txt0000644000175100001730000000000114400345500023546 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839168.0 mongoengine-0.27.0/mongoengine.egg-info/requires.txt0000644000175100001730000000002214400345500022072 0ustar00runnerdockerpymongo<5.0,>=3.4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839168.0 mongoengine-0.27.0/mongoengine.egg-info/top_level.txt0000644000175100001730000000001414400345500022225 0ustar00runnerdockermongoengine ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677839168.9152808 mongoengine-0.27.0/setup.cfg0000644000175100001730000000063714400345501015331 0ustar00runnerdocker[flake8] ignore = E501,F403,F405,I201,I202,W504,W605,W503,B007 exclude = build,dist,docs,venv,venv3,.tox,.eggs,tests max-complexity = 47 [tool:pytest] testpaths = tests [isort] known_first_party = mongoengine,tests default_section = THIRDPARTY multi_line_output = 3 include_trailing_comma = True combine_as_imports = True line_length = 70 ensure_newline_before_comments = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677839165.0 mongoengine-0.27.0/setup.py0000644000175100001730000001217114400345475015230 0ustar00runnerdockerimport os import sys from pkg_resources import normalize_path from setuptools import find_packages, setup from setuptools.command.test import test as TestCommand # Hack to silence atexit traceback in newer python versions try: import multiprocessing # noqa: F401 except ImportError: pass DESCRIPTION = "MongoEngine is a Python Object-Document Mapper for working with MongoDB." try: with open("README.rst") as fin: LONG_DESCRIPTION = fin.read() except Exception: LONG_DESCRIPTION = None def get_version(version_tuple): """Return the version tuple as a string, e.g. for (0, 10, 7), return '0.10.7'. """ return ".".join(map(str, version_tuple)) class PyTest(TestCommand): """Will force pytest to search for tests inside the build directory for 2to3 converted code (used by tox), instead of the current directory. Required as long as we need 2to3 Known Limitation: https://tox.readthedocs.io/en/latest/example/pytest.html#known-issues-and-limitations Source: https://www.hackzine.org/python-testing-with-pytest-and-2to3-plus-tox-and-travis-ci.html """ # https://pytest.readthedocs.io/en/2.7.3/goodpractises.html#integration-with-setuptools-test-commands # Allows to provide pytest command argument through the test runner command `python setup.py test` # e.g: `python setup.py test -a "-k=test"` # This only works for 1 argument though user_options = [("pytest-args=", "a", "Arguments to pass to py.test")] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = "" def finalize_options(self): TestCommand.finalize_options(self) self.test_args = ["tests"] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import pytest from pkg_resources import _namespace_packages # Purge modules under test from sys.modules. The test loader will # re-import them from the build location. Required when 2to3 is used # with namespace packages. if sys.version_info >= (3,) and getattr(self.distribution, "use_2to3", False): module = self.test_args[-1].split(".")[0] if module in _namespace_packages: del_modules = [] if module in sys.modules: del_modules.append(module) module += "." for name in sys.modules: if name.startswith(module): del_modules.append(name) map(sys.modules.__delitem__, del_modules) # Run on the build directory for 2to3-built code # This will prevent the old 2.x code from being found # by py.test discovery mechanism, that apparently # ignores sys.path.. ei_cmd = self.get_finalized_command("egg_info") self.test_args = [normalize_path(ei_cmd.egg_base)] cmd_args = self.test_args + ([self.pytest_args] if self.pytest_args else []) errno = pytest.main(cmd_args) sys.exit(errno) # Dirty hack to get version number from monogengine/__init__.py - we can't # import it as it depends on PyMongo and PyMongo isn't installed until this # file is read init = os.path.join(os.path.dirname(__file__), "mongoengine", "__init__.py") version_line = list(filter(lambda line: line.startswith("VERSION"), open(init)))[0] VERSION = get_version(eval(version_line.split("=")[-1])) CLASSIFIERS = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Database", "Topic :: Software Development :: Libraries :: Python Modules", ] extra_opts = { "packages": find_packages(exclude=["tests", "tests.*"]), "tests_require": [ "pytest", "pytest-cov", "coverage", "blinker", "Pillow>=7.0.0", ], } if "test" in sys.argv: extra_opts["packages"] = find_packages() extra_opts["package_data"] = { "tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"] } setup( name="mongoengine", version=VERSION, author="Harry Marr", author_email="harry.marr@gmail.com", maintainer="Stefan Wojcik", maintainer_email="wojcikstefan@gmail.com", url="http://mongoengine.org/", download_url="https://github.com/MongoEngine/mongoengine/tarball/master", license="MIT", include_package_data=True, description=DESCRIPTION, long_description=LONG_DESCRIPTION, platforms=["any"], classifiers=CLASSIFIERS, python_requires=">=3.7", install_requires=["pymongo>=3.4,<5.0"], cmdclass={"test": PyTest}, **extra_opts )