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