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
=== modified file 'cosas/cosas.py'
--- cosas/cosas.py 2012-08-17 20:52:21 +0000
+++ cosas/cosas.py 2012-08-21 13:44:17 +0000
@@ -16,14 +16,13 @@
1616
17"""cosas example application."""17"""cosas example application."""
1818
19import json
20import os19import os
21import re20import re
22import u1db21import u1db
22import copy
2323
24from dirspec.basedir import save_data_path24from dirspec.basedir import save_data_path
2525
26EMPTY_TASK = json.dumps({"title": "", "done": False, "tags": []})
2726
28TAGS_INDEX = 'tags'27TAGS_INDEX = 'tags'
29DONE_INDEX = 'done'28DONE_INDEX = 'done'
@@ -34,10 +33,13 @@
3433
35TAGS = re.compile('#(\w+)|\[(.+)\]')34TAGS = re.compile('#(\w+)|\[(.+)\]')
3635
36EMPTY_TASK = {"title": "", "done": False, "tags": []}
37
38get_empty_task = lambda: copy.deepcopy(EMPTY_TASK)
39
3740
38def get_database():41def get_database():
39 """Get the path that the database is stored in."""42 """Get the path that the database is stored in."""
40
41 # setting document_factory to Task means that any database method that43 # setting document_factory to Task means that any database method that
42 # would return documents, now returns Tasks instead.44 # would return documents, now returns Tasks instead.
43 return u1db.open(45 return u1db.open(
@@ -108,7 +110,6 @@
108 # If results is empty, we're done: there are no110 # If results is empty, we're done: there are no
109 # documents with all tags.111 # documents with all tags.
110 return []112 return []
111 # Wrap each document in results in a Task object, and return them.
112 return results.values()113 return results.values()
113114
114 def get_task(self, doc_id):115 def get_task(self, doc_id):
@@ -121,7 +122,6 @@
121 # The document id exists, but the document's content was previously122 # The document id exists, but the document's content was previously
122 # deleted.123 # deleted.
123 raise KeyError("Task with id %s was deleted." % (doc_id,))124 raise KeyError("Task with id %s was deleted." % (doc_id,))
124 # Wrap the document in a Task object and return it.
125 return task125 return task
126126
127 def delete_task(self, task):127 def delete_task(self, task):
@@ -132,24 +132,17 @@
132 """Create a new task document."""132 """Create a new task document."""
133 if tags is None:133 if tags is None:
134 tags = []134 tags = []
135 # We copy the JSON string representing a pristine task with no title135 # We make a fresh copy of a pristine task with no title.
136 # and no tags.136 content = get_empty_task()
137 content = EMPTY_TASK
138 # If we were passed a title or tags, or both, we set them in the object137 # If we were passed a title or tags, or both, we set them in the object
139 # before storing it in the database.138 # before storing it in the database.
140 if title or tags:139 if title or tags:
141 # Load the json string into a Python object.140 content['title'] = title
142 content_object = json.loads(content)141 content['tags'] = tags
143 content_object['title'] = title
144 content_object['tags'] = tags
145 # Convert the Python object back into a JSON string.
146 content = json.dumps(content_object)
147 # Store the document in the database. Since we did not set a document142 # Store the document in the database. Since we did not set a document
148 # id, the database will store it as a new document, and generate143 # id, the database will store it as a new document, and generate
149 # a valid id.144 # a valid id.
150 task = self.db.create_doc_from_json(content)145 return self.db.create_doc(content)
151 # Wrap the document in a Task object.
152 return task
153146
154 def save_task(self, task):147 def save_task(self, task):
155 """Save task to the database."""148 """Save task to the database."""
156149
=== modified file 'cosas/test_cosas.py'
--- cosas/test_cosas.py 2012-08-02 13:14:54 +0000
+++ cosas/test_cosas.py 2012-08-21 13:44:17 +0000
@@ -18,7 +18,7 @@
1818
19from testtools import TestCase19from testtools import TestCase
20from cosas import (20from cosas import (
21 Task, TodoStore, INDEXES, TAGS_INDEX, EMPTY_TASK, extract_tags)21 Task, TodoStore, INDEXES, TAGS_INDEX, get_empty_task, extract_tags)
22from u1db.backends import inmemory22from u1db.backends import inmemory
2323
2424
@@ -185,7 +185,7 @@
185 super(TaskTestCase, self).setUp()185 super(TaskTestCase, self).setUp()
186 self.db = inmemory.InMemoryDatabase("cosas")186 self.db = inmemory.InMemoryDatabase("cosas")
187 self.db.set_document_factory(Task)187 self.db.set_document_factory(Task)
188 self.document = self.db.create_doc_from_json(EMPTY_TASK)188 self.document = self.db.create_doc(get_empty_task())
189189
190 def test_task(self):190 def test_task(self):
191 """Initializing a task."""191 """Initializing a task."""
192192
=== modified file 'cosas/ui.py'
--- cosas/ui.py 2012-08-13 16:09:21 +0000
+++ cosas/ui.py 2012-08-21 13:44:17 +0000
@@ -524,11 +524,13 @@
524 db.set_oauth_credentials(**oauth_creds)524 db.set_oauth_credentials(**oauth_creds)
525 db.open(create=True)525 db.open(create=True)
526 syncer.sync()526 syncer.sync()
527 # refresh the UI to show changed or new tasks
527 self.refresh_filter()528 self.refresh_filter()
528 self.update_status_bar("last synced: %s" % (datetime.now(),))529 self.update_status_bar("last synced: %s" % (datetime.now(),))
529530
530 def resolve(self, doc, revs):531 def resolve(self, doc, revs):
531 self.store.db.resolve_doc(doc, revs)532 self.store.db.resolve_doc(doc, revs)
533 # refresh the UI to show the resolved version
532 self.refresh_filter()534 self.refresh_filter()
533535
534536
535537
=== modified file 'html-docs/high-level-api.rst'
--- html-docs/high-level-api.rst 2012-08-17 19:00:38 +0000
+++ html-docs/high-level-api.rst 2012-08-21 13:44:17 +0000
@@ -19,8 +19,11 @@
19Creating and editing documents19Creating and editing documents
20^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^20^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2121
22To create a document, use ``create_doc()``. Code examples below are22To create a document, use :py:meth:`~u1db.Database.create_doc` or
23from :ref:`reference-implementation` in Python.23:py:meth:`~u1db.Database.create_doc_from_json`. Code examples below are from
24:ref:`reference-implementation` in Python. :py:meth:`~u1db.Database.create_doc`
25takes a dictionary-like object, and
26:py:meth:`~u1db.Database.create_doc_from_json` a JSON string.
2427
25.. testsetup ::28.. testsetup ::
2629
@@ -43,10 +46,54 @@
43 testdoc46 testdoc
4447
4548
46Editing an *existing* document is done with ``put_doc()``. This is separate49Retrieving documents
47from ``create_doc()`` so as to avoid accidental overwrites. ``put_doc()`` takes50^^^^^^^^^^^^^^^^^^^^
48a ``Document`` object, because the object encapsulates revision information for51
49a particular document.52The simplest way to retrieve documents from a u1db is by calling
53:py:meth:`~u1db.Database.get_doc` with a ``doc_id``. This will return a
54:py:class:`~u1db.Document` object [#]_.
55
56.. testcode ::
57
58 import u1db
59 db = u1db.open("mydb4.u1db", create=True)
60 doc = db.create_doc({"key": "value"}, doc_id="testdoc")
61 doc1 = db.get_doc("testdoc")
62 print doc1.content
63 print doc1.doc_id
64
65.. testoutput ::
66
67 {u'key': u'value'}
68 testdoc
69
70And it's also possible to retrieve many documents by ``doc_id``.
71
72.. testcode ::
73
74 import u1db
75 db = u1db.open("mydb5.u1db", create=True)
76 doc1 = db.create_doc({"key": "value"}, doc_id="testdoc1")
77 doc2 = db.create_doc({"key": "value"}, doc_id="testdoc2")
78 for doc in db.get_docs(["testdoc2","testdoc1"]):
79 print doc.doc_id
80
81.. testoutput ::
82
83 testdoc2
84 testdoc1
85
86Note that :py:meth:`u1db.Database.get_docs` returns the documents in the order
87specified.
88
89Editing existing documents
90^^^^^^^^^^^^^^^^^^^^^^^^^^
91
92Storing an *existing* document is done with :py:meth:`~u1db.Database.put_doc`.
93This is separate from :py:meth:`~u1db.Database.create_doc` so as to avoid
94accidental overwrites. :py:meth:`~u1db.Database.put_doc` takes a
95:py:class:`~u1db.Document` object, because the object encapsulates revision
96information for a particular document.
5097
51.. testcode ::98.. testcode ::
5299
@@ -70,7 +117,7 @@
70 Now editing the doc with the doc object we got back...117 Now editing the doc with the doc object we got back...
71 {u'key1': u'edited'}118 {u'key1': u'edited'}
72119
73Finally, deleting a document is done with ``delete_doc()``.120Finally, deleting a document is done with :py:meth:`~u1db.Database.delete_doc`.
74121
75.. testcode ::122.. testcode ::
76123
@@ -87,43 +134,6 @@
87 None134 None
88 None135 None
89136
90Retrieving documents
91^^^^^^^^^^^^^^^^^^^^
92
93The simplest way to retrieve documents from a u1db is by ``doc_id``.
94
95.. testcode ::
96
97 import u1db
98 db = u1db.open("mydb4.u1db", create=True)
99 doc = db.create_doc({"key": "value"}, doc_id="testdoc")
100 doc1 = db.get_doc("testdoc")
101 print doc1.content
102 print doc1.doc_id
103
104.. testoutput ::
105
106 {u'key': u'value'}
107 testdoc
108
109And it's also possible to retrieve many documents by ``doc_id``.
110
111.. testcode ::
112
113 import u1db
114 db = u1db.open("mydb5.u1db", create=True)
115 doc1 = db.create_doc({"key": "value"}, doc_id="testdoc1")
116 doc2 = db.create_doc({"key": "value"}, doc_id="testdoc2")
117 for doc in db.get_docs(["testdoc2","testdoc1"]):
118 print doc.doc_id
119
120.. testoutput ::
121
122 testdoc2
123 testdoc1
124
125Note that ``get_docs()`` returns the documents in the order specified.
126
127Document functions137Document functions
128^^^^^^^^^^^^^^^^^^138^^^^^^^^^^^^^^^^^^
129139
@@ -382,20 +392,25 @@
382Synchronising a database can result in conflicts; if your user changes the same392Synchronising a database can result in conflicts; if your user changes the same
383document in two different places and then syncs again, that document will be393document in two different places and then syncs again, that document will be
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,
385``doc.has_conflicts`` will be true, and put_doc to a conflicted doc will give395:py:attr:`~u1db.Document.has_conflicts` will be true, and put_doc to a
386a ``ConflictedDoc`` error. To get a list of conflicted versions of the396conflicted doc will give a ``ConflictedDoc`` error. To get a list of conflicted
387document, do ``get_doc_conflicts(doc_id)``. Deciding what the final397versions of the document, do :py:meth:`~u1db.Database.get_doc_conflicts`.
388unconflicted document should look like is obviously specific to the user's398Deciding what the final unconflicted document should look like is obviously
389application; once decided, call ``resolve_doc(doc, list_of_conflicted_revisions)``399specific to the user's application; once decided, call
390to resolve and set the final resolved content.400:py:meth:`~u1db.Database.resolve_doc` to resolve and set the final resolved
401content.
391402
392Synchronising functions403Synchronising Functions
393^^^^^^^^^^^^^^^^^^^^^^^404^^^^^^^^^^^^^^^^^^^^^^^
394405
395 * :py:meth:`~u1db.Database.sync`406 * :py:meth:`~u1db.Database.sync`
396 * :py:meth:`~u1db.Database.get_doc_conflicts`407 * :py:meth:`~u1db.Database.get_doc_conflicts`
397 * :py:meth:`~u1db.Database.resolve_doc`408 * :py:meth:`~u1db.Database.resolve_doc`
398409
410.. [#] Alternatively if a factory function was passed into
411 :py:func:`u1db.open`, :py:meth:`~u1db.Database.get_doc` will return
412 whatever type of object the factory function returns.
413
399.. testcleanup ::414.. testcleanup ::
400415
401 os.chdir(old_dir)416 os.chdir(old_dir)
402417
=== modified file 'html-docs/tutorial.rst'
--- html-docs/tutorial.rst 2012-08-17 21:22:56 +0000
+++ html-docs/tutorial.rst 2012-08-21 13:44:17 +0000
@@ -2,13 +2,13 @@
2########2########
33
4In this tutorial we will demonstrate what goes into creating an application4In this tutorial we will demonstrate what goes into creating an application
5that uses u1db as a backend. We will use code from the simple todo list5that uses u1db as a backend. We will use code samples from the simple todo list
6application 'Cosas' as our example. The full source code to Cosas can be found6application 'Cosas' as our example. The full source code to Cosas can be found
7in the u1db source tree. It comes with a user interface, but we will only7in the u1db source tree. It comes with a user interface, but we will only
8focus on the code that interacts with u1db here.8focus on the code that interacts with u1db here.
99
10Tasks10Defining the Task Object
11-----11------------------------
1212
13First we need to define what we'll actually store in u1db. For a todo list13First we need to define what we'll actually store in u1db. For a todo list
14application, it makes sense to have each todo item or task be a single14application, it makes sense to have each todo item or task be a single
@@ -27,13 +27,9 @@
2727
28.. code-block:: python28.. code-block:: python
2929
30 '''30 '{"title": "the task at hand",
31 {31 "done": false,
32 "title": "the task at hand",32 "tags": ["urgent", "priority 1", "today"]}'
33 "done": false,
34 "tags": ["urgent", "priority 1", "today"]
35 }
36 '''
3733
38We can define ``Task`` as follows:34We can define ``Task`` as follows:
3935
@@ -128,3 +124,302 @@
128124
129 True125 True
130126
127This is all we need the task object to do: as long as we have a way to store
128all its data in the .content dictionary, the super class will take care of
129converting that into JSON so it can be stored in the database.
130
131For convenience, we can create a function that returns a fresh copy of the
132content that would make up an empty task:
133
134.. code-block:: python
135
136 EMPTY_TASK = {"title": "", "done": False, "tags": []}
137
138 get_empty_task = lambda: copy.deepcopy(EMPTY_TASK)
139
140Defining Indexes
141----------------
142
143Now that we have tasks defined, we will probably want to query the database
144using their properties. To that end, we will need to use indexes. Let's define
145two for now, one to query by tags, and one to query by done status. We'll
146define some global constants with the name and the definition of the indexes,
147which will make them easier to refer to in the rest of the code:
148
149.. code-block:: python
150
151 TAGS_INDEX = 'tags'
152 DONE_INDEX = 'done'
153 INDEXES = {
154 TAGS_INDEX: ['tags'],
155 DONE_INDEX: ['bool(done)'],
156 }
157
158``INDEXES`` is just a regular dictionary, with the names of the indexes as
159keys, and the index definitions, which are lists of expressions as values. (We
160chose to use lists since an index can be defined on multiple fields, though
161both of the indexes defined above only index a single field.)
162
163The ``tags`` index will index any document that has a top level field ``tags``
164and index its value. Our tasks will have a list value under ``tags`` which
165means that u1db will index each task for each of the values in the list in this
166index. So a task with the following content:
167
168.. code-block:: python
169
170 {
171 "title": "Buy sausages and vimto",
172 "tags": ["shopping", "food"],
173 "done": false
174 }
175
176Would be indexed under both ``"food"`` and ``"shopping"``.
177
178The ``done`` index will index any document that has a boolean value in a top
179level field with the name ``done``.
180
181We will see how the indexes are actually created and queried below.
182
183Storing and Retrieving Tasks
184----------------------------
185
186To store and retrieve our task objects we'll need a u1db
187:py:class:`~u1db.Database`. We can make a little helper function to get a
188reference to our application's database, and create it if it doesn't already
189exist:
190
191
192.. code-block:: python
193
194 from dirspec.basedir import save_data_path
195
196 def get_database():
197 """Get the path that the database is stored in."""
198 return u1db.open(
199 os.path.join(save_data_path("cosas"), "cosas.u1db"), create=True,
200 document_factory=Task)
201
202There are a few things to note here: First of all, we use
203`lp:dirspec <http://launchpad.net/dirspec/>`_ to handle where to find or put
204the database in a way that works across platforms. This is not something
205specific to u1db, so you could choose to use it for your own application or
206not: :py:func:`u1db.open` will happily take any filesystem path. Secondly, we
207pass our Task class into the ``document_factory`` argument of
208:py:func:`u1db.open`. This means that any time we get documents from the
209database, it will return Task objects, so we don't have to do the conversion in
210our code.
211
212Now we create a TodoStore class that will handle all interactions with the
213database:
214
215.. code-block:: python
216
217 class TodoStore(object):
218 """The todo application backend."""
219
220 def __init__(self, db):
221 self.db = db
222
223 def initialize_db(self):
224 """Initialize the database."""
225 # Ask the database for currently existing indexes.
226 db_indexes = dict(self.db.list_indexes())
227 # Loop through the indexes we expect to find.
228 for name, expression in INDEXES.items():
229 if name not in db_indexes:
230 # The index does not yet exist.
231 self.db.create_index(name, *expression)
232 continue
233 if expression == db_indexes[name]:
234 # The index exists and is up to date.
235 continue
236 # The index exists but the definition is not what expected, so we
237 # delete it and add the proper index expression.
238 self.db.delete_index(name)
239 self.db.create_index(name, *expression)
240
241The ``initialize_db()`` method checks whether the database already has the
242indexes we defined above and if it doesn't or if the definition is different
243than the one we have, the index is (re)created. We will call this method every
244time we start the application, to make sure all the indexes are up to date.
245Creating an index is a matter of calling :py:meth:`~u1db.Database.create_index`
246with a name and the expressions that define the index. This will immediately
247index all documents already in the database, and afterwards any that are added
248or updated.
249
250.. code-block:: python
251
252 def get_all_tags(self):
253 """Get all tags in use in the entire database."""
254 return [key[0] for key in self.db.get_index_keys(TAGS_INDEX)]
255
256The py:meth:`~u1db.Database.get_index_keys` method gets a list of all indexed
257*values* from an index. In this case it will give us a list of all tags that
258have been used in the database, which can be useful if we want to present them
259in the user interface of our application.
260
261.. code-block:: python
262
263 def get_tasks_by_tags(self, tags):
264 """Get all tasks that have every tag in tags."""
265 if not tags:
266 # No tags specified, so return all tasks.
267 return self.get_all_tasks()
268 # Get all tasks for the first tag.
269 results = dict(
270 (doc.doc_id, doc) for doc in
271 self.db.get_from_index(TAGS_INDEX, tags[0]))
272 # Now loop over the rest of the tags (if any) and remove from the
273 # results any document that does not have that particular tag.
274 for tag in tags[1:]:
275 # Get the ids of all documents with this tag.
276 ids = [
277 doc.doc_id for doc in self.db.get_from_index(TAGS_INDEX, tag)]
278 for key in results.keys():
279 if key not in ids:
280 # Remove the document from result, because it does not have
281 # this particular tag.
282 del results[key]
283 if not results:
284 # If results is empty, we're done: there are no
285 # documents with all tags.
286 return []
287 return results.values()
288
289This method gives us a way to query the database by a set of tags. We loop
290through the tags one by one and then filter out any documents that don't have
291that particular tag.
292
293.. code-block:: python
294
295 def get_task(self, doc_id):
296 """Get a task from the database."""
297 task = self.db.get_doc(doc_id)
298 if task is None:
299 # No document with that id exists in the database.
300 raise KeyError("No task with id '%s'." % (doc_id,))
301 if task.is_tombstone():
302 # The document id exists, but the document's content was previously
303 # deleted.
304 raise KeyError("Task with id %s was deleted." % (doc_id,))
305 return task
306
307``get_task`` is a thin wrapper around :py:meth:`~u1db.Database.get_doc` that
308takes care of raising appropriate exceptions when a document does not exist or
309has been deleted. (Deleted documents leave a 'tombstone' behind, which is
310necessary to make sure that synchronisation of the database with other replicas
311does the right thing.)
312
313.. code-block:: python
314
315 def new_task(self, title=None, tags=None):
316 """Create a new task document."""
317 if tags is None:
318 tags = []
319 # We make a fresh copy of a pristine task with no title.
320 content = get_empty_task()
321 # If we were passed a title or tags, or both, we set them in the object
322 # before storing it in the database.
323 if title or tags:
324 content['title'] = title
325 content['tags'] = tags
326 # Store the document in the database. Since we did not set a document
327 # id, the database will store it as a new document, and generate
328 # a valid id.
329 return self.db.create_doc(content)
330
331Here we use the convenience function defined above to initialize the content,
332and then set the properties that were passed into ``new_task``. We call
333:py:meth:`~u1db.Database.create_doc` to create a new document from the content.
334This creates the document in the database, assigns it a new unique id (unless
335we pass one in,) and returns a fully initialized Task object. (Since we made
336that the database's factory.)
337
338.. code-block:: python
339
340 def get_all_tasks(self):
341 return self.db.get_from_index(DONE_INDEX, "*")
342
343
344Since the ``DONE_INDEX`` indexes anything that has a value in the field "done",
345and all tasks do (either True or False), it's a good way to get all tasks out
346of the database, especially since it will sort them by done status, so we'll
347get all the active tasks first.
348
349Synchronisation and Conflicts
350-----------------------------
351
352Synchronisation has to be initiated by the application, either periodically,
353while it's running, or by having the user initiate it. Any
354:py:class:`u1db.Database` can be synchronised with any other, either by file
355path or URL. Cosas gives the user the choice between manually synchronising or
356having it happen automatically, every 30 minutes, for as long as it is running.
357
358.. code-block:: python
359
360 from ubuntuone.platform.credentials import CredentialsManagementTool
361
362 def get_ubuntuone_credentials(self):
363 cmt = CredentialsManagementTool()
364 return cmt.find_credentials()
365
366 def _synchronize(self, creds=None):
367 target = self.sync_target
368 if target.startswith('http://') or target.startswith('https://'):
369 st = HTTPSyncTarget.connect(target)
370 oauth_creds = {
371 'token_key': creds['token'],
372 'token_secret': creds['token_secret'],
373 'consumer_key': creds['consumer_key'],
374 'consumer_secret': creds['consumer_secret']}
375 if creds:
376 st.set_oauth_credentials(**oauth_creds)
377 else:
378 db = u1db.open(target, create=True)
379 st = db.get_sync_target()
380 syncer = Synchronizer(self.store.db, st)
381 try:
382 syncer.sync()
383 except DatabaseDoesNotExist:
384 # The server does not yet have the database, so create it.
385 if target.startswith('http://') or target.startswith('https://'):
386 db = HTTPDatabase(target)
387 db.set_oauth_credentials(**oauth_creds)
388 db.open(create=True)
389 syncer.sync()
390 # refresh the UI to show changed or new tasks
391 self.refresh_filter()
392
393 def synchronize(self, finalize):
394 if self.sync_target == 'https://u1db.one.ubuntu.com/~/cosas':
395 d = self.get_ubuntuone_credentials()
396 d.addCallback(self._synchronize)
397 d.addCallback(finalize)
398 else:
399 self._synchronize()
400 finalize()
401
402When synchronising over http(s), servers can (and usually will) require OAuth
403authentication. The code above shows how to acquire and pass in the oauth
404credentials for the Ubuntu One server, in case you want your application to
405synchronize with that.
406
407After synchronising with another replica, it is possible that one or more
408conflicts have arisen, if both replicas independently made changes to the same
409document. Your application should probably check for conflicts after every
410synchronisation, and offer the user a way to resolve them.
411
412Look at the Conflicts class in cosas/ui.py to see an example of how this could
413be presented to the user. The idea is that you show the conflicting versions to
414the user, let them pick one, and then call
415:py:meth:`~u1db.Database.resolve_doc` with the preferred version, and all the
416revisions of the conflicting versions it is meant to resolve.
417
418.. code-block:: python
419
420 def resolve(self, doc, revs):
421 self.store.db.resolve_doc(doc, revs)
422 # refresh the UI to show the resolved version
423 self.refresh_filter()
424
425
131426
=== modified file 'u1db/__init__.py'
--- u1db/__init__.py 2012-08-20 12:55:06 +0000
+++ u1db/__init__.py 2012-08-21 13:44:17 +0000
@@ -189,12 +189,16 @@
189 Creating an index will block until the expressions have been evaluated189 Creating an index will block until the expressions have been evaluated
190 and the index generated.190 and the index generated.
191191
192 :index_name: A unique name which can be used as a key prefix192 :param index_name: A unique name which can be used as a key prefix
193 :index_expressions: index expressions defining the index information.193 :param index_expressions: index expressions defining the index
194 information.
195
194 Examples:196 Examples:
195 "fieldname" to index alphabetically sorted on field.197
196 "number(fieldname, width)", "lower(fieldname)",198 "fieldname", or "fieldname.subfieldname" to index alphabetically
197 "fieldname.subfieldname"199 sorted on the contents of a field.
200
201 "number(fieldname, width)", "lower(fieldname)"
198 """202 """
199 raise NotImplementedError(self.create_index)203 raise NotImplementedError(self.create_index)
200204
@@ -202,7 +206,6 @@
202 """Remove a named index.206 """Remove a named index.
203207
204 :param index_name: The name of the index we are removing208 :param index_name: The name of the index we are removing
205 :return: None
206 """209 """
207 raise NotImplementedError(self.delete_index)210 raise NotImplementedError(self.delete_index)
208211
@@ -223,11 +226,11 @@
223 It is also possible to append a '*' to the last supplied value (eg226 It is also possible to append a '*' to the last supplied value (eg
224 'val*', '*', '*' or 'val', 'val*', '*', but not 'val*', 'val', '*')227 'val*', '*', '*' or 'val', 'val*', '*', but not 'val*', 'val', '*')
225228
226 :return: List of [Document]
227 :param index_name: The index to query229 :param index_name: The index to query
228 :param key_values: values to match. eg, if you have230 :param key_values: values to match. eg, if you have
229 an index with 3 fields then you would have:231 an index with 3 fields then you would have:
230 get_from_index(index_name, val1, val2, val3)232 get_from_index(index_name, val1, val2, val3)
233 :return: List of [Document]
231 """234 """
232 raise NotImplementedError(self.get_from_index)235 raise NotImplementedError(self.get_from_index)
233236
@@ -244,7 +247,6 @@
244 possible to append a '*' to the last supplied value (eg 'val*', '*',247 possible to append a '*' to the last supplied value (eg 'val*', '*',
245 '*' or 'val', 'val*', '*', but not 'val*', 'val', '*')248 '*' or 'val', 'val*', '*', but not 'val*', 'val', '*')
246249
247 :return: List of [Document]
248 :param index_name: The index to query250 :param index_name: The index to query
249 :param start_values: tuples of values that define the lower bound of251 :param start_values: tuples of values that define the lower bound of
250 the range. eg, if you have an index with 3 fields then you would252 the range. eg, if you have an index with 3 fields then you would
@@ -252,14 +254,15 @@
252 :param end_values: tuples of values that define the upper bound of the254 :param end_values: tuples of values that define the upper bound of the
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:
254 (val1, val2, val3)256 (val1, val2, val3)
257 :return: List of [Document]
255 """258 """
256 raise NotImplementedError(self.get_range_from_index)259 raise NotImplementedError(self.get_range_from_index)
257260
258 def get_index_keys(self, index_name):261 def get_index_keys(self, index_name):
259 """Return all keys under which documents are indexed in this index.262 """Return all keys under which documents are indexed in this index.
260263
264 :param index_name: The index to query
261 :return: [] A list of tuples of indexed keys.265 :return: [] A list of tuples of indexed keys.
262 :param index_name: The index to query
263 """266 """
264 raise NotImplementedError(self.get_index_keys)267 raise NotImplementedError(self.get_index_keys)
265268
@@ -286,8 +289,6 @@
286 :param doc: A Document with the new content to be inserted.289 :param doc: A Document with the new content to be inserted.
287 :param conflicted_doc_revs: A list of revisions that the new content290 :param conflicted_doc_revs: A list of revisions that the new content
288 supersedes.291 supersedes.
289 :return: None, doc will be updated with the new revision and
290 has_conflict flags.
291 """292 """
292 raise NotImplementedError(self.resolve_doc)293 raise NotImplementedError(self.resolve_doc)
293294
@@ -303,7 +304,14 @@
303 raise NotImplementedError(self.close)304 raise NotImplementedError(self.close)
304305
305 def sync(self, url):306 def sync(self, url):
306 """Synchronize documents with remote replica exposed at url."""307 """Synchronize documents with remote replica exposed at url.
308
309 :param url: the url of the target replica to sync with.
310 :return: local_gen_before_sync The local generation before the
311 synchronisation was performed. This is useful to pass into
312 whatschanged, if an application wants to know which documents were
313 affected by a synchronisation.
314 """
307 from u1db.sync import Synchronizer315 from u1db.sync import Synchronizer
308 from u1db.remote.http_target import HTTPSyncTarget316 from u1db.remote.http_target import HTTPSyncTarget
309 return Synchronizer(self, HTTPSyncTarget(url)).sync()317 return Synchronizer(self, HTTPSyncTarget(url)).sync()
@@ -337,7 +345,6 @@
337 :param other_generation: The generation number for the other replica.345 :param other_generation: The generation number for the other replica.
338 :param other_transaction_id: The transaction id associated with the346 :param other_transaction_id: The transaction id associated with the
339 generation.347 generation.
340 :return: None
341 """348 """
342 raise NotImplementedError(self._set_replica_gen_and_trans_id)349 raise NotImplementedError(self._set_replica_gen_and_trans_id)
343350
@@ -588,8 +595,8 @@
588 :param source_replica_uid: Another replica which we might have595 :param source_replica_uid: Another replica which we might have
589 synchronized with in the past.596 synchronized with in the past.
590 :return: (target_replica_uid, target_replica_generation,597 :return: (target_replica_uid, target_replica_generation,
591 target_trans_id, source_replica_last_known_generation,598 target_trans_id, source_replica_last_known_generation,
592 source_replica_last_known_transaction_id)599 source_replica_last_known_transaction_id)
593 """600 """
594 raise NotImplementedError(self.get_sync_info)601 raise NotImplementedError(self.get_sync_info)
595602
@@ -609,10 +616,9 @@
609616
610 :param source_replica_uid: The identifier for the source replica.617 :param source_replica_uid: The identifier for the source replica.
611 :param source_replica_generation:618 :param source_replica_generation:
612 The database generation for the source replica.619 The database generation for the source replica.
613 :param source_replica_transaction_id: The transaction id associated620 :param source_replica_transaction_id: The transaction id associated
614 with the source replica generation.621 with the source replica generation.
615 :return: None
616 """622 """
617 raise NotImplementedError(self.record_sync_info)623 raise NotImplementedError(self.record_sync_info)
618624
@@ -646,10 +652,10 @@
646 :param last_known_trans_id: The last transaction id that the source652 :param last_known_trans_id: The last transaction id that the source
647 replica knows about this target replica653 replica knows about this target replica
648 :param: return_doc_cb(doc, gen): is a callback654 :param: return_doc_cb(doc, gen): is a callback
649 used to return documents to the source replica, it will655 used to return documents to the source replica, it will
650 be invoked in turn with Documents that have changed since656 be invoked in turn with Documents that have changed since
651 last_known_generation together with the generation of657 last_known_generation together with the generation of
652 their last change.658 their last change.
653 :return: new_generation - After applying docs_by_generation, this is659 :return: new_generation - After applying docs_by_generation, this is
654 the current generation for this replica660 the current generation for this replica
655 """661 """

Subscribers

People subscribed via source and target branches