Merge lp:~thisfred/u1db/documentation5 into lp:u1db
- documentation5
- Merge into trunk
Proposed by
Eric Casteleijn
Status: | Merged |
---|---|
Approved by: | Eric Casteleijn |
Approved revision: | 389 |
Merged at revision: | 387 |
Proposed branch: | lp:~thisfred/u1db/documentation5 |
Merge into: | lp:u1db |
Diff against target: |
767 lines (+412/-101) 6 files modified
cosas/cosas.py (+10/-17) cosas/test_cosas.py (+2/-2) cosas/ui.py (+2/-0) html-docs/high-level-api.rst (+66/-51) html-docs/tutorial.rst (+305/-10) u1db/__init__.py (+27/-21) |
To merge this branch: | bzr merge lp:~thisfred/u1db/documentation5 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Langridge (community) | Approve | ||
Review via email: mp+120465@code.launchpad.net |
Commit message
Finished first version of the Cosas based tutorial.
Description of the change
Finished first version of the Cosas based tutorial.
To post a comment you must log in.
lp:~thisfred/u1db/documentation5
updated
- 389. By Eric Casteleijn
-
fixed typos and clarified
Revision history for this message
Eric Casteleijn (thisfred) wrote : | # |
Pushed fixes
Revision history for this message
Stuart Langridge (sil) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cosas/cosas.py' | |||
2 | --- cosas/cosas.py 2012-08-17 20:52:21 +0000 | |||
3 | +++ cosas/cosas.py 2012-08-21 13:44:17 +0000 | |||
4 | @@ -16,14 +16,13 @@ | |||
5 | 16 | 16 | ||
6 | 17 | """cosas example application.""" | 17 | """cosas example application.""" |
7 | 18 | 18 | ||
8 | 19 | import json | ||
9 | 20 | import os | 19 | import os |
10 | 21 | import re | 20 | import re |
11 | 22 | import u1db | 21 | import u1db |
12 | 22 | import copy | ||
13 | 23 | 23 | ||
14 | 24 | from dirspec.basedir import save_data_path | 24 | from dirspec.basedir import save_data_path |
15 | 25 | 25 | ||
16 | 26 | EMPTY_TASK = json.dumps({"title": "", "done": False, "tags": []}) | ||
17 | 27 | 26 | ||
18 | 28 | TAGS_INDEX = 'tags' | 27 | TAGS_INDEX = 'tags' |
19 | 29 | DONE_INDEX = 'done' | 28 | DONE_INDEX = 'done' |
20 | @@ -34,10 +33,13 @@ | |||
21 | 34 | 33 | ||
22 | 35 | TAGS = re.compile('#(\w+)|\[(.+)\]') | 34 | TAGS = re.compile('#(\w+)|\[(.+)\]') |
23 | 36 | 35 | ||
24 | 36 | EMPTY_TASK = {"title": "", "done": False, "tags": []} | ||
25 | 37 | |||
26 | 38 | get_empty_task = lambda: copy.deepcopy(EMPTY_TASK) | ||
27 | 39 | |||
28 | 37 | 40 | ||
29 | 38 | def get_database(): | 41 | def get_database(): |
30 | 39 | """Get the path that the database is stored in.""" | 42 | """Get the path that the database is stored in.""" |
31 | 40 | |||
32 | 41 | # setting document_factory to Task means that any database method that | 43 | # setting document_factory to Task means that any database method that |
33 | 42 | # would return documents, now returns Tasks instead. | 44 | # would return documents, now returns Tasks instead. |
34 | 43 | return u1db.open( | 45 | return u1db.open( |
35 | @@ -108,7 +110,6 @@ | |||
36 | 108 | # If results is empty, we're done: there are no | 110 | # If results is empty, we're done: there are no |
37 | 109 | # documents with all tags. | 111 | # documents with all tags. |
38 | 110 | return [] | 112 | return [] |
39 | 111 | # Wrap each document in results in a Task object, and return them. | ||
40 | 112 | return results.values() | 113 | return results.values() |
41 | 113 | 114 | ||
42 | 114 | def get_task(self, doc_id): | 115 | def get_task(self, doc_id): |
43 | @@ -121,7 +122,6 @@ | |||
44 | 121 | # The document id exists, but the document's content was previously | 122 | # The document id exists, but the document's content was previously |
45 | 122 | # deleted. | 123 | # deleted. |
46 | 123 | raise KeyError("Task with id %s was deleted." % (doc_id,)) | 124 | raise KeyError("Task with id %s was deleted." % (doc_id,)) |
47 | 124 | # Wrap the document in a Task object and return it. | ||
48 | 125 | return task | 125 | return task |
49 | 126 | 126 | ||
50 | 127 | def delete_task(self, task): | 127 | def delete_task(self, task): |
51 | @@ -132,24 +132,17 @@ | |||
52 | 132 | """Create a new task document.""" | 132 | """Create a new task document.""" |
53 | 133 | if tags is None: | 133 | if tags is None: |
54 | 134 | tags = [] | 134 | tags = [] |
58 | 135 | # We copy the JSON string representing a pristine task with no title | 135 | # We make a fresh copy of a pristine task with no title. |
59 | 136 | # and no tags. | 136 | content = get_empty_task() |
57 | 137 | content = EMPTY_TASK | ||
60 | 138 | # If we were passed a title or tags, or both, we set them in the object | 137 | # If we were passed a title or tags, or both, we set them in the object |
61 | 139 | # before storing it in the database. | 138 | # before storing it in the database. |
62 | 140 | if title or tags: | 139 | if title or tags: |
69 | 141 | # Load the json string into a Python object. | 140 | content['title'] = title |
70 | 142 | content_object = json.loads(content) | 141 | content['tags'] = tags |
65 | 143 | content_object['title'] = title | ||
66 | 144 | content_object['tags'] = tags | ||
67 | 145 | # Convert the Python object back into a JSON string. | ||
68 | 146 | content = json.dumps(content_object) | ||
71 | 147 | # Store the document in the database. Since we did not set a document | 142 | # Store the document in the database. Since we did not set a document |
72 | 148 | # id, the database will store it as a new document, and generate | 143 | # id, the database will store it as a new document, and generate |
73 | 149 | # a valid id. | 144 | # a valid id. |
77 | 150 | task = self.db.create_doc_from_json(content) | 145 | return self.db.create_doc(content) |
75 | 151 | # Wrap the document in a Task object. | ||
76 | 152 | return task | ||
78 | 153 | 146 | ||
79 | 154 | def save_task(self, task): | 147 | def save_task(self, task): |
80 | 155 | """Save task to the database.""" | 148 | """Save task to the database.""" |
81 | 156 | 149 | ||
82 | === modified file 'cosas/test_cosas.py' | |||
83 | --- cosas/test_cosas.py 2012-08-02 13:14:54 +0000 | |||
84 | +++ cosas/test_cosas.py 2012-08-21 13:44:17 +0000 | |||
85 | @@ -18,7 +18,7 @@ | |||
86 | 18 | 18 | ||
87 | 19 | from testtools import TestCase | 19 | from testtools import TestCase |
88 | 20 | from cosas import ( | 20 | from cosas import ( |
90 | 21 | Task, TodoStore, INDEXES, TAGS_INDEX, EMPTY_TASK, extract_tags) | 21 | Task, TodoStore, INDEXES, TAGS_INDEX, get_empty_task, extract_tags) |
91 | 22 | from u1db.backends import inmemory | 22 | from u1db.backends import inmemory |
92 | 23 | 23 | ||
93 | 24 | 24 | ||
94 | @@ -185,7 +185,7 @@ | |||
95 | 185 | super(TaskTestCase, self).setUp() | 185 | super(TaskTestCase, self).setUp() |
96 | 186 | self.db = inmemory.InMemoryDatabase("cosas") | 186 | self.db = inmemory.InMemoryDatabase("cosas") |
97 | 187 | self.db.set_document_factory(Task) | 187 | self.db.set_document_factory(Task) |
99 | 188 | self.document = self.db.create_doc_from_json(EMPTY_TASK) | 188 | self.document = self.db.create_doc(get_empty_task()) |
100 | 189 | 189 | ||
101 | 190 | def test_task(self): | 190 | def test_task(self): |
102 | 191 | """Initializing a task.""" | 191 | """Initializing a task.""" |
103 | 192 | 192 | ||
104 | === modified file 'cosas/ui.py' | |||
105 | --- cosas/ui.py 2012-08-13 16:09:21 +0000 | |||
106 | +++ cosas/ui.py 2012-08-21 13:44:17 +0000 | |||
107 | @@ -524,11 +524,13 @@ | |||
108 | 524 | db.set_oauth_credentials(**oauth_creds) | 524 | db.set_oauth_credentials(**oauth_creds) |
109 | 525 | db.open(create=True) | 525 | db.open(create=True) |
110 | 526 | syncer.sync() | 526 | syncer.sync() |
111 | 527 | # refresh the UI to show changed or new tasks | ||
112 | 527 | self.refresh_filter() | 528 | self.refresh_filter() |
113 | 528 | self.update_status_bar("last synced: %s" % (datetime.now(),)) | 529 | self.update_status_bar("last synced: %s" % (datetime.now(),)) |
114 | 529 | 530 | ||
115 | 530 | def resolve(self, doc, revs): | 531 | def resolve(self, doc, revs): |
116 | 531 | self.store.db.resolve_doc(doc, revs) | 532 | self.store.db.resolve_doc(doc, revs) |
117 | 533 | # refresh the UI to show the resolved version | ||
118 | 532 | self.refresh_filter() | 534 | self.refresh_filter() |
119 | 533 | 535 | ||
120 | 534 | 536 | ||
121 | 535 | 537 | ||
122 | === modified file 'html-docs/high-level-api.rst' | |||
123 | --- html-docs/high-level-api.rst 2012-08-17 19:00:38 +0000 | |||
124 | +++ html-docs/high-level-api.rst 2012-08-21 13:44:17 +0000 | |||
125 | @@ -19,8 +19,11 @@ | |||
126 | 19 | Creating and editing documents | 19 | Creating and editing documents |
127 | 20 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | 20 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
128 | 21 | 21 | ||
131 | 22 | To create a document, use ``create_doc()``. Code examples below are | 22 | To create a document, use :py:meth:`~u1db.Database.create_doc` or |
132 | 23 | from :ref:`reference-implementation` in Python. | 23 | :py:meth:`~u1db.Database.create_doc_from_json`. Code examples below are from |
133 | 24 | :ref:`reference-implementation` in Python. :py:meth:`~u1db.Database.create_doc` | ||
134 | 25 | takes a dictionary-like object, and | ||
135 | 26 | :py:meth:`~u1db.Database.create_doc_from_json` a JSON string. | ||
136 | 24 | 27 | ||
137 | 25 | .. testsetup :: | 28 | .. testsetup :: |
138 | 26 | 29 | ||
139 | @@ -43,10 +46,54 @@ | |||
140 | 43 | testdoc | 46 | testdoc |
141 | 44 | 47 | ||
142 | 45 | 48 | ||
147 | 46 | Editing an *existing* document is done with ``put_doc()``. This is separate | 49 | Retrieving documents |
148 | 47 | from ``create_doc()`` so as to avoid accidental overwrites. ``put_doc()`` takes | 50 | ^^^^^^^^^^^^^^^^^^^^ |
149 | 48 | a ``Document`` object, because the object encapsulates revision information for | 51 | |
150 | 49 | a particular document. | 52 | The simplest way to retrieve documents from a u1db is by calling |
151 | 53 | :py:meth:`~u1db.Database.get_doc` with a ``doc_id``. This will return a | ||
152 | 54 | :py:class:`~u1db.Document` object [#]_. | ||
153 | 55 | |||
154 | 56 | .. testcode :: | ||
155 | 57 | |||
156 | 58 | import u1db | ||
157 | 59 | db = u1db.open("mydb4.u1db", create=True) | ||
158 | 60 | doc = db.create_doc({"key": "value"}, doc_id="testdoc") | ||
159 | 61 | doc1 = db.get_doc("testdoc") | ||
160 | 62 | print doc1.content | ||
161 | 63 | print doc1.doc_id | ||
162 | 64 | |||
163 | 65 | .. testoutput :: | ||
164 | 66 | |||
165 | 67 | {u'key': u'value'} | ||
166 | 68 | testdoc | ||
167 | 69 | |||
168 | 70 | And it's also possible to retrieve many documents by ``doc_id``. | ||
169 | 71 | |||
170 | 72 | .. testcode :: | ||
171 | 73 | |||
172 | 74 | import u1db | ||
173 | 75 | db = u1db.open("mydb5.u1db", create=True) | ||
174 | 76 | doc1 = db.create_doc({"key": "value"}, doc_id="testdoc1") | ||
175 | 77 | doc2 = db.create_doc({"key": "value"}, doc_id="testdoc2") | ||
176 | 78 | for doc in db.get_docs(["testdoc2","testdoc1"]): | ||
177 | 79 | print doc.doc_id | ||
178 | 80 | |||
179 | 81 | .. testoutput :: | ||
180 | 82 | |||
181 | 83 | testdoc2 | ||
182 | 84 | testdoc1 | ||
183 | 85 | |||
184 | 86 | Note that :py:meth:`u1db.Database.get_docs` returns the documents in the order | ||
185 | 87 | specified. | ||
186 | 88 | |||
187 | 89 | Editing existing documents | ||
188 | 90 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
189 | 91 | |||
190 | 92 | Storing an *existing* document is done with :py:meth:`~u1db.Database.put_doc`. | ||
191 | 93 | This is separate from :py:meth:`~u1db.Database.create_doc` so as to avoid | ||
192 | 94 | accidental overwrites. :py:meth:`~u1db.Database.put_doc` takes a | ||
193 | 95 | :py:class:`~u1db.Document` object, because the object encapsulates revision | ||
194 | 96 | information for a particular document. | ||
195 | 50 | 97 | ||
196 | 51 | .. testcode :: | 98 | .. testcode :: |
197 | 52 | 99 | ||
198 | @@ -70,7 +117,7 @@ | |||
199 | 70 | Now editing the doc with the doc object we got back... | 117 | Now editing the doc with the doc object we got back... |
200 | 71 | {u'key1': u'edited'} | 118 | {u'key1': u'edited'} |
201 | 72 | 119 | ||
203 | 73 | Finally, deleting a document is done with ``delete_doc()``. | 120 | Finally, deleting a document is done with :py:meth:`~u1db.Database.delete_doc`. |
204 | 74 | 121 | ||
205 | 75 | .. testcode :: | 122 | .. testcode :: |
206 | 76 | 123 | ||
207 | @@ -87,43 +134,6 @@ | |||
208 | 87 | None | 134 | None |
209 | 88 | None | 135 | None |
210 | 89 | 136 | ||
211 | 90 | Retrieving documents | ||
212 | 91 | ^^^^^^^^^^^^^^^^^^^^ | ||
213 | 92 | |||
214 | 93 | The simplest way to retrieve documents from a u1db is by ``doc_id``. | ||
215 | 94 | |||
216 | 95 | .. testcode :: | ||
217 | 96 | |||
218 | 97 | import u1db | ||
219 | 98 | db = u1db.open("mydb4.u1db", create=True) | ||
220 | 99 | doc = db.create_doc({"key": "value"}, doc_id="testdoc") | ||
221 | 100 | doc1 = db.get_doc("testdoc") | ||
222 | 101 | print doc1.content | ||
223 | 102 | print doc1.doc_id | ||
224 | 103 | |||
225 | 104 | .. testoutput :: | ||
226 | 105 | |||
227 | 106 | {u'key': u'value'} | ||
228 | 107 | testdoc | ||
229 | 108 | |||
230 | 109 | And it's also possible to retrieve many documents by ``doc_id``. | ||
231 | 110 | |||
232 | 111 | .. testcode :: | ||
233 | 112 | |||
234 | 113 | import u1db | ||
235 | 114 | db = u1db.open("mydb5.u1db", create=True) | ||
236 | 115 | doc1 = db.create_doc({"key": "value"}, doc_id="testdoc1") | ||
237 | 116 | doc2 = db.create_doc({"key": "value"}, doc_id="testdoc2") | ||
238 | 117 | for doc in db.get_docs(["testdoc2","testdoc1"]): | ||
239 | 118 | print doc.doc_id | ||
240 | 119 | |||
241 | 120 | .. testoutput :: | ||
242 | 121 | |||
243 | 122 | testdoc2 | ||
244 | 123 | testdoc1 | ||
245 | 124 | |||
246 | 125 | Note that ``get_docs()`` returns the documents in the order specified. | ||
247 | 126 | |||
248 | 127 | Document functions | 137 | Document functions |
249 | 128 | ^^^^^^^^^^^^^^^^^^ | 138 | ^^^^^^^^^^^^^^^^^^ |
250 | 129 | 139 | ||
251 | @@ -382,20 +392,25 @@ | |||
252 | 382 | Synchronising a database can result in conflicts; if your user changes the same | 392 | Synchronising a database can result in conflicts; if your user changes the same |
253 | 383 | document in two different places and then syncs again, that document will be | 393 | document in two different places and then syncs again, that document will be |
254 | 384 | ''in conflict'', meaning that it has incompatible changes. If this is the case, | 394 | ''in conflict'', meaning that it has incompatible changes. If this is the case, |
261 | 385 | ``doc.has_conflicts`` will be true, and put_doc to a conflicted doc will give | 395 | :py:attr:`~u1db.Document.has_conflicts` will be true, and put_doc to a |
262 | 386 | a ``ConflictedDoc`` error. To get a list of conflicted versions of the | 396 | conflicted doc will give a ``ConflictedDoc`` error. To get a list of conflicted |
263 | 387 | document, do ``get_doc_conflicts(doc_id)``. Deciding what the final | 397 | versions of the document, do :py:meth:`~u1db.Database.get_doc_conflicts`. |
264 | 388 | unconflicted document should look like is obviously specific to the user's | 398 | Deciding what the final unconflicted document should look like is obviously |
265 | 389 | application; once decided, call ``resolve_doc(doc, list_of_conflicted_revisions)`` | 399 | specific to the user's application; once decided, call |
266 | 390 | to resolve and set the final resolved content. | 400 | :py:meth:`~u1db.Database.resolve_doc` to resolve and set the final resolved |
267 | 401 | content. | ||
268 | 391 | 402 | ||
270 | 392 | Synchronising functions | 403 | Synchronising Functions |
271 | 393 | ^^^^^^^^^^^^^^^^^^^^^^^ | 404 | ^^^^^^^^^^^^^^^^^^^^^^^ |
272 | 394 | 405 | ||
273 | 395 | * :py:meth:`~u1db.Database.sync` | 406 | * :py:meth:`~u1db.Database.sync` |
274 | 396 | * :py:meth:`~u1db.Database.get_doc_conflicts` | 407 | * :py:meth:`~u1db.Database.get_doc_conflicts` |
275 | 397 | * :py:meth:`~u1db.Database.resolve_doc` | 408 | * :py:meth:`~u1db.Database.resolve_doc` |
276 | 398 | 409 | ||
277 | 410 | .. [#] Alternatively if a factory function was passed into | ||
278 | 411 | :py:func:`u1db.open`, :py:meth:`~u1db.Database.get_doc` will return | ||
279 | 412 | whatever type of object the factory function returns. | ||
280 | 413 | |||
281 | 399 | .. testcleanup :: | 414 | .. testcleanup :: |
282 | 400 | 415 | ||
283 | 401 | os.chdir(old_dir) | 416 | os.chdir(old_dir) |
284 | 402 | 417 | ||
285 | === modified file 'html-docs/tutorial.rst' | |||
286 | --- html-docs/tutorial.rst 2012-08-17 21:22:56 +0000 | |||
287 | +++ html-docs/tutorial.rst 2012-08-21 13:44:17 +0000 | |||
288 | @@ -2,13 +2,13 @@ | |||
289 | 2 | ######## | 2 | ######## |
290 | 3 | 3 | ||
291 | 4 | In this tutorial we will demonstrate what goes into creating an application | 4 | In this tutorial we will demonstrate what goes into creating an application |
293 | 5 | that uses u1db as a backend. We will use code from the simple todo list | 5 | that uses u1db as a backend. We will use code samples from the simple todo list |
294 | 6 | application 'Cosas' as our example. The full source code to Cosas can be found | 6 | application 'Cosas' as our example. The full source code to Cosas can be found |
295 | 7 | in the u1db source tree. It comes with a user interface, but we will only | 7 | in the u1db source tree. It comes with a user interface, but we will only |
296 | 8 | focus on the code that interacts with u1db here. | 8 | focus on the code that interacts with u1db here. |
297 | 9 | 9 | ||
300 | 10 | Tasks | 10 | Defining the Task Object |
301 | 11 | ----- | 11 | ------------------------ |
302 | 12 | 12 | ||
303 | 13 | First we need to define what we'll actually store in u1db. For a todo list | 13 | First we need to define what we'll actually store in u1db. For a todo list |
304 | 14 | application, it makes sense to have each todo item or task be a single | 14 | application, it makes sense to have each todo item or task be a single |
305 | @@ -27,13 +27,9 @@ | |||
306 | 27 | 27 | ||
307 | 28 | .. code-block:: python | 28 | .. code-block:: python |
308 | 29 | 29 | ||
316 | 30 | ''' | 30 | '{"title": "the task at hand", |
317 | 31 | { | 31 | "done": false, |
318 | 32 | "title": "the task at hand", | 32 | "tags": ["urgent", "priority 1", "today"]}' |
312 | 33 | "done": false, | ||
313 | 34 | "tags": ["urgent", "priority 1", "today"] | ||
314 | 35 | } | ||
315 | 36 | ''' | ||
319 | 37 | 33 | ||
320 | 38 | We can define ``Task`` as follows: | 34 | We can define ``Task`` as follows: |
321 | 39 | 35 | ||
322 | @@ -128,3 +124,302 @@ | |||
323 | 128 | 124 | ||
324 | 129 | True | 125 | True |
325 | 130 | 126 | ||
326 | 127 | This is all we need the task object to do: as long as we have a way to store | ||
327 | 128 | all its data in the .content dictionary, the super class will take care of | ||
328 | 129 | converting that into JSON so it can be stored in the database. | ||
329 | 130 | |||
330 | 131 | For convenience, we can create a function that returns a fresh copy of the | ||
331 | 132 | content that would make up an empty task: | ||
332 | 133 | |||
333 | 134 | .. code-block:: python | ||
334 | 135 | |||
335 | 136 | EMPTY_TASK = {"title": "", "done": False, "tags": []} | ||
336 | 137 | |||
337 | 138 | get_empty_task = lambda: copy.deepcopy(EMPTY_TASK) | ||
338 | 139 | |||
339 | 140 | Defining Indexes | ||
340 | 141 | ---------------- | ||
341 | 142 | |||
342 | 143 | Now that we have tasks defined, we will probably want to query the database | ||
343 | 144 | using their properties. To that end, we will need to use indexes. Let's define | ||
344 | 145 | two for now, one to query by tags, and one to query by done status. We'll | ||
345 | 146 | define some global constants with the name and the definition of the indexes, | ||
346 | 147 | which will make them easier to refer to in the rest of the code: | ||
347 | 148 | |||
348 | 149 | .. code-block:: python | ||
349 | 150 | |||
350 | 151 | TAGS_INDEX = 'tags' | ||
351 | 152 | DONE_INDEX = 'done' | ||
352 | 153 | INDEXES = { | ||
353 | 154 | TAGS_INDEX: ['tags'], | ||
354 | 155 | DONE_INDEX: ['bool(done)'], | ||
355 | 156 | } | ||
356 | 157 | |||
357 | 158 | ``INDEXES`` is just a regular dictionary, with the names of the indexes as | ||
358 | 159 | keys, and the index definitions, which are lists of expressions as values. (We | ||
359 | 160 | chose to use lists since an index can be defined on multiple fields, though | ||
360 | 161 | both of the indexes defined above only index a single field.) | ||
361 | 162 | |||
362 | 163 | The ``tags`` index will index any document that has a top level field ``tags`` | ||
363 | 164 | and index its value. Our tasks will have a list value under ``tags`` which | ||
364 | 165 | means that u1db will index each task for each of the values in the list in this | ||
365 | 166 | index. So a task with the following content: | ||
366 | 167 | |||
367 | 168 | .. code-block:: python | ||
368 | 169 | |||
369 | 170 | { | ||
370 | 171 | "title": "Buy sausages and vimto", | ||
371 | 172 | "tags": ["shopping", "food"], | ||
372 | 173 | "done": false | ||
373 | 174 | } | ||
374 | 175 | |||
375 | 176 | Would be indexed under both ``"food"`` and ``"shopping"``. | ||
376 | 177 | |||
377 | 178 | The ``done`` index will index any document that has a boolean value in a top | ||
378 | 179 | level field with the name ``done``. | ||
379 | 180 | |||
380 | 181 | We will see how the indexes are actually created and queried below. | ||
381 | 182 | |||
382 | 183 | Storing and Retrieving Tasks | ||
383 | 184 | ---------------------------- | ||
384 | 185 | |||
385 | 186 | To store and retrieve our task objects we'll need a u1db | ||
386 | 187 | :py:class:`~u1db.Database`. We can make a little helper function to get a | ||
387 | 188 | reference to our application's database, and create it if it doesn't already | ||
388 | 189 | exist: | ||
389 | 190 | |||
390 | 191 | |||
391 | 192 | .. code-block:: python | ||
392 | 193 | |||
393 | 194 | from dirspec.basedir import save_data_path | ||
394 | 195 | |||
395 | 196 | def get_database(): | ||
396 | 197 | """Get the path that the database is stored in.""" | ||
397 | 198 | return u1db.open( | ||
398 | 199 | os.path.join(save_data_path("cosas"), "cosas.u1db"), create=True, | ||
399 | 200 | document_factory=Task) | ||
400 | 201 | |||
401 | 202 | There are a few things to note here: First of all, we use | ||
402 | 203 | `lp:dirspec <http://launchpad.net/dirspec/>`_ to handle where to find or put | ||
403 | 204 | the database in a way that works across platforms. This is not something | ||
404 | 205 | specific to u1db, so you could choose to use it for your own application or | ||
405 | 206 | not: :py:func:`u1db.open` will happily take any filesystem path. Secondly, we | ||
406 | 207 | pass our Task class into the ``document_factory`` argument of | ||
407 | 208 | :py:func:`u1db.open`. This means that any time we get documents from the | ||
408 | 209 | database, it will return Task objects, so we don't have to do the conversion in | ||
409 | 210 | our code. | ||
410 | 211 | |||
411 | 212 | Now we create a TodoStore class that will handle all interactions with the | ||
412 | 213 | database: | ||
413 | 214 | |||
414 | 215 | .. code-block:: python | ||
415 | 216 | |||
416 | 217 | class TodoStore(object): | ||
417 | 218 | """The todo application backend.""" | ||
418 | 219 | |||
419 | 220 | def __init__(self, db): | ||
420 | 221 | self.db = db | ||
421 | 222 | |||
422 | 223 | def initialize_db(self): | ||
423 | 224 | """Initialize the database.""" | ||
424 | 225 | # Ask the database for currently existing indexes. | ||
425 | 226 | db_indexes = dict(self.db.list_indexes()) | ||
426 | 227 | # Loop through the indexes we expect to find. | ||
427 | 228 | for name, expression in INDEXES.items(): | ||
428 | 229 | if name not in db_indexes: | ||
429 | 230 | # The index does not yet exist. | ||
430 | 231 | self.db.create_index(name, *expression) | ||
431 | 232 | continue | ||
432 | 233 | if expression == db_indexes[name]: | ||
433 | 234 | # The index exists and is up to date. | ||
434 | 235 | continue | ||
435 | 236 | # The index exists but the definition is not what expected, so we | ||
436 | 237 | # delete it and add the proper index expression. | ||
437 | 238 | self.db.delete_index(name) | ||
438 | 239 | self.db.create_index(name, *expression) | ||
439 | 240 | |||
440 | 241 | The ``initialize_db()`` method checks whether the database already has the | ||
441 | 242 | indexes we defined above and if it doesn't or if the definition is different | ||
442 | 243 | than the one we have, the index is (re)created. We will call this method every | ||
443 | 244 | time we start the application, to make sure all the indexes are up to date. | ||
444 | 245 | Creating an index is a matter of calling :py:meth:`~u1db.Database.create_index` | ||
445 | 246 | with a name and the expressions that define the index. This will immediately | ||
446 | 247 | index all documents already in the database, and afterwards any that are added | ||
447 | 248 | or updated. | ||
448 | 249 | |||
449 | 250 | .. code-block:: python | ||
450 | 251 | |||
451 | 252 | def get_all_tags(self): | ||
452 | 253 | """Get all tags in use in the entire database.""" | ||
453 | 254 | return [key[0] for key in self.db.get_index_keys(TAGS_INDEX)] | ||
454 | 255 | |||
455 | 256 | The py:meth:`~u1db.Database.get_index_keys` method gets a list of all indexed | ||
456 | 257 | *values* from an index. In this case it will give us a list of all tags that | ||
457 | 258 | have been used in the database, which can be useful if we want to present them | ||
458 | 259 | in the user interface of our application. | ||
459 | 260 | |||
460 | 261 | .. code-block:: python | ||
461 | 262 | |||
462 | 263 | def get_tasks_by_tags(self, tags): | ||
463 | 264 | """Get all tasks that have every tag in tags.""" | ||
464 | 265 | if not tags: | ||
465 | 266 | # No tags specified, so return all tasks. | ||
466 | 267 | return self.get_all_tasks() | ||
467 | 268 | # Get all tasks for the first tag. | ||
468 | 269 | results = dict( | ||
469 | 270 | (doc.doc_id, doc) for doc in | ||
470 | 271 | self.db.get_from_index(TAGS_INDEX, tags[0])) | ||
471 | 272 | # Now loop over the rest of the tags (if any) and remove from the | ||
472 | 273 | # results any document that does not have that particular tag. | ||
473 | 274 | for tag in tags[1:]: | ||
474 | 275 | # Get the ids of all documents with this tag. | ||
475 | 276 | ids = [ | ||
476 | 277 | doc.doc_id for doc in self.db.get_from_index(TAGS_INDEX, tag)] | ||
477 | 278 | for key in results.keys(): | ||
478 | 279 | if key not in ids: | ||
479 | 280 | # Remove the document from result, because it does not have | ||
480 | 281 | # this particular tag. | ||
481 | 282 | del results[key] | ||
482 | 283 | if not results: | ||
483 | 284 | # If results is empty, we're done: there are no | ||
484 | 285 | # documents with all tags. | ||
485 | 286 | return [] | ||
486 | 287 | return results.values() | ||
487 | 288 | |||
488 | 289 | This method gives us a way to query the database by a set of tags. We loop | ||
489 | 290 | through the tags one by one and then filter out any documents that don't have | ||
490 | 291 | that particular tag. | ||
491 | 292 | |||
492 | 293 | .. code-block:: python | ||
493 | 294 | |||
494 | 295 | def get_task(self, doc_id): | ||
495 | 296 | """Get a task from the database.""" | ||
496 | 297 | task = self.db.get_doc(doc_id) | ||
497 | 298 | if task is None: | ||
498 | 299 | # No document with that id exists in the database. | ||
499 | 300 | raise KeyError("No task with id '%s'." % (doc_id,)) | ||
500 | 301 | if task.is_tombstone(): | ||
501 | 302 | # The document id exists, but the document's content was previously | ||
502 | 303 | # deleted. | ||
503 | 304 | raise KeyError("Task with id %s was deleted." % (doc_id,)) | ||
504 | 305 | return task | ||
505 | 306 | |||
506 | 307 | ``get_task`` is a thin wrapper around :py:meth:`~u1db.Database.get_doc` that | ||
507 | 308 | takes care of raising appropriate exceptions when a document does not exist or | ||
508 | 309 | has been deleted. (Deleted documents leave a 'tombstone' behind, which is | ||
509 | 310 | necessary to make sure that synchronisation of the database with other replicas | ||
510 | 311 | does the right thing.) | ||
511 | 312 | |||
512 | 313 | .. code-block:: python | ||
513 | 314 | |||
514 | 315 | def new_task(self, title=None, tags=None): | ||
515 | 316 | """Create a new task document.""" | ||
516 | 317 | if tags is None: | ||
517 | 318 | tags = [] | ||
518 | 319 | # We make a fresh copy of a pristine task with no title. | ||
519 | 320 | content = get_empty_task() | ||
520 | 321 | # If we were passed a title or tags, or both, we set them in the object | ||
521 | 322 | # before storing it in the database. | ||
522 | 323 | if title or tags: | ||
523 | 324 | content['title'] = title | ||
524 | 325 | content['tags'] = tags | ||
525 | 326 | # Store the document in the database. Since we did not set a document | ||
526 | 327 | # id, the database will store it as a new document, and generate | ||
527 | 328 | # a valid id. | ||
528 | 329 | return self.db.create_doc(content) | ||
529 | 330 | |||
530 | 331 | Here we use the convenience function defined above to initialize the content, | ||
531 | 332 | and then set the properties that were passed into ``new_task``. We call | ||
532 | 333 | :py:meth:`~u1db.Database.create_doc` to create a new document from the content. | ||
533 | 334 | This creates the document in the database, assigns it a new unique id (unless | ||
534 | 335 | we pass one in,) and returns a fully initialized Task object. (Since we made | ||
535 | 336 | that the database's factory.) | ||
536 | 337 | |||
537 | 338 | .. code-block:: python | ||
538 | 339 | |||
539 | 340 | def get_all_tasks(self): | ||
540 | 341 | return self.db.get_from_index(DONE_INDEX, "*") | ||
541 | 342 | |||
542 | 343 | |||
543 | 344 | Since the ``DONE_INDEX`` indexes anything that has a value in the field "done", | ||
544 | 345 | and all tasks do (either True or False), it's a good way to get all tasks out | ||
545 | 346 | of the database, especially since it will sort them by done status, so we'll | ||
546 | 347 | get all the active tasks first. | ||
547 | 348 | |||
548 | 349 | Synchronisation and Conflicts | ||
549 | 350 | ----------------------------- | ||
550 | 351 | |||
551 | 352 | Synchronisation has to be initiated by the application, either periodically, | ||
552 | 353 | while it's running, or by having the user initiate it. Any | ||
553 | 354 | :py:class:`u1db.Database` can be synchronised with any other, either by file | ||
554 | 355 | path or URL. Cosas gives the user the choice between manually synchronising or | ||
555 | 356 | having it happen automatically, every 30 minutes, for as long as it is running. | ||
556 | 357 | |||
557 | 358 | .. code-block:: python | ||
558 | 359 | |||
559 | 360 | from ubuntuone.platform.credentials import CredentialsManagementTool | ||
560 | 361 | |||
561 | 362 | def get_ubuntuone_credentials(self): | ||
562 | 363 | cmt = CredentialsManagementTool() | ||
563 | 364 | return cmt.find_credentials() | ||
564 | 365 | |||
565 | 366 | def _synchronize(self, creds=None): | ||
566 | 367 | target = self.sync_target | ||
567 | 368 | if target.startswith('http://') or target.startswith('https://'): | ||
568 | 369 | st = HTTPSyncTarget.connect(target) | ||
569 | 370 | oauth_creds = { | ||
570 | 371 | 'token_key': creds['token'], | ||
571 | 372 | 'token_secret': creds['token_secret'], | ||
572 | 373 | 'consumer_key': creds['consumer_key'], | ||
573 | 374 | 'consumer_secret': creds['consumer_secret']} | ||
574 | 375 | if creds: | ||
575 | 376 | st.set_oauth_credentials(**oauth_creds) | ||
576 | 377 | else: | ||
577 | 378 | db = u1db.open(target, create=True) | ||
578 | 379 | st = db.get_sync_target() | ||
579 | 380 | syncer = Synchronizer(self.store.db, st) | ||
580 | 381 | try: | ||
581 | 382 | syncer.sync() | ||
582 | 383 | except DatabaseDoesNotExist: | ||
583 | 384 | # The server does not yet have the database, so create it. | ||
584 | 385 | if target.startswith('http://') or target.startswith('https://'): | ||
585 | 386 | db = HTTPDatabase(target) | ||
586 | 387 | db.set_oauth_credentials(**oauth_creds) | ||
587 | 388 | db.open(create=True) | ||
588 | 389 | syncer.sync() | ||
589 | 390 | # refresh the UI to show changed or new tasks | ||
590 | 391 | self.refresh_filter() | ||
591 | 392 | |||
592 | 393 | def synchronize(self, finalize): | ||
593 | 394 | if self.sync_target == 'https://u1db.one.ubuntu.com/~/cosas': | ||
594 | 395 | d = self.get_ubuntuone_credentials() | ||
595 | 396 | d.addCallback(self._synchronize) | ||
596 | 397 | d.addCallback(finalize) | ||
597 | 398 | else: | ||
598 | 399 | self._synchronize() | ||
599 | 400 | finalize() | ||
600 | 401 | |||
601 | 402 | When synchronising over http(s), servers can (and usually will) require OAuth | ||
602 | 403 | authentication. The code above shows how to acquire and pass in the oauth | ||
603 | 404 | credentials for the Ubuntu One server, in case you want your application to | ||
604 | 405 | synchronize with that. | ||
605 | 406 | |||
606 | 407 | After synchronising with another replica, it is possible that one or more | ||
607 | 408 | conflicts have arisen, if both replicas independently made changes to the same | ||
608 | 409 | document. Your application should probably check for conflicts after every | ||
609 | 410 | synchronisation, and offer the user a way to resolve them. | ||
610 | 411 | |||
611 | 412 | Look at the Conflicts class in cosas/ui.py to see an example of how this could | ||
612 | 413 | be presented to the user. The idea is that you show the conflicting versions to | ||
613 | 414 | the user, let them pick one, and then call | ||
614 | 415 | :py:meth:`~u1db.Database.resolve_doc` with the preferred version, and all the | ||
615 | 416 | revisions of the conflicting versions it is meant to resolve. | ||
616 | 417 | |||
617 | 418 | .. code-block:: python | ||
618 | 419 | |||
619 | 420 | def resolve(self, doc, revs): | ||
620 | 421 | self.store.db.resolve_doc(doc, revs) | ||
621 | 422 | # refresh the UI to show the resolved version | ||
622 | 423 | self.refresh_filter() | ||
623 | 424 | |||
624 | 425 | |||
625 | 131 | 426 | ||
626 | === modified file 'u1db/__init__.py' | |||
627 | --- u1db/__init__.py 2012-08-20 12:55:06 +0000 | |||
628 | +++ u1db/__init__.py 2012-08-21 13:44:17 +0000 | |||
629 | @@ -189,12 +189,16 @@ | |||
630 | 189 | Creating an index will block until the expressions have been evaluated | 189 | Creating an index will block until the expressions have been evaluated |
631 | 190 | and the index generated. | 190 | and the index generated. |
632 | 191 | 191 | ||
635 | 192 | :index_name: A unique name which can be used as a key prefix | 192 | :param index_name: A unique name which can be used as a key prefix |
636 | 193 | :index_expressions: index expressions defining the index information. | 193 | :param index_expressions: index expressions defining the index |
637 | 194 | information. | ||
638 | 195 | |||
639 | 194 | Examples: | 196 | Examples: |
643 | 195 | "fieldname" to index alphabetically sorted on field. | 197 | |
644 | 196 | "number(fieldname, width)", "lower(fieldname)", | 198 | "fieldname", or "fieldname.subfieldname" to index alphabetically |
645 | 197 | "fieldname.subfieldname" | 199 | sorted on the contents of a field. |
646 | 200 | |||
647 | 201 | "number(fieldname, width)", "lower(fieldname)" | ||
648 | 198 | """ | 202 | """ |
649 | 199 | raise NotImplementedError(self.create_index) | 203 | raise NotImplementedError(self.create_index) |
650 | 200 | 204 | ||
651 | @@ -202,7 +206,6 @@ | |||
652 | 202 | """Remove a named index. | 206 | """Remove a named index. |
653 | 203 | 207 | ||
654 | 204 | :param index_name: The name of the index we are removing | 208 | :param index_name: The name of the index we are removing |
655 | 205 | :return: None | ||
656 | 206 | """ | 209 | """ |
657 | 207 | raise NotImplementedError(self.delete_index) | 210 | raise NotImplementedError(self.delete_index) |
658 | 208 | 211 | ||
659 | @@ -223,11 +226,11 @@ | |||
660 | 223 | It is also possible to append a '*' to the last supplied value (eg | 226 | It is also possible to append a '*' to the last supplied value (eg |
661 | 224 | 'val*', '*', '*' or 'val', 'val*', '*', but not 'val*', 'val', '*') | 227 | 'val*', '*', '*' or 'val', 'val*', '*', but not 'val*', 'val', '*') |
662 | 225 | 228 | ||
663 | 226 | :return: List of [Document] | ||
664 | 227 | :param index_name: The index to query | 229 | :param index_name: The index to query |
665 | 228 | :param key_values: values to match. eg, if you have | 230 | :param key_values: values to match. eg, if you have |
666 | 229 | an index with 3 fields then you would have: | 231 | an index with 3 fields then you would have: |
667 | 230 | get_from_index(index_name, val1, val2, val3) | 232 | get_from_index(index_name, val1, val2, val3) |
668 | 233 | :return: List of [Document] | ||
669 | 231 | """ | 234 | """ |
670 | 232 | raise NotImplementedError(self.get_from_index) | 235 | raise NotImplementedError(self.get_from_index) |
671 | 233 | 236 | ||
672 | @@ -244,7 +247,6 @@ | |||
673 | 244 | possible to append a '*' to the last supplied value (eg 'val*', '*', | 247 | possible to append a '*' to the last supplied value (eg 'val*', '*', |
674 | 245 | '*' or 'val', 'val*', '*', but not 'val*', 'val', '*') | 248 | '*' or 'val', 'val*', '*', but not 'val*', 'val', '*') |
675 | 246 | 249 | ||
676 | 247 | :return: List of [Document] | ||
677 | 248 | :param index_name: The index to query | 250 | :param index_name: The index to query |
678 | 249 | :param start_values: tuples of values that define the lower bound of | 251 | :param start_values: tuples of values that define the lower bound of |
679 | 250 | the range. eg, if you have an index with 3 fields then you would | 252 | the range. eg, if you have an index with 3 fields then you would |
680 | @@ -252,14 +254,15 @@ | |||
681 | 252 | :param end_values: tuples of values that define the upper bound of the | 254 | :param end_values: tuples of values that define the upper bound of the |
682 | 253 | range. eg, if you have an index with 3 fields then you would have: | 255 | range. eg, if you have an index with 3 fields then you would have: |
683 | 254 | (val1, val2, val3) | 256 | (val1, val2, val3) |
684 | 257 | :return: List of [Document] | ||
685 | 255 | """ | 258 | """ |
686 | 256 | raise NotImplementedError(self.get_range_from_index) | 259 | raise NotImplementedError(self.get_range_from_index) |
687 | 257 | 260 | ||
688 | 258 | def get_index_keys(self, index_name): | 261 | def get_index_keys(self, index_name): |
689 | 259 | """Return all keys under which documents are indexed in this index. | 262 | """Return all keys under which documents are indexed in this index. |
690 | 260 | 263 | ||
691 | 264 | :param index_name: The index to query | ||
692 | 261 | :return: [] A list of tuples of indexed keys. | 265 | :return: [] A list of tuples of indexed keys. |
693 | 262 | :param index_name: The index to query | ||
694 | 263 | """ | 266 | """ |
695 | 264 | raise NotImplementedError(self.get_index_keys) | 267 | raise NotImplementedError(self.get_index_keys) |
696 | 265 | 268 | ||
697 | @@ -286,8 +289,6 @@ | |||
698 | 286 | :param doc: A Document with the new content to be inserted. | 289 | :param doc: A Document with the new content to be inserted. |
699 | 287 | :param conflicted_doc_revs: A list of revisions that the new content | 290 | :param conflicted_doc_revs: A list of revisions that the new content |
700 | 288 | supersedes. | 291 | supersedes. |
701 | 289 | :return: None, doc will be updated with the new revision and | ||
702 | 290 | has_conflict flags. | ||
703 | 291 | """ | 292 | """ |
704 | 292 | raise NotImplementedError(self.resolve_doc) | 293 | raise NotImplementedError(self.resolve_doc) |
705 | 293 | 294 | ||
706 | @@ -303,7 +304,14 @@ | |||
707 | 303 | raise NotImplementedError(self.close) | 304 | raise NotImplementedError(self.close) |
708 | 304 | 305 | ||
709 | 305 | def sync(self, url): | 306 | def sync(self, url): |
711 | 306 | """Synchronize documents with remote replica exposed at url.""" | 307 | """Synchronize documents with remote replica exposed at url. |
712 | 308 | |||
713 | 309 | :param url: the url of the target replica to sync with. | ||
714 | 310 | :return: local_gen_before_sync The local generation before the | ||
715 | 311 | synchronisation was performed. This is useful to pass into | ||
716 | 312 | whatschanged, if an application wants to know which documents were | ||
717 | 313 | affected by a synchronisation. | ||
718 | 314 | """ | ||
719 | 307 | from u1db.sync import Synchronizer | 315 | from u1db.sync import Synchronizer |
720 | 308 | from u1db.remote.http_target import HTTPSyncTarget | 316 | from u1db.remote.http_target import HTTPSyncTarget |
721 | 309 | return Synchronizer(self, HTTPSyncTarget(url)).sync() | 317 | return Synchronizer(self, HTTPSyncTarget(url)).sync() |
722 | @@ -337,7 +345,6 @@ | |||
723 | 337 | :param other_generation: The generation number for the other replica. | 345 | :param other_generation: The generation number for the other replica. |
724 | 338 | :param other_transaction_id: The transaction id associated with the | 346 | :param other_transaction_id: The transaction id associated with the |
725 | 339 | generation. | 347 | generation. |
726 | 340 | :return: None | ||
727 | 341 | """ | 348 | """ |
728 | 342 | raise NotImplementedError(self._set_replica_gen_and_trans_id) | 349 | raise NotImplementedError(self._set_replica_gen_and_trans_id) |
729 | 343 | 350 | ||
730 | @@ -588,8 +595,8 @@ | |||
731 | 588 | :param source_replica_uid: Another replica which we might have | 595 | :param source_replica_uid: Another replica which we might have |
732 | 589 | synchronized with in the past. | 596 | synchronized with in the past. |
733 | 590 | :return: (target_replica_uid, target_replica_generation, | 597 | :return: (target_replica_uid, target_replica_generation, |
736 | 591 | target_trans_id, source_replica_last_known_generation, | 598 | target_trans_id, source_replica_last_known_generation, |
737 | 592 | source_replica_last_known_transaction_id) | 599 | source_replica_last_known_transaction_id) |
738 | 593 | """ | 600 | """ |
739 | 594 | raise NotImplementedError(self.get_sync_info) | 601 | raise NotImplementedError(self.get_sync_info) |
740 | 595 | 602 | ||
741 | @@ -609,10 +616,9 @@ | |||
742 | 609 | 616 | ||
743 | 610 | :param source_replica_uid: The identifier for the source replica. | 617 | :param source_replica_uid: The identifier for the source replica. |
744 | 611 | :param source_replica_generation: | 618 | :param source_replica_generation: |
746 | 612 | The database generation for the source replica. | 619 | The database generation for the source replica. |
747 | 613 | :param source_replica_transaction_id: The transaction id associated | 620 | :param source_replica_transaction_id: The transaction id associated |
748 | 614 | with the source replica generation. | 621 | with the source replica generation. |
749 | 615 | :return: None | ||
750 | 616 | """ | 622 | """ |
751 | 617 | raise NotImplementedError(self.record_sync_info) | 623 | raise NotImplementedError(self.record_sync_info) |
752 | 618 | 624 | ||
753 | @@ -646,10 +652,10 @@ | |||
754 | 646 | :param last_known_trans_id: The last transaction id that the source | 652 | :param last_known_trans_id: The last transaction id that the source |
755 | 647 | replica knows about this target replica | 653 | replica knows about this target replica |
756 | 648 | :param: return_doc_cb(doc, gen): is a callback | 654 | :param: return_doc_cb(doc, gen): is a callback |
761 | 649 | used to return documents to the source replica, it will | 655 | used to return documents to the source replica, it will |
762 | 650 | be invoked in turn with Documents that have changed since | 656 | be invoked in turn with Documents that have changed since |
763 | 651 | last_known_generation together with the generation of | 657 | last_known_generation together with the generation of |
764 | 652 | their last change. | 658 | their last change. |
765 | 653 | :return: new_generation - After applying docs_by_generation, this is | 659 | :return: new_generation - After applying docs_by_generation, this is |
766 | 654 | the current generation for this replica | 660 | the current generation for this replica |
767 | 655 | """ | 661 | """ |
When syncronising over http(s) servers can (and usually will) require OAuth -> When synchronising over http(s), servers can (and usually will) require OAuth
The section about editing docs (":py:meth: `~u1db. Database. put_doc` takes a :py:class: `~u1db. Document` object, because the object encapsulates revision information for a particular document") should, I think, say something like "the u1db.Document object is what you get from get_doc", so that people know what a Document object is. However, get_doc is described *after* put_doc. The get_doc bit also doesn't explain that it returns a Document at all. This whole section probably needs changing to reflect that (almost) everything is done with Document objects, you get a Document from get_doc, and you save that Document object back after making changes with it, so the procedure for altering an existing db entry is get_doc, edit the returned Document, put_doc it.
The section on the tags index ("The ``tags`` index will index any document that has a top level field ``tags`` and index its value.") could benefit from an example showing that a document with tags ["shopping", "food"] goes into the index twice. (I know this is explained in more detail elsewhere, but it'd be handy to help people understand if it's presented in context.)