├── .coveragerc ├── .github ├── stale.yml └── workflows │ ├── ci-workflow.yml │ └── publish-workflow.yml ├── .gitignore ├── .readthedocs.yml ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── artwork ├── logo.png └── logo.svg ├── docs ├── .gitignore ├── Makefile ├── _static │ └── logo.png ├── _templates │ ├── links.html │ └── sidebarlogo.html ├── _themes │ ├── .gitignore │ ├── LICENSE │ ├── README │ ├── flask │ │ ├── layout.html │ │ ├── page.html │ │ ├── relations.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── api.rst ├── changelog.rst ├── conf.py ├── contribute.rst ├── extend.rst ├── extensions.rst ├── getting-started.rst ├── index.rst ├── intro.rst ├── make.bat ├── upgrade.rst └── usage.rst ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── tests ├── __init__.py ├── conftest.py ├── test_middlewares.py ├── test_operations.py ├── test_queries.py ├── test_storages.py ├── test_tables.py ├── test_tinydb.py └── test_utils.py └── tinydb ├── __init__.py ├── database.py ├── middlewares.py ├── mypy_plugin.py ├── operations.py ├── py.typed ├── queries.py ├── storages.py ├── table.py ├── utils.py └── version.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | raise NotImplementedError.* 8 | warnings\.warn.* 9 | def __repr__ 10 | def __str__ 11 | def main() 12 | if __name__ == .__main__.: 13 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | - pinned 9 | - contributions-welcome 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Feel 16 | free to reopen this if needed. Thank you for your contributions :heart: 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: {} 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: 16 | - "3.8" 17 | - "3.9" 18 | - "3.10" 19 | - "3.11" 20 | - "3.12" 21 | - "3.13" 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | include: 24 | - python-version: "pypy-3.9" 25 | os: ubuntu-latest 26 | - python-version: "pypy-3.10" 27 | os: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Set up Python ${{ matrix.python-version }} 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | allow-prereleases: true 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip twine 39 | pip install poetry 40 | poetry install 41 | - name: Run test suite 42 | run: | 43 | poetry run py.test -v --cov=tinydb 44 | - name: Perform type check 45 | run: | 46 | poetry run pip install pytest-mypy 47 | poetry run pytest --mypy -m mypy tinydb tests 48 | - name: Verify dist package format 49 | run: | 50 | poetry build 51 | twine check dist/* 52 | if: ${{ contains(matrix.python-version, '3.12') }} 53 | - name: Upload coverage result 54 | if: ${{ env.COVERALLS_REPO_TOKEN != 'windows-latest' }} 55 | env: 56 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: | 59 | poetry run coveralls 60 | -------------------------------------------------------------------------------- /.github/workflows/publish-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install poetry 23 | poetry install 24 | - name: Publish package 25 | env: 26 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} 27 | run: | 28 | poetry publish --build 29 | - name: Create Release 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ github.ref }} 35 | release_name: ${{ github.ref }} 36 | draft: false 37 | prerelease: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | .pytest_cache/ 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Pycharm 40 | .idea 41 | 42 | *.db.yml 43 | 44 | .DS_Store -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.8 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: 9 | - docs 10 | 11 | formats: all -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ####################### 3 | 4 | Whether reporting bugs, discussing improvements and new ideas or writing 5 | extensions: Contributions to TinyDB are welcome! Here's how to get started: 6 | 7 | 1. Check for open issues or open a fresh issue to start a discussion around 8 | a feature idea or a bug 9 | 2. Fork `the repository `_ on GitHub, 10 | create a new branch off the `master` branch and start making your changes 11 | (known as `GitHub Flow `_) 12 | 3. Write a test which shows that the bug was fixed or that the feature works 13 | as expected 14 | 4. Send a pull request and bug the maintainer until it gets merged and 15 | published :) 16 | 17 | Philosophy of TinyDB 18 | ******************** 19 | 20 | TinyDB aims to be simple and fun to use. Therefore two key values are simplicity 21 | and elegance of interfaces and code. These values will contradict each other 22 | from time to time. In these cases , try using as little magic as possible. 23 | In any case don't forget documenting code that isn't clear at first glance. 24 | 25 | Code Conventions 26 | **************** 27 | 28 | In general the TinyDB source should always follow `PEP 8 `_. 29 | Exceptions are allowed in well justified and documented cases. However we make 30 | a small exception concerning docstrings: 31 | 32 | When using multiline docstrings, keep the opening and closing triple quotes 33 | on their own lines and add an empty line after it. 34 | 35 | .. code-block:: python 36 | 37 | def some_function(): 38 | """ 39 | Documentation ... 40 | """ 41 | 42 | # implementation ... 43 | 44 | Version Numbers 45 | *************** 46 | 47 | TinyDB follows the `SemVer versioning guidelines `_. 48 | This implies that backwards incompatible changes in the API will increment 49 | the major version. So think twice before making such changes. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Markus Siemens 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | recursive-include tests *.py -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://raw.githubusercontent.com/msiemens/tinydb/master/artwork/logo.png 2 | :height: 150px 3 | 4 | |Build Status| |Coverage| |Version| 5 | 6 | Quick Links 7 | *********** 8 | 9 | - `Example Code`_ 10 | - `Supported Python Versions`_ 11 | - `Documentation `_ 12 | - `Changelog `_ 13 | - `Extensions `_ 14 | - `Contributing`_ 15 | 16 | Introduction 17 | ************ 18 | 19 | TinyDB is a lightweight document oriented database optimized for your happiness :) 20 | It's written in pure Python and has no external dependencies. The target are 21 | small apps that would be blown away by a SQL-DB or an external database server. 22 | 23 | TinyDB is: 24 | 25 | - **tiny:** The current source code has 1800 lines of code (with about 40% 26 | documentation) and 1600 lines tests. 27 | 28 | - **document oriented:** Like MongoDB_, you can store any document 29 | (represented as ``dict``) in TinyDB. 30 | 31 | - **optimized for your happiness:** TinyDB is designed to be simple and 32 | fun to use by providing a simple and clean API. 33 | 34 | - **written in pure Python:** TinyDB neither needs an external server (as 35 | e.g. `PyMongo `_) nor any dependencies 36 | from PyPI. 37 | 38 | - **works on Python 3.8+ and PyPy3:** TinyDB works on all modern versions of Python 39 | and PyPy. 40 | 41 | - **powerfully extensible:** You can easily extend TinyDB by writing new 42 | storages or modify the behaviour of storages with Middlewares. 43 | 44 | - **100% test coverage:** No explanation needed. 45 | 46 | To dive straight into all the details, head over to the `TinyDB docs 47 | `_. You can also discuss everything related 48 | to TinyDB like general development, extensions or showcase your TinyDB-based 49 | projects on the `discussion forum `_. 50 | 51 | Supported Python Versions 52 | ************************* 53 | 54 | TinyDB has been tested with Python 3.8 - 3.13 and PyPy3. 55 | 56 | Project Status 57 | ************** 58 | 59 | This project is in maintenance mode. It has reached a mature, stable state 60 | where significant new features or architectural changes are not planned. That 61 | said, there will still be releases for bugfixes or features contributed by 62 | the community. Read more about what this means in particular 63 | `here `_. 64 | 65 | Example Code 66 | ************ 67 | 68 | .. code-block:: python 69 | 70 | >>> from tinydb import TinyDB, Query 71 | >>> db = TinyDB('/path/to/db.json') 72 | >>> db.insert({'int': 1, 'char': 'a'}) 73 | >>> db.insert({'int': 1, 'char': 'b'}) 74 | 75 | Query Language 76 | ============== 77 | 78 | .. code-block:: python 79 | 80 | >>> User = Query() 81 | >>> # Search for a field value 82 | >>> db.search(User.name == 'John') 83 | [{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}] 84 | 85 | >>> # Combine two queries with logical and 86 | >>> db.search((User.name == 'John') & (User.age <= 30)) 87 | [{'name': 'John', 'age': 22}] 88 | 89 | >>> # Combine two queries with logical or 90 | >>> db.search((User.name == 'John') | (User.name == 'Bob')) 91 | [{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}, {'name': 'Bob', 'age': 42}] 92 | 93 | >>> # Negate a query with logical not 94 | >>> db.search(~(User.name == 'John')) 95 | [{'name': 'Megan', 'age': 27}, {'name': 'Bob', 'age': 42}] 96 | 97 | >>> # Apply transformation to field with `map` 98 | >>> db.search((User.age.map(lambda x: x + x) == 44)) 99 | >>> [{'name': 'John', 'age': 22}] 100 | 101 | >>> # More possible comparisons: != < > <= >= 102 | >>> # More possible checks: where(...).matches(regex), where(...).test(your_test_func) 103 | 104 | Tables 105 | ====== 106 | 107 | .. code-block:: python 108 | 109 | >>> table = db.table('name') 110 | >>> table.insert({'value': True}) 111 | >>> table.all() 112 | [{'value': True}] 113 | 114 | Using Middlewares 115 | ================= 116 | 117 | .. code-block:: python 118 | 119 | >>> from tinydb.storages import JSONStorage 120 | >>> from tinydb.middlewares import CachingMiddleware 121 | >>> db = TinyDB('/path/to/db.json', storage=CachingMiddleware(JSONStorage)) 122 | 123 | 124 | Contributing 125 | ************ 126 | 127 | Whether reporting bugs, discussing improvements and new ideas or writing 128 | extensions: Contributions to TinyDB are welcome! Here's how to get started: 129 | 130 | 1. Check for open issues or open a fresh issue to start a discussion around 131 | a feature idea or a bug 132 | 2. Fork `the repository `_ on Github, 133 | create a new branch off the `master` branch and start making your changes 134 | (known as `GitHub Flow `_) 135 | 3. Write a test which shows that the bug was fixed or that the feature works 136 | as expected 137 | 4. Send a pull request and bug the maintainer until it gets merged and 138 | published ☺ 139 | 140 | .. |Build Status| image:: https://img.shields.io/azure-devops/build/msiemens/3e5baa75-12ec-43ac-9728-89823ee8c7e2/2.svg?style=flat-square 141 | :target: https://dev.azure.com/msiemens/github/_build?definitionId=2 142 | .. |Coverage| image:: http://img.shields.io/coveralls/msiemens/tinydb.svg?style=flat-square 143 | :target: https://coveralls.io/r/msiemens/tinydb 144 | .. |Version| image:: http://img.shields.io/pypi/v/tinydb.svg?style=flat-square 145 | :target: https://pypi.python.org/pypi/tinydb/ 146 | .. _Buzhug: http://buzhug.sourceforge.net/ 147 | .. _CodernityDB: https://github.com/perchouli/codernitydb 148 | .. _MongoDB: http://mongodb.org/ 149 | -------------------------------------------------------------------------------- /artwork/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiemens/tinydb/10644a0e07ad180c5b756aba272ee6b0dbd12df8/artwork/logo.png -------------------------------------------------------------------------------- /artwork/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 19 | 23 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TinyDB.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TinyDB.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/TinyDB" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TinyDB" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiemens/tinydb/10644a0e07ad180c5b756aba272ee6b0dbd12df8/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_templates/links.html: -------------------------------------------------------------------------------- 1 |

Useful Links

2 | 8 | -------------------------------------------------------------------------------- /docs/_templates/sidebarlogo.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/_themes/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /docs/_themes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the theme, with or 6 | without modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | We kindly ask you to only use these themes in an unmodified manner just 22 | for Flask and Flask-related products, not for unrelated projects. If you 23 | like the visual style and want to use it for your own projects, please 24 | consider making some larger changes to the themes (such as changing 25 | font faces, sizes, colors or margins). 26 | 27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /docs/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | 3 | {%- block extrahead %} 4 | {{ super() }} 5 | {% if theme_touch_icon %} 6 | 7 | 8 | {% endif %} 9 | 11 | {% endblock %} 12 | 13 | {%- block relbar2 %}{% endblock %} 14 | 15 | {% block header %} 16 | {{ super() }} 17 | {% if pagename == 'index' %} 18 |
19 | {% endif %} 20 | {% endblock %} 21 | 22 | {%- block footer %} 23 | 27 | {% if pagename == 'index' %} 28 |
29 | {% endif %} 30 | {%- endblock %} 31 | -------------------------------------------------------------------------------- /docs/_themes/flask/page.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/page.html" %} 2 | 3 | {% block body %} 4 | {{ super() }} 5 | 6 | {%- if prev or next and pagename != 'index' %} 7 |

8 | {%- if prev %} 9 | « {{ prev.title }} {% if next %}|{% endif %} 11 | {%- endif %} 12 | {%- if next %} 13 | {{ next.title }} » 15 | {%- endif %} 16 |

17 | {%- endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Navigation

2 |
    3 | {%- for parent in parents %} 4 |
  • {{ parent.title }}
      5 | {%- endfor %} 6 | {%- if prev %} 7 |
    • Previous: {{ prev.title }}
    • 9 | {%- endif %} 10 | {%- if next %} 11 |
    • Next: {{ next.title }}
    • 13 | {%- endif %} 14 | {%- for parent in parents %} 15 |
  • 16 | {%- endfor %} 17 |
18 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '940px' %} 10 | {% set sidebar_width = '220px' %} 11 | {% set font_family = "'Open Sans', sans-serif" %} 12 | {% set monospace_font_family = "'Source Code Pro', 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace" %} 13 | {% set accent_color = '#2d4e84' %}{# original: #004B6B #} 14 | {% set accent_color_alternate = '#2069e1' %}{# original: #6D4100 #} 15 | 16 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:400,700,400italic|Source+Code+Pro); 17 | @import url("basic.css"); 18 | 19 | /* -- page layout ----------------------------------------------------------- */ 20 | 21 | html { 22 | overflow-y: scroll; 23 | } 24 | 25 | body { 26 | font-family: {{ font_family }}; 27 | font-size: 17px; 28 | background-color: white; 29 | color: #000; 30 | margin: 0; 31 | padding: 0; 32 | } 33 | 34 | div.document { 35 | width: {{ page_width }}; 36 | margin: 30px auto 0 auto; 37 | } 38 | 39 | div.documentwrapper { 40 | float: left; 41 | width: 100%; 42 | } 43 | 44 | div.bodywrapper { 45 | margin: 0 0 0 {{ sidebar_width }}; 46 | } 47 | 48 | div.sphinxsidebar { 49 | width: {{ sidebar_width }}; 50 | } 51 | 52 | hr { 53 | border: 1px solid #B1B4B6; 54 | } 55 | 56 | div.body { 57 | background-color: #ffffff; 58 | color: #3E4349; 59 | padding: 0 30px 0 30px; 60 | } 61 | 62 | img.floatingflask { 63 | padding: 0 0 10px 10px; 64 | float: right; 65 | } 66 | 67 | div.footer { 68 | width: {{ page_width }}; 69 | margin: 20px auto 30px auto; 70 | font-size: 14px; 71 | color: #888; 72 | text-align: right; 73 | } 74 | 75 | div.footer a { 76 | color: #888; 77 | } 78 | 79 | div.related { 80 | display: none; 81 | } 82 | 83 | div.sphinxsidebar a { 84 | color: #444; 85 | text-decoration: none; 86 | border-bottom: 1px dotted #999; 87 | } 88 | 89 | div.sphinxsidebar a:hover { 90 | border-bottom: 1px solid #999; 91 | } 92 | 93 | div.sphinxsidebar { 94 | font-size: 14px; 95 | line-height: 1.5; 96 | } 97 | 98 | div.sphinxsidebarwrapper { 99 | padding: 18px 10px; 100 | } 101 | 102 | div.sphinxsidebarwrapper p.logo { 103 | padding: 0 0 20px 0; 104 | margin: 0; 105 | text-align: center; 106 | } 107 | 108 | div.sphinxsidebar h3, 109 | div.sphinxsidebar h4 { 110 | font-family: {{ font_family }}; 111 | color: #444; 112 | font-size: 24px; 113 | font-weight: normal; 114 | margin: 0 0 5px 0; 115 | padding: 0; 116 | } 117 | 118 | div.sphinxsidebar h4 { 119 | font-size: 20px; 120 | } 121 | 122 | div.sphinxsidebar h3 a { 123 | color: #444; 124 | } 125 | 126 | div.sphinxsidebar p.logo a, 127 | div.sphinxsidebar h3 a, 128 | div.sphinxsidebar p.logo a:hover, 129 | div.sphinxsidebar h3 a:hover { 130 | border: none; 131 | } 132 | 133 | div.sphinxsidebar p { 134 | color: #555; 135 | margin: 10px 0; 136 | } 137 | 138 | div.sphinxsidebar ul { 139 | margin: 10px 0; 140 | padding: 0; 141 | color: #000; 142 | } 143 | 144 | div.sphinxsidebar input { 145 | border: 1px solid #ccc; 146 | font-family: {{ font_family }}; 147 | font-size: 1em; 148 | } 149 | 150 | /* -- body styles ----------------------------------------------------------- */ 151 | 152 | a { 153 | color: {{ accent_color }}; 154 | text-decoration: underline; 155 | } 156 | 157 | a:hover { 158 | color: {{ accent_color_alternate }}; 159 | text-decoration: underline; 160 | } 161 | 162 | div.body h1, 163 | div.body h2, 164 | div.body h3, 165 | div.body h4, 166 | div.body h5, 167 | div.body h6 { 168 | font-family: {{ font_family }}; 169 | font-weight: normal; 170 | margin: 30px 0px 10px 0px; 171 | padding: 0; 172 | } 173 | 174 | {% if theme_index_logo %} 175 | div.indexwrapper h1 { 176 | text-indent: -999999px; 177 | background: url({{ theme_index_logo }}) no-repeat center center; 178 | height: {{ theme_index_logo_height }}; 179 | } 180 | {% endif %} 181 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 182 | div.body h2 { font-size: 180%; } 183 | div.body h3 { font-size: 150%; } 184 | div.body h4 { font-size: 130%; } 185 | div.body h5 { font-size: 100%; } 186 | div.body h6 { font-size: 100%; } 187 | 188 | a.headerlink { 189 | color: #ddd; 190 | padding: 0 4px; 191 | text-decoration: none; 192 | } 193 | 194 | a.headerlink:hover { 195 | color: #444; 196 | background: #eaeaea; 197 | } 198 | 199 | div.body p, div.body dd, div.body li { 200 | line-height: 1.4em; 201 | } 202 | 203 | div.admonition { 204 | background: #fafafa; 205 | margin: 20px -30px; 206 | padding: 10px 30px; 207 | border-top: 1px solid #ccc; 208 | border-bottom: 1px solid #ccc; 209 | } 210 | 211 | div.admonition tt.xref, div.admonition a tt { 212 | border-bottom: 1px solid #fafafa; 213 | } 214 | 215 | dd div.admonition { 216 | margin-left: -60px; 217 | padding-left: 60px; 218 | } 219 | 220 | div.admonition p.admonition-title { 221 | font-family: {{ font_family }}; 222 | font-weight: normal; 223 | font-size: 24px; 224 | margin: 0 0 10px 0; 225 | padding: 0; 226 | line-height: 1; 227 | } 228 | 229 | div.admonition p.last { 230 | margin-bottom: 0; 231 | } 232 | 233 | div.highlight { 234 | background-color: white; 235 | } 236 | 237 | dt:target, .highlight { 238 | background: #FAF3E8; 239 | } 240 | 241 | div.note { 242 | background-color: #eee; 243 | border: 1px solid #ccc; 244 | } 245 | 246 | div.seealso { 247 | background-color: #ffc; 248 | border: 1px solid #ff6; 249 | } 250 | 251 | div.topic { 252 | background-color: #eee; 253 | } 254 | 255 | p.admonition-title { 256 | display: inline; 257 | } 258 | 259 | p.admonition-title:after { 260 | content: ":"; 261 | } 262 | 263 | pre, tt { 264 | font-family: {{ monospace_font_family }}; 265 | font-size: 0.9em; 266 | } 267 | 268 | img.screenshot { 269 | } 270 | 271 | tt.descname, tt.descclassname { 272 | font-size: 0.95em; 273 | } 274 | 275 | tt.descname { 276 | padding-right: 0.08em; 277 | } 278 | 279 | img.screenshot { 280 | -moz-box-shadow: 2px 2px 4px #eee; 281 | -webkit-box-shadow: 2px 2px 4px #eee; 282 | box-shadow: 2px 2px 4px #eee; 283 | } 284 | 285 | table.docutils { 286 | border: 1px solid #888; 287 | -moz-box-shadow: 2px 2px 4px #eee; 288 | -webkit-box-shadow: 2px 2px 4px #eee; 289 | box-shadow: 2px 2px 4px #eee; 290 | } 291 | 292 | table.docutils td, table.docutils th { 293 | border: 1px solid #888; 294 | padding: 0.25em 0.7em; 295 | } 296 | 297 | table.field-list, table.footnote { 298 | border: none; 299 | -moz-box-shadow: none; 300 | -webkit-box-shadow: none; 301 | box-shadow: none; 302 | } 303 | 304 | table.footnote { 305 | margin: 15px 0; 306 | width: 100%; 307 | border: 1px solid #eee; 308 | background: #fdfdfd; 309 | font-size: 0.9em; 310 | } 311 | 312 | table.footnote + table.footnote { 313 | margin-top: -15px; 314 | border-top: none; 315 | } 316 | 317 | table.field-list th { 318 | padding: 0 0.8em 0 0; 319 | } 320 | 321 | table.field-list td { 322 | padding: 0; 323 | } 324 | 325 | table.footnote td.label { 326 | width: 0px; 327 | padding: 0.3em 0 0.3em 0.5em; 328 | } 329 | 330 | table.footnote td { 331 | padding: 0.3em 0.5em; 332 | } 333 | 334 | dl { 335 | margin: 0; 336 | padding: 0; 337 | } 338 | 339 | dl dd { 340 | margin-left: 30px; 341 | } 342 | 343 | blockquote { 344 | margin: 0 0 0 30px; 345 | padding: 0; 346 | } 347 | 348 | ul, ol { 349 | margin: 10px 0 10px 30px; 350 | padding: 0; 351 | } 352 | 353 | pre { 354 | background: #eee; 355 | padding: 7px 30px; 356 | margin: 15px -30px; 357 | line-height: 1.3em; 358 | } 359 | 360 | dl pre, blockquote pre, li pre { 361 | margin-left: -60px; 362 | padding-left: 60px; 363 | } 364 | 365 | dl dl pre { 366 | margin-left: -90px; 367 | padding-left: 90px; 368 | } 369 | 370 | tt { 371 | background-color: #ecf0f3; 372 | color: #222; 373 | /* padding: 1px 2px; */ 374 | } 375 | 376 | tt.xref, a tt { 377 | background-color: #FBFBFB; 378 | border-bottom: 1px solid white; 379 | } 380 | 381 | a.reference { 382 | text-decoration: none; 383 | border-bottom: 1px dotted {{ accent_color }}; 384 | } 385 | 386 | a.reference:hover { 387 | border-bottom: 1px solid {{ accent_color_alternate }}; 388 | } 389 | 390 | a.footnote-reference { 391 | text-decoration: none; 392 | font-size: 0.7em; 393 | vertical-align: top; 394 | border-bottom: 1px dotted {{ accent_color }}; 395 | } 396 | 397 | a.footnote-reference:hover { 398 | border-bottom: 1px solid {{ accent_color_alternate }}; 399 | } 400 | 401 | a:hover tt { 402 | background: #EEE; 403 | } 404 | 405 | 406 | @media screen and (max-width: 870px) { 407 | 408 | div.sphinxsidebar { 409 | display: none; 410 | } 411 | 412 | div.document { 413 | width: 100%; 414 | 415 | } 416 | 417 | div.documentwrapper { 418 | margin-left: 0; 419 | margin-top: 0; 420 | margin-right: 0; 421 | margin-bottom: 0; 422 | } 423 | 424 | div.bodywrapper { 425 | margin-top: 0; 426 | margin-right: 0; 427 | margin-bottom: 0; 428 | margin-left: 0; 429 | } 430 | 431 | ul { 432 | margin-left: 0; 433 | } 434 | 435 | .document { 436 | width: auto; 437 | } 438 | 439 | .footer { 440 | width: auto; 441 | } 442 | 443 | .bodywrapper { 444 | margin: 0; 445 | } 446 | 447 | .footer { 448 | width: auto; 449 | } 450 | 451 | .github { 452 | display: none; 453 | } 454 | 455 | 456 | 457 | } 458 | 459 | 460 | 461 | @media screen and (max-width: 875px) { 462 | 463 | body { 464 | margin: 0; 465 | padding: 20px 30px; 466 | } 467 | 468 | div.documentwrapper { 469 | float: none; 470 | background: white; 471 | } 472 | 473 | div.sphinxsidebar { 474 | display: block; 475 | float: none; 476 | width: 102.5%; 477 | margin: 50px -30px -20px -30px; 478 | padding: 10px 20px; 479 | background: #333; 480 | color: white; 481 | } 482 | 483 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 484 | div.sphinxsidebar h3 a { 485 | color: white; 486 | } 487 | 488 | div.sphinxsidebar a { 489 | color: #aaa; 490 | } 491 | 492 | div.sphinxsidebar p.logo { 493 | display: none; 494 | } 495 | 496 | div.document { 497 | width: 100%; 498 | margin: 0; 499 | } 500 | 501 | div.related { 502 | display: block; 503 | margin: 0; 504 | padding: 10px 0 20px 0; 505 | } 506 | 507 | div.related ul, 508 | div.related ul li { 509 | margin: 0; 510 | padding: 0; 511 | } 512 | 513 | div.footer { 514 | display: none; 515 | } 516 | 517 | div.bodywrapper { 518 | margin: 0; 519 | } 520 | 521 | div.body { 522 | min-height: 0; 523 | padding: 0; 524 | } 525 | 526 | .rtd_doc_footer { 527 | display: none; 528 | } 529 | 530 | .document { 531 | width: auto; 532 | } 533 | 534 | .footer { 535 | width: auto; 536 | } 537 | 538 | .footer { 539 | width: auto; 540 | } 541 | 542 | .github { 543 | display: none; 544 | } 545 | } 546 | 547 | 548 | /* scrollbars */ 549 | 550 | ::-webkit-scrollbar { 551 | width: 6px; 552 | height: 6px; 553 | } 554 | 555 | ::-webkit-scrollbar-button:start:decrement, 556 | ::-webkit-scrollbar-button:end:increment { 557 | display: block; 558 | height: 10px; 559 | } 560 | 561 | ::-webkit-scrollbar-button:vertical:increment { 562 | background-color: #fff; 563 | } 564 | 565 | ::-webkit-scrollbar-track-piece { 566 | background-color: #eee; 567 | -webkit-border-radius: 3px; 568 | } 569 | 570 | ::-webkit-scrollbar-thumb:vertical { 571 | height: 50px; 572 | background-color: #ccc; 573 | -webkit-border-radius: 3px; 574 | } 575 | 576 | ::-webkit-scrollbar-thumb:horizontal { 577 | width: 50px; 578 | background-color: #ccc; 579 | -webkit-border-radius: 3px; 580 | } 581 | 582 | /* misc. */ 583 | 584 | .revsys-inline { 585 | display: none!important; 586 | } 587 | 588 | 589 | .admonition.warning { 590 | background-color: #F5CDCD; 591 | border-color: #7B1B1B; 592 | } 593 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | # Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api_docs: 2 | 3 | API Documentation 4 | ================= 5 | 6 | ``tinydb.database`` 7 | ------------------- 8 | 9 | .. autoclass:: tinydb.database.TinyDB 10 | :members: 11 | :private-members: 12 | :member-order: bysource 13 | 14 | .. _table_api: 15 | 16 | ``tinydb.table`` 17 | ---------------- 18 | 19 | .. autoclass:: tinydb.table.Table 20 | :members: 21 | :special-members: 22 | :exclude-members: __dict__, __weakref__ 23 | :member-order: bysource 24 | 25 | .. autoclass:: tinydb.table.Document 26 | :members: 27 | :special-members: 28 | :exclude-members: __dict__, __weakref__ 29 | :member-order: bysource 30 | 31 | .. py:attribute:: doc_id 32 | 33 | The document's id 34 | 35 | ``tinydb.queries`` 36 | ------------------ 37 | 38 | .. autoclass:: tinydb.queries.Query 39 | :members: 40 | :special-members: 41 | :exclude-members: __weakref__ 42 | :member-order: bysource 43 | 44 | .. autoclass:: tinydb.queries.QueryInstance 45 | :members: 46 | :special-members: 47 | :exclude-members: __weakref__ 48 | :member-order: bysource 49 | 50 | ``tinydb.operations`` 51 | --------------------- 52 | 53 | .. automodule:: tinydb.operations 54 | :members: 55 | :special-members: 56 | :exclude-members: __weakref__ 57 | :member-order: bysource 58 | 59 | ``tinydb.storage`` 60 | ------------------ 61 | 62 | .. automodule:: tinydb.storages 63 | :members: JSONStorage, MemoryStorage 64 | :special-members: 65 | :exclude-members: __weakref__ 66 | 67 | .. class:: Storage 68 | 69 | The abstract base class for all Storages. 70 | 71 | A Storage (de)serializes the current state of the database and stores 72 | it in some place (memory, file on disk, ...). 73 | 74 | .. method:: read() 75 | 76 | Read the last stored state. 77 | 78 | .. method:: write(data) 79 | 80 | Write the current state of the database to the storage. 81 | 82 | .. method:: close() 83 | 84 | Optional: Close open file handles, etc. 85 | 86 | ``tinydb.middlewares`` 87 | ---------------------- 88 | 89 | .. automodule:: tinydb.middlewares 90 | :members: CachingMiddleware 91 | :special-members: 92 | :exclude-members: __weakref__ 93 | 94 | .. class:: Middleware 95 | 96 | The base class for all Middlewares. 97 | 98 | Middlewares hook into the read/write process of TinyDB allowing you to 99 | extend the behaviour by adding caching, logging, ... 100 | 101 | If ``read()`` or ``write()`` are not overloaded, they will be forwarded 102 | directly to the storage instance. 103 | 104 | .. attribute:: storage 105 | 106 | :type: :class:`.Storage` 107 | 108 | Access to the underlying storage instance. 109 | 110 | .. method:: read() 111 | 112 | Read the last stored state. 113 | 114 | .. method:: write(data) 115 | 116 | Write the current state of the database to the storage. 117 | 118 | .. method:: close() 119 | 120 | Optional: Close open file handles, etc. 121 | 122 | ``tinydb.utils`` 123 | ---------------- 124 | 125 | .. autoclass:: tinydb.utils.LRUCache 126 | :members: 127 | :special-members: 128 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version Numbering 5 | ^^^^^^^^^^^^^^^^^ 6 | 7 | TinyDB follows the SemVer versioning guidelines. For more information, 8 | see `semver.org `_ 9 | 10 | .. note:: When new methods are added to the ``Query`` API, this may 11 | result in breaking existing code that uses the property syntax 12 | to access document fields (e.g. ``Query().some.nested.field``) 13 | where the field name is equal to the newly added query method. 14 | Thus, breaking changes may occur in feature releases even though 15 | they don't change the public API in a backwards-incompatible 16 | manner. 17 | 18 | To prevent this from happening, one can use the dict access 19 | syntax (``Query()['some']['nested']['field']``) that will 20 | not break even when new methods are added to the ``Query`` API. 21 | 22 | unreleased 23 | ^^^^^^^^^^ 24 | 25 | - *nothing yet* 26 | 27 | v4.8.2 (2024-10-12) 28 | ^^^^^^^^^^^^^^^^^^^ 29 | 30 | - Fix: Correctly update query cache when search results have changed 31 | (see `issue 560 `_). 32 | 33 | v4.8.1 (2024-10-07) 34 | ^^^^^^^^^^^^^^^^^^^ 35 | 36 | - Feature: Allow persisting empty tables 37 | (see `pull request 518 `_). 38 | - Fix: Make replacing ``doc_id`` type work properly 39 | (see `issue 545 `_). 40 | 41 | v4.8.0 (2023-06-12) 42 | ^^^^^^^^^^^^^^^^^^^ 43 | 44 | - Feature: Allow retrieve multiple documents by document ID using 45 | ``Table.get(doc_ids=[...])`` 46 | (see `pull request 504 `_). 47 | 48 | v4.7.1 (2023-01-14) 49 | ^^^^^^^^^^^^^^^^^^^ 50 | 51 | - Improvement: Improve typing annotations 52 | (see `pull request 477 `_). 53 | - Improvement: Fix some typos in the documentation 54 | (see `pull request 479 `_ 55 | and `pull request 498 `_). 56 | 57 | v4.7.0 (2022-02-19) 58 | ^^^^^^^^^^^^^^^^^^^ 59 | 60 | - Feature: Allow inserting ``Document`` instances using ``Table.insert_multiple`` 61 | (see `pull request 455 `_). 62 | - Performance: Only convert document IDs of a table when returning documents. 63 | This improves performance the ``Table.count`` and ``Table.get`` operations 64 | and also for ``Table.search`` when only returning a few documents 65 | (see `pull request 460 `_). 66 | - Internal change: Run all ``Table`` tests ``JSONStorage`` in addition to 67 | ``MemoryStorage``. 68 | 69 | v4.6.1 (2022-01-18) 70 | ^^^^^^^^^^^^^^^^^^^ 71 | 72 | - Fix: Make using callables as queries work again 73 | (see `issue 454 `__) 74 | 75 | v4.6.0 (2022-01-17) 76 | ^^^^^^^^^^^^^^^^^^^ 77 | 78 | - Feature: Add `map()` query operation to apply a transformation 79 | to a document or field when evaluating a query 80 | (see `pull request 445 `_). 81 | **Note**: This may break code that queries for a field named ``map`` 82 | using the ``Query`` APIs property access syntax 83 | - Feature: Add support for `typing-extensions `_ 84 | v4 85 | - Documentation: Fix a couple of typos in the documentation (see 86 | `pull request 446 `_, 87 | `pull request 449 `_ and 88 | `pull request 453 `_) 89 | 90 | v4.5.2 (2021-09-23) 91 | ^^^^^^^^^^^^^^^^^^^ 92 | 93 | - Fix: Make ``Table.delete()``'s argument priorities consistent with 94 | other table methods. This means that if you pass both ``cond`` as 95 | well as ``doc_ids`` to ``Table.delete()``, the latter will be preferred 96 | (see `issue 424 `__) 97 | 98 | v4.5.1 (2021-07-17) 99 | ^^^^^^^^^^^^^^^^^^^ 100 | 101 | - Fix: Correctly install ``typing-extensions`` on Python 3.7 102 | (see `issue 413 `__) 103 | 104 | v4.5.0 (2021-06-25) 105 | ^^^^^^^^^^^^^^^^^^^ 106 | 107 | - Feature: Better type hinting/IntelliSense for PyCharm, VS Code and MyPy 108 | (see `issue 372 `__). 109 | PyCharm and VS Code should work out of the box, for MyPy see 110 | :ref:`MyPy Type Checking ` 111 | 112 | v4.4.0 (2021-02-11) 113 | ^^^^^^^^^^^^^^^^^^^ 114 | 115 | - Feature: Add operation for searching for all documents that match a ``dict`` 116 | fragment (see `issue 300 `_) 117 | - Fix: Correctly handle queries that use fields that are also Query methods, 118 | e.g. ``Query()['test']`` for searching for documents with a ``test`` field 119 | (see `issue 373 `_) 120 | 121 | v4.3.0 (2020-11-14) 122 | ^^^^^^^^^^^^^^^^^^^ 123 | 124 | - Feature: Add operation for updating multiple documents: ``update_multiple`` 125 | (see `issue 346 `_) 126 | - Improvement: Expose type information for MyPy typechecking (PEP 561) 127 | (see `pull request 352 `_) 128 | 129 | v4.2.0 (2020-10-03) 130 | ^^^^^^^^^^^^^^^^^^^ 131 | 132 | - Feature: Add support for specifying document IDs during insertion 133 | (see `issue 303 `_) 134 | - Internal change: Use ``OrderedDict.move_to_end()`` in the query cache 135 | (see `issue 338 `_) 136 | 137 | v4.1.1 (2020-05-08) 138 | ^^^^^^^^^^^^^^^^^^^ 139 | 140 | - Fix: Don't install dev-dependencies when installing from PyPI (see 141 | `issue 315 `_) 142 | 143 | v4.1.0 (2020-05-07) 144 | ^^^^^^^^^^^^^^^^^^^ 145 | 146 | - Feature: Add a no-op query ``Query().noop()`` (see 147 | `issue 313 `_) 148 | - Feature: Add a ``access_mode`` flag to ``JSONStorage`` to allow opening 149 | files read-only (see `issue 297 `_) 150 | - Fix: Don't drop the first document that's being inserted when inserting 151 | data on an existing database (see `issue 314 152 | `_) 153 | 154 | v4.0.0 (2020-05-02) 155 | ^^^^^^^^^^^^^^^^^^^ 156 | 157 | :ref:`Upgrade Notes ` 158 | 159 | Breaking Changes 160 | ---------------- 161 | 162 | - Python 2 support has been removed, see `issue 284 163 | `_ 164 | for background 165 | - API changes: 166 | 167 | - Removed classes: ``DataProxy``, ``StorageProxy`` 168 | - Attributes removed from ``TinyDB`` in favor of 169 | customizing ``TinyDB``'s behavior by subclassing it and overloading 170 | ``__init__(...)`` and ``table(...)``: 171 | 172 | - ``DEFAULT_TABLE`` 173 | - ``DEFAULT_TABLE_KWARGS`` 174 | - ``DEFAULT_STORAGE`` 175 | 176 | - Arguments removed from ``TinyDB(...)``: 177 | 178 | - ``default_table``: replace with ``TinyDB.default_table_name = 'name'`` 179 | - ``table_class``: replace with ``TinyDB.table_class = Class`` 180 | 181 | - ``TinyDB.contains(...)``'s ``doc_ids`` parameter has been renamed to 182 | ``doc_id`` and now only takes a single document ID 183 | - ``TinyDB.purge_tables(...)`` has been renamed to ``TinyDB.drop_tables(...)`` 184 | - ``TinyDB.purge_table(...)`` has been renamed to ``TinyDB.drop_table(...)`` 185 | - ``TinyDB.write_back(...)`` has been removed 186 | - ``TinyDB.process_elements(...)`` has been removed 187 | - ``Table.purge()`` has been renamed to ``Table.truncate()`` 188 | - Evaluating an empty ``Query()`` without any test operators will now result 189 | in an exception, use ``Query().noop()`` (introduced in v4.1.0) instead 190 | 191 | - ``ujson`` support has been removed, see `issue 263 192 | `_ and `issue 306 193 | `_ for background 194 | - The deprecated Element ID API has been removed (e.g. using the ``Element`` 195 | class or ``eids`` parameter) in favor the Document API, see 196 | `pull request 158 `_ for details 197 | on the replacement 198 | 199 | Improvements 200 | ------------ 201 | 202 | - TinyDB's internal architecture has been reworked to be more simple and 203 | streamlined in order to make it easier to customize TinyDB's behavior 204 | - With the new architecture, TinyDB performance will improve for many 205 | applications 206 | 207 | Bugfixes 208 | -------- 209 | 210 | - Don't break the tests when ``ujson`` is installed (see `issue 262 211 | `_) 212 | - Fix performance when reading data (see `issue 250 213 | `_) 214 | - Fix inconsistent purge function names (see `issue 103 215 | `_) 216 | 217 | v3.15.1 (2019-10-26) 218 | ^^^^^^^^^^^^^^^^^^^^ 219 | 220 | - Internal change: fix missing values handling for ``LRUCache`` 221 | 222 | v3.15.0 (2019-10-12) 223 | ^^^^^^^^^^^^^^^^^^^^ 224 | 225 | - Feature: allow setting the parameters of TinyDB's default table 226 | (see `issue 278 `_) 227 | 228 | v3.14.2 (2019-09-13) 229 | ^^^^^^^^^^^^^^^^^^^^ 230 | 231 | - Internal change: support correct iteration for ``LRUCache`` objects 232 | 233 | v3.14.1 (2019-07-03) 234 | ^^^^^^^^^^^^^^^^^^^^ 235 | 236 | - Internal change: fix Query class to permit subclass creation 237 | (see `pull request 270 `_) 238 | 239 | v3.14.0 (2019-06-18) 240 | ^^^^^^^^^^^^^^^^^^^^ 241 | 242 | - Change: support for ``ujson`` is now deprecated 243 | (see `issue 263 `_) 244 | 245 | v3.13.0 (2019-03-16) 246 | ^^^^^^^^^^^^^^^^^^^^ 247 | 248 | - Feature: direct access to a TinyDB instance's storage 249 | (see `issue 258 `_) 250 | 251 | v3.12.2 (2018-12-12) 252 | ^^^^^^^^^^^^^^^^^^^^ 253 | 254 | - Internal change: convert documents to dicts during insertion 255 | (see `pull request 256 `_) 256 | - Internal change: use tuple literals instead of tuple class/constructor 257 | (see `pull request 247 `_) 258 | - Infra: ensure YAML tests are run 259 | (see `pull request 252 `_) 260 | 261 | v3.12.1 (2018-11-09) 262 | ^^^^^^^^^^^^^^^^^^^^ 263 | 264 | - Fix: Don't break when searching the same query multiple times 265 | (see `pull request 249 `_) 266 | - Internal change: allow ``collections.abc.Mutable`` as valid document types 267 | (see `pull request 245 `_) 268 | 269 | v3.12.0 (2018-11-06) 270 | ^^^^^^^^^^^^^^^^^^^^ 271 | 272 | - Feature: Add encoding option to ``JSONStorage`` 273 | (see `pull request 238 `_) 274 | - Internal change: allow ``collections.abc.Mutable`` as valid document types 275 | (see `pull request 245 `_) 276 | 277 | v3.11.1 (2018-09-13) 278 | ^^^^^^^^^^^^^^^^^^^^ 279 | 280 | - Bugfix: Make path queries (``db.search(where('key))``) work again 281 | (see `issue 232 `_) 282 | - Improvement: Add custom ``repr`` representations for main classes 283 | (see `pull request 229 `_) 284 | 285 | v3.11.0 (2018-08-20) 286 | ^^^^^^^^^^^^^^^^^^^^ 287 | 288 | - **Drop official support for Python 3.3**. Python 3.3 has reached its 289 | official End Of Life as of September 29, 2017. It will probably continue 290 | to work, but will not be tested against 291 | (`issue 217 `_) 292 | 293 | - Feature: Allow extending TinyDB with a custom storage proxy class 294 | (see `pull request 224 `_) 295 | - Bugfix: Return list of document IDs for upsert when creating a new 296 | document (see `issue 223 `_) 297 | 298 | v3.10.0 (2018-07-21) 299 | ^^^^^^^^^^^^^^^^^^^^ 300 | 301 | - Feature: Add support for regex flags 302 | (see `pull request 216 `_) 303 | 304 | v3.9.0 (2018-04-24) 305 | ^^^^^^^^^^^^^^^^^^^ 306 | 307 | - Feature: Allow setting a table class for single table only 308 | (see `issue 197 `_) 309 | - Internal change: call fsync after flushing ``JSONStorage`` 310 | (see `issue 208 `_) 311 | 312 | v3.8.1 (2018-03-26) 313 | ^^^^^^^^^^^^^^^^^^^ 314 | 315 | - Bugfix: Don't install tests as a package anymore 316 | (see `pull request #195 `_) 317 | 318 | v3.8.0 (2018-03-01) 319 | ^^^^^^^^^^^^^^^^^^^ 320 | 321 | - Feature: Allow disabling the query cache with ``db.table(name, cache_size=0)`` 322 | (see `pull request #187 `_) 323 | - Feature: Add ``db.write_back(docs)`` for replacing documents 324 | (see `pull request #184 `_) 325 | 326 | v3.7.0 (2017-11-11) 327 | ^^^^^^^^^^^^^^^^^^^ 328 | 329 | - Feature: ``one_of`` for checking if a value is contained in a list 330 | (see `issue 164 `_) 331 | - Feature: Upsert (insert if document doesn't exist, otherwise update; 332 | see https://forum.m-siemens.de/d/30-primary-key-well-sort-of) 333 | - Internal change: don't read from storage twice during initialization 334 | (see https://forum.m-siemens.de/d/28-reads-the-whole-data-file-twice) 335 | 336 | v3.6.0 (2017-10-05) 337 | ^^^^^^^^^^^^^^^^^^^ 338 | 339 | - Allow updating all documents using ``db.update(fields)`` (see 340 | `issue #157 `_). 341 | - Rename elements to documents. Document IDs now available with ``doc.doc_id``, 342 | using ``doc.eid`` is now deprecated 343 | (see `pull request #158 `_) 344 | 345 | v3.5.0 (2017-08-30) 346 | ^^^^^^^^^^^^^^^^^^^ 347 | 348 | - Expose the table name via ``table.name`` (see 349 | `issue #147 `_). 350 | - Allow better subclassing of the ``TinyDB`` class 351 | (see `pull request #150 `_). 352 | 353 | v3.4.1 (2017-08-23) 354 | ^^^^^^^^^^^^^^^^^^^ 355 | 356 | - Expose TinyDB version via ``import tinyb; tinydb.__version__`` (see 357 | `issue #148 `_). 358 | 359 | v3.4.0 (2017-08-08) 360 | ^^^^^^^^^^^^^^^^^^^ 361 | 362 | - Add new update operations: ``add(key, value)``, ``subtract(key, value)``, 363 | and ``set(key, value)`` 364 | (see `pull request #145 `_). 365 | 366 | v3.3.1 (2017-06-27) 367 | ^^^^^^^^^^^^^^^^^^^ 368 | 369 | - Use relative imports to allow vendoring TinyDB in other packages 370 | (see `pull request #142 `_). 371 | 372 | v3.3.0 (2017-06-05) 373 | ^^^^^^^^^^^^^^^^^^^ 374 | 375 | - Allow iterating over a database or table yielding all documents 376 | (see `pull request #139 `_). 377 | 378 | v3.2.3 (2017-04-22) 379 | ^^^^^^^^^^^^^^^^^^^ 380 | 381 | - Fix bug with accidental modifications to the query cache when modifying 382 | the list of search results (see `issue #132 `_). 383 | 384 | v3.2.2 (2017-01-16) 385 | ^^^^^^^^^^^^^^^^^^^ 386 | 387 | - Fix the ``Query`` constructor to prevent wrong usage 388 | (see `issue #117 `_). 389 | 390 | v3.2.1 (2016-06-29) 391 | ^^^^^^^^^^^^^^^^^^^ 392 | 393 | - Fix a bug with queries on documents that have a ``path`` key 394 | (see `pull request #107 `_). 395 | - Don't write to the database file needlessly when opening the database 396 | (see `pull request #104 `_). 397 | 398 | v3.2.0 (2016-04-25) 399 | ^^^^^^^^^^^^^^^^^^^ 400 | 401 | - Add a way to specify the default table name via :ref:`default_table ` 402 | (see `pull request #98 `_). 403 | - Add ``db.purge_table(name)`` to remove a single table 404 | (see `pull request #100 `_). 405 | 406 | - Along the way: celebrating 100 issues and pull requests! Thanks everyone for every single contribution! 407 | 408 | - Extend API documentation (see `issue #96 `_). 409 | 410 | v3.1.3 (2016-02-14) 411 | ^^^^^^^^^^^^^^^^^^^ 412 | 413 | - Fix a bug when using unhashable documents (lists, dicts) with 414 | ``Query.any`` or ``Query.all`` queries 415 | (see `a forum post by karibul `_). 416 | 417 | v3.1.2 (2016-01-30) 418 | ^^^^^^^^^^^^^^^^^^^ 419 | 420 | - Fix a bug when using unhashable documents (lists, dicts) with 421 | ``Query.any`` or ``Query.all`` queries 422 | (see `a forum post by karibul `_). 423 | 424 | v3.1.1 (2016-01-23) 425 | ^^^^^^^^^^^^^^^^^^^ 426 | 427 | - Inserting a dictionary with data that is not JSON serializable doesn't 428 | lead to corrupt files anymore (see `issue #89 `_). 429 | - Fix a bug in the LRU cache that may lead to an invalid query cache 430 | (see `issue #87 `_). 431 | 432 | v3.1.0 (2015-12-31) 433 | ^^^^^^^^^^^^^^^^^^^ 434 | 435 | - ``db.update(...)`` and ``db.remove(...)`` now return affected document IDs 436 | (see `issue #83 `_). 437 | - Inserting an invalid document (i.e. not a ``dict``) now raises an error 438 | instead of corrupting the database (see 439 | `issue #74 `_). 440 | 441 | v3.0.0 (2015-11-13) 442 | ^^^^^^^^^^^^^^^^^^^ 443 | 444 | - Overhauled Query model: 445 | 446 | - ``where('...').contains('...')`` has been renamed to 447 | ``where('...').search('...')``. 448 | - Support for ORM-like usage: 449 | ``User = Query(); db.search(User.name == 'John')``. 450 | - ``where('foo')`` is an alias for ``Query().foo``. 451 | - ``where('foo').has('bar')`` is replaced by either 452 | ``where('foo').bar`` or ``Query().foo.bar``. 453 | 454 | - In case the key is not a valid Python identifier, array 455 | notation can be used: ``where('a.b.c')`` is now 456 | ``Query()['a.b.c']``. 457 | 458 | - Checking for the existence of a key has to be done explicitly: 459 | ``where('foo').exists()``. 460 | 461 | - Migrations from v1 to v2 have been removed. 462 | - ``SmartCacheTable`` has been moved to `msiemens/tinydb-smartcache`_. 463 | - Serialization has been moved to `msiemens/tinydb-serialization`_. 464 | - Empty storages are now expected to return ``None`` instead of raising ``ValueError``. 465 | (see `issue #67 `_. 466 | 467 | .. _msiemens/tinydb-smartcache: https://github.com/msiemens/tinydb-smartcache 468 | .. _msiemens/tinydb-serialization: https://github.com/msiemens/tinydb-serialization 469 | 470 | v2.4.0 (2015-08-14) 471 | ^^^^^^^^^^^^^^^^^^^ 472 | 473 | - Allow custom parameters for custom test functions 474 | (see `issue #63 `_ and 475 | `pull request #64 `_). 476 | 477 | v2.3.2 (2015-05-20) 478 | ^^^^^^^^^^^^^^^^^^^ 479 | 480 | - Fix a forgotten debug output in the ``SerializationMiddleware`` 481 | (see `issue #55 `_). 482 | - Fix an "ignored exception" warning when using the ``CachingMiddleware`` 483 | (see `pull request #54 `_) 484 | - Fix a problem with symlinks when checking out TinyDB on OSX Yosemite 485 | (see `issue #52 `_). 486 | 487 | v2.3.1 (2015-04-30) 488 | ^^^^^^^^^^^^^^^^^^^ 489 | 490 | - Hopefully fix a problem with using TinyDB as a dependency in a ``setup.py`` script 491 | (see `issue #51 `_). 492 | 493 | v2.3.0 (2015-04-08) 494 | ^^^^^^^^^^^^^^^^^^^ 495 | 496 | - Added support for custom serialization. That way, you can teach TinyDB 497 | to store ``datetime`` objects in a JSON file :) 498 | (see `issue #48 `_ and 499 | `pull request #50 `_) 500 | - Fixed a performance regression when searching became slower with every search 501 | (see `issue #49 `_) 502 | - Internal code has been cleaned up 503 | 504 | v2.2.2 (2015-02-12) 505 | ^^^^^^^^^^^^^^^^^^^ 506 | 507 | - Fixed a data loss when using ``CachingMiddleware`` together with ``JSONStorage`` 508 | (see `issue #47 `_) 509 | 510 | v2.2.1 (2015-01-09) 511 | ^^^^^^^^^^^^^^^^^^^ 512 | 513 | - Fixed handling of IDs with the JSON backend that converted integers 514 | to strings (see `issue #45 `_) 515 | 516 | v2.2.0 (2014-11-10) 517 | ^^^^^^^^^^^^^^^^^^^ 518 | 519 | - Extended ``any`` and ``all`` queries to take lists as conditions 520 | (see `pull request #38 `_) 521 | - Fixed an ``decode error`` when installing TinyDB in a non-UTF-8 environment 522 | (see `pull request #37 `_) 523 | - Fixed some issues with ``CachingMiddleware`` in combination with 524 | ``JSONStorage`` (see `pull request #39 `_) 525 | 526 | v2.1.0 (2014-10-14) 527 | ^^^^^^^^^^^^^^^^^^^ 528 | 529 | - Added ``where(...).contains(regex)`` 530 | (see `issue #32 `_) 531 | - Fixed a bug that corrupted data after reopening a database 532 | (see `issue #34 `_) 533 | 534 | v2.0.1 (2014-09-22) 535 | ^^^^^^^^^^^^^^^^^^^ 536 | 537 | - Fixed handling of Unicode data in Python 2 538 | (see `issue #28 `_). 539 | 540 | v2.0.0 (2014-09-05) 541 | ^^^^^^^^^^^^^^^^^^^ 542 | 543 | :ref:`Upgrade Notes ` 544 | 545 | .. warning:: TinyDB changed the way data is stored. You may need to migrate 546 | your databases to the new scheme. Check out the 547 | :ref:`Upgrade Notes ` for details. 548 | 549 | - The syntax ``query in db`` has been removed, use ``db.contains`` instead. 550 | - The ``ConcurrencyMiddleware`` has been removed due to a insecure implementation 551 | (see `issue #18 `_). Consider 552 | :ref:`tinyrecord` instead. 553 | 554 | - Better support for working with :ref:`Document IDs `. 555 | - Added support for `nested comparisons `_. 556 | - Added ``all`` and ``any`` `comparisons on lists `_. 557 | - Added optional :`_. 558 | - The query cache is now a :ref:`fixed size LRU cache `. 559 | 560 | v1.4.0 (2014-07-22) 561 | ^^^^^^^^^^^^^^^^^^^ 562 | 563 | - Added ``insert_multiple`` function 564 | (see `issue #8 `_). 565 | 566 | v1.3.0 (2014-07-02) 567 | ^^^^^^^^^^^^^^^^^^^ 568 | 569 | - Fixed `bug #7 `_: IDs not unique. 570 | - Extended the API: ``db.count(where(...))`` and ``db.contains(where(...))``. 571 | - The syntax ``query in db`` is now **deprecated** and replaced 572 | by ``db.contains``. 573 | 574 | v1.2.0 (2014-06-19) 575 | ^^^^^^^^^^^^^^^^^^^ 576 | 577 | - Added ``update`` method 578 | (see `issue #6 `_). 579 | 580 | v1.1.1 (2014-06-14) 581 | ^^^^^^^^^^^^^^^^^^^ 582 | 583 | - Merged `PR #5 `_: Fix minor 584 | documentation typos and style issues. 585 | 586 | v1.1.0 (2014-05-06) 587 | ^^^^^^^^^^^^^^^^^^^ 588 | 589 | - Improved the docs and fixed some typos. 590 | - Refactored some internal code. 591 | - Fixed a bug with multiple ``TinyDB?`` instances. 592 | 593 | v1.0.1 (2014-04-26) 594 | ^^^^^^^^^^^^^^^^^^^ 595 | 596 | - Fixed a bug in ``JSONStorage`` that broke the database when removing entries. 597 | 598 | v1.0.0 (2013-07-20) 599 | ^^^^^^^^^^^^^^^^^^^ 600 | 601 | - First official release – consider TinyDB stable now. 602 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # TinyDB documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Jul 13 20:14:55 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing 7 | # dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | 18 | import pkg_resources 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | # sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ---------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 33 | 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 34 | 'sphinx.ext.todo', 'sphinx.ext.extlinks'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | # source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'TinyDB' 50 | copyright = u'2021, Markus Siemens' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | 56 | try: 57 | release = pkg_resources.get_distribution('tinydb').version 58 | except pkg_resources.DistributionNotFound: 59 | print('To build the documentation, The distribution information of TinyDB') 60 | print('has to be available. Either install the package into your') 61 | print('development environment or run "pip install -e ." to setup the') 62 | print('metadata. A virtualenv is recommended!') 63 | sys.exit(1) 64 | del pkg_resources 65 | 66 | if 'dev' in release: 67 | release = release.split('dev')[0] + 'dev' 68 | version = '.'.join(release.split('.')[:2]) 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | # today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | # today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | # default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | # add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | # add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | # show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | # modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | # keep_warnings = False 107 | 108 | 109 | # -- Options for HTML output -------------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | # html_theme = 'default' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | # html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | # html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | # html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | # html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | # html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | # html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 145 | # using the given strftime format. 146 | # html_last_updated_fmt = '%b %d, %Y' 147 | 148 | # If true, SmartyPants will be used to convert quotes and dashes to 149 | # typographically correct entities. 150 | # html_use_smartypants = True 151 | 152 | # Custom sidebar templates, maps document names to template names. 153 | html_sidebars = { 154 | 'index': ['sidebarlogo.html', 'links.html', 'searchbox.html'], 155 | '**': ['sidebarlogo.html', 'localtoc.html', 'links.html', 156 | 'searchbox.html'] 157 | } 158 | 159 | # Additional templates that should be rendered to pages, maps page names to 160 | # template names. 161 | # html_additional_pages = {} 162 | 163 | # If false, no module index is generated. 164 | # html_domain_indices = True 165 | 166 | # If false, no index is generated. 167 | # html_use_index = True 168 | 169 | # If true, the index is split into individual pages for each letter. 170 | # html_split_index = False 171 | 172 | # If true, links to the reST sources are added to the pages. 173 | html_show_sourcelink = False 174 | 175 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 176 | # html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 179 | # html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages will 182 | # contain a tag referring to it. The value of this option must be the 183 | # base URL from which the finished HTML is served. 184 | # html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | # html_file_suffix = None 188 | 189 | # Output file base name for HTML help builder. 190 | htmlhelp_basename = 'TinyDBdoc' 191 | 192 | # -- Options for LaTeX output ------------------------------------------------- 193 | 194 | latex_elements = { 195 | # The paper size ('letterpaper' or 'a4paper'). 196 | # 'papersize': 'letterpaper', 197 | 198 | # The font size ('10pt', '11pt' or '12pt'). 199 | # 'pointsize': '10pt', 200 | 201 | # Additional stuff for the LaTeX preamble. 202 | # 'preamble': '', 203 | } 204 | 205 | # Grouping the document tree into LaTeX files. List of tuples 206 | # (source start file, target name, title, author, documentclass 207 | # [howto/manual]). 208 | latex_documents = [ 209 | ('index', 'TinyDB.tex', u'TinyDB Documentation', 210 | u'Markus Siemens', 'manual'), 211 | ] 212 | 213 | # The name of an image file (relative to this directory) to place at the top of 214 | # the title page. 215 | # latex_logo = None 216 | 217 | # For "manual" documents, if this is true, then toplevel headings are parts, 218 | # not chapters. 219 | # latex_use_parts = False 220 | 221 | # If true, show page references after internal links. 222 | # latex_show_pagerefs = False 223 | 224 | # If true, show URL addresses after external links. 225 | # latex_show_urls = False 226 | 227 | # Documents to append as an appendix to all manuals. 228 | # latex_appendices = [] 229 | 230 | # If false, no module index is generated. 231 | # latex_domain_indices = True 232 | 233 | 234 | # -- Options for manual page output ------------------------------------------- 235 | 236 | # One entry per manual page. List of tuples 237 | # (source start file, name, description, authors, manual section). 238 | man_pages = [ 239 | ('index', 'tinydb', u'TinyDB Documentation', 240 | [u'Markus Siemens'], 1) 241 | ] 242 | 243 | # If true, show URL addresses after external links. 244 | # man_show_urls = False 245 | 246 | 247 | # -- Options for Texinfo output ----------------------------------------------- 248 | 249 | # Grouping the document tree into Texinfo files. List of tuples 250 | # (source start file, target name, title, author, 251 | # dir menu entry, description, category) 252 | texinfo_documents = [ 253 | ('index', 'TinyDB', u'TinyDB Documentation', 254 | u'Markus Siemens', 'TinyDB', 'One line description of project.', 255 | 'Miscellaneous'), 256 | ] 257 | 258 | # Documents to append as an appendix to all manuals. 259 | # texinfo_appendices = [] 260 | 261 | # If false, no module index is generated. 262 | # texinfo_domain_indices = True 263 | 264 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 265 | # texinfo_show_urls = 'footnote' 266 | 267 | # If true, do not generate a @detailmenu in the "Top" node's menu. 268 | # texinfo_no_detailmenu = False 269 | 270 | extlinks = {'issue': ('https://https://github.com/msiemens/tinydb/issues/%s', 271 | 'issue ')} 272 | 273 | sys.path.append(os.path.abspath('_themes')) 274 | html_theme_path = ['_themes'] 275 | html_theme = 'flask' 276 | 277 | todo_include_todos = True 278 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ####################### 3 | 4 | Whether reporting bugs, discussing improvements and new ideas or writing 5 | extensions: Contributions to TinyDB are welcome! Here's how to get started: 6 | 7 | 1. Check for open issues or open a fresh issue to start a discussion around 8 | a feature idea or a bug 9 | 2. Fork `the repository `_ on Github, 10 | create a new branch off the `master` branch and start making your changes 11 | (known as `GitHub Flow `_) 12 | 3. Write a test which shows that the bug was fixed or that the feature works 13 | as expected 14 | 4. Send a pull request and bug the maintainer until it gets merged and 15 | published :) 16 | 17 | Philosophy of TinyDB 18 | ******************** 19 | 20 | TinyDB aims to be simple and fun to use. Therefore two key values are simplicity 21 | and elegance of interfaces and code. These values will contradict each other 22 | from time to time. In these cases , try using as little magic as possible. 23 | In any case don't forget documenting code that isn't clear at first glance. 24 | 25 | Code Conventions 26 | **************** 27 | 28 | In general the TinyDB source should always follow `PEP 8 `_. 29 | Exceptions are allowed in well justified and documented cases. However we make 30 | a small exception concerning docstrings: 31 | 32 | When using multiline docstrings, keep the opening and closing triple quotes 33 | on their own lines and add an empty line after it. 34 | 35 | .. code-block:: python 36 | 37 | def some_function(): 38 | """ 39 | Documentation ... 40 | """ 41 | 42 | # implementation ... 43 | 44 | Version Numbers 45 | *************** 46 | 47 | TinyDB follows the `SemVer versioning guidelines `_. 48 | This implies that backwards incompatible changes in the API will increment 49 | the major version. So think twice before making such changes. 50 | -------------------------------------------------------------------------------- /docs/extend.rst: -------------------------------------------------------------------------------- 1 | How to Extend TinyDB 2 | ==================== 3 | 4 | There are three main ways to extend TinyDB and modify its behaviour: 5 | 6 | 1. custom storages, 7 | 2. custom middlewares, 8 | 3. use hooks and overrides, and 9 | 4. subclassing ``TinyDB`` and ``Table``. 10 | 11 | Let's look at them in this order. 12 | 13 | Write a Custom Storage 14 | ---------------------- 15 | 16 | First, we have support for custom storages. By default TinyDB comes with an 17 | in-memory storage and a JSON file storage. But of course you can add your own. 18 | Let's look how you could add a `YAML `_ storage using 19 | `PyYAML `_: 20 | 21 | .. code-block:: python 22 | 23 | import yaml 24 | 25 | class YAMLStorage(Storage): 26 | def __init__(self, filename): # (1) 27 | self.filename = filename 28 | 29 | def read(self): 30 | with open(self.filename) as handle: 31 | try: 32 | data = yaml.safe_load(handle.read()) # (2) 33 | return data 34 | except yaml.YAMLError: 35 | return None # (3) 36 | 37 | def write(self, data): 38 | with open(self.filename, 'w+') as handle: 39 | yaml.dump(data, handle) 40 | 41 | def close(self): # (4) 42 | pass 43 | 44 | There are some things we should look closer at: 45 | 46 | 1. The constructor will receive all arguments passed to TinyDB when creating 47 | the database instance (except ``storage`` which TinyDB itself consumes). 48 | In other words calling ``TinyDB('something', storage=YAMLStorage)`` will 49 | pass ``'something'`` as an argument to ``YAMLStorage``. 50 | 2. We use ``yaml.safe_load`` as recommended by the 51 | `PyYAML documentation `_ 52 | when processing data from a potentially untrusted source. 53 | 3. If the storage is uninitialized, TinyDB expects the storage to return 54 | ``None`` so it can do any internal initialization that is necessary. 55 | 4. If your storage needs any cleanup (like closing file handles) before an 56 | instance is destroyed, you can put it in the ``close()`` method. To run 57 | these, you'll either have to run ``db.close()`` on your ``TinyDB`` instance 58 | or use it as a context manager, like this: 59 | 60 | .. code-block:: python 61 | 62 | with TinyDB('db.yml', storage=YAMLStorage) as db: 63 | # ... 64 | 65 | Finally, using the YAML storage is very straight-forward: 66 | 67 | .. code-block:: python 68 | 69 | db = TinyDB('db.yml', storage=YAMLStorage) 70 | # ... 71 | 72 | 73 | Write Custom Middleware 74 | ------------------------- 75 | 76 | Sometimes you don't want to write a new storage module but rather modify the 77 | behaviour of an existing one. As an example we'll build middleware that filters 78 | out empty items. 79 | 80 | Because middleware acts as a wrapper around a storage, they needs a ``read()`` 81 | and a ``write(data)`` method. In addition, they can access the underlying storage 82 | via ``self.storage``. Before we start implementing we should look at the structure 83 | of the data that the middleware receives. Here's what the data that goes through 84 | the middleware looks like: 85 | 86 | .. code-block:: python 87 | 88 | { 89 | '_default': { 90 | 1: {'key': 'value'}, 91 | 2: {'key': 'value'}, 92 | # other items 93 | }, 94 | # other tables 95 | } 96 | 97 | Thus, we'll need two nested loops: 98 | 99 | 1. Process every table 100 | 2. Process every item 101 | 102 | Now let's implement that: 103 | 104 | .. code-block:: python 105 | 106 | class RemoveEmptyItemsMiddleware(Middleware): 107 | def __init__(self, storage_cls): 108 | # Any middleware *has* to call the super constructor 109 | # with storage_cls 110 | super().__init__(storage_cls) # (1) 111 | 112 | def read(self): 113 | data = self.storage.read() 114 | 115 | for table_name in data: 116 | table_data = data[table_name] 117 | 118 | for doc_id in table_data: 119 | item = table_data[doc_id] 120 | 121 | if item == {}: 122 | del table_data[doc_id] 123 | 124 | return data 125 | 126 | def write(self, data): 127 | for table_name in data: 128 | table_data = data[table_name] 129 | 130 | for doc_id in table_data: 131 | item = table_data[doc_id] 132 | 133 | if item == {}: 134 | del table_data[doc_id] 135 | 136 | self.storage.write(data) 137 | 138 | def close(self): 139 | self.storage.close() 140 | 141 | 142 | Note that the constructor calls the middleware constructor (1) and passes 143 | the storage class to the middleware constructor. 144 | 145 | To wrap storage with this new middleware, we use it like this: 146 | 147 | .. code-block:: python 148 | 149 | db = TinyDB(storage=RemoveEmptyItemsMiddleware(SomeStorageClass)) 150 | 151 | Here ``SomeStorageClass`` should be replaced with the storage you want to use. 152 | If you leave it empty, the default storage will be used (which is the ``JSONStorage``). 153 | 154 | Use hooks and overrides 155 | ----------------------- 156 | 157 | .. _extend_hooks: 158 | 159 | There are cases when neither creating a custom storage nor using a custom 160 | middleware will allow you to adapt TinyDB in the way you need. In this case 161 | you can modify TinyDB's behavior by using predefined hooks and override points. 162 | For example you can configure the name of the default table by setting 163 | ``TinyDB.default_table_name``: 164 | 165 | .. code-block:: python 166 | 167 | TinyDB.default_table_name = 'my_table_name' 168 | 169 | Both :class:`~tinydb.database.TinyDB` and the :class:`~tinydb.table.Table` 170 | classes allow modifying their behavior using hooks and overrides. To use 171 | ``Table``'s overrides, you can access the class using ``TinyDB.table_class``: 172 | 173 | .. code-block:: python 174 | 175 | TinyDB.table_class.default_query_cache_capacity = 100 176 | 177 | Read the :ref:`api_docs` for more details on the available hooks and override 178 | points. 179 | 180 | Subclassing ``TinyDB`` and ``Table`` 181 | ------------------------------------ 182 | 183 | Finally, there's the last option to modify TinyDB's behavior. That way you 184 | can change how TinyDB itself works more deeply than using the other extension 185 | mechanisms. 186 | 187 | When creating a subclass you can use it by using hooks and overrides to override 188 | the default classes that TinyDB uses: 189 | 190 | .. code-block:: python 191 | 192 | class MyTable(Table): 193 | # Add your method overrides 194 | ... 195 | 196 | TinyDB.table_class = MyTable 197 | 198 | # Continue using TinyDB as usual 199 | 200 | TinyDB's source code is documented with extensions in mind, explaining how 201 | everything works even for internal methods and classes. Feel free to dig into 202 | the source and adapt everything you need for your projects. 203 | -------------------------------------------------------------------------------- /docs/extensions.rst: -------------------------------------------------------------------------------- 1 | Extensions 2 | ========== 3 | 4 | Here are some extensions that might be useful to you: 5 | 6 | ``aiotinydb`` 7 | ************* 8 | 9 | | **Repo:** https://github.com/ASMfreaK/aiotinydb 10 | | **Status:** *stable* 11 | | **Description:** asyncio compatibility shim for TinyDB. Enables usage of 12 | TinyDB in asyncio-aware contexts without slow synchronous 13 | IO. 14 | 15 | 16 | ``BetterJSONStorage`` 17 | ********************* 18 | 19 | | **Repo:** https://github.com/MrPigss/BetterJSONStorage 20 | | **Status:** *stable* 21 | | **Description:** BetterJSONStorage is a faster 'Storage Type' for TinyDB. It 22 | uses the faster Orjson library for parsing the JSON and BLOSC 23 | for compression. 24 | 25 | 26 | ``tinydb-appengine`` 27 | ******************** 28 | 29 | | **Repo:** https://github.com/imalento/tinydb-appengine 30 | | **Status:** *stable* 31 | | **Description:** ``tinydb-appengine`` provides TinyDB storage for 32 | App Engine. You can use JSON readonly. 33 | 34 | 35 | ``tinydb-serialization`` 36 | ************************ 37 | 38 | | **Repo:** https://github.com/msiemens/tinydb-serialization 39 | | **Status:** *stable* 40 | | **Description:** ``tinydb-serialization`` provides serialization for objects 41 | that TinyDB otherwise couldn't handle. 42 | 43 | 44 | ``tinydb-smartcache`` 45 | ********************* 46 | 47 | | **Repo:** https://github.com/msiemens/tinydb-smartcache 48 | | **Status:** *stable* 49 | | **Description:** ``tinydb-smartcache`` provides a smart query cache for 50 | TinyDB. It updates the query cache when 51 | inserting/removing/updating documents so the cache doesn't 52 | get invalidated. It's useful if you perform lots of queries 53 | while the data changes only little. 54 | 55 | 56 | ``TinyDBTimestamps`` 57 | ******************** 58 | 59 | | **Repo:** https://github.com/pachacamac/TinyDBTimestamps 60 | | **Status:** *experimental* 61 | | **Description:** Automatically add create at/ update at timestamps to TinyDB 62 | documents. 63 | 64 | 65 | ``tinyindex`` 66 | ************* 67 | 68 | | **Repo:** https://github.com/eugene-eeo/tinyindex 69 | | **Status:** *experimental* 70 | | **Description:** Document indexing for TinyDB. Basically ensures deterministic 71 | (as long as there aren't any changes to the table) yielding 72 | of documents. 73 | 74 | 75 | ``tinymongo`` 76 | ************* 77 | 78 | | **Repo:** https://github.com/schapman1974/tinymongo 79 | | **Status:** *experimental* 80 | | **Description:** A simple wrapper that allows to use TinyDB as a flat file 81 | drop-in replacement for MongoDB. 82 | 83 | 84 | ``TinyMP`` 85 | ************* 86 | 87 | | **Repo:** https://github.com/alshapton/TinyMP 88 | | **Status:** *no longer maintained* 89 | | **Description:** A MessagePack-based storage extension to tinydb using 90 | http://msgpack.org 91 | 92 | .. _tinyrecord: 93 | 94 | ``tinyrecord`` 95 | ************** 96 | 97 | | **Repo:** https://github.com/eugene-eeo/tinyrecord 98 | | **Status:** *stable* 99 | | **Description:** Tinyrecord is a library which implements experimental atomic 100 | transaction support for the TinyDB NoSQL database. It uses a 101 | record-first then execute architecture which allows us to 102 | minimize the time that we are within a thread lock. 103 | -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 3 2 | 3 | Getting Started 4 | =============== 5 | 6 | Installing TinyDB 7 | ----------------- 8 | 9 | To install TinyDB from PyPI, run:: 10 | 11 | $ pip install tinydb 12 | 13 | You can also grab the latest development version from GitHub_. After downloading 14 | and unpacking it, you can install it using:: 15 | 16 | $ pip install . 17 | 18 | 19 | Basic Usage 20 | ----------- 21 | 22 | Let's cover the basics before going more into detail. We'll start by setting up 23 | a TinyDB database: 24 | 25 | >>> from tinydb import TinyDB, Query 26 | >>> db = TinyDB('db.json') 27 | 28 | You now have a TinyDB database that stores its data in ``db.json``. 29 | What about inserting some data? TinyDB expects the data to be Python ``dict``\s: 30 | 31 | >>> db.insert({'type': 'apple', 'count': 7}) 32 | >>> db.insert({'type': 'peach', 'count': 3}) 33 | 34 | .. note:: The ``insert`` method returns the inserted document's ID. Read more 35 | about it here: :ref:`document_ids`. 36 | 37 | 38 | Now you can get all documents stored in the database by running: 39 | 40 | >>> db.all() 41 | [{'count': 7, 'type': 'apple'}, {'count': 3, 'type': 'peach'}] 42 | 43 | You can also iter over stored documents: 44 | 45 | >>> for item in db: 46 | >>> print(item) 47 | {'count': 7, 'type': 'apple'} 48 | {'count': 3, 'type': 'peach'} 49 | 50 | Of course you'll also want to search for specific documents. Let's try: 51 | 52 | >>> Fruit = Query() 53 | >>> db.search(Fruit.type == 'peach') 54 | [{'count': 3, 'type': 'peach'}] 55 | >>> db.search(Fruit.count > 5) 56 | [{'count': 7, 'type': 'apple'}] 57 | 58 | 59 | Next we'll update the ``count`` field of the apples: 60 | 61 | >>> db.update({'count': 10}, Fruit.type == 'apple') 62 | >>> db.all() 63 | [{'count': 10, 'type': 'apple'}, {'count': 3, 'type': 'peach'}] 64 | 65 | 66 | In the same manner you can also remove documents: 67 | 68 | >>> db.remove(Fruit.count < 5) 69 | >>> db.all() 70 | [{'count': 10, 'type': 'apple'}] 71 | 72 | And of course you can throw away all data to start with an empty database: 73 | 74 | >>> db.truncate() 75 | >>> db.all() 76 | [] 77 | 78 | 79 | Recap 80 | ***** 81 | 82 | Before we dive deeper, let's recapitulate the basics: 83 | 84 | +-------------------------------+---------------------------------------------------------------+ 85 | | **Inserting** | 86 | +-------------------------------+---------------------------------------------------------------+ 87 | | ``db.insert(...)`` | Insert a document | 88 | +-------------------------------+---------------------------------------------------------------+ 89 | | **Getting data** | 90 | +-------------------------------+---------------------------------------------------------------+ 91 | | ``db.all()`` | Get all documents | 92 | +-------------------------------+---------------------------------------------------------------+ 93 | | ``iter(db)`` | Iter over all documents | 94 | +-------------------------------+---------------------------------------------------------------+ 95 | | ``db.search(query)`` | Get a list of documents matching the query | 96 | +-------------------------------+---------------------------------------------------------------+ 97 | | **Updating** | 98 | +-------------------------------+---------------------------------------------------------------+ 99 | | ``db.update(fields, query)`` | Update all documents matching the query to contain ``fields`` | 100 | +-------------------------------+---------------------------------------------------------------+ 101 | | **Removing** | 102 | +-------------------------------+---------------------------------------------------------------+ 103 | | ``db.remove(query)`` | Remove all documents matching the query | 104 | +-------------------------------+---------------------------------------------------------------+ 105 | | ``db.truncate()`` | Remove all documents | 106 | +-------------------------------+---------------------------------------------------------------+ 107 | | **Querying** | 108 | +-------------------------------+---------------------------------------------------------------+ 109 | | ``Query()`` | Create a new query object | 110 | +-------------------------------+---------------------------------------------------------------+ 111 | | ``Query().field == 2`` | Match any document that has a key ``field`` with value | 112 | | | ``== 2`` (also possible: ``!=``, ``>``, ``>=``, ``<``, ``<=``)| 113 | +-------------------------------+---------------------------------------------------------------+ 114 | 115 | .. References 116 | .. _GitHub: http://github.com/msiemens/tinydb/ 117 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to TinyDB! 2 | ================== 3 | 4 | Welcome to TinyDB, your tiny, document oriented database optimized for your 5 | happiness :) 6 | 7 | >>> from tinydb import TinyDB, Query 8 | >>> db = TinyDB('path/to/db.json') 9 | >>> User = Query() 10 | >>> db.insert({'name': 'John', 'age': 22}) 11 | >>> db.search(User.name == 'John') 12 | [{'name': 'John', 'age': 22}] 13 | 14 | User's Guide 15 | ------------ 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | intro 21 | getting-started 22 | usage 23 | 24 | Extending TinyDB 25 | ---------------- 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | Extending TinyDB 31 | TinyDB Extensions 32 | 33 | API Reference 34 | ------------- 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | 39 | api 40 | 41 | Additional Notes 42 | ---------------- 43 | 44 | .. toctree:: 45 | :maxdepth: 2 46 | 47 | contribute 48 | changelog 49 | Upgrade Notes 50 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Great that you've taken time to check out the TinyDB docs! Before we begin 5 | looking at TinyDB itself, let's take some time to see whether you should use 6 | TinyDB. 7 | 8 | Why Use TinyDB? 9 | --------------- 10 | 11 | - **tiny:** The current source code has 1800 lines of code (with about 40% 12 | documentation) and 1600 lines tests. 13 | 14 | - **document oriented:** Like MongoDB_, you can store any document 15 | (represented as ``dict``) in TinyDB. 16 | 17 | - **optimized for your happiness:** TinyDB is designed to be simple and 18 | fun to use by providing a simple and clean API. 19 | 20 | - **written in pure Python:** TinyDB neither needs an external server (as 21 | e.g. `PyMongo `_) nor any dependencies 22 | from PyPI. 23 | 24 | - **works on Python 3.5+ and PyPy:** TinyDB works on all modern versions of Python 25 | and PyPy. 26 | 27 | - **powerfully extensible:** You can easily extend TinyDB by writing new 28 | storages or modify the behaviour of storages with Middlewares. 29 | 30 | - **100% test coverage:** No explanation needed. 31 | 32 | In short: If you need a simple database with a clean API that just works 33 | without lots of configuration, TinyDB might be the right choice for you. 34 | 35 | 36 | Why **Not** Use TinyDB? 37 | ----------------------- 38 | 39 | - You need **advanced features** like: 40 | - access from multiple processes or threads (e.g. when using Flask!), 41 | - creating indexes for tables, 42 | - an HTTP server, 43 | - managing relationships between tables or similar, 44 | - `ACID guarantees `_. 45 | - You are really concerned about **performance** and need a high speed 46 | database. 47 | 48 | To put it plainly: If you need advanced features or high performance, TinyDB 49 | is the wrong database for you – consider using databases like SQLite_, Buzhug_, 50 | CodernityDB_ or MongoDB_. 51 | 52 | .. References 53 | .. _Buzhug: https://buzhug.sourceforge.net/ 54 | .. _CodernityDB: http://labs.codernity.com/codernitydb/ 55 | .. _MongoDB: https://mongodb.org/ 56 | .. _SQLite: https://www.sqlite.org/ 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\TinyDB.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\TinyDB.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/upgrade.rst: -------------------------------------------------------------------------------- 1 | Upgrading to Newer Releases 2 | =========================== 3 | 4 | Version 4.0 5 | ----------- 6 | 7 | .. _upgrade_v4_0: 8 | 9 | - API changes: 10 | - Replace ``TinyDB.purge_tables(...)`` with ``TinyDB.drop_tables(...)`` 11 | - Replace ``TinyDB.purge_table(...)`` with ``TinyDB.drop_table(...)`` 12 | - Replace ``Table.purge()`` with ``Table.truncate()`` 13 | - Replace ``TinyDB(default_table='name')`` with ``TinyDB.default_table_name = 'name'`` 14 | - Replace ``TinyDB(table_class=Class)`` with ``TinyDB.table_class = Class`` 15 | - If you were using ``TinyDB.DEFAULT_TABLE``, ``TinyDB.DEFAULT_TABLE_KWARGS``, 16 | or ``TinyDB.DEFAULT_STORAGE``: Use the new methods for customizing TinyDB 17 | described in :ref:`How to Extend TinyDB ` 18 | 19 | Version 3.0 20 | ----------- 21 | 22 | .. _upgrade_v3_0: 23 | 24 | Breaking API Changes 25 | ^^^^^^^^^^^^^^^^^^^^ 26 | 27 | - Querying (see `Issue #62 `_): 28 | 29 | - ``where('...').contains('...')`` has been renamed to 30 | ``where('...').search('...')``. 31 | - ``where('foo').has('bar')`` is replaced by either 32 | ``where('foo').bar`` or ``Query().foo.bar``. 33 | 34 | - In case the key is not a valid Python identifier, array 35 | notation can be used: ``where('a.b.c')`` is now 36 | ``Query()['a.b.c']``. 37 | 38 | - Checking for the existence of a key has to be done explicitly: 39 | ``where('foo').exists()``. 40 | 41 | - ``SmartCacheTable`` has been moved to `msiemens/tinydb-smartcache`_. 42 | - Serialization has been moved to `msiemens/tinydb-serialization`_. 43 | - Empty storages are now expected to return ``None`` instead of raising 44 | ``ValueError`` (see `Issue #67 `_). 45 | 46 | .. _msiemens/tinydb-smartcache: https://github.com/msiemens/tinydb-smartcache 47 | .. _msiemens/tinydb-serialization: https://github.com/msiemens/tinydb-serialization 48 | 49 | .. _upgrade_v2_0: 50 | 51 | Version 2.0 52 | ----------- 53 | 54 | Breaking API Changes 55 | ^^^^^^^^^^^^^^^^^^^^ 56 | 57 | - The syntax ``query in db`` is not supported any more. Use ``db.contains(...)`` 58 | instead. 59 | - The ``ConcurrencyMiddleware`` has been removed due to a insecure implementation 60 | (see `Issue #18 `_). Consider 61 | :ref:`tinyrecord` instead. 62 | 63 | Apart from that the API remains compatible to v1.4 and prior. 64 | 65 | For migration from v1 to v2, check out the `v2.0 documentation `_ 66 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = tinydb/mypy_plugin.py 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tinydb" 3 | version = "4.8.2" 4 | description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" 5 | authors = ["Markus Siemens "] 6 | license = "MIT" 7 | 8 | readme = "README.rst" 9 | 10 | homepage = "https://github.com/msiemens/tinydb" 11 | documentation = "https://tinydb.readthedocs.org/" 12 | 13 | keywords = ["database", "nosql"] 14 | 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Developers", 18 | "Intended Audience :: System Administrators", 19 | "License :: OSI Approved :: MIT License", 20 | "Topic :: Database", 21 | "Topic :: Database :: Database Engines/Servers", 22 | "Topic :: Utilities", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Programming Language :: Python :: Implementation :: CPython", 31 | "Programming Language :: Python :: Implementation :: PyPy", 32 | "Operating System :: OS Independent", 33 | "Typing :: Typed", 34 | ] 35 | 36 | packages = [ 37 | { include = "tinydb" }, 38 | { include = "tests", format = "sdist" } 39 | ] 40 | 41 | [tool.poetry.urls] 42 | "Changelog" = "https://tinydb.readthedocs.io/en/latest/changelog.html" 43 | "Issues" = "https://github.com/msiemens/tinydb/issues" 44 | 45 | [tool.poetry.dependencies] 46 | python = "^3.8" 47 | 48 | [tool.poetry.dev-dependencies] 49 | pytest = "^7.2.0" 50 | pytest-pycodestyle = "^2.3.1" 51 | pytest-cov = "^4.0.0" 52 | pycodestyle = "^2.10.0" 53 | sphinx = "^7.0.0" 54 | coveralls = "^3.3.1" 55 | pyyaml = "^6.0" 56 | pytest-mypy = { version = "^0.10.2", markers = "platform_python_implementation != 'PyPy'" } 57 | types-PyYAML = "^6.0.0" 58 | 59 | [build-system] 60 | requires = ["poetry-core"] 61 | build-backend = "poetry.core.masonry.api" 62 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts=--verbose --cov-append --cov-report term --cov tinydb -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiemens/tinydb/10644a0e07ad180c5b756aba272ee6b0dbd12df8/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import tempfile 3 | from pathlib import Path 4 | 5 | import pytest # type: ignore 6 | 7 | from tinydb.middlewares import CachingMiddleware 8 | from tinydb.storages import MemoryStorage 9 | from tinydb import TinyDB, JSONStorage 10 | 11 | 12 | @pytest.fixture(params=['memory', 'json']) 13 | def db(request, tmp_path: Path): 14 | if request.param == 'json': 15 | db_ = TinyDB(tmp_path / 'test.db', storage=JSONStorage) 16 | else: 17 | db_ = TinyDB(storage=MemoryStorage) 18 | 19 | db_.drop_tables() 20 | db_.insert_multiple({'int': 1, 'char': c} for c in 'abc') 21 | 22 | yield db_ 23 | 24 | 25 | @pytest.fixture 26 | def storage(): 27 | return CachingMiddleware(MemoryStorage)() 28 | -------------------------------------------------------------------------------- /tests/test_middlewares.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tinydb import TinyDB 4 | from tinydb.middlewares import CachingMiddleware 5 | from tinydb.storages import MemoryStorage, JSONStorage 6 | 7 | doc = {'none': [None, None], 'int': 42, 'float': 3.1415899999999999, 8 | 'list': ['LITE', 'RES_ACID', 'SUS_DEXT'], 9 | 'dict': {'hp': 13, 'sp': 5}, 10 | 'bool': [True, False, True, False]} 11 | 12 | 13 | def test_caching(storage): 14 | # Write contents 15 | storage.write(doc) 16 | 17 | # Verify contents 18 | assert doc == storage.read() 19 | 20 | 21 | def test_caching_read(): 22 | db = TinyDB(storage=CachingMiddleware(MemoryStorage)) 23 | assert db.all() == [] 24 | 25 | 26 | def test_caching_write_many(storage): 27 | storage.WRITE_CACHE_SIZE = 3 28 | 29 | # Storage should be still empty 30 | assert storage.memory is None 31 | 32 | # Write contents 33 | for x in range(2): 34 | storage.write(doc) 35 | assert storage.memory is None # Still cached 36 | 37 | storage.write(doc) 38 | 39 | # Verify contents: Cache should be emptied and written to storage 40 | assert storage.memory 41 | 42 | 43 | def test_caching_flush(storage): 44 | # Write contents 45 | for _ in range(CachingMiddleware.WRITE_CACHE_SIZE - 1): 46 | storage.write(doc) 47 | 48 | # Not yet flushed... 49 | assert storage.memory is None 50 | 51 | storage.write(doc) 52 | 53 | # Verify contents: Cache should be emptied and written to storage 54 | assert storage.memory 55 | 56 | 57 | def test_caching_flush_manually(storage): 58 | # Write contents 59 | storage.write(doc) 60 | 61 | storage.flush() 62 | 63 | # Verify contents: Cache should be emptied and written to storage 64 | assert storage.memory 65 | 66 | 67 | def test_caching_write(storage): 68 | # Write contents 69 | storage.write(doc) 70 | 71 | storage.close() 72 | 73 | # Verify contents: Cache should be emptied and written to storage 74 | assert storage.storage.memory 75 | 76 | 77 | def test_nested(): 78 | storage = CachingMiddleware(MemoryStorage) 79 | storage() # Initialization 80 | 81 | # Write contents 82 | storage.write(doc) 83 | 84 | # Verify contents 85 | assert doc == storage.read() 86 | 87 | 88 | def test_caching_json_write(tmpdir): 89 | path = str(tmpdir.join('test.db')) 90 | 91 | with TinyDB(path, storage=CachingMiddleware(JSONStorage)) as db: 92 | db.insert({'key': 'value'}) 93 | 94 | # Verify database filesize 95 | statinfo = os.stat(path) 96 | assert statinfo.st_size != 0 97 | 98 | # Assert JSON file has been closed 99 | assert db._storage._handle.closed 100 | 101 | del db 102 | 103 | # Reopen database 104 | with TinyDB(path, storage=CachingMiddleware(JSONStorage)) as db: 105 | assert db.all() == [{'key': 'value'}] 106 | -------------------------------------------------------------------------------- /tests/test_operations.py: -------------------------------------------------------------------------------- 1 | from tinydb import where 2 | from tinydb.operations import delete, increment, decrement, add, subtract, set 3 | 4 | 5 | def test_delete(db): 6 | db.update(delete('int'), where('char') == 'a') 7 | assert 'int' not in db.get(where('char') == 'a') 8 | 9 | 10 | def test_add_int(db): 11 | db.update(add('int', 5), where('char') == 'a') 12 | assert db.get(where('char') == 'a')['int'] == 6 13 | 14 | 15 | def test_add_str(db): 16 | db.update(add('char', 'xyz'), where('char') == 'a') 17 | assert db.get(where('char') == 'axyz')['int'] == 1 18 | 19 | 20 | def test_subtract(db): 21 | db.update(subtract('int', 5), where('char') == 'a') 22 | assert db.get(where('char') == 'a')['int'] == -4 23 | 24 | 25 | def test_set(db): 26 | db.update(set('char', 'xyz'), where('char') == 'a') 27 | assert db.get(where('char') == 'xyz')['int'] == 1 28 | 29 | 30 | def test_increment(db): 31 | db.update(increment('int'), where('char') == 'a') 32 | assert db.get(where('char') == 'a')['int'] == 2 33 | 34 | 35 | def test_decrement(db): 36 | db.update(decrement('int'), where('char') == 'a') 37 | assert db.get(where('char') == 'a')['int'] == 0 38 | -------------------------------------------------------------------------------- /tests/test_queries.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | from tinydb.queries import Query, where 6 | 7 | 8 | def test_no_path(): 9 | with pytest.raises(ValueError): 10 | _ = Query() == 2 11 | 12 | 13 | def test_path_exists(): 14 | query = Query()['value'].exists() 15 | assert query == where('value').exists() 16 | assert query({'value': 1}) 17 | assert not query({'something': 1}) 18 | assert hash(query) 19 | assert hash(query) != hash(where('asd')) 20 | 21 | query = Query()['value']['val'].exists() 22 | assert query == where('value')['val'].exists() 23 | assert query({'value': {'val': 2}}) 24 | assert not query({'value': 1}) 25 | assert not query({'value': {'asd': 1}}) 26 | assert not query({'something': 1}) 27 | assert hash(query) 28 | assert hash(query) != hash(where('asd')) 29 | 30 | 31 | def test_path_and(): 32 | query = Query()['value'].exists() & (Query()['value'] == 5) 33 | assert query({'value': 5}) 34 | assert not query({'value': 10}) 35 | assert not query({'something': 1}) 36 | assert hash(query) 37 | assert hash(query) != hash(where('value')) 38 | 39 | 40 | def test_callable_in_path_with_map(): 41 | double = lambda x: x + x 42 | query = Query().value.map(double) == 10 43 | assert query({'value': 5}) 44 | assert not query({'value': 10}) 45 | 46 | 47 | def test_callable_in_path_with_chain(): 48 | rekey = lambda x: {'y': x['a'], 'z': x['b']} 49 | query = Query().map(rekey).z == 10 50 | assert query({'a': 5, 'b': 10}) 51 | 52 | 53 | def test_eq(): 54 | query = Query().value == 1 55 | assert query({'value': 1}) 56 | assert not query({'value': 2}) 57 | assert hash(query) 58 | 59 | query = Query().value == [0, 1] 60 | assert query({'value': [0, 1]}) 61 | assert not query({'value': [0, 1, 2]}) 62 | assert hash(query) 63 | 64 | 65 | def test_ne(): 66 | query = Query().value != 1 67 | assert query({'value': 0}) 68 | assert query({'value': 2}) 69 | assert not query({'value': 1}) 70 | assert hash(query) 71 | 72 | query = Query().value != [0, 1] 73 | assert query({'value': [0, 1, 2]}) 74 | assert not query({'value': [0, 1]}) 75 | assert hash(query) 76 | 77 | 78 | def test_lt(): 79 | query = Query().value < 1 80 | assert query({'value': 0}) 81 | assert not query({'value': 1}) 82 | assert not query({'value': 2}) 83 | assert hash(query) 84 | 85 | 86 | def test_le(): 87 | query = Query().value <= 1 88 | assert query({'value': 0}) 89 | assert query({'value': 1}) 90 | assert not query({'value': 2}) 91 | assert hash(query) 92 | 93 | 94 | def test_gt(): 95 | query = Query().value > 1 96 | assert query({'value': 2}) 97 | assert not query({'value': 1}) 98 | assert hash(query) 99 | 100 | 101 | def test_ge(): 102 | query = Query().value >= 1 103 | assert query({'value': 2}) 104 | assert query({'value': 1}) 105 | assert not query({'value': 0}) 106 | assert hash(query) 107 | 108 | 109 | def test_or(): 110 | query = ( 111 | (Query().val1 == 1) | 112 | (Query().val2 == 2) 113 | ) 114 | assert query({'val1': 1}) 115 | assert query({'val2': 2}) 116 | assert query({'val1': 1, 'val2': 2}) 117 | assert not query({'val1': '', 'val2': ''}) 118 | assert hash(query) 119 | 120 | 121 | def test_and(): 122 | query = ( 123 | (Query().val1 == 1) & 124 | (Query().val2 == 2) 125 | ) 126 | assert query({'val1': 1, 'val2': 2}) 127 | assert not query({'val1': 1}) 128 | assert not query({'val2': 2}) 129 | assert not query({'val1': '', 'val2': ''}) 130 | assert hash(query) 131 | 132 | 133 | def test_not(): 134 | query = ~ (Query().val1 == 1) 135 | assert query({'val1': 5, 'val2': 2}) 136 | assert not query({'val1': 1, 'val2': 2}) 137 | assert hash(query) 138 | 139 | query = ( 140 | (~ (Query().val1 == 1)) & 141 | (Query().val2 == 2) 142 | ) 143 | assert query({'val1': '', 'val2': 2}) 144 | assert query({'val2': 2}) 145 | assert not query({'val1': 1, 'val2': 2}) 146 | assert not query({'val1': 1}) 147 | assert not query({'val1': '', 'val2': ''}) 148 | assert hash(query) 149 | 150 | 151 | def test_has_key(): 152 | query = Query().val3.exists() 153 | 154 | assert query({'val3': 1}) 155 | assert not query({'val1': 1, 'val2': 2}) 156 | assert hash(query) 157 | 158 | 159 | def test_regex(): 160 | query = Query().val.matches(r'\d{2}\.') 161 | 162 | assert query({'val': '42.'}) 163 | assert not query({'val': '44'}) 164 | assert not query({'val': 'ab.'}) 165 | assert not query({'val': 155}) 166 | assert not query({'val': False}) 167 | assert not query({'': None}) 168 | assert hash(query) 169 | 170 | query = Query().val.search(r'\d+') 171 | 172 | assert query({'val': 'ab3'}) 173 | assert not query({'val': 'abc'}) 174 | assert not query({'val': ''}) 175 | assert not query({'val': True}) 176 | assert not query({'': None}) 177 | assert hash(query) 178 | 179 | query = Query().val.search(r'JOHN', flags=re.IGNORECASE) 180 | assert query({'val': 'john'}) 181 | assert query({'val': 'xJohNx'}) 182 | assert not query({'val': 'JOH'}) 183 | assert not query({'val': 12}) 184 | assert not query({'': None}) 185 | assert hash(query) 186 | 187 | 188 | def test_custom(): 189 | def test(value): 190 | return value == 42 191 | 192 | query = Query().val.test(test) 193 | 194 | assert query({'val': 42}) 195 | assert not query({'val': 40}) 196 | assert not query({'val': '44'}) 197 | assert not query({'': None}) 198 | assert hash(query) 199 | 200 | def in_list(value, l): 201 | return value in l 202 | 203 | query = Query().val.test(in_list, tuple([25, 35])) 204 | assert not query({'val': 20}) 205 | assert query({'val': 25}) 206 | assert not query({'val': 30}) 207 | assert query({'val': 35}) 208 | assert not query({'val': 36}) 209 | assert hash(query) 210 | 211 | 212 | def test_custom_with_params(): 213 | def test(value, minimum, maximum): 214 | return minimum <= value <= maximum 215 | 216 | query = Query().val.test(test, 1, 10) 217 | 218 | assert query({'val': 5}) 219 | assert not query({'val': 0}) 220 | assert not query({'val': 11}) 221 | assert not query({'': None}) 222 | assert hash(query) 223 | 224 | 225 | def test_any(): 226 | query = Query().followers.any(Query().name == 'don') 227 | 228 | assert query({'followers': [{'name': 'don'}, {'name': 'john'}]}) 229 | assert not query({'followers': 1}) 230 | assert not query({}) 231 | assert hash(query) 232 | 233 | query = Query().followers.any(Query().num.matches('\\d+')) 234 | assert query({'followers': [{'num': '12'}, {'num': 'abc'}]}) 235 | assert not query({'followers': [{'num': 'abc'}]}) 236 | assert hash(query) 237 | 238 | query = Query().followers.any(['don', 'jon']) 239 | assert query({'followers': ['don', 'greg', 'bill']}) 240 | assert not query({'followers': ['greg', 'bill']}) 241 | assert not query({}) 242 | assert hash(query) 243 | 244 | query = Query().followers.any([{'name': 'don'}, {'name': 'john'}]) 245 | assert query({'followers': [{'name': 'don'}, {'name': 'greg'}]}) 246 | assert not query({'followers': [{'name': 'greg'}]}) 247 | assert hash(query) 248 | 249 | 250 | def test_all(): 251 | query = Query().followers.all(Query().name == 'don') 252 | assert query({'followers': [{'name': 'don'}]}) 253 | assert not query({'followers': [{'name': 'don'}, {'name': 'john'}]}) 254 | assert hash(query) 255 | 256 | query = Query().followers.all(Query().num.matches('\\d+')) 257 | assert query({'followers': [{'num': '123'}, {'num': '456'}]}) 258 | assert not query({'followers': [{'num': '123'}, {'num': 'abc'}]}) 259 | assert hash(query) 260 | 261 | query = Query().followers.all(['don', 'john']) 262 | assert query({'followers': ['don', 'john', 'greg']}) 263 | assert not query({'followers': ['don', 'greg']}) 264 | assert not query({}) 265 | assert hash(query) 266 | 267 | query = Query().followers.all([{'name': 'jane'}, {'name': 'john'}]) 268 | assert query({'followers': [{'name': 'john'}, {'name': 'jane'}]}) 269 | assert query({'followers': [{'name': 'john'}, 270 | {'name': 'jane'}, 271 | {'name': 'bob'}]}) 272 | assert not query({'followers': [{'name': 'john'}, {'name': 'bob'}]}) 273 | assert hash(query) 274 | 275 | 276 | def test_has(): 277 | query = Query().key1.key2.exists() 278 | str(query) # This used to cause a bug... 279 | 280 | assert query({'key1': {'key2': {'key3': 1}}}) 281 | assert query({'key1': {'key2': 1}}) 282 | assert not query({'key1': 3}) 283 | assert not query({'key1': {'key1': 1}}) 284 | assert not query({'key2': {'key1': 1}}) 285 | assert hash(query) 286 | 287 | query = Query().key1.key2 == 1 288 | 289 | assert query({'key1': {'key2': 1}}) 290 | assert not query({'key1': {'key2': 2}}) 291 | assert hash(query) 292 | 293 | # Nested has: key exists 294 | query = Query().key1.key2.key3.exists() 295 | assert query({'key1': {'key2': {'key3': 1}}}) 296 | # Not a dict 297 | assert not query({'key1': 1}) 298 | assert not query({'key1': {'key2': 1}}) 299 | # Wrong key 300 | assert not query({'key1': {'key2': {'key0': 1}}}) 301 | assert not query({'key1': {'key0': {'key3': 1}}}) 302 | assert not query({'key0': {'key2': {'key3': 1}}}) 303 | 304 | assert hash(query) 305 | 306 | # Nested has: check for value 307 | query = Query().key1.key2.key3 == 1 308 | assert query({'key1': {'key2': {'key3': 1}}}) 309 | assert not query({'key1': {'key2': {'key3': 0}}}) 310 | assert hash(query) 311 | 312 | # Test special methods: regex matches 313 | query = Query().key1.value.matches(r'\d+') 314 | assert query({'key1': {'value': '123'}}) 315 | assert not query({'key2': {'value': '123'}}) 316 | assert not query({'key2': {'value': 'abc'}}) 317 | assert hash(query) 318 | 319 | # Test special methods: regex contains 320 | query = Query().key1.value.search(r'\d+') 321 | assert query({'key1': {'value': 'a2c'}}) 322 | assert not query({'key2': {'value': 'a2c'}}) 323 | assert not query({'key2': {'value': 'abc'}}) 324 | assert hash(query) 325 | 326 | # Test special methods: nested has and regex matches 327 | query = Query().key1.x.y.matches(r'\d+') 328 | assert query({'key1': {'x': {'y': '123'}}}) 329 | assert not query({'key1': {'x': {'y': 'abc'}}}) 330 | assert hash(query) 331 | 332 | # Test special method: nested has and regex contains 333 | query = Query().key1.x.y.search(r'\d+') 334 | assert query({'key1': {'x': {'y': 'a2c'}}}) 335 | assert not query({'key1': {'x': {'y': 'abc'}}}) 336 | assert hash(query) 337 | 338 | # Test special methods: custom test 339 | query = Query().key1.int.test(lambda x: x == 3) 340 | assert query({'key1': {'int': 3}}) 341 | assert hash(query) 342 | 343 | 344 | def test_one_of(): 345 | query = Query().key1.one_of(['value 1', 'value 2']) 346 | assert query({'key1': 'value 1'}) 347 | assert query({'key1': 'value 2'}) 348 | assert not query({'key1': 'value 3'}) 349 | 350 | 351 | def test_hash(): 352 | d = { 353 | Query().key1 == 2: True, 354 | Query().key1.key2.key3.exists(): True, 355 | Query().key1.exists() & Query().key2.exists(): True, 356 | Query().key1.exists() | Query().key2.exists(): True, 357 | } 358 | 359 | assert (Query().key1 == 2) in d 360 | assert (Query().key1.key2.key3.exists()) in d 361 | assert (Query()['key1.key2'].key3.exists()) not in d 362 | 363 | # Commutative property of & and | 364 | assert (Query().key1.exists() & Query().key2.exists()) in d 365 | assert (Query().key2.exists() & Query().key1.exists()) in d 366 | assert (Query().key1.exists() | Query().key2.exists()) in d 367 | assert (Query().key2.exists() | Query().key1.exists()) in d 368 | 369 | 370 | def test_orm_usage(): 371 | data = {'name': 'John', 'age': {'year': 2000}} 372 | 373 | User = Query() 374 | query1 = User.name == 'John' 375 | query2 = User.age.year == 2000 376 | assert query1(data) 377 | assert query2(data) 378 | 379 | 380 | def test_repr(): 381 | Fruit = Query() 382 | 383 | assert repr(Fruit) == "Query()" 384 | assert repr(Fruit.type == 'peach') == "QueryImpl('==', ('type',), 'peach')" 385 | 386 | 387 | def test_subclass(): 388 | # Test that a new query test method in a custom subclass is properly usable 389 | class MyQueryClass(Query): 390 | def equal_double(self, rhs): 391 | return self._generate_test( 392 | lambda value: value == rhs * 2, 393 | ('equal_double', self._path, rhs) 394 | ) 395 | 396 | query = MyQueryClass().val.equal_double('42') 397 | 398 | assert query({'val': '4242'}) 399 | assert not query({'val': '42'}) 400 | assert not query({'': None}) 401 | assert hash(query) 402 | 403 | 404 | def test_noop(): 405 | query = Query().noop() 406 | 407 | assert query({'foo': True}) 408 | assert query({'foo': None}) 409 | assert query({}) 410 | 411 | 412 | def test_equality(): 413 | q = Query() 414 | assert (q.foo == 2) != 0 415 | assert (q.foo == 'yes') != '' 416 | 417 | 418 | def test_empty_query_error(): 419 | with pytest.raises(RuntimeError, match='Empty query was evaluated'): 420 | Query()({}) 421 | 422 | 423 | def test_fragment(): 424 | query = Query().fragment({'a': 4, 'b': True}) 425 | 426 | assert query({'a': 4, 'b': True, 'c': 'yes'}) 427 | assert not query({'a': 4, 'c': 'yes'}) 428 | assert not query({'b': True, 'c': 'yes'}) 429 | assert not query({'a': 5, 'b': True, 'c': 'yes'}) 430 | assert not query({'a': 4, 'b': 'no', 'c': 'yes'}) 431 | 432 | 433 | def test_fragment_with_path(): 434 | query = Query().doc.fragment({'a': 4, 'b': True}) 435 | 436 | assert query({'doc': {'a': 4, 'b': True, 'c': 'yes'}}) 437 | assert not query({'a': 4, 'b': True, 'c': 'yes'}) 438 | assert not query({'doc': {'a': 4, 'c': 'yes'}}) 439 | 440 | 441 | def test_get_item(): 442 | query = Query()['test'] == 1 443 | 444 | assert query({'test': 1}) 445 | assert not query({'test': 0}) 446 | -------------------------------------------------------------------------------- /tests/test_storages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | import tempfile 5 | 6 | import pytest 7 | 8 | from tinydb import TinyDB, where 9 | from tinydb.storages import JSONStorage, MemoryStorage, Storage, touch 10 | from tinydb.table import Document 11 | 12 | random.seed() 13 | 14 | doc = {'none': [None, None], 'int': 42, 'float': 3.1415899999999999, 15 | 'list': ['LITE', 'RES_ACID', 'SUS_DEXT'], 16 | 'dict': {'hp': 13, 'sp': 5}, 17 | 'bool': [True, False, True, False]} 18 | 19 | 20 | def test_json(tmpdir): 21 | # Write contents 22 | path = str(tmpdir.join('test.db')) 23 | storage = JSONStorage(path) 24 | storage.write(doc) 25 | 26 | # Verify contents 27 | assert doc == storage.read() 28 | storage.close() 29 | 30 | 31 | def test_json_kwargs(tmpdir): 32 | db_file = tmpdir.join('test.db') 33 | db = TinyDB(str(db_file), sort_keys=True, indent=4, separators=(',', ': ')) 34 | 35 | # Write contents 36 | db.insert({'b': 1}) 37 | db.insert({'a': 1}) 38 | 39 | assert db_file.read() == '''{ 40 | "_default": { 41 | "1": { 42 | "b": 1 43 | }, 44 | "2": { 45 | "a": 1 46 | } 47 | } 48 | }''' 49 | db.close() 50 | 51 | 52 | def test_json_readwrite(tmpdir): 53 | """ 54 | Regression test for issue #1 55 | """ 56 | path = str(tmpdir.join('test.db')) 57 | 58 | # Create TinyDB instance 59 | db = TinyDB(path, storage=JSONStorage) 60 | 61 | item = {'name': 'A very long entry'} 62 | item2 = {'name': 'A short one'} 63 | 64 | def get(s): 65 | return db.get(where('name') == s) 66 | 67 | db.insert(item) 68 | assert get('A very long entry') == item 69 | 70 | db.remove(where('name') == 'A very long entry') 71 | assert get('A very long entry') is None 72 | 73 | db.insert(item2) 74 | assert get('A short one') == item2 75 | 76 | db.remove(where('name') == 'A short one') 77 | assert get('A short one') is None 78 | 79 | db.close() 80 | 81 | 82 | def test_json_read(tmpdir): 83 | r"""Open a database only for reading""" 84 | path = str(tmpdir.join('test.db')) 85 | with pytest.raises(FileNotFoundError): 86 | db = TinyDB(path, storage=JSONStorage, access_mode='r') 87 | # Create small database 88 | db = TinyDB(path, storage=JSONStorage) 89 | db.insert({'b': 1}) 90 | db.insert({'a': 1}) 91 | db.close() 92 | # Access in read mode 93 | db = TinyDB(path, storage=JSONStorage, access_mode='r') 94 | assert db.get(where('a') == 1) == {'a': 1} # reading is fine 95 | with pytest.raises(IOError): 96 | db.insert({'c': 1}) # writing is not 97 | db.close() 98 | 99 | 100 | def test_create_dirs(): 101 | temp_dir = tempfile.gettempdir() 102 | 103 | while True: 104 | dname = os.path.join(temp_dir, str(random.getrandbits(20))) 105 | if not os.path.exists(dname): 106 | db_dir = dname 107 | db_file = os.path.join(db_dir, 'db.json') 108 | break 109 | 110 | with pytest.raises(IOError): 111 | JSONStorage(db_file) 112 | 113 | JSONStorage(db_file, create_dirs=True).close() 114 | assert os.path.exists(db_file) 115 | 116 | # Use create_dirs with already existing directory 117 | JSONStorage(db_file, create_dirs=True).close() 118 | assert os.path.exists(db_file) 119 | 120 | os.remove(db_file) 121 | os.rmdir(db_dir) 122 | 123 | 124 | def test_json_invalid_directory(): 125 | with pytest.raises(IOError): 126 | with TinyDB('/this/is/an/invalid/path/db.json', storage=JSONStorage): 127 | pass 128 | 129 | 130 | def test_in_memory(): 131 | # Write contents 132 | storage = MemoryStorage() 133 | storage.write(doc) 134 | 135 | # Verify contents 136 | assert doc == storage.read() 137 | 138 | # Test case for #21 139 | other = MemoryStorage() 140 | other.write({}) 141 | assert other.read() != storage.read() 142 | 143 | 144 | def test_in_memory_close(): 145 | with TinyDB(storage=MemoryStorage) as db: 146 | db.insert({}) 147 | 148 | 149 | def test_custom(): 150 | # noinspection PyAbstractClass 151 | class MyStorage(Storage): 152 | pass 153 | 154 | with pytest.raises(TypeError): 155 | MyStorage() 156 | 157 | 158 | def test_read_once(): 159 | count = 0 160 | 161 | # noinspection PyAbstractClass 162 | class MyStorage(Storage): 163 | def __init__(self): 164 | self.memory = None 165 | 166 | def read(self): 167 | nonlocal count 168 | count += 1 169 | 170 | return self.memory 171 | 172 | def write(self, data): 173 | self.memory = data 174 | 175 | with TinyDB(storage=MyStorage) as db: 176 | assert count == 0 177 | 178 | db.table(db.default_table_name) 179 | 180 | assert count == 0 181 | 182 | db.all() 183 | 184 | assert count == 1 185 | 186 | db.insert({'foo': 'bar'}) 187 | 188 | assert count == 3 # One for getting the next ID, one for the insert 189 | 190 | db.all() 191 | 192 | assert count == 4 193 | 194 | 195 | def test_custom_with_exception(): 196 | class MyStorage(Storage): 197 | def read(self): 198 | pass 199 | 200 | def write(self, data): 201 | pass 202 | 203 | def __init__(self): 204 | raise ValueError() 205 | 206 | def close(self): 207 | raise RuntimeError() 208 | 209 | with pytest.raises(ValueError): 210 | with TinyDB(storage=MyStorage) as db: 211 | pass 212 | 213 | 214 | def test_yaml(tmpdir): 215 | """ 216 | :type tmpdir: py._path.local.LocalPath 217 | """ 218 | 219 | try: 220 | import yaml 221 | except ImportError: 222 | return pytest.skip('PyYAML not installed') 223 | 224 | def represent_doc(dumper, data): 225 | # Represent `Document` objects as their dict's string representation 226 | # which PyYAML understands 227 | return dumper.represent_data(dict(data)) 228 | 229 | yaml.add_representer(Document, represent_doc) 230 | 231 | class YAMLStorage(Storage): 232 | def __init__(self, filename): 233 | self.filename = filename 234 | touch(filename, False) 235 | 236 | def read(self): 237 | with open(self.filename) as handle: 238 | data = yaml.safe_load(handle.read()) 239 | return data 240 | 241 | def write(self, data): 242 | with open(self.filename, 'w') as handle: 243 | yaml.dump(data, handle) 244 | 245 | def close(self): 246 | pass 247 | 248 | # Write contents 249 | path = str(tmpdir.join('test.db')) 250 | db = TinyDB(path, storage=YAMLStorage) 251 | db.insert(doc) 252 | assert db.all() == [doc] 253 | 254 | db.update({'name': 'foo'}) 255 | 256 | assert '!' not in tmpdir.join('test.db').read() 257 | 258 | assert db.contains(where('name') == 'foo') 259 | assert len(db) == 1 260 | 261 | 262 | def test_encoding(tmpdir): 263 | japanese_doc = {"Test": u"こんにちは世界"} 264 | 265 | path = str(tmpdir.join('test.db')) 266 | # cp936 is used for japanese encodings 267 | jap_storage = JSONStorage(path, encoding="cp936") 268 | jap_storage.write(japanese_doc) 269 | 270 | try: 271 | exception = json.decoder.JSONDecodeError 272 | except AttributeError: 273 | exception = ValueError 274 | 275 | with pytest.raises(exception): 276 | # cp037 is used for english encodings 277 | eng_storage = JSONStorage(path, encoding="cp037") 278 | eng_storage.read() 279 | 280 | jap_storage = JSONStorage(path, encoding="cp936") 281 | assert japanese_doc == jap_storage.read() 282 | -------------------------------------------------------------------------------- /tests/test_tables.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | from tinydb import where 6 | 7 | 8 | def test_next_id(db): 9 | db.truncate() 10 | 11 | assert db._get_next_id() == 1 12 | assert db._get_next_id() == 2 13 | assert db._get_next_id() == 3 14 | 15 | 16 | def test_tables_list(db): 17 | db.table('table1').insert({'a': 1}) 18 | db.table('table2').insert({'a': 1}) 19 | 20 | assert db.tables() == {'_default', 'table1', 'table2'} 21 | 22 | 23 | def test_one_table(db): 24 | table1 = db.table('table1') 25 | 26 | table1.insert_multiple({'int': 1, 'char': c} for c in 'abc') 27 | 28 | assert table1.get(where('int') == 1)['char'] == 'a' 29 | assert table1.get(where('char') == 'b')['char'] == 'b' 30 | 31 | 32 | def test_multiple_tables(db): 33 | table1 = db.table('table1') 34 | table2 = db.table('table2') 35 | table3 = db.table('table3') 36 | 37 | table1.insert({'int': 1, 'char': 'a'}) 38 | table2.insert({'int': 1, 'char': 'b'}) 39 | table3.insert({'int': 1, 'char': 'c'}) 40 | 41 | assert table1.count(where('char') == 'a') == 1 42 | assert table2.count(where('char') == 'b') == 1 43 | assert table3.count(where('char') == 'c') == 1 44 | 45 | db.drop_tables() 46 | 47 | assert len(table1) == 0 48 | assert len(table2) == 0 49 | assert len(table3) == 0 50 | 51 | 52 | def test_caching(db): 53 | table1 = db.table('table1') 54 | table2 = db.table('table1') 55 | 56 | assert table1 is table2 57 | 58 | 59 | def test_query_cache(db): 60 | query1 = where('int') == 1 61 | 62 | assert db.count(query1) == 3 63 | assert query1 in db._query_cache 64 | 65 | assert db.count(query1) == 3 66 | assert query1 in db._query_cache 67 | 68 | query2 = where('int') == 0 69 | 70 | assert db.count(query2) == 0 71 | assert query2 in db._query_cache 72 | 73 | assert db.count(query2) == 0 74 | assert query2 in db._query_cache 75 | 76 | 77 | def test_query_cache_with_mutable_callable(db): 78 | table = db.table('table') 79 | table.insert({'val': 5}) 80 | 81 | mutable = 5 82 | increase = lambda x: x + mutable 83 | 84 | assert where('val').is_cacheable() 85 | assert not where('val').map(increase).is_cacheable() 86 | assert not (where('val').map(increase) == 10).is_cacheable() 87 | 88 | search = where('val').map(increase) == 10 89 | assert table.count(search) == 1 90 | 91 | # now `increase` would yield 15, not 10 92 | mutable = 10 93 | 94 | assert table.count(search) == 0 95 | assert len(table._query_cache) == 0 96 | 97 | 98 | def test_zero_cache_size(db): 99 | table = db.table('table3', cache_size=0) 100 | query = where('int') == 1 101 | 102 | table.insert({'int': 1}) 103 | table.insert({'int': 1}) 104 | 105 | assert table.count(query) == 2 106 | assert table.count(where('int') == 2) == 0 107 | assert len(table._query_cache) == 0 108 | 109 | 110 | def test_query_cache_size(db): 111 | table = db.table('table3', cache_size=1) 112 | query = where('int') == 1 113 | 114 | table.insert({'int': 1}) 115 | table.insert({'int': 1}) 116 | 117 | assert table.count(query) == 2 118 | assert table.count(where('int') == 2) == 0 119 | assert len(table._query_cache) == 1 120 | 121 | 122 | def test_lru_cache(db): 123 | # Test integration into TinyDB 124 | table = db.table('table3', cache_size=2) 125 | query = where('int') == 1 126 | 127 | table.search(query) 128 | table.search(where('int') == 2) 129 | table.search(where('int') == 3) 130 | assert query not in table._query_cache 131 | 132 | table.remove(where('int') == 1) 133 | assert not table._query_cache.lru 134 | 135 | table.search(query) 136 | 137 | assert len(table._query_cache) == 1 138 | table.clear_cache() 139 | assert len(table._query_cache) == 0 140 | 141 | 142 | def test_table_is_iterable(db): 143 | table = db.table('table1') 144 | 145 | table.insert_multiple({'int': i} for i in range(3)) 146 | 147 | assert [r for r in table] == table.all() 148 | 149 | 150 | def test_table_name(db): 151 | name = 'table3' 152 | table = db.table(name) 153 | assert name == table.name 154 | 155 | with pytest.raises(AttributeError): 156 | table.name = 'foo' 157 | 158 | 159 | def test_table_repr(db): 160 | name = 'table4' 161 | table = db.table(name) 162 | 163 | assert re.match( 164 | r">", 166 | repr(table)) 167 | 168 | 169 | def test_truncate_table(db): 170 | db.truncate() 171 | assert db._get_next_id() == 1 172 | 173 | 174 | def test_persist_table(db): 175 | db.table("persisted", persist_empty=True) 176 | assert "persisted" in db.tables() 177 | 178 | db.table("nonpersisted", persist_empty=False) 179 | assert "nonpersisted" not in db.tables() 180 | -------------------------------------------------------------------------------- /tests/test_tinydb.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections.abc import Mapping 3 | 4 | import pytest 5 | 6 | from tinydb import TinyDB, where, Query 7 | from tinydb.middlewares import Middleware, CachingMiddleware 8 | from tinydb.storages import MemoryStorage, JSONStorage 9 | from tinydb.table import Document 10 | 11 | 12 | def test_drop_tables(db: TinyDB): 13 | db.drop_tables() 14 | 15 | db.insert({}) 16 | db.drop_tables() 17 | 18 | assert len(db) == 0 19 | 20 | 21 | def test_all(db: TinyDB): 22 | db.drop_tables() 23 | 24 | for i in range(10): 25 | db.insert({}) 26 | 27 | assert len(db.all()) == 10 28 | 29 | 30 | def test_insert(db: TinyDB): 31 | db.drop_tables() 32 | db.insert({'int': 1, 'char': 'a'}) 33 | 34 | assert db.count(where('int') == 1) == 1 35 | 36 | db.drop_tables() 37 | 38 | db.insert({'int': 1, 'char': 'a'}) 39 | db.insert({'int': 1, 'char': 'b'}) 40 | db.insert({'int': 1, 'char': 'c'}) 41 | 42 | assert db.count(where('int') == 1) == 3 43 | assert db.count(where('char') == 'a') == 1 44 | 45 | 46 | def test_insert_ids(db: TinyDB): 47 | db.drop_tables() 48 | assert db.insert({'int': 1, 'char': 'a'}) == 1 49 | assert db.insert({'int': 1, 'char': 'a'}) == 2 50 | 51 | 52 | def test_insert_with_doc_id(db: TinyDB): 53 | db.drop_tables() 54 | assert db.insert({'int': 1, 'char': 'a'}) == 1 55 | assert db.insert(Document({'int': 1, 'char': 'a'}, 12)) == 12 56 | assert db.insert(Document({'int': 1, 'char': 'a'}, 77)) == 77 57 | assert db.insert({'int': 1, 'char': 'a'}) == 78 58 | 59 | 60 | def test_insert_with_duplicate_doc_id(db: TinyDB): 61 | db.drop_tables() 62 | assert db.insert({'int': 1, 'char': 'a'}) == 1 63 | 64 | with pytest.raises(ValueError): 65 | db.insert(Document({'int': 1, 'char': 'a'}, 1)) 66 | 67 | 68 | def test_insert_multiple(db: TinyDB): 69 | db.drop_tables() 70 | assert not db.contains(where('int') == 1) 71 | 72 | # Insert multiple from list 73 | db.insert_multiple([{'int': 1, 'char': 'a'}, 74 | {'int': 1, 'char': 'b'}, 75 | {'int': 1, 'char': 'c'}]) 76 | 77 | assert db.count(where('int') == 1) == 3 78 | assert db.count(where('char') == 'a') == 1 79 | 80 | # Insert multiple from generator function 81 | def generator(): 82 | for j in range(10): 83 | yield {'int': j} 84 | 85 | db.drop_tables() 86 | 87 | db.insert_multiple(generator()) 88 | 89 | for i in range(10): 90 | assert db.count(where('int') == i) == 1 91 | assert db.count(where('int').exists()) == 10 92 | 93 | # Insert multiple from inline generator 94 | db.drop_tables() 95 | 96 | db.insert_multiple({'int': i} for i in range(10)) 97 | 98 | for i in range(10): 99 | assert db.count(where('int') == i) == 1 100 | 101 | 102 | def test_insert_multiple_with_ids(db: TinyDB): 103 | db.drop_tables() 104 | 105 | # Insert multiple from list 106 | assert db.insert_multiple([{'int': 1, 'char': 'a'}, 107 | {'int': 1, 'char': 'b'}, 108 | {'int': 1, 'char': 'c'}]) == [1, 2, 3] 109 | 110 | 111 | def test_insert_multiple_with_doc_ids(db: TinyDB): 112 | db.drop_tables() 113 | 114 | assert db.insert_multiple([ 115 | Document({'int': 1, 'char': 'a'}, 12), 116 | Document({'int': 1, 'char': 'b'}, 77) 117 | ]) == [12, 77] 118 | assert db.get(doc_id=12) == {'int': 1, 'char': 'a'} 119 | assert db.get(doc_id=77) == {'int': 1, 'char': 'b'} 120 | 121 | with pytest.raises(ValueError): 122 | db.insert_multiple([Document({'int': 1, 'char': 'a'}, 12)]) 123 | 124 | 125 | def test_insert_invalid_type_raises_error(db: TinyDB): 126 | with pytest.raises(ValueError, match='Document is not a Mapping'): 127 | # object() as an example of a non-mapping-type 128 | db.insert(object()) # type: ignore 129 | 130 | 131 | def test_insert_valid_mapping_type(db: TinyDB): 132 | class CustomDocument(Mapping): 133 | def __init__(self, data): 134 | self.data = data 135 | 136 | def __getitem__(self, key): 137 | return self.data[key] 138 | 139 | def __iter__(self): 140 | return iter(self.data) 141 | 142 | def __len__(self): 143 | return len(self.data) 144 | 145 | db.drop_tables() 146 | db.insert(CustomDocument({'int': 1, 'char': 'a'})) 147 | assert db.count(where('int') == 1) == 1 148 | 149 | 150 | def test_custom_mapping_type_with_json(tmpdir): 151 | class CustomDocument(Mapping): 152 | def __init__(self, data): 153 | self.data = data 154 | 155 | def __getitem__(self, key): 156 | return self.data[key] 157 | 158 | def __iter__(self): 159 | return iter(self.data) 160 | 161 | def __len__(self): 162 | return len(self.data) 163 | 164 | # Insert 165 | db = TinyDB(str(tmpdir.join('test.db'))) 166 | db.drop_tables() 167 | db.insert(CustomDocument({'int': 1, 'char': 'a'})) 168 | assert db.count(where('int') == 1) == 1 169 | 170 | # Insert multiple 171 | db.insert_multiple([ 172 | CustomDocument({'int': 2, 'char': 'a'}), 173 | CustomDocument({'int': 3, 'char': 'a'}) 174 | ]) 175 | assert db.count(where('int') == 1) == 1 176 | assert db.count(where('int') == 2) == 1 177 | assert db.count(where('int') == 3) == 1 178 | 179 | # Write back 180 | doc_id = db.get(where('int') == 3).doc_id 181 | db.update(CustomDocument({'int': 4, 'char': 'a'}), doc_ids=[doc_id]) 182 | assert db.count(where('int') == 3) == 0 183 | assert db.count(where('int') == 4) == 1 184 | 185 | 186 | def test_remove(db: TinyDB): 187 | db.remove(where('char') == 'b') 188 | 189 | assert len(db) == 2 190 | assert db.count(where('int') == 1) == 2 191 | 192 | 193 | def test_remove_all_fails(db: TinyDB): 194 | with pytest.raises(RuntimeError): 195 | db.remove() 196 | 197 | 198 | def test_remove_multiple(db: TinyDB): 199 | db.remove(where('int') == 1) 200 | 201 | assert len(db) == 0 202 | 203 | 204 | def test_remove_ids(db: TinyDB): 205 | db.remove(doc_ids=[1, 2]) 206 | 207 | assert len(db) == 1 208 | 209 | 210 | def test_remove_returns_ids(db: TinyDB): 211 | assert db.remove(where('char') == 'b') == [2] 212 | 213 | 214 | def test_update(db: TinyDB): 215 | assert len(db) == 3 216 | 217 | db.update({'int': 2}, where('char') == 'a') 218 | 219 | assert db.count(where('int') == 2) == 1 220 | assert db.count(where('int') == 1) == 2 221 | 222 | 223 | def test_update_all(db: TinyDB): 224 | assert db.count(where('int') == 1) == 3 225 | 226 | db.update({'newField': True}) 227 | 228 | assert db.count(where('newField') == True) == 3 # noqa 229 | 230 | 231 | def test_update_returns_ids(db: TinyDB): 232 | db.drop_tables() 233 | assert db.insert({'int': 1, 'char': 'a'}) == 1 234 | assert db.insert({'int': 1, 'char': 'a'}) == 2 235 | 236 | assert db.update({'char': 'b'}, where('int') == 1) == [1, 2] 237 | 238 | 239 | def test_update_transform(db: TinyDB): 240 | def increment(field): 241 | def transform(el): 242 | el[field] += 1 243 | 244 | return transform 245 | 246 | def delete(field): 247 | def transform(el): 248 | del el[field] 249 | 250 | return transform 251 | 252 | assert db.count(where('int') == 1) == 3 253 | 254 | db.update(increment('int'), where('char') == 'a') 255 | db.update(delete('char'), where('char') == 'a') 256 | 257 | assert db.count(where('int') == 2) == 1 258 | assert db.count(where('char') == 'a') == 0 259 | assert db.count(where('int') == 1) == 2 260 | 261 | 262 | def test_update_ids(db: TinyDB): 263 | db.update({'int': 2}, doc_ids=[1, 2]) 264 | 265 | assert db.count(where('int') == 2) == 2 266 | 267 | 268 | def test_update_multiple(db: TinyDB): 269 | assert len(db) == 3 270 | 271 | db.update_multiple([ 272 | ({'int': 2}, where('char') == 'a'), 273 | ({'int': 4}, where('char') == 'b'), 274 | ]) 275 | 276 | assert db.count(where('int') == 1) == 1 277 | assert db.count(where('int') == 2) == 1 278 | assert db.count(where('int') == 4) == 1 279 | 280 | 281 | def test_update_multiple_operation(db: TinyDB): 282 | def increment(field): 283 | def transform(el): 284 | el[field] += 1 285 | 286 | return transform 287 | 288 | assert db.count(where('int') == 1) == 3 289 | 290 | db.update_multiple([ 291 | (increment('int'), where('char') == 'a'), 292 | (increment('int'), where('char') == 'b') 293 | ]) 294 | 295 | assert db.count(where('int') == 2) == 2 296 | 297 | 298 | def test_upsert(db: TinyDB): 299 | assert len(db) == 3 300 | 301 | # Document existing 302 | db.upsert({'int': 5}, where('char') == 'a') 303 | assert db.count(where('int') == 5) == 1 304 | 305 | # Document missing 306 | assert db.upsert({'int': 9, 'char': 'x'}, where('char') == 'x') == [4] 307 | assert db.count(where('int') == 9) == 1 308 | 309 | 310 | def test_upsert_by_id(db: TinyDB): 311 | assert len(db) == 3 312 | 313 | # Single document existing 314 | extant_doc = Document({'char': 'v'}, doc_id=1) 315 | assert db.upsert(extant_doc) == [1] 316 | doc = db.get(where('char') == 'v') 317 | assert isinstance(doc, Document) 318 | assert doc is not None 319 | assert doc.doc_id == 1 320 | assert len(db) == 3 321 | 322 | # Single document missing 323 | missing_doc = Document({'int': 5, 'char': 'w'}, doc_id=5) 324 | assert db.upsert(missing_doc) == [5] 325 | doc = db.get(where('char') == 'w') 326 | assert isinstance(doc, Document) 327 | assert doc is not None 328 | assert doc.doc_id == 5 329 | assert len(db) == 4 330 | 331 | # Missing doc_id and condition 332 | with pytest.raises(ValueError, match=r"(?=.*\bdoc_id\b)(?=.*\bquery\b)"): 333 | db.upsert({'no_Document': 'no_query'}) 334 | 335 | # Make sure we didn't break anything 336 | assert db.insert({'check': '_next_id'}) == 6 337 | 338 | 339 | def test_search(db: TinyDB): 340 | assert not db._query_cache 341 | assert len(db.search(where('int') == 1)) == 3 342 | 343 | assert len(db._query_cache) == 1 344 | assert len(db.search(where('int') == 1)) == 3 # Query result from cache 345 | 346 | 347 | def test_search_path(db: TinyDB): 348 | assert not db._query_cache 349 | assert len(db.search(where('int').exists())) == 3 350 | assert len(db._query_cache) == 1 351 | 352 | assert len(db.search(where('asd').exists())) == 0 353 | assert len(db.search(where('int').exists())) == 3 # Query result from cache 354 | 355 | 356 | def test_search_no_results_cache(db: TinyDB): 357 | assert len(db.search(where('missing').exists())) == 0 358 | assert len(db.search(where('missing').exists())) == 0 359 | 360 | 361 | def test_get(db: TinyDB): 362 | item = db.get(where('char') == 'b') 363 | assert isinstance(item, Document) 364 | assert item is not None 365 | assert item['char'] == 'b' 366 | 367 | 368 | def test_get_ids(db: TinyDB): 369 | el = db.all()[0] 370 | assert db.get(doc_id=el.doc_id) == el 371 | assert db.get(doc_id=float('NaN')) is None # type: ignore 372 | 373 | 374 | def test_get_multiple_ids(db: TinyDB): 375 | el = db.all() 376 | assert db.get(doc_ids=[x.doc_id for x in el]) == el 377 | 378 | 379 | def test_get_invalid(db: TinyDB): 380 | with pytest.raises(RuntimeError): 381 | db.get() 382 | 383 | 384 | def test_count(db: TinyDB): 385 | assert db.count(where('int') == 1) == 3 386 | assert db.count(where('char') == 'd') == 0 387 | 388 | 389 | def test_contains(db: TinyDB): 390 | assert db.contains(where('int') == 1) 391 | assert not db.contains(where('int') == 0) 392 | 393 | 394 | def test_contains_ids(db: TinyDB): 395 | assert db.contains(doc_id=1) 396 | assert db.contains(doc_id=2) 397 | assert not db.contains(doc_id=88) 398 | 399 | 400 | def test_contains_invalid(db: TinyDB): 401 | with pytest.raises(RuntimeError): 402 | db.contains() 403 | 404 | 405 | def test_get_idempotent(db: TinyDB): 406 | u = db.get(where('int') == 1) 407 | z = db.get(where('int') == 1) 408 | assert u == z 409 | 410 | 411 | def test_multiple_dbs(): 412 | """ 413 | Regression test for issue #3 414 | """ 415 | db1 = TinyDB(storage=MemoryStorage) 416 | db2 = TinyDB(storage=MemoryStorage) 417 | 418 | db1.insert({'int': 1, 'char': 'a'}) 419 | db1.insert({'int': 1, 'char': 'b'}) 420 | db1.insert({'int': 1, 'value': 5.0}) 421 | 422 | db2.insert({'color': 'blue', 'animal': 'turtle'}) 423 | 424 | assert len(db1) == 3 425 | assert len(db2) == 1 426 | 427 | 428 | def test_storage_closed_once(): 429 | class Storage: 430 | def __init__(self): 431 | self.closed = False 432 | 433 | def read(self): 434 | return {} 435 | 436 | def write(self, data): 437 | pass 438 | 439 | def close(self): 440 | assert not self.closed 441 | self.closed = True 442 | 443 | with TinyDB(storage=Storage) as db: 444 | db.close() 445 | 446 | del db 447 | # If db.close() is called during cleanup, the assertion will fail and throw 448 | # and exception 449 | 450 | 451 | def test_unique_ids(tmpdir): 452 | """ 453 | :type tmpdir: py._path.local.LocalPath 454 | """ 455 | path = str(tmpdir.join('db.json')) 456 | 457 | # Verify ids are unique when reopening the DB and inserting 458 | with TinyDB(path) as _db: 459 | _db.insert({'x': 1}) 460 | 461 | with TinyDB(path) as _db: 462 | _db.insert({'x': 1}) 463 | 464 | with TinyDB(path) as _db: 465 | data = _db.all() 466 | 467 | assert data[0].doc_id != data[1].doc_id 468 | 469 | # Verify ids stay unique when inserting/removing 470 | with TinyDB(path) as _db: 471 | _db.drop_tables() 472 | _db.insert_multiple({'x': i} for i in range(5)) 473 | _db.remove(where('x') == 2) 474 | 475 | assert len(_db) == 4 476 | 477 | ids = [e.doc_id for e in _db.all()] 478 | assert len(ids) == len(set(ids)) 479 | 480 | 481 | def test_lastid_after_open(tmpdir): 482 | """ 483 | Regression test for issue #34 484 | 485 | :type tmpdir: py._path.local.LocalPath 486 | """ 487 | 488 | NUM = 100 489 | path = str(tmpdir.join('db.json')) 490 | 491 | with TinyDB(path) as _db: 492 | _db.insert_multiple({'i': i} for i in range(NUM)) 493 | 494 | with TinyDB(path) as _db: 495 | assert _db._get_next_id() - 1 == NUM 496 | 497 | 498 | def test_doc_ids_json(tmpdir): 499 | """ 500 | Regression test for issue #45 501 | """ 502 | 503 | path = str(tmpdir.join('db.json')) 504 | 505 | with TinyDB(path) as _db: 506 | _db.drop_tables() 507 | assert _db.insert({'int': 1, 'char': 'a'}) == 1 508 | assert _db.insert({'int': 1, 'char': 'a'}) == 2 509 | 510 | _db.drop_tables() 511 | assert _db.insert_multiple([{'int': 1, 'char': 'a'}, 512 | {'int': 1, 'char': 'b'}, 513 | {'int': 1, 'char': 'c'}]) == [1, 2, 3] 514 | 515 | assert _db.contains(doc_id=1) 516 | assert _db.contains(doc_id=2) 517 | assert not _db.contains(doc_id=88) 518 | 519 | _db.update({'int': 2}, doc_ids=[1, 2]) 520 | assert _db.count(where('int') == 2) == 2 521 | 522 | el = _db.all()[0] 523 | assert _db.get(doc_id=el.doc_id) == el 524 | assert _db.get(doc_id=float('NaN')) is None 525 | 526 | _db.remove(doc_ids=[1, 2]) 527 | assert len(_db) == 1 528 | 529 | 530 | def test_insert_string(tmpdir): 531 | path = str(tmpdir.join('db.json')) 532 | 533 | with TinyDB(path) as _db: 534 | data = [{'int': 1}, {'int': 2}] 535 | _db.insert_multiple(data) 536 | 537 | with pytest.raises(ValueError): 538 | _db.insert([1, 2, 3]) # Fails 539 | 540 | with pytest.raises(ValueError): 541 | _db.insert({'bark'}) # Fails 542 | 543 | assert data == _db.all() 544 | 545 | _db.insert({'int': 3}) # Does not fail 546 | 547 | 548 | def test_insert_invalid_dict(tmpdir): 549 | path = str(tmpdir.join('db.json')) 550 | 551 | with TinyDB(path) as _db: 552 | data = [{'int': 1}, {'int': 2}] 553 | _db.insert_multiple(data) 554 | 555 | with pytest.raises(TypeError): 556 | _db.insert({'int': _db}) # Fails 557 | 558 | assert data == _db.all() 559 | 560 | _db.insert({'int': 3}) # Does not fail 561 | 562 | 563 | def test_gc(tmpdir): 564 | # See https://github.com/msiemens/tinydb/issues/92 565 | path = str(tmpdir.join('db.json')) 566 | db = TinyDB(path) 567 | table = db.table('foo') 568 | table.insert({'something': 'else'}) 569 | table.insert({'int': 13}) 570 | assert len(table.search(where('int') == 13)) == 1 571 | assert table.all() == [{'something': 'else'}, {'int': 13}] 572 | db.close() 573 | 574 | 575 | def test_drop_table(): 576 | db = TinyDB(storage=MemoryStorage) 577 | default_table_name = db.table(db.default_table_name).name 578 | 579 | assert [] == list(db.tables()) 580 | db.drop_table(default_table_name) 581 | 582 | db.insert({'a': 1}) 583 | assert [default_table_name] == list(db.tables()) 584 | 585 | db.drop_table(default_table_name) 586 | assert [] == list(db.tables()) 587 | 588 | table_name = 'some-other-table' 589 | db = TinyDB(storage=MemoryStorage) 590 | db.table(table_name).insert({'a': 1}) 591 | assert {table_name} == db.tables() 592 | 593 | db.drop_table(table_name) 594 | assert set() == db.tables() 595 | assert table_name not in db._tables 596 | 597 | db.drop_table('non-existent-table-name') 598 | assert set() == db.tables() 599 | 600 | 601 | def test_empty_write(tmpdir): 602 | path = str(tmpdir.join('db.json')) 603 | 604 | class ReadOnlyMiddleware(Middleware): 605 | def write(self, data): 606 | raise AssertionError('No write for unchanged db') 607 | 608 | TinyDB(path).close() 609 | TinyDB(path, storage=ReadOnlyMiddleware(JSONStorage)).close() 610 | 611 | 612 | def test_query_cache(): 613 | db = TinyDB(storage=MemoryStorage) 614 | db.insert_multiple([ 615 | {'name': 'foo', 'value': 42}, 616 | {'name': 'bar', 'value': -1337} 617 | ]) 618 | 619 | query = where('value') > 0 620 | 621 | results = db.search(query) 622 | assert len(results) == 1 623 | 624 | # Modify the db instance to not return any results when 625 | # bypassing the query cache 626 | db._tables[db.table(db.default_table_name).name]._read_table = lambda: {} 627 | 628 | # Make sure we got an independent copy of the result list 629 | results.extend([1]) 630 | assert db.search(query) == [{'name': 'foo', 'value': 42}] 631 | 632 | 633 | def test_tinydb_is_iterable(db: TinyDB): 634 | assert [r for r in db] == db.all() 635 | 636 | 637 | def test_repr(tmpdir): 638 | path = str(tmpdir.join('db.json')) 639 | 640 | db = TinyDB(path) 641 | db.insert({'a': 1}) 642 | 643 | assert re.match( 644 | r"", 649 | repr(db)) 650 | 651 | 652 | def test_delete(tmpdir): 653 | path = str(tmpdir.join('db.json')) 654 | 655 | db = TinyDB(path, ensure_ascii=False) 656 | q = Query() 657 | db.insert({'network': {'id': '114', 'name': 'ok', 'rpc': 'dac', 658 | 'ticker': 'mkay'}}) 659 | assert db.search(q.network.id == '114') == [ 660 | {'network': {'id': '114', 'name': 'ok', 'rpc': 'dac', 661 | 'ticker': 'mkay'}} 662 | ] 663 | db.remove(q.network.id == '114') 664 | assert db.search(q.network.id == '114') == [] 665 | 666 | 667 | def test_insert_multiple_with_single_dict(db: TinyDB): 668 | with pytest.raises(ValueError): 669 | d = {'first': 'John', 'last': 'smith'} 670 | db.insert_multiple(d) # type: ignore 671 | db.close() 672 | 673 | 674 | def test_access_storage(): 675 | assert isinstance(TinyDB(storage=MemoryStorage).storage, 676 | MemoryStorage) 677 | assert isinstance(TinyDB(storage=CachingMiddleware(MemoryStorage)).storage, 678 | CachingMiddleware) 679 | 680 | 681 | def test_empty_db_len(): 682 | db = TinyDB(storage=MemoryStorage) 683 | assert len(db) == 0 684 | 685 | 686 | def test_insert_on_existing_db(tmpdir): 687 | path = str(tmpdir.join('db.json')) 688 | 689 | db = TinyDB(path, ensure_ascii=False) 690 | db.insert({'foo': 'bar'}) 691 | 692 | assert len(db) == 1 693 | 694 | db.close() 695 | 696 | db = TinyDB(path, ensure_ascii=False) 697 | db.insert({'foo': 'bar'}) 698 | db.insert({'foo': 'bar'}) 699 | 700 | assert len(db) == 3 701 | 702 | 703 | def test_storage_access(): 704 | db = TinyDB(storage=MemoryStorage) 705 | 706 | assert isinstance(db.storage, MemoryStorage) 707 | 708 | 709 | def test_lambda_query(): 710 | db = TinyDB(storage=MemoryStorage) 711 | db.insert({'foo': 'bar'}) 712 | 713 | query = lambda doc: doc.get('foo') == 'bar' 714 | query.is_cacheable = lambda: False 715 | assert db.search(query) == [{'foo': 'bar'}] 716 | assert not db._query_cache 717 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tinydb.utils import LRUCache, freeze, FrozenDict 4 | 5 | 6 | def test_lru_cache(): 7 | cache = LRUCache(capacity=3) 8 | cache["a"] = 1 9 | cache["b"] = 2 10 | cache["c"] = 3 11 | _ = cache["a"] # move to front in lru queue 12 | cache["d"] = 4 # move oldest item out of lru queue 13 | 14 | try: 15 | _ = cache['f'] 16 | except KeyError: 17 | pass 18 | 19 | assert cache.lru == ["c", "a", "d"] 20 | 21 | 22 | def test_lru_cache_set_multiple(): 23 | cache = LRUCache(capacity=3) 24 | cache["a"] = 1 25 | cache["a"] = 2 26 | cache["a"] = 3 27 | cache["a"] = 4 28 | 29 | assert cache.lru == ["a"] 30 | 31 | 32 | def test_lru_cache_set_update(): 33 | cache = LRUCache(capacity=3) 34 | cache["a"] = 1 35 | cache["a"] = 2 36 | 37 | assert cache["a"] == 2 38 | 39 | 40 | def test_lru_cache_get(): 41 | cache = LRUCache(capacity=3) 42 | cache["a"] = 1 43 | cache["b"] = 1 44 | cache["c"] = 1 45 | cache.get("a") 46 | cache["d"] = 4 47 | 48 | assert cache.lru == ["c", "a", "d"] 49 | 50 | 51 | def test_lru_cache_delete(): 52 | cache = LRUCache(capacity=3) 53 | cache["a"] = 1 54 | cache["b"] = 2 55 | del cache["a"] 56 | 57 | try: 58 | del cache['f'] 59 | except KeyError: 60 | pass 61 | 62 | assert cache.lru == ["b"] 63 | 64 | 65 | def test_lru_cache_clear(): 66 | cache = LRUCache(capacity=3) 67 | cache["a"] = 1 68 | cache["b"] = 2 69 | cache.clear() 70 | 71 | assert cache.lru == [] 72 | 73 | 74 | def test_lru_cache_unlimited(): 75 | cache = LRUCache() 76 | for i in range(100): 77 | cache[i] = i 78 | 79 | assert len(cache.lru) == 100 80 | 81 | 82 | def test_lru_cache_unlimited_explicit(): 83 | cache = LRUCache(capacity=None) 84 | for i in range(100): 85 | cache[i] = i 86 | 87 | assert len(cache.lru) == 100 88 | 89 | 90 | def test_lru_cache_iteration_works(): 91 | cache = LRUCache() 92 | count = 0 93 | for _ in cache: 94 | assert False, 'there should be no elements in the cache' 95 | 96 | assert count == 0 97 | 98 | 99 | def test_freeze(): 100 | frozen = freeze([0, 1, 2, {'a': [1, 2, 3]}, {1, 2}]) 101 | assert isinstance(frozen, tuple) 102 | assert isinstance(frozen[3], FrozenDict) 103 | assert isinstance(frozen[3]['a'], tuple) 104 | assert isinstance(frozen[4], frozenset) 105 | 106 | with pytest.raises(TypeError): 107 | frozen[0] = 10 108 | 109 | with pytest.raises(TypeError): 110 | frozen[3]['a'] = 10 111 | 112 | with pytest.raises(TypeError): 113 | frozen[3].pop('a') 114 | 115 | with pytest.raises(TypeError): 116 | frozen[3].update({'a': 9}) 117 | -------------------------------------------------------------------------------- /tinydb/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TinyDB is a tiny, document oriented database optimized for your happiness :) 3 | 4 | TinyDB stores different types of Python data types using a configurable 5 | storage mechanism. It comes with a syntax for querying data and storing 6 | data in multiple tables. 7 | 8 | .. codeauthor:: Markus Siemens 9 | 10 | Usage example: 11 | 12 | >>> from tinydb import TinyDB, where 13 | >>> from tinydb.storages import MemoryStorage 14 | >>> db = TinyDB(storage=MemoryStorage) 15 | >>> db.insert({'data': 5}) # Insert into '_default' table 16 | >>> db.search(where('data') == 5) 17 | [{'data': 5, '_id': 1}] 18 | >>> # Now let's create a new table 19 | >>> tbl = db.table('our_table') 20 | >>> for i in range(10): 21 | ... tbl.insert({'data': i}) 22 | ... 23 | >>> len(tbl.search(where('data') < 5)) 24 | 5 25 | """ 26 | 27 | from .queries import Query, where 28 | from .storages import Storage, JSONStorage 29 | from .database import TinyDB 30 | from .version import __version__ 31 | 32 | __all__ = ('TinyDB', 'Storage', 'JSONStorage', 'Query', 'where') 33 | -------------------------------------------------------------------------------- /tinydb/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the main component of TinyDB: the database. 3 | """ 4 | from typing import Dict, Iterator, Set, Type 5 | 6 | from . import JSONStorage 7 | from .storages import Storage 8 | from .table import Table, Document 9 | from .utils import with_typehint 10 | 11 | # The table's base class. This is used to add type hinting from the Table 12 | # class to TinyDB. Currently, this supports PyCharm, Pyright/VS Code and MyPy. 13 | TableBase: Type[Table] = with_typehint(Table) 14 | 15 | 16 | class TinyDB(TableBase): 17 | """ 18 | The main class of TinyDB. 19 | 20 | The ``TinyDB`` class is responsible for creating the storage class instance 21 | that will store this database's documents, managing the database 22 | tables as well as providing access to the default table. 23 | 24 | For table management, a simple ``dict`` is used that stores the table class 25 | instances accessible using their table name. 26 | 27 | Default table access is provided by forwarding all unknown method calls 28 | and property access operations to the default table by implementing 29 | ``__getattr__``. 30 | 31 | When creating a new instance, all arguments and keyword arguments (except 32 | for ``storage``) will be passed to the storage class that is provided. If 33 | no storage class is specified, :class:`~tinydb.storages.JSONStorage` will be 34 | used. 35 | 36 | .. admonition:: Customization 37 | 38 | For customization, the following class variables can be set: 39 | 40 | - ``table_class`` defines the class that is used to create tables, 41 | - ``default_table_name`` defines the name of the default table, and 42 | - ``default_storage_class`` will define the class that will be used to 43 | create storage instances if no other storage is passed. 44 | 45 | .. versionadded:: 4.0 46 | 47 | .. admonition:: Data Storage Model 48 | 49 | Data is stored using a storage class that provides persistence for a 50 | ``dict`` instance. This ``dict`` contains all tables and their data. 51 | The data is modelled like this:: 52 | 53 | { 54 | 'table1': { 55 | 0: {document...}, 56 | 1: {document...}, 57 | }, 58 | 'table2': { 59 | ... 60 | } 61 | } 62 | 63 | Each entry in this ``dict`` uses the table name as its key and a 64 | ``dict`` of documents as its value. The document ``dict`` contains 65 | document IDs as keys and the documents themselves as values. 66 | 67 | :param storage: The class of the storage to use. Will be initialized 68 | with ``args`` and ``kwargs``. 69 | """ 70 | 71 | #: The class that will be used to create table instances 72 | #: 73 | #: .. versionadded:: 4.0 74 | table_class = Table 75 | 76 | #: The name of the default table 77 | #: 78 | #: .. versionadded:: 4.0 79 | default_table_name = '_default' 80 | 81 | #: The class that will be used by default to create storage instances 82 | #: 83 | #: .. versionadded:: 4.0 84 | default_storage_class = JSONStorage 85 | 86 | def __init__(self, *args, **kwargs) -> None: 87 | """ 88 | Create a new instance of TinyDB. 89 | """ 90 | 91 | storage = kwargs.pop('storage', self.default_storage_class) 92 | 93 | # Prepare the storage 94 | self._storage: Storage = storage(*args, **kwargs) 95 | 96 | self._opened = True 97 | self._tables: Dict[str, Table] = {} 98 | 99 | def __repr__(self): 100 | args = [ 101 | 'tables={}'.format(list(self.tables())), 102 | 'tables_count={}'.format(len(self.tables())), 103 | 'default_table_documents_count={}'.format(self.__len__()), 104 | 'all_tables_documents_count={}'.format( 105 | ['{}={}'.format(table, len(self.table(table))) 106 | for table in self.tables()]), 107 | ] 108 | 109 | return '<{} {}>'.format(type(self).__name__, ', '.join(args)) 110 | 111 | def table(self, name: str, **kwargs) -> Table: 112 | """ 113 | Get access to a specific table. 114 | 115 | If the table hasn't been accessed yet, a new table instance will be 116 | created using the :attr:`~tinydb.database.TinyDB.table_class` class. 117 | Otherwise, the previously created table instance will be returned. 118 | 119 | All further options besides the name are passed to the table class which 120 | by default is :class:`~tinydb.table.Table`. Check its documentation 121 | for further parameters you can pass. 122 | 123 | :param name: The name of the table. 124 | :param kwargs: Keyword arguments to pass to the table class constructor 125 | """ 126 | 127 | if name in self._tables: 128 | return self._tables[name] 129 | 130 | table = self.table_class(self.storage, name, **kwargs) 131 | self._tables[name] = table 132 | 133 | return table 134 | 135 | def tables(self) -> Set[str]: 136 | """ 137 | Get the names of all tables in the database. 138 | 139 | :returns: a set of table names 140 | """ 141 | 142 | # TinyDB stores data as a dict of tables like this: 143 | # 144 | # { 145 | # '_default': { 146 | # 0: {document...}, 147 | # 1: {document...}, 148 | # }, 149 | # 'table1': { 150 | # ... 151 | # } 152 | # } 153 | # 154 | # To get a set of table names, we thus construct a set of this main 155 | # dict which returns a set of the dict keys which are the table names. 156 | # 157 | # Storage.read() may return ``None`` if the database file is empty, 158 | # so we need to consider this case to and return an empty set in this 159 | # case. 160 | 161 | return set(self.storage.read() or {}) 162 | 163 | def drop_tables(self) -> None: 164 | """ 165 | Drop all tables from the database. **CANNOT BE REVERSED!** 166 | """ 167 | 168 | # We drop all tables from this database by writing an empty dict 169 | # to the storage thereby returning to the initial state with no tables. 170 | self.storage.write({}) 171 | 172 | # After that we need to remember to empty the ``_tables`` dict, so we'll 173 | # create new table instances when a table is accessed again. 174 | self._tables.clear() 175 | 176 | def drop_table(self, name: str) -> None: 177 | """ 178 | Drop a specific table from the database. **CANNOT BE REVERSED!** 179 | 180 | :param name: The name of the table to drop. 181 | """ 182 | 183 | # If the table is currently opened, we need to forget the table class 184 | # instance 185 | if name in self._tables: 186 | del self._tables[name] 187 | 188 | data = self.storage.read() 189 | 190 | # The database is uninitialized, there's nothing to do 191 | if data is None: 192 | return 193 | 194 | # The table does not exist, there's nothing to do 195 | if name not in data: 196 | return 197 | 198 | # Remove the table from the data dict 199 | del data[name] 200 | 201 | # Store the updated data back to the storage 202 | self.storage.write(data) 203 | 204 | @property 205 | def storage(self) -> Storage: 206 | """ 207 | Get the storage instance used for this TinyDB instance. 208 | 209 | :return: This instance's storage 210 | :rtype: Storage 211 | """ 212 | return self._storage 213 | 214 | def close(self) -> None: 215 | """ 216 | Close the database. 217 | 218 | This may be needed if the storage instance used for this database 219 | needs to perform cleanup operations like closing file handles. 220 | 221 | To ensure this method is called, the TinyDB instance can be used as a 222 | context manager:: 223 | 224 | with TinyDB('data.json') as db: 225 | db.insert({'foo': 'bar'}) 226 | 227 | Upon leaving this context, the ``close`` method will be called. 228 | """ 229 | self._opened = False 230 | self.storage.close() 231 | 232 | def __enter__(self): 233 | """ 234 | Use the database as a context manager. 235 | 236 | Using the database as a context manager ensures that the 237 | :meth:`~tinydb.database.TinyDB.close` method is called upon leaving 238 | the context. 239 | 240 | :return: The current instance 241 | """ 242 | return self 243 | 244 | def __exit__(self, *args): 245 | """ 246 | Close the storage instance when leaving a context. 247 | """ 248 | if self._opened: 249 | self.close() 250 | 251 | def __getattr__(self, name): 252 | """ 253 | Forward all unknown attribute calls to the default table instance. 254 | """ 255 | return getattr(self.table(self.default_table_name), name) 256 | 257 | # Here we forward magic methods to the default table instance. These are 258 | # not handled by __getattr__ so we need to forward them manually here 259 | 260 | def __len__(self): 261 | """ 262 | Get the total number of documents in the default table. 263 | 264 | >>> db = TinyDB('db.json') 265 | >>> len(db) 266 | 0 267 | """ 268 | return len(self.table(self.default_table_name)) 269 | 270 | def __iter__(self) -> Iterator[Document]: 271 | """ 272 | Return an iterator for the default table's documents. 273 | """ 274 | return iter(self.table(self.default_table_name)) 275 | -------------------------------------------------------------------------------- /tinydb/middlewares.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the :class:`base class ` for 3 | middlewares and implementations. 4 | """ 5 | from typing import Optional 6 | 7 | from tinydb import Storage 8 | 9 | 10 | class Middleware: 11 | """ 12 | The base class for all Middlewares. 13 | 14 | Middlewares hook into the read/write process of TinyDB allowing you to 15 | extend the behaviour by adding caching, logging, ... 16 | 17 | Your middleware's ``__init__`` method has to call the parent class 18 | constructor so the middleware chain can be configured properly. 19 | """ 20 | 21 | def __init__(self, storage_cls) -> None: 22 | self._storage_cls = storage_cls 23 | self.storage: Storage = None # type: ignore 24 | 25 | def __call__(self, *args, **kwargs): 26 | """ 27 | Create the storage instance and store it as self.storage. 28 | 29 | Usually a user creates a new TinyDB instance like this:: 30 | 31 | TinyDB(storage=StorageClass) 32 | 33 | The storage keyword argument is used by TinyDB this way:: 34 | 35 | self.storage = storage(*args, **kwargs) 36 | 37 | As we can see, ``storage(...)`` runs the constructor and returns the 38 | new storage instance. 39 | 40 | 41 | Using Middlewares, the user will call:: 42 | 43 | The 'real' storage class 44 | v 45 | TinyDB(storage=Middleware(StorageClass)) 46 | ^ 47 | Already an instance! 48 | 49 | So, when running ``self.storage = storage(*args, **kwargs)`` Python 50 | now will call ``__call__`` and TinyDB will expect the return value to 51 | be the storage (or Middleware) instance. Returning the instance is 52 | simple, but we also got the underlying (*real*) StorageClass as an 53 | __init__ argument that still is not an instance. 54 | So, we initialize it in __call__ forwarding any arguments we receive 55 | from TinyDB (``TinyDB(arg1, kwarg1=value, storage=...)``). 56 | 57 | In case of nested Middlewares, calling the instance as if it was a 58 | class results in calling ``__call__`` what initializes the next 59 | nested Middleware that itself will initialize the next Middleware and 60 | so on. 61 | """ 62 | 63 | self.storage = self._storage_cls(*args, **kwargs) 64 | 65 | return self 66 | 67 | def __getattr__(self, name): 68 | """ 69 | Forward all unknown attribute calls to the underlying storage, so we 70 | remain as transparent as possible. 71 | """ 72 | 73 | return getattr(self.__dict__['storage'], name) 74 | 75 | 76 | class CachingMiddleware(Middleware): 77 | """ 78 | Add some caching to TinyDB. 79 | 80 | This Middleware aims to improve the performance of TinyDB by writing only 81 | the last DB state every :attr:`WRITE_CACHE_SIZE` time and reading always 82 | from cache. 83 | """ 84 | 85 | #: The number of write operations to cache before writing to disc 86 | WRITE_CACHE_SIZE = 1000 87 | 88 | def __init__(self, storage_cls): 89 | # Initialize the parent constructor 90 | super().__init__(storage_cls) 91 | 92 | # Prepare the cache 93 | self.cache = None 94 | self._cache_modified_count = 0 95 | 96 | def read(self): 97 | if self.cache is None: 98 | # Empty cache: read from the storage 99 | self.cache = self.storage.read() 100 | 101 | # Return the cached data 102 | return self.cache 103 | 104 | def write(self, data): 105 | # Store data in cache 106 | self.cache = data 107 | self._cache_modified_count += 1 108 | 109 | # Check if we need to flush the cache 110 | if self._cache_modified_count >= self.WRITE_CACHE_SIZE: 111 | self.flush() 112 | 113 | def flush(self): 114 | """ 115 | Flush all unwritten data to disk. 116 | """ 117 | if self._cache_modified_count > 0: 118 | # Force-flush the cache by writing the data to the storage 119 | self.storage.write(self.cache) 120 | self._cache_modified_count = 0 121 | 122 | def close(self): 123 | # Flush potentially unwritten data 124 | self.flush() 125 | 126 | # Let the storage clean up too 127 | self.storage.close() 128 | -------------------------------------------------------------------------------- /tinydb/mypy_plugin.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Optional, Callable, Dict 2 | 3 | from mypy.nodes import NameExpr 4 | from mypy.options import Options 5 | from mypy.plugin import Plugin, DynamicClassDefContext 6 | 7 | T = TypeVar('T') 8 | CB = Optional[Callable[[T], None]] 9 | DynamicClassDef = DynamicClassDefContext 10 | 11 | 12 | class TinyDBPlugin(Plugin): 13 | def __init__(self, options: Options): 14 | super().__init__(options) 15 | 16 | self.named_placeholders: Dict[str, str] = {} 17 | 18 | def get_dynamic_class_hook(self, fullname: str) -> CB[DynamicClassDef]: 19 | if fullname == 'tinydb.utils.with_typehint': 20 | def hook(ctx: DynamicClassDefContext): 21 | klass = ctx.call.args[0] 22 | assert isinstance(klass, NameExpr) 23 | 24 | type_name = klass.fullname 25 | assert type_name is not None 26 | 27 | qualified = self.lookup_fully_qualified(type_name) 28 | assert qualified is not None 29 | 30 | ctx.api.add_symbol_table_node(ctx.name, qualified) 31 | 32 | return hook 33 | 34 | return None 35 | 36 | 37 | def plugin(_version: str): 38 | return TinyDBPlugin 39 | -------------------------------------------------------------------------------- /tinydb/operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | A collection of update operations for TinyDB. 3 | 4 | They are used for updates like this: 5 | 6 | >>> db.update(delete('foo'), where('foo') == 2) 7 | 8 | This would delete the ``foo`` field from all documents where ``foo`` equals 2. 9 | """ 10 | 11 | 12 | def delete(field): 13 | """ 14 | Delete a given field from the document. 15 | """ 16 | def transform(doc): 17 | del doc[field] 18 | 19 | return transform 20 | 21 | 22 | def add(field, n): 23 | """ 24 | Add ``n`` to a given field in the document. 25 | """ 26 | def transform(doc): 27 | doc[field] += n 28 | 29 | return transform 30 | 31 | 32 | def subtract(field, n): 33 | """ 34 | Subtract ``n`` to a given field in the document. 35 | """ 36 | def transform(doc): 37 | doc[field] -= n 38 | 39 | return transform 40 | 41 | 42 | def set(field, val): 43 | """ 44 | Set a given field to ``val``. 45 | """ 46 | def transform(doc): 47 | doc[field] = val 48 | 49 | return transform 50 | 51 | 52 | def increment(field): 53 | """ 54 | Increment a given field in the document by 1. 55 | """ 56 | def transform(doc): 57 | doc[field] += 1 58 | 59 | return transform 60 | 61 | 62 | def decrement(field): 63 | """ 64 | Decrement a given field in the document by 1. 65 | """ 66 | def transform(doc): 67 | doc[field] -= 1 68 | 69 | return transform 70 | -------------------------------------------------------------------------------- /tinydb/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiemens/tinydb/10644a0e07ad180c5b756aba272ee6b0dbd12df8/tinydb/py.typed -------------------------------------------------------------------------------- /tinydb/queries.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the querying interface. 3 | 4 | Starting with :class:`~tinydb.queries.Query` you can construct complex 5 | queries: 6 | 7 | >>> ((where('f1') == 5) & (where('f2') != 2)) | where('s').matches(r'^\\w+$') 8 | (('f1' == 5) and ('f2' != 2)) or ('s' ~= ^\\w+$ ) 9 | 10 | Queries are executed by using the ``__call__``: 11 | 12 | >>> q = where('val') == 5 13 | >>> q({'val': 5}) 14 | True 15 | >>> q({'val': 1}) 16 | False 17 | """ 18 | 19 | import re 20 | from typing import Mapping, Tuple, Callable, Any, Union, List, Optional, Protocol 21 | 22 | from .utils import freeze 23 | 24 | __all__ = ('Query', 'QueryLike', 'where') 25 | 26 | 27 | def is_sequence(obj): 28 | return hasattr(obj, '__iter__') 29 | 30 | 31 | class QueryLike(Protocol): 32 | """ 33 | A typing protocol that acts like a query. 34 | 35 | Something that we use as a query must have two properties: 36 | 37 | 1. It must be callable, accepting a `Mapping` object and returning a 38 | boolean that indicates whether the value matches the query, and 39 | 2. it must have a stable hash that will be used for query caching. 40 | 41 | In addition, to mark a query as non-cacheable (e.g. if it involves 42 | some remote lookup) it needs to have a method called ``is_cacheable`` 43 | that returns ``False``. 44 | 45 | This query protocol is used to make MyPy correctly support the query 46 | pattern that TinyDB uses. 47 | 48 | See also https://mypy.readthedocs.io/en/stable/protocols.html#simple-user-defined-protocols 49 | """ 50 | def __call__(self, value: Mapping) -> bool: ... 51 | 52 | def __hash__(self) -> int: ... 53 | 54 | 55 | class QueryInstance: 56 | """ 57 | A query instance. 58 | 59 | This is the object on which the actual query operations are performed. The 60 | :class:`~tinydb.queries.Query` class acts like a query builder and 61 | generates :class:`~tinydb.queries.QueryInstance` objects which will 62 | evaluate their query against a given document when called. 63 | 64 | Query instances can be combined using logical OR and AND and inverted using 65 | logical NOT. 66 | 67 | In order to be usable in a query cache, a query needs to have a stable hash 68 | value with the same query always returning the same hash. That way a query 69 | instance can be used as a key in a dictionary. 70 | """ 71 | 72 | def __init__(self, test: Callable[[Mapping], bool], hashval: Optional[Tuple]): 73 | self._test = test 74 | self._hash = hashval 75 | 76 | def is_cacheable(self) -> bool: 77 | return self._hash is not None 78 | 79 | def __call__(self, value: Mapping) -> bool: 80 | """ 81 | Evaluate the query to check if it matches a specified value. 82 | 83 | :param value: The value to check. 84 | :return: Whether the value matches this query. 85 | """ 86 | return self._test(value) 87 | 88 | def __hash__(self) -> int: 89 | # We calculate the query hash by using the ``hashval`` object which 90 | # describes this query uniquely, so we can calculate a stable hash 91 | # value by simply hashing it 92 | return hash(self._hash) 93 | 94 | def __repr__(self): 95 | return 'QueryImpl{}'.format(self._hash) 96 | 97 | def __eq__(self, other: object): 98 | if isinstance(other, QueryInstance): 99 | return self._hash == other._hash 100 | 101 | return False 102 | 103 | # --- Query modifiers ----------------------------------------------------- 104 | 105 | def __and__(self, other: 'QueryInstance') -> 'QueryInstance': 106 | # We use a frozenset for the hash as the AND operation is commutative 107 | # (a & b == b & a) and the frozenset does not consider the order of 108 | # elements 109 | if self.is_cacheable() and other.is_cacheable(): 110 | hashval = ('and', frozenset([self._hash, other._hash])) 111 | else: 112 | hashval = None 113 | return QueryInstance(lambda value: self(value) and other(value), hashval) 114 | 115 | def __or__(self, other: 'QueryInstance') -> 'QueryInstance': 116 | # We use a frozenset for the hash as the OR operation is commutative 117 | # (a | b == b | a) and the frozenset does not consider the order of 118 | # elements 119 | if self.is_cacheable() and other.is_cacheable(): 120 | hashval = ('or', frozenset([self._hash, other._hash])) 121 | else: 122 | hashval = None 123 | return QueryInstance(lambda value: self(value) or other(value), hashval) 124 | 125 | def __invert__(self) -> 'QueryInstance': 126 | hashval = ('not', self._hash) if self.is_cacheable() else None 127 | return QueryInstance(lambda value: not self(value), hashval) 128 | 129 | 130 | class Query(QueryInstance): 131 | """ 132 | TinyDB Queries. 133 | 134 | Allows building queries for TinyDB databases. There are two main ways of 135 | using queries: 136 | 137 | 1) ORM-like usage: 138 | 139 | >>> User = Query() 140 | >>> db.search(User.name == 'John Doe') 141 | >>> db.search(User['logged-in'] == True) 142 | 143 | 2) Classical usage: 144 | 145 | >>> db.search(where('value') == True) 146 | 147 | Note that ``where(...)`` is a shorthand for ``Query(...)`` allowing for 148 | a more fluent syntax. 149 | 150 | Besides the methods documented here you can combine queries using the 151 | binary AND and OR operators: 152 | 153 | >>> # Binary AND: 154 | >>> db.search((where('field1').exists()) & (where('field2') == 5)) 155 | >>> # Binary OR: 156 | >>> db.search((where('field1').exists()) | (where('field2') == 5)) 157 | 158 | Queries are executed by calling the resulting object. They expect to get 159 | the document to test as the first argument and return ``True`` or 160 | ``False`` depending on whether the documents match the query or not. 161 | """ 162 | 163 | def __init__(self) -> None: 164 | # The current path of fields to access when evaluating the object 165 | self._path: Tuple[Union[str, Callable], ...] = () 166 | 167 | # Prevent empty queries to be evaluated 168 | def notest(_): 169 | raise RuntimeError('Empty query was evaluated') 170 | 171 | super().__init__( 172 | test=notest, 173 | hashval=(None,) 174 | ) 175 | 176 | def __repr__(self): 177 | return '{}()'.format(type(self).__name__) 178 | 179 | def __hash__(self): 180 | return super().__hash__() 181 | 182 | def __getattr__(self, item: str): 183 | # Generate a new query object with the new query path 184 | # We use type(self) to get the class of the current query in case 185 | # someone uses a subclass of ``Query`` 186 | query = type(self)() 187 | 188 | # Now we add the accessed item to the query path ... 189 | query._path = self._path + (item,) 190 | 191 | # ... and update the query hash 192 | query._hash = ('path', query._path) if self.is_cacheable() else None 193 | 194 | return query 195 | 196 | def __getitem__(self, item: str): 197 | # A different syntax for ``__getattr__`` 198 | 199 | # We cannot call ``getattr(item)`` here as it would try to resolve 200 | # the name as a method name first, only then call our ``__getattr__`` 201 | # method. By calling ``__getattr__`` directly, we make sure that 202 | # calling e.g. ``Query()['test']`` will always generate a query for a 203 | # document's ``test`` field instead of returning a reference to the 204 | # ``Query.test`` method 205 | return self.__getattr__(item) 206 | 207 | def _generate_test( 208 | self, 209 | test: Callable[[Any], bool], 210 | hashval: Tuple, 211 | allow_empty_path: bool = False 212 | ) -> QueryInstance: 213 | """ 214 | Generate a query based on a test function that first resolves the query 215 | path. 216 | 217 | :param test: The test the query executes. 218 | :param hashval: The hash of the query. 219 | :return: A :class:`~tinydb.queries.QueryInstance` object 220 | """ 221 | if not self._path and not allow_empty_path: 222 | raise ValueError('Query has no path') 223 | 224 | def runner(value): 225 | try: 226 | # Resolve the path 227 | for part in self._path: 228 | if isinstance(part, str): 229 | value = value[part] 230 | else: 231 | value = part(value) 232 | except (KeyError, TypeError): 233 | return False 234 | else: 235 | # Perform the specified test 236 | return test(value) 237 | 238 | return QueryInstance( 239 | lambda value: runner(value), 240 | (hashval if self.is_cacheable() else None) 241 | ) 242 | 243 | def __eq__(self, rhs: Any): 244 | """ 245 | Test a dict value for equality. 246 | 247 | >>> Query().f1 == 42 248 | 249 | :param rhs: The value to compare against 250 | """ 251 | return self._generate_test( 252 | lambda value: value == rhs, 253 | ('==', self._path, freeze(rhs)) 254 | ) 255 | 256 | def __ne__(self, rhs: Any): 257 | """ 258 | Test a dict value for inequality. 259 | 260 | >>> Query().f1 != 42 261 | 262 | :param rhs: The value to compare against 263 | """ 264 | return self._generate_test( 265 | lambda value: value != rhs, 266 | ('!=', self._path, freeze(rhs)) 267 | ) 268 | 269 | def __lt__(self, rhs: Any) -> QueryInstance: 270 | """ 271 | Test a dict value for being lower than another value. 272 | 273 | >>> Query().f1 < 42 274 | 275 | :param rhs: The value to compare against 276 | """ 277 | return self._generate_test( 278 | lambda value: value < rhs, 279 | ('<', self._path, rhs) 280 | ) 281 | 282 | def __le__(self, rhs: Any) -> QueryInstance: 283 | """ 284 | Test a dict value for being lower than or equal to another value. 285 | 286 | >>> where('f1') <= 42 287 | 288 | :param rhs: The value to compare against 289 | """ 290 | return self._generate_test( 291 | lambda value: value <= rhs, 292 | ('<=', self._path, rhs) 293 | ) 294 | 295 | def __gt__(self, rhs: Any) -> QueryInstance: 296 | """ 297 | Test a dict value for being greater than another value. 298 | 299 | >>> Query().f1 > 42 300 | 301 | :param rhs: The value to compare against 302 | """ 303 | return self._generate_test( 304 | lambda value: value > rhs, 305 | ('>', self._path, rhs) 306 | ) 307 | 308 | def __ge__(self, rhs: Any) -> QueryInstance: 309 | """ 310 | Test a dict value for being greater than or equal to another value. 311 | 312 | >>> Query().f1 >= 42 313 | 314 | :param rhs: The value to compare against 315 | """ 316 | return self._generate_test( 317 | lambda value: value >= rhs, 318 | ('>=', self._path, rhs) 319 | ) 320 | 321 | def exists(self) -> QueryInstance: 322 | """ 323 | Test for a dict where a provided key exists. 324 | 325 | >>> Query().f1.exists() 326 | """ 327 | return self._generate_test( 328 | lambda _: True, 329 | ('exists', self._path) 330 | ) 331 | 332 | def matches(self, regex: str, flags: int = 0) -> QueryInstance: 333 | """ 334 | Run a regex test against a dict value (whole string has to match). 335 | 336 | >>> Query().f1.matches(r'^\\w+$') 337 | 338 | :param regex: The regular expression to use for matching 339 | :param flags: regex flags to pass to ``re.match`` 340 | """ 341 | def test(value): 342 | if not isinstance(value, str): 343 | return False 344 | 345 | return re.match(regex, value, flags) is not None 346 | 347 | return self._generate_test(test, ('matches', self._path, regex)) 348 | 349 | def search(self, regex: str, flags: int = 0) -> QueryInstance: 350 | """ 351 | Run a regex test against a dict value (only substring string has to 352 | match). 353 | 354 | >>> Query().f1.search(r'^\\w+$') 355 | 356 | :param regex: The regular expression to use for matching 357 | :param flags: regex flags to pass to ``re.match`` 358 | """ 359 | 360 | def test(value): 361 | if not isinstance(value, str): 362 | return False 363 | 364 | return re.search(regex, value, flags) is not None 365 | 366 | return self._generate_test(test, ('search', self._path, regex)) 367 | 368 | def test(self, func: Callable[[Mapping], bool], *args) -> QueryInstance: 369 | """ 370 | Run a user-defined test function against a dict value. 371 | 372 | >>> def test_func(val): 373 | ... return val == 42 374 | ... 375 | >>> Query().f1.test(test_func) 376 | 377 | .. warning:: 378 | 379 | The test function provided needs to be deterministic (returning the 380 | same value when provided with the same arguments), otherwise this 381 | may mess up the query cache that :class:`~tinydb.table.Table` 382 | implements. 383 | 384 | :param func: The function to call, passing the dict as the first 385 | argument 386 | :param args: Additional arguments to pass to the test function 387 | """ 388 | return self._generate_test( 389 | lambda value: func(value, *args), 390 | ('test', self._path, func, args) 391 | ) 392 | 393 | def any(self, cond: Union[QueryInstance, List[Any]]) -> QueryInstance: 394 | """ 395 | Check if a condition is met by any document in a list, 396 | where a condition can also be a sequence (e.g. list). 397 | 398 | >>> Query().f1.any(Query().f2 == 1) 399 | 400 | Matches:: 401 | 402 | {'f1': [{'f2': 1}, {'f2': 0}]} 403 | 404 | >>> Query().f1.any([1, 2, 3]) 405 | 406 | Matches:: 407 | 408 | {'f1': [1, 2]} 409 | {'f1': [3, 4, 5]} 410 | 411 | :param cond: Either a query that at least one document has to match or 412 | a list of which at least one document has to be contained 413 | in the tested document. 414 | """ 415 | if callable(cond): 416 | def test(value): 417 | return is_sequence(value) and any(cond(e) for e in value) 418 | 419 | else: 420 | def test(value): 421 | return is_sequence(value) and any(e in cond for e in value) 422 | 423 | return self._generate_test( 424 | lambda value: test(value), 425 | ('any', self._path, freeze(cond)) 426 | ) 427 | 428 | def all(self, cond: Union['QueryInstance', List[Any]]) -> QueryInstance: 429 | """ 430 | Check if a condition is met by all documents in a list, 431 | where a condition can also be a sequence (e.g. list). 432 | 433 | >>> Query().f1.all(Query().f2 == 1) 434 | 435 | Matches:: 436 | 437 | {'f1': [{'f2': 1}, {'f2': 1}]} 438 | 439 | >>> Query().f1.all([1, 2, 3]) 440 | 441 | Matches:: 442 | 443 | {'f1': [1, 2, 3, 4, 5]} 444 | 445 | :param cond: Either a query that all documents have to match or a list 446 | which has to be contained in the tested document. 447 | """ 448 | if callable(cond): 449 | def test(value): 450 | return is_sequence(value) and all(cond(e) for e in value) 451 | 452 | else: 453 | def test(value): 454 | return is_sequence(value) and all(e in value for e in cond) 455 | 456 | return self._generate_test( 457 | lambda value: test(value), 458 | ('all', self._path, freeze(cond)) 459 | ) 460 | 461 | def one_of(self, items: List[Any]) -> QueryInstance: 462 | """ 463 | Check if the value is contained in a list or generator. 464 | 465 | >>> Query().f1.one_of(['value 1', 'value 2']) 466 | 467 | :param items: The list of items to check with 468 | """ 469 | return self._generate_test( 470 | lambda value: value in items, 471 | ('one_of', self._path, freeze(items)) 472 | ) 473 | 474 | def fragment(self, document: Mapping) -> QueryInstance: 475 | def test(value): 476 | for key in document: 477 | if key not in value or value[key] != document[key]: 478 | return False 479 | 480 | return True 481 | 482 | return self._generate_test( 483 | lambda value: test(value), 484 | ('fragment', freeze(document)), 485 | allow_empty_path=True 486 | ) 487 | 488 | def noop(self) -> QueryInstance: 489 | """ 490 | Always evaluate to ``True``. 491 | 492 | Useful for having a base value when composing queries dynamically. 493 | """ 494 | 495 | return QueryInstance( 496 | lambda value: True, 497 | () 498 | ) 499 | 500 | def map(self, fn: Callable[[Any], Any]) -> 'Query': 501 | """ 502 | Add a function to the query path. Similar to __getattr__ but for 503 | arbitrary functions. 504 | """ 505 | query = type(self)() 506 | 507 | # Now we add the callable to the query path ... 508 | query._path = self._path + (fn,) 509 | 510 | # ... and kill the hash - callable objects can be mutable, so it's 511 | # harmful to cache their results. 512 | query._hash = None 513 | 514 | return query 515 | 516 | def where(key: str) -> Query: 517 | """ 518 | A shorthand for ``Query()[key]`` 519 | """ 520 | return Query()[key] 521 | -------------------------------------------------------------------------------- /tinydb/storages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the :class:`base class ` for storages and 3 | implementations. 4 | """ 5 | 6 | import io 7 | import json 8 | import os 9 | import warnings 10 | from abc import ABC, abstractmethod 11 | from typing import Dict, Any, Optional 12 | 13 | __all__ = ('Storage', 'JSONStorage', 'MemoryStorage') 14 | 15 | 16 | def touch(path: str, create_dirs: bool): 17 | """ 18 | Create a file if it doesn't exist yet. 19 | 20 | :param path: The file to create. 21 | :param create_dirs: Whether to create all missing parent directories. 22 | """ 23 | if create_dirs: 24 | base_dir = os.path.dirname(path) 25 | 26 | # Check if we need to create missing parent directories 27 | if not os.path.exists(base_dir): 28 | os.makedirs(base_dir) 29 | 30 | # Create the file by opening it in 'a' mode which creates the file if it 31 | # does not exist yet but does not modify its contents 32 | with open(path, 'a'): 33 | pass 34 | 35 | 36 | class Storage(ABC): 37 | """ 38 | The abstract base class for all Storages. 39 | 40 | A Storage (de)serializes the current state of the database and stores it in 41 | some place (memory, file on disk, ...). 42 | """ 43 | 44 | # Using ABCMeta as metaclass allows instantiating only storages that have 45 | # implemented read and write 46 | 47 | @abstractmethod 48 | def read(self) -> Optional[Dict[str, Dict[str, Any]]]: 49 | """ 50 | Read the current state. 51 | 52 | Any kind of deserialization should go here. 53 | 54 | Return ``None`` here to indicate that the storage is empty. 55 | """ 56 | 57 | raise NotImplementedError('To be overridden!') 58 | 59 | @abstractmethod 60 | def write(self, data: Dict[str, Dict[str, Any]]) -> None: 61 | """ 62 | Write the current state of the database to the storage. 63 | 64 | Any kind of serialization should go here. 65 | 66 | :param data: The current state of the database. 67 | """ 68 | 69 | raise NotImplementedError('To be overridden!') 70 | 71 | def close(self) -> None: 72 | """ 73 | Optional: Close open file handles, etc. 74 | """ 75 | 76 | pass 77 | 78 | 79 | class JSONStorage(Storage): 80 | """ 81 | Store the data in a JSON file. 82 | """ 83 | 84 | def __init__(self, path: str, create_dirs=False, encoding=None, access_mode='r+', **kwargs): 85 | """ 86 | Create a new instance. 87 | 88 | Also creates the storage file, if it doesn't exist and the access mode 89 | is appropriate for writing. 90 | 91 | Note: Using an access mode other than `r` or `r+` will probably lead to 92 | data loss or data corruption! 93 | 94 | :param path: Where to store the JSON data. 95 | :param access_mode: mode in which the file is opened (r, r+) 96 | :type access_mode: str 97 | """ 98 | 99 | super().__init__() 100 | 101 | self._mode = access_mode 102 | self.kwargs = kwargs 103 | 104 | if access_mode not in ('r', 'rb', 'r+', 'rb+'): 105 | warnings.warn( 106 | 'Using an `access_mode` other than \'r\', \'rb\', \'r+\' ' 107 | 'or \'rb+\' can cause data loss or corruption' 108 | ) 109 | 110 | # Create the file if it doesn't exist and creating is allowed by the 111 | # access mode 112 | if any([character in self._mode for character in ('+', 'w', 'a')]): # any of the writing modes 113 | touch(path, create_dirs=create_dirs) 114 | 115 | # Open the file for reading/writing 116 | self._handle = open(path, mode=self._mode, encoding=encoding) 117 | 118 | def close(self) -> None: 119 | self._handle.close() 120 | 121 | def read(self) -> Optional[Dict[str, Dict[str, Any]]]: 122 | # Get the file size by moving the cursor to the file end and reading 123 | # its location 124 | self._handle.seek(0, os.SEEK_END) 125 | size = self._handle.tell() 126 | 127 | if not size: 128 | # File is empty, so we return ``None`` so TinyDB can properly 129 | # initialize the database 130 | return None 131 | else: 132 | # Return the cursor to the beginning of the file 133 | self._handle.seek(0) 134 | 135 | # Load the JSON contents of the file 136 | return json.load(self._handle) 137 | 138 | def write(self, data: Dict[str, Dict[str, Any]]): 139 | # Move the cursor to the beginning of the file just in case 140 | self._handle.seek(0) 141 | 142 | # Serialize the database state using the user-provided arguments 143 | serialized = json.dumps(data, **self.kwargs) 144 | 145 | # Write the serialized data to the file 146 | try: 147 | self._handle.write(serialized) 148 | except io.UnsupportedOperation: 149 | raise IOError('Cannot write to the database. Access mode is "{0}"'.format(self._mode)) 150 | 151 | # Ensure the file has been written 152 | self._handle.flush() 153 | os.fsync(self._handle.fileno()) 154 | 155 | # Remove data that is behind the new cursor in case the file has 156 | # gotten shorter 157 | self._handle.truncate() 158 | 159 | 160 | class MemoryStorage(Storage): 161 | """ 162 | Store the data as JSON in memory. 163 | """ 164 | 165 | def __init__(self): 166 | """ 167 | Create a new instance. 168 | """ 169 | 170 | super().__init__() 171 | self.memory = None 172 | 173 | def read(self) -> Optional[Dict[str, Dict[str, Any]]]: 174 | return self.memory 175 | 176 | def write(self, data: Dict[str, Dict[str, Any]]): 177 | self.memory = data 178 | -------------------------------------------------------------------------------- /tinydb/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions. 3 | """ 4 | 5 | from collections import OrderedDict, abc 6 | from typing import List, Iterator, TypeVar, Generic, Union, Optional, Type, \ 7 | TYPE_CHECKING 8 | 9 | K = TypeVar('K') 10 | V = TypeVar('V') 11 | D = TypeVar('D') 12 | T = TypeVar('T') 13 | 14 | __all__ = ('LRUCache', 'freeze', 'with_typehint') 15 | 16 | 17 | def with_typehint(baseclass: Type[T]): 18 | """ 19 | Add type hints from a specified class to a base class: 20 | 21 | >>> class Foo(with_typehint(Bar)): 22 | ... pass 23 | 24 | This would add type hints from class ``Bar`` to class ``Foo``. 25 | 26 | Note that while PyCharm and Pyright (for VS Code) understand this pattern, 27 | MyPy does not. For that reason TinyDB has a MyPy plugin in 28 | ``mypy_plugin.py`` that adds support for this pattern. 29 | """ 30 | if TYPE_CHECKING: 31 | # In the case of type checking: pretend that the target class inherits 32 | # from the specified base class 33 | return baseclass 34 | 35 | # Otherwise: just inherit from `object` like a regular Python class 36 | return object 37 | 38 | 39 | class LRUCache(abc.MutableMapping, Generic[K, V]): 40 | """ 41 | A least-recently used (LRU) cache with a fixed cache size. 42 | 43 | This class acts as a dictionary but has a limited size. If the number of 44 | entries in the cache exceeds the cache size, the least-recently accessed 45 | entry will be discarded. 46 | 47 | This is implemented using an ``OrderedDict``. On every access the accessed 48 | entry is moved to the front by re-inserting it into the ``OrderedDict``. 49 | When adding an entry and the cache size is exceeded, the last entry will 50 | be discarded. 51 | """ 52 | 53 | def __init__(self, capacity=None) -> None: 54 | self.capacity = capacity 55 | self.cache: OrderedDict[K, V] = OrderedDict() 56 | 57 | @property 58 | def lru(self) -> List[K]: 59 | return list(self.cache.keys()) 60 | 61 | @property 62 | def length(self) -> int: 63 | return len(self.cache) 64 | 65 | def clear(self) -> None: 66 | self.cache.clear() 67 | 68 | def __len__(self) -> int: 69 | return self.length 70 | 71 | def __contains__(self, key: object) -> bool: 72 | return key in self.cache 73 | 74 | def __setitem__(self, key: K, value: V) -> None: 75 | self.set(key, value) 76 | 77 | def __delitem__(self, key: K) -> None: 78 | del self.cache[key] 79 | 80 | def __getitem__(self, key) -> V: 81 | value = self.get(key) 82 | if value is None: 83 | raise KeyError(key) 84 | 85 | return value 86 | 87 | def __iter__(self) -> Iterator[K]: 88 | return iter(self.cache) 89 | 90 | def get(self, key: K, default: Optional[D] = None) -> Optional[Union[V, D]]: 91 | value = self.cache.get(key) 92 | 93 | if value is not None: 94 | self.cache.move_to_end(key, last=True) 95 | 96 | return value 97 | 98 | return default 99 | 100 | def set(self, key: K, value: V): 101 | if self.cache.get(key): 102 | self.cache[key] = value 103 | self.cache.move_to_end(key, last=True) 104 | else: 105 | self.cache[key] = value 106 | 107 | # Check, if the cache is full and we have to remove old items 108 | # If the queue is of unlimited size, self.capacity is NaN and 109 | # x > NaN is always False in Python and the cache won't be cleared. 110 | if self.capacity is not None and self.length > self.capacity: 111 | self.cache.popitem(last=False) 112 | 113 | 114 | class FrozenDict(dict): 115 | """ 116 | An immutable dictionary. 117 | 118 | This is used to generate stable hashes for queries that contain dicts. 119 | Usually, Python dicts are not hashable because they are mutable. This 120 | class removes the mutability and implements the ``__hash__`` method. 121 | """ 122 | 123 | def __hash__(self): 124 | # Calculate the has by hashing a tuple of all dict items 125 | return hash(tuple(sorted(self.items()))) 126 | 127 | def _immutable(self, *args, **kws): 128 | raise TypeError('object is immutable') 129 | 130 | # Disable write access to the dict 131 | __setitem__ = _immutable 132 | __delitem__ = _immutable 133 | clear = _immutable 134 | setdefault = _immutable # type: ignore 135 | popitem = _immutable 136 | 137 | def update(self, e=None, **f): 138 | raise TypeError('object is immutable') 139 | 140 | def pop(self, k, d=None): 141 | raise TypeError('object is immutable') 142 | 143 | 144 | def freeze(obj): 145 | """ 146 | Freeze an object by making it immutable and thus hashable. 147 | """ 148 | if isinstance(obj, dict): 149 | # Transform dicts into ``FrozenDict``s 150 | return FrozenDict((k, freeze(v)) for k, v in obj.items()) 151 | elif isinstance(obj, list): 152 | # Transform lists into tuples 153 | return tuple(freeze(el) for el in obj) 154 | elif isinstance(obj, set): 155 | # Transform sets into ``frozenset``s 156 | return frozenset(obj) 157 | else: 158 | # Don't handle all other objects 159 | return obj 160 | -------------------------------------------------------------------------------- /tinydb/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '4.8.2' 2 | --------------------------------------------------------------------------------