Merge lp:~thisfred/u1db/documentation5 into lp:u1db

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
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.
Revision history for this message
Stuart Langridge (sil) wrote :

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.)

review: Needs Fixing
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 """

Subscribers

People subscribed via source and target branches