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 |
2 |
3 |
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 |
--------------------------------------------------------------------------------