Merge lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/snapshots-with-packaging into lp:ubuntu/karmic/desktopcouch

Proposed by Chad Miller
Status: Merged
Merged at revision: not available
Proposed branch: lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/snapshots-with-packaging
Merge into: lp:ubuntu/karmic/desktopcouch
Diff against target: 3284 lines
37 files modified
MANIFEST.in (+3/-1)
PKG-INFO (+0/-10)
config/desktop-couch/compulsory-auth.ini (+0/-3)
debian/changelog (+26/-0)
debian/desktopcouch-tools.install (+0/-1)
debian/desktopcouch.install (+4/-2)
debian/python-desktopcouch-records.install (+5/-5)
debian/python-desktopcouch.install (+1/-1)
debian/rules (+1/-0)
desktopcouch.egg-info/PKG-INFO (+0/-10)
desktopcouch.egg-info/SOURCES.txt (+0/-64)
desktopcouch.egg-info/dependency_links.txt (+0/-1)
desktopcouch.egg-info/top_level.txt (+0/-1)
desktopcouch/contacts/schema.txt (+50/-0)
desktopcouch/contacts/tests/test_create.py (+0/-62)
desktopcouch/local_files.py (+12/-0)
desktopcouch/notes/__init__.py (+0/-19)
desktopcouch/notes/record.py (+0/-31)
desktopcouch/pair/couchdb_pairing/couchdb_io.py (+36/-25)
desktopcouch/pair/couchdb_pairing/dbus_io.py (+42/-49)
desktopcouch/pair/tests/test_couchdb_io.py (+0/-133)
desktopcouch/records/couchgrid.py (+1/-18)
desktopcouch/records/doc/field_registry.txt (+213/-0)
desktopcouch/records/doc/records.txt (+13/-7)
desktopcouch/records/server.py (+2/-1)
desktopcouch/records/server_base.py (+0/-326)
desktopcouch/records/tests/test_couchgrid.py (+21/-0)
desktopcouch/records/tests/test_field_registry.py (+5/-1)
desktopcouch/records/tests/test_record.py (+5/-0)
desktopcouch/records/tests/test_server.py (+8/-0)
desktopcouch/replication.py (+0/-242)
desktopcouch/replication_services/__init__.py (+0/-4)
desktopcouch/replication_services/example.py (+0/-26)
desktopcouch/replication_services/ubuntuone.py (+0/-125)
po/desktopcouch.pot (+102/-0)
setup.cfg (+6/-6)
setup.py (+6/-4)
To merge this branch: bzr merge lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/snapshots-with-packaging
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Review via email: mp+13209@code.launchpad.net

Commit message

New upstream version 0.4.4.

Include compulsory-auth INI file to be secure by default.
Make debhelper warn about files not installed to some package (sort |uniq -c |grep -v 3 == errors) .
Shorten/simplify debhelper install paths using dh_install exlusions.
Update MANIFEST and setup.py for new files.
Remove buggy couchgrid selected_records property.
Make couchgrid correctly retrieve record id.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

The upstream tarball seems to be incomplete as discussed on IRC.

Thanks,

James

review: Needs Fixing
Revision history for this message
Chad Miller (cmiller) wrote :

> The upstream tarball seems to be incomplete as discussed on IRC.

This was already presend. I don't understand why it was in the list.
desktopcouch-0.4.3/setup.cfg

Now included these.
desktopcouch-0.4.3/po/desktopcouch.pot
desktopcouch/contacts/schema.txt
desktopcouch/records/doc/field_registry.txt

Revision history for this message
James Westby (james-w) wrote :

Looks good now.

Thanks,

James

review: Approve
9. By James Westby

Merging shared upstream rev into target branch.

10. By James Westby

* New upstream release.
  + Include doc "txt" and translation files in sources.
  + couchgrid does not correctly retrieve record id (LP: #447512)
  + couchgrid selected_records property is buggy and should be removed for
    karmic if possible (LP: #448357)
* Include compulsory-auth INI file to be secure by default.
  (LP: #438800)
* Make debhelper warn about files not installed to some package.
* Shorten debhelper install paths using dh_install exlusions.
* New upstream release:
  + couchgrid did not correctly retrieve record id (LP: #447512)
  + HTTP 401 for valid auth information when talking to couchdb over SSL
    (LP: #446516)
  + Support headless apps. (LP: #428681)
  + desktopcouch-service "ValueError: dictionary update sequence..." on
    stdout(LP: #446511)

11. By James Westby

Upload to karmic.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANIFEST.in'
2--- MANIFEST.in 2009-09-23 14:22:38 +0000
3+++ MANIFEST.in 2009-10-12 14:29:10 +0000
4@@ -1,10 +1,12 @@
5 include COPYING COPYING.LESSER README
6 recursive-include data *.tmpl
7 include desktopcouch-pair.desktop.in
8+include setup.cfg
9 include po/POTFILES.in
10 include start-desktop-couchdb.sh
11 include stop-desktop-couchdb.sh
12-include desktopcouch/records/doc/records.txt
13+recursive-include desktopcouch *.txt
14+recursive-include po *.pot
15 include bin/*
16 include docs/man/*
17 include MANIFEST.in MANIFEST
18
19=== removed file 'PKG-INFO'
20--- PKG-INFO 2009-09-28 12:06:08 +0000
21+++ PKG-INFO 1970-01-01 00:00:00 +0000
22@@ -1,10 +0,0 @@
23-Metadata-Version: 1.0
24-Name: desktopcouch
25-Version: 0.4.2
26-Summary: A Desktop CouchDB instance.
27-Home-page: https://launchpad.net/desktopcouch
28-Author: Stuart Langridge
29-Author-email: stuart.langridge@canonical.com
30-License: LGPL-3
31-Description: UNKNOWN
32-Platform: UNKNOWN
33
34=== added directory 'config'
35=== removed directory 'config'
36=== added directory 'config/desktop-couch'
37=== removed directory 'config/desktop-couch'
38=== added file 'config/desktop-couch/compulsory-auth.ini'
39--- config/desktop-couch/compulsory-auth.ini 1970-01-01 00:00:00 +0000
40+++ config/desktop-couch/compulsory-auth.ini 2009-10-12 14:29:10 +0000
41@@ -0,0 +1,3 @@
42+[couch_httpd_auth]
43+require_valid_user = true
44+
45
46=== removed file 'config/desktop-couch/compulsory-auth.ini'
47--- config/desktop-couch/compulsory-auth.ini 2009-09-23 14:22:38 +0000
48+++ config/desktop-couch/compulsory-auth.ini 1970-01-01 00:00:00 +0000
49@@ -1,3 +0,0 @@
50-[couch_httpd_auth]
51-require_valid_user = true
52-
53
54=== modified file 'debian/changelog'
55--- debian/changelog 2009-09-28 12:06:08 +0000
56+++ debian/changelog 2009-10-12 14:29:10 +0000
57@@ -1,3 +1,29 @@
58+desktopcouch (0.4.4-0ubuntu1) UNRELEASED; urgency=low
59+
60+ * New upstream release.
61+ + Include doc "txt" and translation files in sources.
62+ + couchgrid does not correctly retrieve record id (LP: #447512)
63+ + couchgrid selected_records property is buggy and should be removed for
64+ karmic if possible (LP: #448357)
65+
66+ -- Chad MILLER <chad.miller@canonical.com> Mon, 12 Oct 2009 10:17:50 -0400
67+
68+desktopcouch (0.4.3-0ubuntu1) karmic; urgency=low
69+
70+ * Include compulsory-auth INI file to be secure by default.
71+ (LP: #438800)
72+ * Make debhelper warn about files not installed to some package.
73+ * Shorten debhelper install paths using dh_install exlusions.
74+ * New upstream release:
75+ + couchgrid did not correctly retrieve record id (LP: #447512)
76+ + HTTP 401 for valid auth information when talking to couchdb over SSL
77+ (LP: #446516)
78+ + Support headless apps. (LP: #428681)
79+ + desktopcouch-service "ValueError: dictionary update sequence..." on
80+ stdout(LP: #446511)
81+
82+ -- Chad Miller <chad.miller@canonical.com> Mon, 12 Oct 2009 07:02:07 -0400
83+
84 desktopcouch (0.4.2-0ubuntu1) karmic; urgency=low
85
86 * Include missing 0.4.0 changelog entry.
87
88=== modified file 'debian/desktopcouch-tools.install'
89--- debian/desktopcouch-tools.install 2009-07-31 13:44:45 +0000
90+++ debian/desktopcouch-tools.install 2009-10-12 14:29:10 +0000
91@@ -1,4 +1,3 @@
92 debian/tmp/usr/share/applications/desktopcouch-pair.desktop
93 debian/tmp/usr/bin/desktopcouch-pair
94 debian/tmp/usr/share/man/man1/desktopcouch-pair.1
95-#debian/tmp/usr/share/locale/*/LC_MESSAGES/desktopcouch.mo
96
97=== modified file 'debian/desktopcouch.install'
98--- debian/desktopcouch.install 2009-07-31 13:44:45 +0000
99+++ debian/desktopcouch.install 2009-10-12 14:29:10 +0000
100@@ -1,3 +1,5 @@
101-debian/tmp/usr/share/desktopcouch
102-debian/tmp/usr/lib/desktopcouch/desktopcouch-{stop,service}
103+debian/tmp/etc/xdg/desktop-couch/
104+debian/tmp/usr/share/desktopcouch/
105+debian/tmp/usr/lib/desktopcouch/desktopcouch-service
106+debian/tmp/usr/lib/desktopcouch/desktopcouch-stop
107 debian/tmp/usr/share/dbus-1/services/org.desktopcouch.CouchDB.service
108
109=== modified file 'debian/python-desktopcouch-records.install'
110--- debian/python-desktopcouch-records.install 2009-09-28 12:06:08 +0000
111+++ debian/python-desktopcouch-records.install 2009-10-12 14:29:10 +0000
112@@ -1,5 +1,5 @@
113-debian/tmp/usr/share/doc/python-desktopcouch-records/api
114-debian/tmp/usr/lib/*/*/desktopcouch/records/*
115-debian/tmp/usr/lib/*/*/desktopcouch/contacts/*
116-debian/tmp/usr/lib/*/*/desktopcouch/notes/*
117-debian/tmp/usr/lib/*/*/desktopcouch/replication_services/*
118+debian/tmp/usr/share/doc/python-desktopcouch-records/api/
119+debian/tmp/usr/lib/*/*/desktopcouch/records/
120+debian/tmp/usr/lib/*/*/desktopcouch/contacts/
121+debian/tmp/usr/lib/*/*/desktopcouch/notes/
122+debian/tmp/usr/lib/*/*/desktopcouch/replication_services/
123
124=== modified file 'debian/python-desktopcouch.install'
125--- debian/python-desktopcouch.install 2009-07-31 13:44:45 +0000
126+++ debian/python-desktopcouch.install 2009-10-12 14:29:10 +0000
127@@ -1,2 +1,2 @@
128 debian/tmp/usr/lib/*/*/desktopcouch/*.py
129-debian/tmp/usr/lib/*/*/desktopcouch/pair/{couchdb_pairing,__init__.py}
130+debian/tmp/usr/lib/*/*/desktopcouch/pair/
131
132=== modified file 'debian/rules'
133--- debian/rules 2009-07-31 13:44:45 +0000
134+++ debian/rules 2009-10-12 14:29:10 +0000
135@@ -1,6 +1,7 @@
136 #!/usr/bin/make -f
137
138 DEB_PYTHON_SYSTEM := pycentral
139+DEB_DH_INSTALL_ARGS := --list-missing --exclude=/tests/ --exclude=egg-info/
140
141 include /usr/share/cdbs/1/rules/debhelper.mk
142 include /usr/share/cdbs/1/class/python-distutils.mk
143
144=== removed directory 'desktopcouch.egg-info'
145=== removed file 'desktopcouch.egg-info/PKG-INFO'
146--- desktopcouch.egg-info/PKG-INFO 2009-09-28 12:06:08 +0000
147+++ desktopcouch.egg-info/PKG-INFO 1970-01-01 00:00:00 +0000
148@@ -1,10 +0,0 @@
149-Metadata-Version: 1.0
150-Name: desktopcouch
151-Version: 0.4.2
152-Summary: A Desktop CouchDB instance.
153-Home-page: https://launchpad.net/desktopcouch
154-Author: Stuart Langridge
155-Author-email: stuart.langridge@canonical.com
156-License: LGPL-3
157-Description: UNKNOWN
158-Platform: UNKNOWN
159
160=== removed file 'desktopcouch.egg-info/SOURCES.txt'
161--- desktopcouch.egg-info/SOURCES.txt 2009-09-23 14:22:38 +0000
162+++ desktopcouch.egg-info/SOURCES.txt 1970-01-01 00:00:00 +0000
163@@ -1,64 +0,0 @@
164-COPYING
165-COPYING.LESSER
166-MANIFEST.in
167-README
168-desktopcouch-pair.desktop.in
169-org.desktopcouch.CouchDB.service
170-setup.cfg
171-setup.py
172-start-desktop-couchdb.sh
173-stop-desktop-couchdb.sh
174-bin/desktopcouch-pair
175-bin/desktopcouch-service
176-bin/desktopcouch-stop
177-config/desktop-couch/compulsory-auth.ini
178-contrib/mocker.py
179-data/couchdb.tmpl
180-desktopcouch/__init__.py
181-desktopcouch/local_files.py
182-desktopcouch/replication.py
183-desktopcouch/start_local_couchdb.py
184-desktopcouch/stop_local_couchdb.py
185-desktopcouch.egg-info/PKG-INFO
186-desktopcouch.egg-info/SOURCES.txt
187-desktopcouch.egg-info/dependency_links.txt
188-desktopcouch.egg-info/top_level.txt
189-desktopcouch/contacts/__init__.py
190-desktopcouch/contacts/contactspicker.py
191-desktopcouch/contacts/record.py
192-desktopcouch/contacts/testing/__init__.py
193-desktopcouch/contacts/testing/create.py
194-desktopcouch/contacts/tests/__init__.py
195-desktopcouch/contacts/tests/test_contactspicker.py
196-desktopcouch/contacts/tests/test_create.py
197-desktopcouch/contacts/tests/test_record.py
198-desktopcouch/notes/__init__.py
199-desktopcouch/notes/record.py
200-desktopcouch/pair/__init__.py
201-desktopcouch/pair/couchdb_pairing/__init__.py
202-desktopcouch/pair/couchdb_pairing/couchdb_io.py
203-desktopcouch/pair/couchdb_pairing/dbus_io.py
204-desktopcouch/pair/couchdb_pairing/network_io.py
205-desktopcouch/pair/tests/__init__.py
206-desktopcouch/pair/tests/test_couchdb_io.py
207-desktopcouch/pair/tests/test_network_io.py
208-desktopcouch/records/__init__.py
209-desktopcouch/records/couchgrid.py
210-desktopcouch/records/field_registry.py
211-desktopcouch/records/record.py
212-desktopcouch/records/server.py
213-desktopcouch/records/server_base.py
214-desktopcouch/records/doc/records.txt
215-desktopcouch/records/tests/__init__.py
216-desktopcouch/records/tests/test_couchgrid.py
217-desktopcouch/records/tests/test_field_registry.py
218-desktopcouch/records/tests/test_record.py
219-desktopcouch/records/tests/test_server.py
220-desktopcouch/replication_services/__init__.py
221-desktopcouch/replication_services/example.py
222-desktopcouch/replication_services/ubuntuone.py
223-desktopcouch/tests/__init__.py
224-desktopcouch/tests/test_local_files.py
225-desktopcouch/tests/test_start_local_couchdb.py
226-docs/man/desktopcouch-pair.1
227-po/POTFILES.in
228\ No newline at end of file
229
230=== removed file 'desktopcouch.egg-info/dependency_links.txt'
231--- desktopcouch.egg-info/dependency_links.txt 2009-09-23 14:22:38 +0000
232+++ desktopcouch.egg-info/dependency_links.txt 1970-01-01 00:00:00 +0000
233@@ -1,1 +0,0 @@
234-
235
236=== removed file 'desktopcouch.egg-info/top_level.txt'
237--- desktopcouch.egg-info/top_level.txt 2009-09-23 14:22:38 +0000
238+++ desktopcouch.egg-info/top_level.txt 1970-01-01 00:00:00 +0000
239@@ -1,1 +0,0 @@
240-desktopcouch
241
242=== added file 'desktopcouch/contacts/schema.txt'
243--- desktopcouch/contacts/schema.txt 1970-01-01 00:00:00 +0000
244+++ desktopcouch/contacts/schema.txt 2009-10-12 14:29:10 +0000
245@@ -0,0 +1,50 @@
246+# Copyright 2009 Canonical Ltd.
247+#
248+# This file is part of desktopcouch-contacts.
249+#
250+# desktopcouch is free software: you can redistribute it and/or modify
251+# it under the terms of the GNU Lesser General Public License version 3
252+# as published by the Free Software Foundation.
253+#
254+# desktopcouch is distributed in the hope that it will be useful,
255+# but WITHOUT ANY WARRANTY; without even the implied warranty of
256+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
257+# GNU Lesser General Public License for more details.
258+#
259+# You should have received a copy of the GNU Lesser General Public License
260+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
261+
262+Schema
263+
264+The proposed CouchDB contact schema is as follows:
265+
266+Core fields
267+
268+ * record_type 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact'
269+ * first_name (string)
270+ * last_name (string)
271+ * birth_date (string, "YYYY-MM-DD")
272+ * addresses (MergeableList of "address" dictionaries)
273+ o city (string)
274+ o address1 (string)
275+ o address2 (string)
276+ o pobox (string)
277+ o state (string)
278+ o country (string)
279+ o postalcode (string)
280+ o description (string, e.g., "Home")
281+ * email_addresses (MergeableList of "emailaddress" dictionaries)
282+ o address (string),
283+ o description (string)
284+ * phone_numbers (MergeableList of "phone number" dictionaries)
285+ o number (string)
286+ o description (string)
287+ * application_annotations Everything else, organized per application.
288+
289+Note: None of the core fields are mandatory, but applications should
290+not add any other fields at the top level of the record. Any fields
291+needed not defined here should be put under application_annotations in
292+the namespace of the application there. So for Ubuntu One:
293+
294+ "application_annotations": {
295+ "Ubuntu One": {<Ubuntu One specific fields here>}}
296
297=== added file 'desktopcouch/contacts/tests/test_create.py'
298--- desktopcouch/contacts/tests/test_create.py 1970-01-01 00:00:00 +0000
299+++ desktopcouch/contacts/tests/test_create.py 2009-10-12 14:29:10 +0000
300@@ -0,0 +1,62 @@
301+# Copyright 2009 Canonical Ltd.
302+#
303+# This file is part of desktopcouch-contacts.
304+#
305+# desktopcouch is free software: you can redistribute it and/or modify
306+# it under the terms of the GNU Lesser General Public License version 3
307+# as published by the Free Software Foundation.
308+#
309+# desktopcouch is distributed in the hope that it will be useful,
310+# but WITHOUT ANY WARRANTY; without even the implied warranty of
311+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
312+# GNU Lesser General Public License for more details.
313+#
314+# You should have received a copy of the GNU Lesser General Public License
315+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
316+#
317+# Authors: Nicola Larosa <nicola.larosa@canonical.com>
318+
319+"""
320+Tests for the random contacts creation testing support code.
321+
322+These tests depend on the specific random generation algorithm used in the
323+"random" stdlib module.
324+"""
325+
326+import random
327+
328+import testtools
329+
330+from desktopcouch.contacts.testing import create as create
331+
332+class TestCreate(testtools.TestCase):
333+ """Test the random creation testing support code."""
334+
335+ def test_head_or_tails(self):
336+ """
337+ Test the head_or_tails function.
338+ Once the rndgen algo is seeded, the first four calls to
339+ create.head_or_tails will yield True, True, False, False.
340+ """
341+ random.seed(0)
342+ self.assert_(create.head_or_tails())
343+ self.assert_(create.head_or_tails())
344+ self.assertFalse(create.head_or_tails())
345+ self.assertFalse(create.head_or_tails())
346+
347+ def test_random_bools(self):
348+ """
349+ Test the random_bools function. See the doc for the head_or_tails test.
350+ """
351+ self.assertRaises(RuntimeError, create.random_bools, 1)
352+ random.seed(0)
353+ self.assertEqual(len(create.random_bools(2)), 2) # [True, True]
354+ self.assert_(any(create.random_bools(2))) # orig.: [False, False]
355+ random.seed(0)
356+ create.random_bools(2) # [True, True]
357+ self.assertFalse(any(
358+ create.random_bools(2, at_least_one_true=False))) # [False, False]
359+
360+ def test_create_many_contacts(self):
361+ """Run the create_many_contacts function."""
362+ create.create_many_contacts()
363
364=== removed file 'desktopcouch/contacts/tests/test_create.py'
365--- desktopcouch/contacts/tests/test_create.py 2009-09-23 14:22:38 +0000
366+++ desktopcouch/contacts/tests/test_create.py 1970-01-01 00:00:00 +0000
367@@ -1,62 +0,0 @@
368-# Copyright 2009 Canonical Ltd.
369-#
370-# This file is part of desktopcouch-contacts.
371-#
372-# desktopcouch is free software: you can redistribute it and/or modify
373-# it under the terms of the GNU Lesser General Public License version 3
374-# as published by the Free Software Foundation.
375-#
376-# desktopcouch is distributed in the hope that it will be useful,
377-# but WITHOUT ANY WARRANTY; without even the implied warranty of
378-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
379-# GNU Lesser General Public License for more details.
380-#
381-# You should have received a copy of the GNU Lesser General Public License
382-# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
383-#
384-# Authors: Nicola Larosa <nicola.larosa@canonical.com>
385-
386-"""
387-Tests for the random contacts creation testing support code.
388-
389-These tests depend on the specific random generation algorithm used in the
390-"random" stdlib module.
391-"""
392-
393-import random
394-
395-import testtools
396-
397-from desktopcouch.contacts.testing import create as create
398-
399-class TestCreate(testtools.TestCase):
400- """Test the random creation testing support code."""
401-
402- def test_head_or_tails(self):
403- """
404- Test the head_or_tails function.
405- Once the rndgen algo is seeded, the first four calls to
406- create.head_or_tails will yield True, True, False, False.
407- """
408- random.seed(0)
409- self.assert_(create.head_or_tails())
410- self.assert_(create.head_or_tails())
411- self.assertFalse(create.head_or_tails())
412- self.assertFalse(create.head_or_tails())
413-
414- def test_random_bools(self):
415- """
416- Test the random_bools function. See the doc for the head_or_tails test.
417- """
418- self.assertRaises(RuntimeError, create.random_bools, 1)
419- random.seed(0)
420- self.assertEqual(len(create.random_bools(2)), 2) # [True, True]
421- self.assert_(any(create.random_bools(2))) # orig.: [False, False]
422- random.seed(0)
423- create.random_bools(2) # [True, True]
424- self.assertFalse(any(
425- create.random_bools(2, at_least_one_true=False))) # [False, False]
426-
427- def test_create_many_contacts(self):
428- """Run the create_many_contacts function."""
429- create.create_many_contacts()
430
431=== modified file 'desktopcouch/local_files.py'
432--- desktopcouch/local_files.py 2009-09-23 14:22:38 +0000
433+++ desktopcouch/local_files.py 2009-10-12 14:29:10 +0000
434@@ -136,6 +136,8 @@
435
436 def set_bind_address(address, config_file_name=FILE_INI):
437 c = configparser.SafeConfigParser()
438+ # monkeypatch ConfigParser to stop it lower-casing option names
439+ c.optionxform = lambda s: s
440 c.read(config_file_name)
441 if not c.has_section("httpd"):
442 c.add_section("httpd")
443@@ -147,3 +149,13 @@
444 # You will need to add -b or -k on the end of this
445 COUCH_EXEC_COMMAND = [COUCH_EXE, couch_chain_ini_files(), '-p', FILE_PID,
446 '-o', FILE_STDOUT, '-e', FILE_STDERR]
447+
448+
449+# Set appropriate permissions on relevant files and folders
450+for fn in [FILE_PID, FILE_STDOUT, FILE_STDERR, FILE_INI, FILE_LOG]:
451+ if os.path.exists(fn):
452+ os.chmod(fn, 0600)
453+for dn in [rootdir, config_dir, DIR_DB]:
454+ if os.path.isdir(dn):
455+ os.chmod(dn, 0700)
456+
457
458=== added directory 'desktopcouch/notes'
459=== removed directory 'desktopcouch/notes'
460=== added file 'desktopcouch/notes/__init__.py'
461--- desktopcouch/notes/__init__.py 1970-01-01 00:00:00 +0000
462+++ desktopcouch/notes/__init__.py 2009-10-12 14:29:10 +0000
463@@ -0,0 +1,19 @@
464+# Copyright 2009 Canonical Ltd.
465+#
466+# This file is part of desktopcouch-notes.
467+#
468+# desktopcouch is free software: you can redistribute it and/or modify
469+# it under the terms of the GNU Lesser General Public License version 3
470+# as published by the Free Software Foundation.
471+#
472+# desktopcouch is distributed in the hope that it will be useful,
473+# but WITHOUT ANY WARRANTY; without even the implied warranty of
474+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
475+# GNU Lesser General Public License for more details.
476+#
477+# You should have received a copy of the GNU Lesser General Public License
478+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
479+#
480+# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
481+
482+"""UbuntuOne Notes API"""
483
484=== removed file 'desktopcouch/notes/__init__.py'
485--- desktopcouch/notes/__init__.py 2009-09-23 14:22:38 +0000
486+++ desktopcouch/notes/__init__.py 1970-01-01 00:00:00 +0000
487@@ -1,19 +0,0 @@
488-# Copyright 2009 Canonical Ltd.
489-#
490-# This file is part of desktopcouch-notes.
491-#
492-# desktopcouch is free software: you can redistribute it and/or modify
493-# it under the terms of the GNU Lesser General Public License version 3
494-# as published by the Free Software Foundation.
495-#
496-# desktopcouch is distributed in the hope that it will be useful,
497-# but WITHOUT ANY WARRANTY; without even the implied warranty of
498-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
499-# GNU Lesser General Public License for more details.
500-#
501-# You should have received a copy of the GNU Lesser General Public License
502-# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
503-#
504-# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
505-
506-"""UbuntuOne Notes API"""
507
508=== added file 'desktopcouch/notes/record.py'
509--- desktopcouch/notes/record.py 1970-01-01 00:00:00 +0000
510+++ desktopcouch/notes/record.py 2009-10-12 14:29:10 +0000
511@@ -0,0 +1,31 @@
512+# Copyright 2009 Canonical Ltd.
513+#
514+# This file is part of desktopcouch-notes.
515+#
516+# desktopcouch is free software: you can redistribute it and/or modify
517+# it under the terms of the GNU Lesser General Public License version 3
518+# as published by the Free Software Foundation.
519+#
520+# desktopcouch is distributed in the hope that it will be useful,
521+# but WITHOUT ANY WARRANTY; without even the implied warranty of
522+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
523+# GNU Lesser General Public License for more details.
524+#
525+# You should have received a copy of the GNU Lesser General Public License
526+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
527+#
528+# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
529+
530+
531+"""A dictionary based note record representation."""
532+
533+from desktopcouch.records.record import Record
534+
535+NOTE_RECORD_TYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/note'
536+
537+class Note(Record):
538+ """An Ubuntuone Note Record."""
539+
540+ def __init__(self, data=None, record_id=None):
541+ super(Note, self).__init__(
542+ record_id=record_id, data=data, record_type=NOTE_RECORD_TYPE)
543
544=== removed file 'desktopcouch/notes/record.py'
545--- desktopcouch/notes/record.py 2009-09-23 14:22:38 +0000
546+++ desktopcouch/notes/record.py 1970-01-01 00:00:00 +0000
547@@ -1,31 +0,0 @@
548-# Copyright 2009 Canonical Ltd.
549-#
550-# This file is part of desktopcouch-notes.
551-#
552-# desktopcouch is free software: you can redistribute it and/or modify
553-# it under the terms of the GNU Lesser General Public License version 3
554-# as published by the Free Software Foundation.
555-#
556-# desktopcouch is distributed in the hope that it will be useful,
557-# but WITHOUT ANY WARRANTY; without even the implied warranty of
558-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
559-# GNU Lesser General Public License for more details.
560-#
561-# You should have received a copy of the GNU Lesser General Public License
562-# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
563-#
564-# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
565-
566-
567-"""A dictionary based note record representation."""
568-
569-from desktopcouch.records.record import Record
570-
571-NOTE_RECORD_TYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/note'
572-
573-class Note(Record):
574- """An Ubuntuone Note Record."""
575-
576- def __init__(self, data=None, record_id=None):
577- super(Note, self).__init__(
578- record_id=record_id, data=data, record_type=NOTE_RECORD_TYPE)
579
580=== modified file 'desktopcouch/pair/couchdb_pairing/couchdb_io.py'
581--- desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-09-28 12:06:08 +0000
582+++ desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-10-12 14:29:10 +0000
583@@ -18,6 +18,7 @@
584 """Communicate with CouchDB."""
585
586 import logging
587+import urllib
588
589 from desktopcouch import find_port as desktopcouch_find_port
590 from desktopcouch.records import server
591@@ -25,6 +26,7 @@
592 import socket
593 import uuid
594 import datetime
595+import urllib
596
597 RECTYPE_BASE = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/"
598 PAIRED_SERVER_RECORD_TYPE = RECTYPE_BASE + "paired_server"
599@@ -33,10 +35,15 @@
600 def mkuri(hostname, port, has_ssl=False, path="", auth_pair=None):
601 """Create a URI from parts."""
602 protocol = "https" if has_ssl else "http"
603- auth = (":".join(map(urllib.quote, auth_pair) + "@")) if auth_pair else ""
604- port = int(port)
605- uri = "%(protocol)s://%(auth)s%(hostname)s:%(port)d/%(path)s" % locals()
606- return uri
607+ if auth_pair:
608+ auth = (":".join(map(urllib.quote, auth_pair)) + "@")
609+ else:
610+ auth = ""
611+ if (protocol, port) in (("http", 80), ("https", 443)):
612+ return "%s://%s%s/%s" % (protocol, auth, hostname, path)
613+ else:
614+ port = str(port)
615+ return "%s://%s%s:%s/%s" % (protocol, auth, hostname, port, path)
616
617 def _get_db(name, create=True, uri=None):
618 """Get (and create?) a database."""
619@@ -115,6 +122,7 @@
620
621 excluded = set()
622 excluded.add("management")
623+ excluded.add("users")
624 excluded_msets = _get_management_data(PAIRED_SERVER_RECORD_TYPE,
625 "excluded_names", uri=uri)
626 for excluded_mset in excluded_msets:
627@@ -158,6 +166,8 @@
628 v = dict()
629 v["record_id"] = row.id
630 v["active"] = True
631+ if "oauth" in row.value:
632+ v["oauth"] = row.value["oauth"]
633 if "unpaired" in row.value:
634 v["active"] = not row.value["unpaired"]
635 hostid = row.value["pairing_identifier"]
636@@ -193,15 +203,27 @@
637 target_oauth=None):
638 """This replication is instant and blocking, and does not persist. """
639
640+ try:
641+ if target_host:
642+ # Target databases must exist before replicating to them.
643+ logging.debug("creating %r %s:%d %s", target_database, target_host,
644+ target_port, target_oauth)
645+ create_database(target_host, target_port, target_database,
646+ target_ssl, target_oauth)
647+ logging.debug("db exists, and we're ready to replicate")
648+ except:
649+ logging.exception("can't create/verify %r %s:%d oauth=%s",
650+ target_database, target_host, target_port, target_oauth)
651+
652 if source_host:
653- source = mkuri(source_host, source_port, source_ssl, source_database)
654+ source = mkuri(source_host, source_port, source_ssl, urllib.quote(source_database, safe=""))
655 else:
656- source = source_database
657+ source = urllib.quote(source_database, safe="")
658
659 if target_host:
660- target = mkuri(target_host, target_port, target_ssl, target_database)
661+ target = mkuri(target_host, target_port, target_ssl, urllib.quote(target_database, safe=""))
662 else:
663- target = target_database
664+ target = urllib.quote(target_database, safe="")
665
666 if source_oauth:
667 assert "consumer_secret" in source_oauth
668@@ -212,35 +234,24 @@
669 target = dict(url=target, auth=dict(oauth=target_oauth))
670
671 record = dict(source=source, target=target)
672- try:
673-
674- if target_host:
675- # Target databases must exist before replicating to them.
676- logging.debug("creating %r %s:%d", target_database, target_host,
677- target_port)
678- create_database(target_host, target_port, target_database,
679- target_ssl, target_oauth)
680- except:
681- logging.exception("can't talk to couchdb. %r %s:%d oauth=%s",
682- target_database, target_host, target_port, target_oauth)
683-
684- logging.debug("db exists, and we're ready to replicate")
685+
686 try:
687 # regardless of source and target, we talk to our local couchdb :(
688 port = int(desktopcouch_find_port())
689 url = mkuri("localhost", port,)
690
691- logging.debug("asking %r to send %s to %s", url, source, target)
692+ logging.debug("asking %r to replicate %s to %s, using record %s", url, source, target, record)
693
694 ### All until python-couchdb gets a Server.replicate() function
695 local_server = server.OAuthCapableServer(url)
696- resp, data = local_server.resource.post(path='/_replicate', content=record)
697+ resp, data = local_server.resource.post(path='/_replicate',
698+ content=record)
699
700 logging.debug("replicate result: %r %r", resp, data)
701 ###
702 except:
703- logging.error("can't talk to couchdb. %r <== %r", url, record)
704- raise
705+ logging.exception("can't replicate %r %r <== %r", source_database,
706+ url, record)
707
708 def get_pairings(uri=None):
709 """Get a list of paired servers."""
710
711=== modified file 'desktopcouch/pair/couchdb_pairing/dbus_io.py'
712--- desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-09-23 14:22:38 +0000
713+++ desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-10-12 14:29:10 +0000
714@@ -103,18 +103,14 @@
715 class LocationAdvertisement(Advertisement):
716 """An advertised couchdb location. See Advertisement class."""
717 def __init__(self, *args, **kwargs):
718- if "stype" in kwargs:
719- kwargs.pop(stype)
720- super(LocationAdvertisement, self).__init__(
721- stype=location_discovery_service_type, *args, **kwargs)
722+ kwargs["stype"] = location_discovery_service_type
723+ super(LocationAdvertisement, self).__init__(*args, **kwargs)
724
725 class PairAdvertisement(Advertisement):
726 """An advertised couchdb pairing opportunity. See Advertisement class."""
727 def __init__(self, *args, **kwargs):
728- if "stype" in kwargs:
729- kwargs.pop(stype)
730- super(PairAdvertisement, self).__init__(
731- stype=invitations_discovery_service_type, *args, **kwargs)
732+ kwargs["stype"] = invitations_discovery_service_type
733+ super(PairAdvertisement, self).__init__(*args, **kwargs)
734
735 def avahitext_to_dict(avahitext):
736 text = {}
737@@ -141,7 +137,13 @@
738 def get_seen_paired_hosts():
739 pairing_encyclopedia = couchdb_io.get_all_known_pairings()
740 return (
741- (uuid, addr, port, pairing_encyclopedia[uuid]["active"])
742+ (
743+ uuid,
744+ addr,
745+ port,
746+ not pairing_encyclopedia[uuid]["active"],
747+ pairing_encyclopedia[uuid]["oauth"],
748+ )
749 for uuid, (addr, port)
750 in nearby_desktop_couch_instances.items()
751 if uuid in pairing_encyclopedia)
752@@ -149,51 +151,39 @@
753 def maintain_discovered_servers(add_cb=cb_found_desktopcouch_server,
754 del_cb=cb_lost_desktopcouch_server):
755
756- def remove_item_handler(interface, protocol, name, stype, domain, flags):
757+ def remove_item_handler(cb, interface, protocol, name, stype, domain,
758+ flags):
759 """A service disappeared."""
760
761- def handle_error(*args):
762- """An error in resolving a new service."""
763- logging.error("zeroconf ItemNew error for services, %s", args)
764-
765- def handle_resolved(*args):
766- """Successfully resolved a new service, which we decode and send
767- back to our calling environment with the callback function."""
768-
769- name, host, port = args[2], args[5], args[8]
770- if name.startswith("desktopcouch "):
771- hostid = name[13:]
772- logging.debug("lost sight of %r", hostid)
773- del_cb(hostid)
774- else:
775- logging.error("no UUID in zeroconf message, %r", args)
776-
777- server.ResolveService(interface, protocol, name, stype,
778- domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
779- reply_handler=handle_resolved, error_handler=handle_error)
780-
781- def new_item_handler(interface, protocol, name, stype, domain, flags):
782+ if name.startswith("desktopcouch "):
783+ hostid = name[13:]
784+ logging.debug("lost sight of %r", hostid)
785+ cb(hostid)
786+ else:
787+ logging.error("annc doesn't look like one of ours. %r", name)
788+
789+ def new_item_handler(cb, interface, protocol, name, stype, domain, flags):
790 """A service appeared."""
791
792 def handle_error(*args):
793 """An error in resolving a new service."""
794 logging.error("zeroconf ItemNew error for services, %s", args)
795
796- def handle_resolved(*args):
797+ def handle_resolved(cb, *args):
798 """Successfully resolved a new service, which we decode and send
799 back to our calling environment with the callback function."""
800
801 name, host, port = args[2], args[5], args[8]
802- # FIXME strip off "desktopcouch "
803 if name.startswith("desktopcouch "):
804- add_cb(name[13:], host, port)
805+ cb(name[13:], host, port)
806 else:
807- logging.error("no UUID in zeroconf message, %r", name)
808+ logging.error("annc doesn't look like one of ours. %r", name)
809 return True
810
811 server.ResolveService(interface, protocol, name, stype,
812 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
813- reply_handler=handle_resolved, error_handler=handle_error)
814+ reply_handler=lambda *a: handle_resolved(cb, *a),
815+ error_handler=handle_error)
816
817 bus, server = get_dbus_bus_server()
818 domain_name = get_local_hostname()[1]
819@@ -203,8 +193,10 @@
820
821 sbrowser = dbus.Interface(browser_name,
822 avahi.DBUS_INTERFACE_SERVICE_BROWSER)
823- sbrowser.connect_to_signal("ItemNew", new_item_handler)
824- sbrowser.connect_to_signal("ItemRemove", remove_item_handler)
825+ sbrowser.connect_to_signal("ItemNew",
826+ lambda *a: new_item_handler(add_cb, *a))
827+ sbrowser.connect_to_signal("ItemRemove",
828+ lambda *a: remove_item_handler(del_cb, *a))
829 sbrowser.connect_to_signal("Failure",
830 lambda *a: logging.error("avahi error %r", a))
831
832@@ -214,27 +206,26 @@
833 """Start looking for services. Use two callbacks to handle seeing
834 new services and seeing services disappear."""
835
836- def remove_item_handler(interface, protocol, name, stype, domain, flags):
837+ def remove_item_handler(cb, interface, protocol, name, stype, domain, flags):
838 """A service disappeared."""
839
840 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:
841 return
842-
843- del_commport_name_cb(name)
844-
845- def new_item_handler(interface, protocol, name, stype, domain, flags):
846+ cb(name)
847+
848+ def new_item_handler(cb, interface, protocol, name, stype, domain, flags):
849 """A service appeared."""
850
851 def handle_error(*args):
852 """An error in resolving a new service."""
853 logging.error("zeroconf ItemNew error for services, %s", args)
854
855- def handle_resolved(*args):
856+ def handle_resolved(cb, *args):
857 """Successfully resolved a new service, which we decode and send
858 back to our calling environment with the callback function."""
859 text = avahitext_to_dict(args[9])
860 name, host, port = args[2], args[5], args[8]
861- add_commport_name_cb(name, text.get("description", "?"),
862+ cb(name, text.get("description", "?"),
863 host, port, text.get("version", None))
864
865 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:
866@@ -242,8 +233,8 @@
867
868 server.ResolveService(interface, protocol, name, stype,
869 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
870- reply_handler=handle_resolved, error_handler=handle_error)
871-
872+ reply_handler=lambda *a: handle_resolved(cb, *a),
873+ error_handler=handle_error)
874
875 bus, server = get_dbus_bus_server()
876 domain_name = get_local_hostname()[1]
877@@ -254,7 +245,9 @@
878
879 sbrowser = dbus.Interface(browser_name,
880 avahi.DBUS_INTERFACE_SERVICE_BROWSER)
881- sbrowser.connect_to_signal("ItemNew", new_item_handler)
882- sbrowser.connect_to_signal("ItemRemove", remove_item_handler)
883+ sbrowser.connect_to_signal("ItemNew",
884+ lambda *a: new_item_handler(add_commport_name_cb, *a))
885+ sbrowser.connect_to_signal("ItemRemove",
886+ lambda *a: remove_item_handler(del_commport_name_cb, *a))
887 sbrowser.connect_to_signal("Failure",
888 lambda *a: logging.error("avahi error %r", a))
889
890=== added file 'desktopcouch/pair/tests/test_couchdb_io.py'
891--- desktopcouch/pair/tests/test_couchdb_io.py 1970-01-01 00:00:00 +0000
892+++ desktopcouch/pair/tests/test_couchdb_io.py 2009-10-12 14:29:10 +0000
893@@ -0,0 +1,140 @@
894+# Copyright 2009 Canonical Ltd.
895+#
896+# This file is part of desktopcouch.
897+#
898+# desktopcouch is free software: you can redistribute it and/or modify
899+# it under the terms of the GNU Lesser General Public License version 3
900+# as published by the Free Software Foundation.
901+#
902+# desktopcouch is distributed in the hope that it will be useful,
903+# but WITHOUT ANY WARRANTY; without even the implied warranty of
904+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
905+# GNU Lesser General Public License for more details.
906+#
907+# You should have received a copy of the GNU Lesser General Public License
908+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
909+
910+
911+import pygtk
912+pygtk.require('2.0')
913+
914+import desktopcouch.tests as dctests
915+
916+from desktopcouch.pair.couchdb_pairing import couchdb_io
917+from desktopcouch.records.server import CouchDatabase
918+from desktopcouch.records.record import Record
919+import unittest
920+import uuid
921+import os
922+import httplib2
923+URI = None # use autodiscovery that desktopcouch.tests permits.
924+
925+class TestCouchdbIo(unittest.TestCase):
926+
927+ def setUp(self):
928+ """setup each test"""
929+ self.mgt_database = CouchDatabase('management', create=True, uri=URI)
930+ self.foo_database = CouchDatabase('foo', create=True, uri=URI)
931+ #create some records to pull out and test
932+ self.foo_database.put_record(Record({
933+ "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
934+ "record_type": "test.com"}))
935+ self.foo_database.put_record(Record({
936+ "key2_1": "val2_1", "key2_2": "val2_2", "key2_3": "val2_3",
937+ "record_type": "test.com"}))
938+ self.foo_database.put_record(Record({
939+ "key13_1": "va31_1", "key3_2": "val3_2", "key3_3": "val3_3",
940+ "record_type": "test.com"}))
941+
942+ def tearDown(self):
943+ """tear down each test"""
944+ del self.mgt_database._server['management']
945+ del self.mgt_database._server['foo']
946+
947+ def test_put_static_paired_service(self):
948+ service_name = "dummyfortest"
949+ oauth_data = {
950+ "consumer_key": str("abcdef"),
951+ "consumer_secret": str("ghighjklm"),
952+ "token": str("opqrst"),
953+ "token_secret": str("uvwxyz"),
954+ }
955+ couchdb_io.put_static_paired_service(oauth_data, service_name, uri=URI)
956+ pairings = list(couchdb_io.get_pairings())
957+
958+ def test_put_dynamic_paired_host(self):
959+ hostname = "host%d" % (os.getpid(),)
960+ remote_uuid = str(uuid.uuid4())
961+ oauth_data = {
962+ "consumer_key": str("abcdef"),
963+ "consumer_secret": str("ghighjklm"),
964+ "token": str("opqrst"),
965+ "token_secret": str("uvwxyz"),
966+ }
967+
968+ couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
969+ uri=URI)
970+ couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
971+ uri=URI)
972+ couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
973+ uri=URI)
974+
975+ pairings = list(couchdb_io.get_pairings())
976+ self.assertEqual(3, len(pairings))
977+ self.assertEqual(pairings[0].value["oauth"], oauth_data)
978+ self.assertEqual(pairings[0].value["server"], hostname)
979+ self.assertEqual(pairings[0].value["pairing_identifier"], remote_uuid)
980+
981+ for i, row in enumerate(pairings):
982+ couchdb_io.remove_pairing(row.id, i == 1)
983+
984+ pairings = list(couchdb_io.get_pairings())
985+ self.assertEqual(0, len(pairings))
986+
987+
988+ def test_get_database_names_replicatable_bad_server(self):
989+ # If this resolves, FIRE YOUR DNS PROVIDER.
990+
991+ try:
992+ names = couchdb_io.get_database_names_replicatable(
993+ uri='http://test.desktopcouch.example.com:9/')
994+ self.assertEqual(set(), names)
995+ except httplib2.ServerNotFoundError:
996+ pass
997+
998+ def test_get_database_names_replicatable(self):
999+ names = couchdb_io.get_database_names_replicatable(uri=URI)
1000+ self.assertFalse('management' in names)
1001+ self.assertTrue('foo' in names)
1002+
1003+ def test_get_my_host_unique_id(self):
1004+ got = couchdb_io.get_my_host_unique_id(uri=URI)
1005+ again = couchdb_io.get_my_host_unique_id(uri=URI)
1006+ self.assertEquals(len(got), 1)
1007+ self.assertEquals(got, again)
1008+
1009+ def test_mkuri(self):
1010+ uri = couchdb_io.mkuri(
1011+ 'fnord.org', 55241, has_ssl=True, path='a/b/c',
1012+ auth_pair=('f o o', 'b=a=r'))
1013+ self.assertEquals(
1014+ 'https://f%20o%20o:b%3Da%3Dr@fnord.org:55241/a/b/c', uri)
1015+
1016+ def Xtest_replication_good(self):
1017+ pass
1018+
1019+ def Xtest_replication_no_oauth_remote(self):
1020+ pass
1021+
1022+ def Xtest_replication_bad_oauth_remote(self):
1023+ pass
1024+
1025+ def Xtest_replication_no_oauth_local(self):
1026+ pass
1027+
1028+ def Xtest_replication_bad_oauth_local(self):
1029+ pass
1030+
1031+
1032+if __name__ == "__main__":
1033+ unittest.main()
1034
1035=== removed file 'desktopcouch/pair/tests/test_couchdb_io.py'
1036--- desktopcouch/pair/tests/test_couchdb_io.py 2009-09-28 12:06:08 +0000
1037+++ desktopcouch/pair/tests/test_couchdb_io.py 1970-01-01 00:00:00 +0000
1038@@ -1,133 +0,0 @@
1039-# Copyright 2009 Canonical Ltd.
1040-#
1041-# This file is part of desktopcouch.
1042-#
1043-# desktopcouch is free software: you can redistribute it and/or modify
1044-# it under the terms of the GNU Lesser General Public License version 3
1045-# as published by the Free Software Foundation.
1046-#
1047-# desktopcouch is distributed in the hope that it will be useful,
1048-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1049-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1050-# GNU Lesser General Public License for more details.
1051-#
1052-# You should have received a copy of the GNU Lesser General Public License
1053-# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
1054-
1055-
1056-import pygtk
1057-pygtk.require('2.0')
1058-
1059-import desktopcouch.tests as dctests
1060-
1061-from desktopcouch.pair.couchdb_pairing import couchdb_io
1062-from desktopcouch.records.server import CouchDatabase
1063-from desktopcouch.records.record import Record
1064-import unittest
1065-import uuid
1066-import os
1067-import httplib2
1068-URI = None # use autodiscovery that desktopcouch.tests permits.
1069-
1070-class TestCouchdbIo(unittest.TestCase):
1071-
1072- def setUp(self):
1073- """setup each test"""
1074- self.mgt_database = CouchDatabase('management', create=True, uri=URI)
1075- self.foo_database = CouchDatabase('foo', create=True, uri=URI)
1076- #create some records to pull out and test
1077- self.foo_database.put_record(Record({
1078- "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
1079- "record_type": "test.com"}))
1080- self.foo_database.put_record(Record({
1081- "key2_1": "val2_1", "key2_2": "val2_2", "key2_3": "val2_3",
1082- "record_type": "test.com"}))
1083- self.foo_database.put_record(Record({
1084- "key13_1": "va31_1", "key3_2": "val3_2", "key3_3": "val3_3",
1085- "record_type": "test.com"}))
1086-
1087- def tearDown(self):
1088- """tear down each test"""
1089- del self.mgt_database._server['management']
1090- del self.mgt_database._server['foo']
1091-
1092- def test_put_static_paired_service(self):
1093- service_name = "dummyfortest"
1094- oauth_data = {
1095- "consumer_key": str("abcdef"),
1096- "consumer_secret": str("ghighjklm"),
1097- "token": str("opqrst"),
1098- "token_secret": str("uvwxyz"),
1099- }
1100- couchdb_io.put_static_paired_service(oauth_data, service_name, uri=URI)
1101- pairings = list(couchdb_io.get_pairings())
1102-
1103- def test_put_dynamic_paired_host(self):
1104- hostname = "host%d" % (os.getpid(),)
1105- remote_uuid = str(uuid.uuid4())
1106- oauth_data = {
1107- "consumer_key": str("abcdef"),
1108- "consumer_secret": str("ghighjklm"),
1109- "token": str("opqrst"),
1110- "token_secret": str("uvwxyz"),
1111- }
1112-
1113- couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
1114- uri=URI)
1115- couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
1116- uri=URI)
1117- couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
1118- uri=URI)
1119-
1120- pairings = list(couchdb_io.get_pairings())
1121- self.assertEqual(3, len(pairings))
1122- self.assertEqual(pairings[0].value["oauth"], oauth_data)
1123- self.assertEqual(pairings[0].value["server"], hostname)
1124- self.assertEqual(pairings[0].value["pairing_identifier"], remote_uuid)
1125-
1126- for i, row in enumerate(pairings):
1127- couchdb_io.remove_pairing(row.id, i == 1)
1128-
1129- pairings = list(couchdb_io.get_pairings())
1130- self.assertEqual(0, len(pairings))
1131-
1132-
1133- def test_get_database_names_replicatable_bad_server(self):
1134- # If this resolves, FIRE YOUR DNS PROVIDER.
1135-
1136- try:
1137- names = couchdb_io.get_database_names_replicatable(
1138- uri='http://test.desktopcouch.example.com:9/')
1139- self.assertEqual(set(), names)
1140- except httplib2.ServerNotFoundError:
1141- pass
1142-
1143- def test_get_database_names_replicatable(self):
1144- names = couchdb_io.get_database_names_replicatable(uri=URI)
1145- self.assertFalse('management' in names)
1146- self.assertTrue('foo' in names)
1147-
1148- def test_get_my_host_unique_id(self):
1149- got = couchdb_io.get_my_host_unique_id(uri=URI)
1150- again = couchdb_io.get_my_host_unique_id(uri=URI)
1151- self.assertEquals(len(got), 1)
1152- self.assertEquals(got, again)
1153-
1154- def Xtest_replication_good(self):
1155- pass
1156-
1157- def Xtest_replication_no_oauth_remote(self):
1158- pass
1159-
1160- def Xtest_replication_bad_oauth_remote(self):
1161- pass
1162-
1163- def Xtest_replication_no_oauth_local(self):
1164- pass
1165-
1166- def Xtest_replication_bad_oauth_local(self):
1167- pass
1168-
1169-
1170-if __name__ == "__main__":
1171- unittest.main()
1172
1173=== modified file 'desktopcouch/records/couchgrid.py'
1174--- desktopcouch/records/couchgrid.py 2009-08-27 15:32:11 +0000
1175+++ desktopcouch/records/couchgrid.py 2009-10-12 14:29:10 +0000
1176@@ -212,7 +212,7 @@
1177 pass
1178
1179 #set the last value as the document_id, and append
1180- row[-1] = r.key
1181+ row[-1] = r.value["_id"]
1182 self.list_store.append(row)
1183
1184 #apply the model tot he Treeview
1185@@ -341,19 +341,6 @@
1186 for r in rows:
1187 selection.select_path(r)
1188
1189- @property
1190- def selected_records(self):
1191- """ selected_records - returns a list of Record objects
1192- for those selected in the CouchGrid.
1193-
1194- This property is read only.
1195-
1196- """
1197- recs = [] #a list of records to return
1198- for id in self.selected_record_ids:
1199- #retrieve a record for each id
1200- recs.append(Record(record_id = id, record_type = self.record_type))
1201- return recs
1202
1203 def __reset_model(self):
1204 """ __reset_model - internal funciton, do not call directly.
1205@@ -434,10 +421,6 @@
1206 for r in cw.selected_record_ids:
1207 disp += str(r) + "\n"
1208
1209- disp += "\n\nRecords:\n"
1210- for r in cw.selected_records:
1211- disp += str(r) + "\n"
1212-
1213 tv.get_buffer().set_text(disp)
1214
1215 def __select_ids(widget, widgets):
1216
1217=== added file 'desktopcouch/records/doc/field_registry.txt'
1218--- desktopcouch/records/doc/field_registry.txt 1970-01-01 00:00:00 +0000
1219+++ desktopcouch/records/doc/field_registry.txt 2009-10-12 14:29:10 +0000
1220@@ -0,0 +1,213 @@
1221+The Field Registry and Transformers
1222+
1223+Creating a field registry and/or a custom Transformer object is an
1224+easy yet flexible way to map data structures between desktopcouch and
1225+existing applications.
1226+
1227+>>> from desktopcouch.records.field_registry import (
1228+... SimpleFieldMapping, MergeableListFieldMapping, Transformer)
1229+>>> from desktopcouch.records.record import Record
1230+
1231+Say we have a very simple audiofile record type that defines 'artist'
1232+and 'title' string fields. Now also say we have an application that
1233+wants to interact with records of this type called 'My Awesome Music
1234+Player' or MAMP. The developers of MAMP use a data structure that has
1235+the same fields, but uses slightly different names for them:
1236+'songtitle' and 'songartist'. We can now define a mapping between the
1237+fields:
1238+
1239+>>> my_registry = {
1240+... 'songartist': SimpleFieldMapping('artist'),
1241+... 'songtitle': SimpleFieldMapping('title')
1242+... }
1243+
1244+and instantiate a Transformer object:
1245+
1246+>>> my_transformer = Transformer('My Awesome Music Player', my_registry)
1247+
1248+If MAMP has the following song object (a plain dictionary):
1249+
1250+>>> my_song = {
1251+... 'songartist': 'Thomas Tantrum',
1252+... 'songtitle': 'Shake It Shake It'
1253+... }
1254+
1255+We can have the transformer transform it into a desktopcouch record
1256+object:
1257+
1258+>>> AUDIO_FILE_RECORD_TYPE = 'http://example.org/record_types/audio_file'
1259+>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
1260+>>> my_transformer.from_app(my_song, new_record)
1261+
1262+Now we can look at the underlying data:
1263+
1264+>>> new_record._data #doctest: +NORMALIZE_WHITESPACE
1265+{'record_type': 'http://example.org/record_types/audio_file',
1266+ 'title': 'Shake It Shake It',
1267+ 'artist': 'Thomas Tantrum'}
1268+
1269+You might think that this doesn't really help all that much and that
1270+the code you would have had to write to do this yourself would not
1271+have been all that much bigger than using the Transformer and you'd be
1272+right, but this is not all the transformers do. Let's say the song in
1273+MAMP also has a field 'number_of_times_played_in_mamp':
1274+
1275+>>> my_song = {
1276+... 'songartist': 'Thomas Tantrum',
1277+... 'songtitle': 'Shake It Shake It',
1278+... 'number_of_times_played_in_mamp': 23
1279+... }
1280+
1281+Obviously that is not a field defined by our record type, since it is
1282+exceedingly unlikely that any other application would be interested in
1283+this data. Let's see what happens if we run the transformation with
1284+this field present, but undefined in the field registry:
1285+
1286+>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
1287+>>> my_transformer.from_app(my_song, new_record)
1288+
1289+>>> new_record._data #doctest: +NORMALIZE_WHITESPACE
1290+{'record_type': 'http://example.org/record_types/audio_file',
1291+ 'title': 'Shake It Shake It',
1292+ 'application_annotations': {'My Awesome Music Player': {'application_fields': {'number_of_times_played_in_mamp': 23}}},
1293+ 'artist': 'Thomas Tantrum'}
1294+
1295+The transformer, when it encountered a field it had no knowledge of,
1296+assumed it was specific to this application, and instead of ignoring
1297+it, stuffed it in the proper place in application_annotations. That's
1298+already quite useful.
1299+
1300+Let's try something a little trickier and more contrived. Say MAMP
1301+annotates each song in some other interesting ways: let's say it
1302+allows three very specific tags on each song:
1303+
1304+>>> my_song = {
1305+... 'songartist': 'Thomas Tantrum',
1306+... 'songtitle': 'Shake It Shake It',
1307+... 'number_of_times_played_in_mamp': 23,
1308+... 'tag_vocals': 'female vocals',
1309+... 'tag_title': 'shaking',
1310+... 'tag_subject': 'talking'
1311+... }
1312+
1313+Our record type is a little more enlightened, and allows any number of
1314+tags, in a field 'tags', where each tag has a field 'tag' and and a
1315+field 'description'. It would be nice if we could keep a mapping
1316+between the tags that MAMP cares about, and the ones in our
1317+record. We'll have to do just a little more work, but we can. We'll
1318+make a new field_registry, and instantiate a new transformer with it:
1319+
1320+>>> my_registry = {
1321+... 'songartist': SimpleFieldMapping('artist'),
1322+... 'songtitle': SimpleFieldMapping('title'),
1323+... 'tag_vocals': MergeableListFieldMapping(
1324+... 'My Awesome Music Player', 'vocals_tag', 'tags', 'tag',
1325+... default_values={'description': 'vocals'}),
1326+... 'tag_title': MergeableListFieldMapping(
1327+... 'My Awesome Music Player', 'title_tag', 'tags', 'tag',
1328+... default_values={'description': 'title'}),
1329+... 'tag_subject': MergeableListFieldMapping(
1330+... 'My Awesome Music Player', 'subject_tag', 'tags', 'tag',
1331+... default_values={'description': 'subject'}),
1332+... }
1333+
1334+>>> my_transformer = Transformer('My Awesome Music Player', my_registry)
1335+>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
1336+>>> my_transformer.from_app(my_song, new_record)
1337+
1338+Since _data will now contain lots of uuids to keep references intact,
1339+it's less readable, and a less clear example, so I'll show you what
1340+using the higher level API results in:
1341+
1342+>>> [tag['tag'] for tag in new_record['tags']]
1343+['shaking', 'talking', 'female vocals']
1344+>>> [tag['description'] for tag in new_record['tags']]
1345+['title', 'subject', 'vocals']
1346+
1347+Let's say we append a tag:
1348+
1349+>>> new_record['tags'].append({'tag': 'yeah yeah no'})
1350+
1351+and we do the same thing:
1352+
1353+>>> [tag['tag'] for tag in new_record['tags']]
1354+['shaking', 'talking', 'female vocals', 'yeah yeah no']
1355+>>> [tag.get('description') for tag in new_record['tags']]
1356+['title', 'subject', 'vocals', None]
1357+
1358+and say we change the first tag:
1359+
1360+>>> new_record['tags'][0]['tag'] = 'shaking it'
1361+
1362+and now look at transforming in the other direction:
1363+
1364+>>> new_song = {}
1365+>>> my_transformer.to_app(new_record, new_song)
1366+>>> new_song #doctest: +NORMALIZE_WHITESPACE
1367+{'tag_title': 'shaking it',
1368+ 'tag_subject': 'talking',
1369+ 'tag_vocals': 'female vocals',
1370+ 'songtitle': 'Shake It Shake It',
1371+ 'songartist': 'Thomas Tantrum',
1372+ 'number_of_times_played_in_mamp': 23}
1373+
1374+We see that we got the data that was in the original song, except with
1375+the tag_title value changed to 'shaking it', exactly as we'd expect'.
1376+
1377+Many more things are possible by creating new Transformers and/or
1378+FieldMapping types. I'll give one last example. Let us say that our
1379+record_type defines a rating field that's a value between 0 and
1380+100. Let's also say that MAMP stores a string with anywhere between
1381+zero and five stars.
1382+
1383+>>> class StarIntMapping(SimpleFieldMapping):
1384+... """Map a five star rating system to a score of 0 to 100 as
1385+... losslessly as possible.
1386+... """
1387+...
1388+... def getValue(self, record):
1389+... """Get the value for the registered field."""
1390+... score = record.get(self._fieldname)
1391+... stars = score / 20
1392+... remainder = score % 20
1393+... if remainder >= 5:
1394+... stars += 1
1395+... return "*" * stars
1396+...
1397+... def setValue(self, record, value):
1398+... """Set the value for the registered field."""
1399+... if value is None:
1400+... self.deleteValue(record)
1401+... return
1402+... star_score = len(value) * 20
1403+... score = record.get(self._fieldname)
1404+... if score is None or abs(star_score - score) > 5:
1405+... record[self._fieldname] = star_score
1406+... # else we keep the original value, since it was close
1407+... # enough and more precise
1408+
1409+And we make a registry and a transformer:
1410+
1411+>>> my_registry = {
1412+... 'songartist': SimpleFieldMapping('artist'),
1413+... 'songtitle': SimpleFieldMapping('title'),
1414+... 'stars': StarIntMapping('score'),
1415+... }
1416+>>> my_transformer = Transformer('My Awesome Music Player', my_registry)
1417+
1418+Create a song with a rating:
1419+
1420+>>> my_song = {
1421+... 'songartist': 'Thomas Tantrum',
1422+... 'songtitle': 'Shake It Shake It',
1423+... 'stars': '*****',
1424+... 'number_of_times_played_in_mamp': 23
1425+... }
1426+
1427+>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
1428+>>> my_transformer.from_app(my_song, new_record)
1429+>>> new_record['score']
1430+100
1431+
1432+And, I don't know if you've ever heard the song in question, but that
1433+is in fact correct! ;)
1434
1435=== modified file 'desktopcouch/records/doc/records.txt'
1436--- desktopcouch/records/doc/records.txt 2009-07-31 13:44:45 +0000
1437+++ desktopcouch/records/doc/records.txt 2009-10-12 14:29:10 +0000
1438@@ -3,15 +3,16 @@
1439 >>> from desktopcouch.records.server import CouchDatabase
1440 >>> from desktopcouch.records.record import Record
1441
1442-Create a database object. Your database needs to exist. If it doesn't, you
1443+Create a database object. Your database needs to exist. If it doesn't, you
1444 can create it by passing create=True.
1445
1446 >>> db = CouchDatabase('testing', create=True)
1447
1448-Create a Record object. Records have a record type, which should be a URL.
1449-The URL should point to a human-readable document which describes your
1450-record type. (This is not checked, though.) You can pass in an initial set
1451-of data.
1452+Create a Record object. Records have a record type, which should be a
1453+URL. The URL should point to a human-readable document which
1454+describes your record type. (This is not checked, though.) You can
1455+pass in an initial set of data.
1456+
1457 >>> r = Record({'a':'b'}, record_type='http://example.com/testrecord')
1458
1459 Records work like Python dicts.
1460@@ -32,6 +33,7 @@
1461 There is no ad-hoc query functionality.
1462
1463 For views, you should specify a design document for most all calls.
1464+
1465 >>> design_doc = "application"
1466
1467 To create a view:
1468@@ -41,20 +43,24 @@
1469 >>> db.add_view("blueberries", map_js, reduce_js, design_doc)
1470
1471 List views for a given design document:
1472+
1473 >>> db.list_views(design_doc)
1474 ['blueberries']
1475
1476 Test that a view exists:
1477+
1478 >>> db.view_exists("blueberries", design_doc)
1479 True
1480
1481-Execute a view. Results from execute_view() take list-like syntax to pick one
1482-or more rows to retreive. Use index or slice notation.
1483+Execute a view. Results from execute_view() take list-like syntax to
1484+pick one or more rows to retrieve. Use index or slice notation.
1485+
1486 >>> result = db.execute_view("blueberries", design_doc)
1487 >>> for row in result["idfoo"]:
1488 ... pass # all rows with id "idfoo". Unlike lists, may be more than one.
1489
1490 Finally, remove a view. It returns a dict containing the deleted view data.
1491+
1492 >>> db.delete_view("blueberries", design_doc)
1493 {'map': 'function(doc) { emit(doc._id, null) }'}
1494
1495
1496=== modified file 'desktopcouch/records/server.py'
1497--- desktopcouch/records/server.py 2009-09-28 12:06:08 +0000
1498+++ desktopcouch/records/server.py 2009-10-12 14:29:10 +0000
1499@@ -22,6 +22,7 @@
1500 """The Desktop Couch Records API."""
1501
1502 from couchdb import Server
1503+from couchdb.client import Resource
1504 import desktopcouch
1505 from desktopcouch.records import server_base
1506
1507@@ -37,7 +38,7 @@
1508 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"],
1509 oauth_tokens["token"], oauth_tokens["token_secret"])
1510 http.add_oauth_tokens(consumer_key, consumer_secret, token, token_secret)
1511- self.resource = server_base.Resource(http, uri)
1512+ self.resource = Resource(http, uri)
1513
1514 class CouchDatabase(server_base.CouchDatabaseBase):
1515 """An small records specific abstraction over a couch db database."""
1516
1517=== added file 'desktopcouch/records/server_base.py'
1518--- desktopcouch/records/server_base.py 1970-01-01 00:00:00 +0000
1519+++ desktopcouch/records/server_base.py 2009-10-12 14:29:10 +0000
1520@@ -0,0 +1,335 @@
1521+# Copyright 2009 Canonical Ltd.
1522+#
1523+# This file is part of desktopcouch.
1524+#
1525+# desktopcouch is free software: you can redistribute it and/or modify
1526+# it under the terms of the GNU Lesser General Public License version 3
1527+# as published by the Free Software Foundation.
1528+#
1529+# desktopcouch is distributed in the hope that it will be useful,
1530+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1531+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1532+# GNU Lesser General Public License for more details.
1533+#
1534+# You should have received a copy of the GNU Lesser General Public License
1535+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
1536+#
1537+# Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
1538+# Mark G. Saye <mark.saye@canonical.com>
1539+# Stuart Langridge <stuart.langridge@canonical.com>
1540+# Chad Miller <chad.miller@canonical.com>
1541+
1542+"""The Desktop Couch Records API."""
1543+
1544+from couchdb import Server
1545+from couchdb.client import ResourceNotFound, ResourceConflict
1546+from couchdb.design import ViewDefinition
1547+from record import Record
1548+import httplib2
1549+from oauth import oauth
1550+import urlparse
1551+import cgi
1552+
1553+#DEFAULT_DESIGN_DOCUMENT = "design"
1554+DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
1555+
1556+
1557+class NoSuchDatabase(Exception):
1558+ "Exception for trying to use a non-existent database"
1559+
1560+ def __init__(self, dbname):
1561+ self.database = dbname
1562+ super(NoSuchDatabase, self).__init__()
1563+
1564+ def __str__(self):
1565+ return ("Database %s does not exist on this server. (Create it by "
1566+ "passing create=True)") % self.database
1567+
1568+class OAuthAuthentication(httplib2.Authentication):
1569+ """An httplib2.Authentication subclass for OAuth"""
1570+ def __init__(self, oauth_data, host, request_uri, headers, response,
1571+ content, http):
1572+ self.oauth_data = oauth_data
1573+ httplib2.Authentication.__init__(self, None, host, request_uri,
1574+ headers, response, content, http)
1575+
1576+ def request(self, method, request_uri, headers, content):
1577+ """Modify the request headers to add the appropriate
1578+ Authorization header."""
1579+ consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
1580+ self.oauth_data['consumer_secret'])
1581+ access_token = oauth.OAuthToken(self.oauth_data['token'],
1582+ self.oauth_data['token_secret'])
1583+ scheme = "http"
1584+ sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
1585+ if ":" in self.host:
1586+ trash, port = self.host.split(":", 1)
1587+ if port == "443":
1588+ scheme = "https"
1589+ sig_method = oauth.OAuthSignatureMethod_PLAINTEXT
1590+ full_http_url = "%s://%s%s" % (scheme, self.host, request_uri)
1591+ schema, netloc, path, params, query, fragment = \
1592+ urlparse.urlparse(full_http_url)
1593+ querystr_as_dict = dict(cgi.parse_qsl(query))
1594+ req = oauth.OAuthRequest.from_consumer_and_token(
1595+ consumer,
1596+ access_token,
1597+ http_method = method,
1598+ http_url = full_http_url,
1599+ parameters = querystr_as_dict
1600+ )
1601+ req.sign_request(sig_method(), consumer, access_token)
1602+ headers.update(httplib2._normalize_headers(req.to_header()))
1603+
1604+class OAuthCapableHttp(httplib2.Http):
1605+ """Subclass of httplib2.Http which specifically uses our OAuth
1606+ Authentication subclass (because httplib2 doesn't know about it)"""
1607+ def add_oauth_tokens(self, consumer_key, consumer_secret,
1608+ token, token_secret):
1609+ self.oauth_data = {
1610+ "consumer_key": consumer_key,
1611+ "consumer_secret": consumer_secret,
1612+ "token": token,
1613+ "token_secret": token_secret
1614+ }
1615+
1616+ def _auth_from_challenge(self, host, request_uri, headers, response,
1617+ content):
1618+ """Since we know we're talking to desktopcouch, and we know that it
1619+ requires OAuth, just return the OAuthAuthentication here rather
1620+ than checking to see which supported auth method is required."""
1621+ yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
1622+ response, content, self)
1623+
1624+def row_is_deleted(row):
1625+ """Test if a row is marked as deleted. Smart views 'maps' should not
1626+ return rows that are marked as deleted, so this function is not often
1627+ required."""
1628+ try:
1629+ return row['application_annotations']['Ubuntu One']\
1630+ ['private_application_annotations']['deleted']
1631+ except KeyError:
1632+ return False
1633+
1634+
1635+class CouchDatabaseBase(object):
1636+ """An small records specific abstraction over a couch db database."""
1637+
1638+ def __init__(self, database, uri, record_factory=None, create=False,
1639+ server_class=Server, **server_class_extras):
1640+ self.server_uri = uri
1641+ self._server = server_class(self.server_uri, **server_class_extras)
1642+ if database not in self._server:
1643+ if create:
1644+ self._server.create(database)
1645+ else:
1646+ raise NoSuchDatabase(database)
1647+ self.db = self._server[database]
1648+ self.record_factory = record_factory or Record
1649+
1650+ def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
1651+ wrapper=None, **options):
1652+ """Pass-through to CouchDB library. Deprecated."""
1653+ return self.db.query(map_fun, reduce_fun, language,
1654+ wrapper, **options)
1655+
1656+ def get_record(self, record_id):
1657+ """Get a record from back end storage."""
1658+ try:
1659+ couch_record = self.db[record_id]
1660+ except ResourceNotFound:
1661+ return None
1662+ data = {}
1663+ if 'deleted' in couch_record.get('application_annotations', {}).get(
1664+ 'Ubuntu One', {}).get('private_application_annotations', {}):
1665+ return None
1666+ data.update(couch_record)
1667+ record = self.record_factory(data=data)
1668+ record.record_id = record_id
1669+ return record
1670+
1671+ def put_record(self, record):
1672+ """Put a record in back end storage."""
1673+ record_id = record.record_id or record._data.get('_id', '')
1674+ record_data = record._data
1675+ if record_id:
1676+ self.db[record_id] = record_data
1677+ else:
1678+ record_id = self._add_record(record_data)
1679+ return record_id
1680+
1681+ def update_fields(self, record_id, fields):
1682+ """Safely update a number of fields. 'fields' being a
1683+ dictionary with fieldname: value for only the fields we want
1684+ to change the value of.
1685+ """
1686+ while True:
1687+ record = self.db[record_id]
1688+ record.update(fields)
1689+ try:
1690+ self.db[record_id] = record
1691+ except ResourceConflict:
1692+ continue
1693+ break
1694+
1695+ def _add_record(self, data):
1696+ """Add a new record to the storage backend."""
1697+ return self.db.create(data)
1698+
1699+ def delete_record(self, record_id):
1700+ """Delete record with given id"""
1701+ record = self.db[record_id]
1702+ record.setdefault('application_annotations', {}).setdefault(
1703+ 'Ubuntu One', {}).setdefault('private_application_annotations', {})[
1704+ 'deleted'] = True
1705+ self.db[record_id] = record
1706+
1707+ def record_exists(self, record_id):
1708+ """Check if record with given id exists."""
1709+ if record_id not in self.db:
1710+ return False
1711+ record = self.db[record_id]
1712+ return 'deleted' not in record.get('application_annotations', {}).get(
1713+ 'Ubuntu One', {}).get('private_application_annotations', {})
1714+
1715+ def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
1716+ """Remove a view, given its name. Raises a KeyError on a unknown
1717+ view. Returns a dict of functions the deleted view defined."""
1718+ if design_doc is None:
1719+ design_doc = view_name
1720+
1721+ doc_id = "_design/%(design_doc)s" % locals()
1722+
1723+ # No atomic updates. Only read & mutate & write. Le sigh.
1724+ # First, get current contents.
1725+ try:
1726+ view_container = self.db[doc_id]["views"]
1727+ except (KeyError, ResourceNotFound):
1728+ raise KeyError
1729+
1730+ deleted_data = view_container.pop(view_name) # Remove target
1731+
1732+ if len(view_container) > 0:
1733+ # Construct a new list of objects representing all views to have.
1734+ views = [
1735+ ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
1736+ for k, v
1737+ in view_container.iteritems()
1738+ ]
1739+ # Push back a new batch of view. Pray to Eris that this doesn't
1740+ # clobber anything we want.
1741+
1742+ # sync_many does nothing if we pass an empty list. It even gets
1743+ # its design-document from the ViewDefinition items, and if there
1744+ # are no items, then it has no idea of a design document to
1745+ # update. This is a serious flaw. Thus, the "else" to follow.
1746+ ViewDefinition.sync_many(self.db, views, remove_missing=True)
1747+ else:
1748+ # There are no views left in this design document.
1749+
1750+ # Remove design document. This assumes there are only views in
1751+ # design documents. :(
1752+ del self.db[doc_id]
1753+
1754+ assert not self.view_exists(view_name, design_doc)
1755+
1756+ return deleted_data
1757+
1758+ def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
1759+ """Execute view and return results."""
1760+ if design_doc is None:
1761+ design_doc = view_name
1762+
1763+ view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
1764+ return self.db.view(view_id_fmt % locals())
1765+
1766+ def add_view(self, view_name, map_js, reduce_js,
1767+ design_doc=DEFAULT_DESIGN_DOCUMENT):
1768+ """Create a view, given a name and the two parts (map and reduce).
1769+ Return the document id."""
1770+ if design_doc is None:
1771+ design_doc = view_name
1772+
1773+ view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
1774+ view.sync(self.db)
1775+ assert self.view_exists(view_name, design_doc)
1776+
1777+ def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
1778+ """Does a view with a given name, in a optional design document
1779+ exist?"""
1780+ if design_doc is None:
1781+ design_doc = view_name
1782+
1783+ doc_id = "_design/%(design_doc)s" % locals()
1784+
1785+ try:
1786+ view_container = self.db[doc_id]["views"]
1787+ return view_name in view_container
1788+ except (KeyError, ResourceNotFound):
1789+ return False
1790+
1791+ def list_views(self, design_doc):
1792+ """Return a list of view names for a given design document. There is
1793+ no error if the design document does not exist or if there are no views
1794+ in it."""
1795+ doc_id = "_design/%(design_doc)s" % locals()
1796+ try:
1797+ return list(self.db[doc_id]["views"])
1798+ except (KeyError, ResourceNotFound):
1799+ return []
1800+
1801+ def get_records(self, record_type=None, create_view=False,
1802+ design_doc=DEFAULT_DESIGN_DOCUMENT):
1803+ """A convenience function to get records from a view named
1804+ C{get_records_and_type}. We optionally create a view in the design
1805+ document. C{create_view} may be True or False, and a special value,
1806+ None, is analogous to O_EXCL|O_CREAT .
1807+
1808+ Set record_type to a string to retrieve records of only that
1809+ specified type. Otherwise, usse the view to return *all* records.
1810+ If there is no view to use or we insist on creating a new view
1811+ and cannot, raise KeyError .
1812+
1813+ You can use index notation on the result to get rows with a
1814+ particular record type.
1815+ =>> results = get_records()
1816+ =>> for foo_document in results["foo"]:
1817+ ... print foo_document
1818+
1819+ Use slice notation to apply start-key and end-key options to the view.
1820+ =>> results = get_records()
1821+ =>> people = results[['Person']:['Person','ZZZZ']]
1822+ """
1823+ view_name = "get_records_and_type"
1824+ view_map_js = """
1825+ function(doc) {
1826+ try {
1827+ if (! doc['application_annotations']['Ubuntu One']
1828+ ['private_application_annotations']['deleted']) {
1829+ emit(doc.record_type, doc);
1830+ }
1831+ } catch (e) {
1832+ emit(doc.record_type, doc);
1833+ }
1834+ }"""
1835+
1836+ if design_doc is None:
1837+ design_doc = view_name
1838+
1839+ exists = self.view_exists(view_name, design_doc)
1840+
1841+ if exists:
1842+ if create_view is None:
1843+ raise KeyError("Exclusive creation failed.")
1844+ else:
1845+ if create_view == False:
1846+ raise KeyError("View doesn't already exist.")
1847+
1848+ if not exists:
1849+ self.add_view(view_name, view_map_js, None, design_doc)
1850+
1851+ viewdata = self.execute_view(view_name, design_doc)
1852+ if record_type is None:
1853+ return viewdata
1854+ else:
1855+ return viewdata[record_type]
1856
1857=== removed file 'desktopcouch/records/server_base.py'
1858--- desktopcouch/records/server_base.py 2009-09-28 12:06:08 +0000
1859+++ desktopcouch/records/server_base.py 1970-01-01 00:00:00 +0000
1860@@ -1,326 +0,0 @@
1861-# Copyright 2009 Canonical Ltd.
1862-#
1863-# This file is part of desktopcouch.
1864-#
1865-# desktopcouch is free software: you can redistribute it and/or modify
1866-# it under the terms of the GNU Lesser General Public License version 3
1867-# as published by the Free Software Foundation.
1868-#
1869-# desktopcouch is distributed in the hope that it will be useful,
1870-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1871-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1872-# GNU Lesser General Public License for more details.
1873-#
1874-# You should have received a copy of the GNU Lesser General Public License
1875-# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
1876-#
1877-# Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
1878-# Mark G. Saye <mark.saye@canonical.com>
1879-# Stuart Langridge <stuart.langridge@canonical.com>
1880-# Chad Miller <chad.miller@canonical.com>
1881-
1882-"""The Desktop Couch Records API."""
1883-
1884-from couchdb import Server
1885-from couchdb.client import ResourceNotFound, ResourceConflict, Resource
1886-from couchdb.design import ViewDefinition
1887-from record import Record
1888-import httplib2
1889-from oauth import oauth
1890-import urlparse
1891-import cgi
1892-import logging
1893-
1894-#DEFAULT_DESIGN_DOCUMENT = "design"
1895-DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
1896-
1897-
1898-class NoSuchDatabase(Exception):
1899- "Exception for trying to use a non-existent database"
1900-
1901- def __init__(self, dbname):
1902- self.database = dbname
1903- super(NoSuchDatabase, self).__init__()
1904-
1905- def __str__(self):
1906- return ("Database %s does not exist on this server. (Create it by "
1907- "passing create=True)") % self.database
1908-
1909-class OAuthAuthentication(httplib2.Authentication):
1910- """An httplib2.Authentication subclass for OAuth"""
1911- def __init__(self, oauth_data, host, request_uri, headers, response,
1912- content, http):
1913- self.oauth_data = oauth_data
1914- httplib2.Authentication.__init__(self, None, host, request_uri,
1915- headers, response, content, http)
1916-
1917- def request(self, method, request_uri, headers, content):
1918- """Modify the request headers to add the appropriate
1919- Authorization header."""
1920- consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
1921- self.oauth_data['consumer_secret'])
1922- access_token = oauth.OAuthToken(self.oauth_data['token'],
1923- self.oauth_data['token_secret'])
1924- full_http_url = "http://%s%s" % (self.host, request_uri)
1925- schema, netloc, path, params, query, fragment = urlparse.urlparse(full_http_url)
1926- querystr_as_dict = dict(cgi.parse_qsl(query))
1927- req = oauth.OAuthRequest.from_consumer_and_token(
1928- consumer,
1929- access_token,
1930- http_method = method,
1931- http_url = full_http_url,
1932- parameters = querystr_as_dict
1933- )
1934- req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), consumer, access_token)
1935- headers.update(httplib2._normalize_headers(req.to_header()))
1936- for header in headers.iteritems():
1937- logging.debug("header %s", header)
1938-
1939-class OAuthCapableHttp(httplib2.Http):
1940- """Subclass of httplib2.Http which specifically uses our OAuth
1941- Authentication subclass (because httplib2 doesn't know about it)"""
1942- def add_oauth_tokens(self, consumer_key, consumer_secret,
1943- token, token_secret):
1944- self.oauth_data = {
1945- "consumer_key": consumer_key,
1946- "consumer_secret": consumer_secret,
1947- "token": token,
1948- "token_secret": token_secret
1949- }
1950-
1951- def _auth_from_challenge(self, host, request_uri, headers, response, content):
1952- """Since we know we're talking to desktopcouch, and we know that it
1953- requires OAuth, just return the OAuthAuthentication here rather
1954- than checking to see which supported auth method is required."""
1955- yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
1956- response, content, self)
1957-
1958-def row_is_deleted(row):
1959- """Test if a row is marked as deleted. Smart views 'maps' should not
1960- return rows that are marked as deleted, so this function is not often
1961- required."""
1962- try:
1963- return row['application_annotations']['Ubuntu One']\
1964- ['private_application_annotations']['deleted']
1965- except KeyError:
1966- return False
1967-
1968-
1969-class CouchDatabaseBase(object):
1970- """An small records specific abstraction over a couch db database."""
1971-
1972- def __init__(self, database, uri, record_factory=None, create=False,
1973- server_class=Server, **server_class_extras):
1974- self.server_uri = uri
1975- self._server = server_class(self.server_uri, **server_class_extras)
1976- if database not in self._server:
1977- if create:
1978- self._server.create(database)
1979- else:
1980- raise NoSuchDatabase(database)
1981- self.db = self._server[database]
1982- self.record_factory = record_factory or Record
1983-
1984- def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
1985- wrapper=None, **options):
1986- """Pass-through to CouchDB library. Deprecated."""
1987- return self.db.query(map_fun, reduce_fun, language,
1988- wrapper, **options)
1989-
1990- def get_record(self, record_id):
1991- """Get a record from back end storage."""
1992- try:
1993- couch_record = self.db[record_id]
1994- except ResourceNotFound:
1995- return None
1996- data = {}
1997- data.update(couch_record)
1998- record = self.record_factory(data=data)
1999- record.record_id = record_id
2000- return record
2001-
2002- def put_record(self, record):
2003- """Put a record in back end storage."""
2004- record_id = record.record_id or record._data.get('_id', '')
2005- record_data = record._data
2006- if record_id:
2007- self.db[record_id] = record_data
2008- else:
2009- record_id = self._add_record(record_data)
2010- return record_id
2011-
2012- def update_fields(self, doc_id, fields):
2013- """Safely update a number of fields. 'fields' being a
2014- dictionary with fieldname: value for only the fields we want
2015- to change the value of.
2016- """
2017- while True:
2018- doc = self.db[doc_id]
2019- doc.update(fields)
2020- try:
2021- self.db[doc.id] = doc
2022- except ResourceConflict:
2023- continue
2024- break
2025-
2026- def _add_record(self, data):
2027- """Add a new record to the storage backend."""
2028- return self.db.create(data)
2029-
2030- def delete_record(self, record_id):
2031- """Delete record with given id"""
2032- record = self.db[record_id]
2033- record.setdefault('application_annotations', {}).setdefault(
2034- 'Ubuntu One', {}).setdefault('private_application_annotations', {})[
2035- 'deleted'] = True
2036- self.db[record_id] = record
2037-
2038- def record_exists(self, record_id):
2039- """Check if record with given id exists."""
2040- if record_id not in self.db:
2041- return False
2042- record = self.db[record_id]
2043- return 'deleted' not in record.get('application_annotations', {}).get(
2044- 'Ubuntu One', {}).get('private_application_annotations', {})
2045-
2046- def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
2047- """Remove a view, given its name. Raises a KeyError on a unknown
2048- view. Returns a dict of functions the deleted view defined."""
2049- if design_doc is None:
2050- design_doc = view_name
2051-
2052- doc_id = "_design/%(design_doc)s" % locals()
2053-
2054- # No atomic updates. Only read & mutate & write. Le sigh.
2055- # First, get current contents.
2056- try:
2057- view_container = self.db[doc_id]["views"]
2058- except (KeyError, ResourceNotFound):
2059- raise KeyError
2060-
2061- deleted_data = view_container.pop(view_name) # Remove target
2062-
2063- if len(view_container) > 0:
2064- # Construct a new list of objects representing all views to have.
2065- views = [
2066- ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
2067- for k, v
2068- in view_container.iteritems()
2069- ]
2070- # Push back a new batch of view. Pray to Eris that this doesn't
2071- # clobber anything we want.
2072-
2073- # sync_many does nothing if we pass an empty list. It even gets
2074- # its design-document from the ViewDefinition items, and if there
2075- # are no items, then it has no idea of a design document to
2076- # update. This is a serious flaw. Thus, the "else" to follow.
2077- ViewDefinition.sync_many(self.db, views, remove_missing=True)
2078- else:
2079- # There are no views left in this design document.
2080-
2081- # Remove design document. This assumes there are only views in
2082- # design documents. :(
2083- del self.db[doc_id]
2084-
2085- assert not self.view_exists(view_name, design_doc)
2086-
2087- return deleted_data
2088-
2089- def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
2090- """Execute view and return results."""
2091- if design_doc is None:
2092- design_doc = view_name
2093-
2094- view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
2095- return self.db.view(view_id_fmt % locals())
2096-
2097- def add_view(self, view_name, map_js, reduce_js,
2098- design_doc=DEFAULT_DESIGN_DOCUMENT):
2099- """Create a view, given a name and the two parts (map and reduce).
2100- Return the document id."""
2101- if design_doc is None:
2102- design_doc = view_name
2103-
2104- view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
2105- view.sync(self.db)
2106- assert self.view_exists(view_name, design_doc)
2107-
2108- def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
2109- """Does a view with a given name, in a optional design document
2110- exist?"""
2111- if design_doc is None:
2112- design_doc = view_name
2113-
2114- doc_id = "_design/%(design_doc)s" % locals()
2115-
2116- try:
2117- view_container = self.db[doc_id]["views"]
2118- return view_name in view_container
2119- except (KeyError, ResourceNotFound):
2120- return False
2121-
2122- def list_views(self, design_doc):
2123- """Return a list of view names for a given design document. There is
2124- no error if the design document does not exist or if there are no views
2125- in it."""
2126- doc_id = "_design/%(design_doc)s" % locals()
2127- try:
2128- return list(self.db[doc_id]["views"])
2129- except (KeyError, ResourceNotFound):
2130- return []
2131-
2132- def get_records(self, record_type=None, create_view=False,
2133- design_doc=DEFAULT_DESIGN_DOCUMENT):
2134- """A convenience function to get records from a view named
2135- C{get_records_and_type}. We optionally create a view in the design
2136- document. C{create_view} may be True or False, and a special value,
2137- None, is analogous to O_EXCL|O_CREAT .
2138-
2139- Set record_type to a string to retrieve records of only that
2140- specified type. Otherwise, usse the view to return *all* records.
2141- If there is no view to use or we insist on creating a new view
2142- and cannot, raise KeyError .
2143-
2144- You can use index notation on the result to get rows with a
2145- particular record type.
2146- =>> results = get_records()
2147- =>> for foo_document in results["foo"]:
2148- ... print foo_document
2149-
2150- Use slice notation to apply start-key and end-key options to the view.
2151- =>> results = get_records()
2152- =>> people = results[['Person']:['Person','ZZZZ']]
2153- """
2154- view_name = "get_records_and_type"
2155- view_map_js = """
2156- function(doc) {
2157- try {
2158- if (! doc['application_annotations']['Ubuntu One']
2159- ['private_application_annotations']['deleted']) {
2160- emit(doc.record_type, doc);
2161- }
2162- } catch (e) {
2163- emit(doc.record_type, doc);
2164- }
2165- }"""
2166-
2167- if design_doc is None:
2168- design_doc = view_name
2169-
2170- exists = self.view_exists(view_name, design_doc)
2171-
2172- if exists:
2173- if create_view is None:
2174- raise KeyError("Exclusive creation failed.")
2175- else:
2176- if create_view == False:
2177- raise KeyError("View doesn't already exist.")
2178-
2179- if not exists:
2180- self.add_view(view_name, view_map_js, None, design_doc)
2181-
2182- viewdata = self.execute_view(view_name, design_doc)
2183- if record_type is None:
2184- return viewdata
2185- else:
2186- return viewdata[record_type]
2187
2188=== modified file 'desktopcouch/records/tests/test_couchgrid.py'
2189--- desktopcouch/records/tests/test_couchgrid.py 2009-09-23 14:22:38 +0000
2190+++ desktopcouch/records/tests/test_couchgrid.py 2009-10-12 14:29:10 +0000
2191@@ -128,6 +128,27 @@
2192 self.assertEqual(cw.get_model().get_n_columns(),4)
2193 self.assertEqual(len(cw.get_model()),2)
2194
2195+ def test_selected_id_property(self):
2196+ #create some records
2197+ db = CouchDatabase(self.dbname, create=True)
2198+ id1 = db.put_record(Record({
2199+ "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
2200+ "record_type": self.record_type}))
2201+ id2 = db.put_record(Record({
2202+ "key1_1": "val2_1", "key1_2": "val2_2", "key1_3": "val2_3",
2203+ "record_type": self.record_type}))
2204+
2205+ #build the CouchGrid
2206+ cw = CouchGrid(self.dbname)
2207+ cw.record_type = self.record_type
2208+
2209+ #make sure the record ids are selected properly
2210+ cw.selected_record_ids = [id1]
2211+ self.assertEqual(cw.selected_record_ids[0], id1)
2212+ cw.selected_record_ids = [id2]
2213+ self.assertEqual(cw.selected_record_ids[0], id2)
2214+
2215+
2216 def test_single_col_from_database(self):
2217 #create some records
2218 self.db.put_record(Record({
2219
2220=== modified file 'desktopcouch/records/tests/test_field_registry.py'
2221--- desktopcouch/records/tests/test_field_registry.py 2009-08-27 15:32:11 +0000
2222+++ desktopcouch/records/tests/test_field_registry.py 2009-10-12 14:29:10 +0000
2223@@ -17,7 +17,7 @@
2224
2225 """Test cases for field mapping"""
2226
2227-import copy
2228+import copy, doctest
2229 from testtools import TestCase
2230 from desktopcouch.records.field_registry import (
2231 SimpleFieldMapping, MergeableListFieldMapping, Transformer)
2232@@ -111,3 +111,7 @@
2233 self.transformer.to_app(record, data)
2234 self.assertEqual(
2235 {'simpleField': 23, 'strawberryField': 'the value'}, data)
2236+
2237+ def test_run_doctests(self):
2238+ results = doctest.testfile('../doc/field_registry.txt')
2239+ self.assertEqual(0, results.failed)
2240
2241=== modified file 'desktopcouch/records/tests/test_record.py'
2242--- desktopcouch/records/tests/test_record.py 2009-08-27 15:32:11 +0000
2243+++ desktopcouch/records/tests/test_record.py 2009-10-12 14:29:10 +0000
2244@@ -19,6 +19,7 @@
2245 """Tests for the RecordDict object on which the Contacts API is built."""
2246
2247 from testtools import TestCase
2248+import doctest
2249
2250 # pylint does not like relative imports from containing packages
2251 # pylint: disable-msg=F0401
2252@@ -179,6 +180,10 @@
2253 self.assertEqual('http://fnord.org/smorgasbord',
2254 self.record.record_type)
2255
2256+ def test_run_doctests(self):
2257+ results = doctest.testfile('../doc/records.txt')
2258+ self.assertEqual(0, results.failed)
2259+
2260
2261 class TestRecordFactory(TestCase):
2262 """Test Record/Mergeable List factories."""
2263
2264=== modified file 'desktopcouch/records/tests/test_server.py' (properties changed: +x to -x)
2265--- desktopcouch/records/tests/test_server.py 2009-09-23 14:22:38 +0000
2266+++ desktopcouch/records/tests/test_server.py 2009-10-12 14:29:10 +0000
2267@@ -89,6 +89,14 @@
2268 self.assert_(deleted_record['application_annotations']['Ubuntu One'][
2269 'private_application_annotations']['deleted'])
2270
2271+ def test_get_deleted_record(self):
2272+ """Test (not) getting a deleted record."""
2273+ record = Record({'record_number': 0}, record_type="http://example.com/")
2274+ record_id = self.database.put_record(record)
2275+ self.database.delete_record(record_id)
2276+ retrieved_record = self.database.get_record(record_id)
2277+ self.assertEqual(None, retrieved_record)
2278+
2279 def test_record_exists(self):
2280 """Test checking whether a record exists."""
2281 record = Record({'record_number': 0}, record_type="http://example.com/")
2282
2283=== added file 'desktopcouch/replication.py'
2284--- desktopcouch/replication.py 1970-01-01 00:00:00 +0000
2285+++ desktopcouch/replication.py 2009-10-12 14:29:10 +0000
2286@@ -0,0 +1,248 @@
2287+# Copyright 2009 Canonical Ltd.
2288+#
2289+# This file is part of desktopcouch.
2290+#
2291+# desktopcouch is free software: you can redistribute it and/or modify
2292+# it under the terms of the GNU Lesser General Public License version 3
2293+# as published by the Free Software Foundation.
2294+#
2295+# desktopcouch is distributed in the hope that it will be useful,
2296+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2297+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2298+# GNU Lesser General Public License for more details.
2299+#
2300+# You should have received a copy of the GNU Lesser General Public License
2301+# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
2302+#
2303+# Authors: Chad Miller <chad.miller@canonical.com>
2304+
2305+import logging
2306+log = logging.getLogger("replication")
2307+
2308+import dbus.exceptions
2309+
2310+from desktopcouch.pair.couchdb_pairing import couchdb_io
2311+from desktopcouch.pair.couchdb_pairing import dbus_io
2312+from desktopcouch import replication_services
2313+
2314+try:
2315+ import urlparse
2316+except ImportError:
2317+ import urllib.parse as urlparse
2318+
2319+from twisted.internet import task, reactor
2320+
2321+
2322+known_bad_service_names = set()
2323+already_replicating = False
2324+is_running = True
2325+
2326+
2327+def db_targetprefix_for_service(service_name):
2328+ """Use the service name to look up what the prefix should be on the
2329+ databases. This gives an egalitarian way for non-UbuntuOne servers to have
2330+ their own remote-db-name scheme."""
2331+ try:
2332+ container = "desktopcouch.replication_services"
2333+ log.debug("Looking up prefix for service %r", service_name)
2334+ mod = __import__(container, fromlist=[service_name])
2335+ return getattr(mod, service_name).db_name_prefix
2336+ except ImportError, e:
2337+ log.error("The service %r is unknown. It is not a "
2338+ "module in the %s package ." % (service_name, container))
2339+ return ""
2340+ except Exception, e:
2341+ log.exception("Not changing remote db name.")
2342+ return ""
2343+
2344+def oauth_info_for_service(service_name):
2345+ """Use the service name to look up what oauth information we should use
2346+ when talking to that service."""
2347+ try:
2348+ container = "desktopcouch.replication_services"
2349+ log.debug("Looking up prefix for service %r", service_name)
2350+ mod = __import__(container, fromlist=[service_name])
2351+ return getattr(mod, service_name).get_oauth_data()
2352+ except ImportError, e:
2353+ log.error("The service %r is unknown. It is not a "
2354+ "module in the %s package ." % (service_name, container))
2355+ return None
2356+
2357+def do_all_replication(local_port):
2358+ log.debug("started replicating")
2359+ try:
2360+ global already_replicating # Fuzzy, as not really critical,
2361+ already_replicating = True # just trying to be polite.
2362+
2363+ try:
2364+ # All machines running desktopcouch must advertise themselves with
2365+ # zeroconf. We collect those elsewhere and filter out the ones
2366+ # that we have paired with. Now, it's time to send our changes to
2367+ # all those.
2368+
2369+ for remote_hostid, addr, port, is_unpaired, remote_oauth in \
2370+ dbus_io.get_seen_paired_hosts():
2371+
2372+ if is_unpaired:
2373+ # The far end doesn't know want to break up.
2374+ count = 0
2375+ for local_identifier in couchdb_io.get_my_host_unique_id():
2376+ last_exception = None
2377+ try:
2378+ # Tell her gently, using each pseudonym.
2379+ couchdb_io.expunge_pairing(local_identifier,
2380+ couchdb_io.mkuri(addr, port), remote_oauth)
2381+ count += 1
2382+ except Exception, e:
2383+ last_exception = e
2384+ if count == 0:
2385+ if last_exception is not None:
2386+ # If she didn't recognize us, something's wrong.
2387+ try:
2388+ raise last_exception
2389+ # push caught exception back...
2390+ except:
2391+ # ... so that we log it here.
2392+ logging.exception(
2393+ "failed to unpair from other end.")
2394+ continue
2395+ else:
2396+ # Finally, find your inner peace...
2397+ couchdb_io.expunge_pairing(remote_hostid)
2398+ # ...and move on.
2399+ continue
2400+
2401+ # Ah, good, this is an active relationship. Be a giver.
2402+ log.debug("want to replipush to discovered host %r @ %s",
2403+ remote_hostid, addr)
2404+ for db_name in couchdb_io.get_database_names_replicatable(
2405+ couchdb_io.mkuri("localhost", local_port)):
2406+ if not is_running: return
2407+ couchdb_io.replicate(db_name, db_name,
2408+ target_host=addr, target_port=port,
2409+ source_port=local_port, target_oauth=remote_oauth)
2410+ log.debug("replication of discovered hosts finished")
2411+ except Exception, e:
2412+ log.exception("replication of discovered hosts aborted")
2413+ pass
2414+
2415+ try:
2416+ # There may be services we send data to. Use the service name (sn)
2417+ # to look up what the service needs from us.
2418+
2419+ for remote_hostid, sn, to_pull, to_push in \
2420+ couchdb_io.get_static_paired_hosts():
2421+
2422+ if not sn in dir(replication_services):
2423+ if not is_running: return
2424+ if sn in known_bad_service_names:
2425+ continue # Don't nag.
2426+ known_bad_service_names.add(sn)
2427+
2428+ remote_oauth_data = oauth_info_for_service(sn)
2429+
2430+ # TODO: push all this into service module.
2431+ try:
2432+ remote_location = db_targetprefix_for_service(sn)
2433+ urlinfo = urlparse.urlsplit(str(remote_location))
2434+ except ValueError, e:
2435+ log.warn("Can't reach service %s. %s", sn, e)
2436+ continue
2437+ if ":" in urlinfo.netloc:
2438+ addr, port = urlinfo.netloc.rsplit(":", 1)
2439+ else:
2440+ addr = urlinfo.netloc
2441+ port = 443 if urlinfo.scheme == "https" else 80
2442+ remote_db_name_prefix = urlinfo.path.strip("/")
2443+ # ^
2444+
2445+ if to_pull:
2446+ for db_name in couchdb_io.get_database_names_replicatable(
2447+ couchdb_io.mkuri("localhost", int(local_port))):
2448+ if not is_running: return
2449+
2450+ remote_db_name = remote_db_name_prefix + "/" + db_name
2451+
2452+ log.debug("want to replipush %r to static host %r @ %s",
2453+ remote_db_name, remote_hostid, addr)
2454+ couchdb_io.replicate(db_name, remote_db_name,
2455+ target_host=addr, target_port=port,
2456+ source_port=local_port, target_ssl=True,
2457+ target_oauth=remote_oauth_data)
2458+ if to_push:
2459+ for remote_db_name in \
2460+ couchdb_io.get_database_names_replicatable(
2461+ couchdb_io.mkuri("localhost",
2462+ int(local_port))):
2463+ if not is_running: return
2464+ try:
2465+ if not remote_db_name.startswith(
2466+ str(remote_db_name_prefix + "/")):
2467+ continue
2468+ except ValueError, e:
2469+ log.error("skipping %r on %s. %s", db_name, sn, e)
2470+ continue
2471+
2472+ prefix_len = len(str(remote_db_name_prefix))
2473+ db_name = remote_db_name[1+prefix_len:]
2474+ if db_name.strip("/") == "management":
2475+ continue # be paranoid about what we accept.
2476+ log.debug(
2477+ "want to replipull %r from static host %r @ %s",
2478+ db_name, remote_hostid, addr)
2479+ couchdb_io.replicate(remote_db_name, db_name,
2480+ source_host=addr, source_port=port,
2481+ target_port=local_port, source_ssl=True,
2482+ source_oauth=remote_oauth_data)
2483+
2484+ except Exception, e:
2485+ log.exception("replication of services aborted")
2486+ pass
2487+ finally:
2488+ already_replicating = False
2489+ log.debug("finished replicating")
2490+
2491+
2492+def replicate_local_databases_to_paired_hosts(local_port):
2493+ if already_replicating:
2494+ log.warn("haven't finished replicating before next time to start.")
2495+ return False
2496+
2497+ reactor.callInThread(do_all_replication, local_port)
2498+
2499+def set_up(port_getter):
2500+ port = port_getter()
2501+ unique_identifiers = couchdb_io.get_my_host_unique_id(
2502+ couchdb_io.mkuri("localhost", int(port)), create=True)
2503+
2504+ beacons = [dbus_io.LocationAdvertisement(port, "desktopcouch " + i)
2505+ for i in unique_identifiers]
2506+ for b in beacons:
2507+ try:
2508+ b.publish()
2509+ except dbus.exceptions.DBusException, e:
2510+ log.error("We seem to be running already, or can't publish "
2511+ "our zeroconf advert. %s", e)
2512+ return None
2513+
2514+ dbus_io.maintain_discovered_servers()
2515+
2516+ t = task.LoopingCall(replicate_local_databases_to_paired_hosts, port)
2517+ t.start(600)
2518+
2519+ # TODO: port may change, so every so often, check it and
2520+ # perhaps refresh the beacons. We return an array of beacons, so we could
2521+ # keep a reference to that array and mutate it when the port-beacons
2522+ # change.
2523+
2524+ return beacons, t
2525+
2526+
2527+def tear_down(beacons, looping_task):
2528+ for b in beacons:
2529+ b.unpublish()
2530+ try:
2531+ is_running = False
2532+ looping_task.stop()
2533+ except:
2534+ pass
2535
2536=== removed file 'desktopcouch/replication.py'
2537--- desktopcouch/replication.py 2009-09-28 12:06:08 +0000
2538+++ desktopcouch/replication.py 1970-01-01 00:00:00 +0000
2539@@ -1,242 +0,0 @@
2540-# Copyright 2009 Canonical Ltd.
2541-#
2542-# This file is part of desktopcouch.
2543-#
2544-# desktopcouch is free software: you can redistribute it and/or modify
2545-# it under the terms of the GNU Lesser General Public License version 3
2546-# as published by the Free Software Foundation.
2547-#
2548-# desktopcouch is distributed in the hope that it will be useful,
2549-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2550-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2551-# GNU Lesser General Public License for more details.
2552-#
2553-# You should have received a copy of the GNU Lesser General Public License
2554-# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
2555-#
2556-# Authors: Chad Miller <chad.miller@canonical.com>
2557-
2558-import threading
2559-import logging
2560-import logging.handlers
2561-log = logging.getLogger("replication")
2562-
2563-import dbus.exceptions
2564-
2565-import desktopcouch
2566-from desktopcouch.pair.couchdb_pairing import couchdb_io
2567-from desktopcouch.pair.couchdb_pairing import dbus_io
2568-from desktopcouch import replication_services
2569-
2570-try:
2571- import urlparse
2572-except ImportError:
2573- import urllib.parse as urlparse
2574-
2575-from twisted.internet import task, reactor
2576-
2577-
2578-known_bad_service_names = set()
2579-already_replicating = False
2580-is_running = True
2581-
2582-
2583-def db_targetprefix_for_service(service_name):
2584- """Use the service name to look up what the prefix should be on the
2585- databases. This gives an egalitarian way for non-UbuntuOne servers to have
2586- their own remote-db-name scheme."""
2587- try:
2588- container = "desktopcouch.replication_services"
2589- log.debug("Looking up prefix for service %r", service_name)
2590- mod = __import__(container, fromlist=[service_name])
2591- return getattr(mod, service_name).db_name_prefix
2592- except ImportError, e:
2593- log.error("The service %r is unknown. It is not a "
2594- "module in the %s package ." % (sn, container))
2595- return ""
2596- except Exception, e:
2597- log.exception("Not changing remote db name.")
2598- return ""
2599-
2600-def oauth_info_for_service(service_name):
2601- """Use the service name to look up what oauth information we should use
2602- when talking to that service."""
2603- try:
2604- container = "desktopcouch.replication_services"
2605- log.debug("Looking up prefix for service %r", service_name)
2606- mod = __import__(container, fromlist=[service_name])
2607- return getattr(mod, service_name).get_oauth_data()
2608- except ImportError, e:
2609- log.error("The service %r is unknown. It is not a "
2610- "module in the %s package ." % (sn, container))
2611- return None
2612-
2613-def do_all_replication(local_port):
2614- log.debug("started replicating")
2615- try:
2616- global already_replicating # Fuzzy, as not really critical,
2617- already_replicating = True # just trying to be polite.
2618-
2619- try:
2620- # All machines running desktopcouch must advertise themselves with
2621- # zeroconf. We collect those elsewhere and filter out the ones
2622- # that we have paired with. Now, it's time to send our changes to
2623- # all those.
2624-
2625- for remote_hostid, addr, port, is_unpaired in \
2626- dbus_io.get_seen_paired_hosts():
2627-
2628- if is_unpaired:
2629- # The far end doesn't know want to break up.
2630- count = 0
2631- for local_identifier in couchdb_io.get_my_host_unique_id():
2632- last_exception = None
2633- try:
2634- # Tell her gently, using each pseudonym.
2635- couchdb_io.expunge_pairing(local_identifier,
2636- couchdb_io.mkuri(addr, port))
2637- count += 1
2638- except Exception, e:
2639- last_exception = e
2640- if count == 0:
2641- if last_exception is not None:
2642- # If she didn't recognize us, something's wrong.
2643- raise last_exception
2644- else:
2645- # Finally, find your inner peace...
2646- couchdb_io.expunge_pairing(remote_identifier)
2647- # ...and move on.
2648- continue
2649-
2650- # Ah, good, this is an active relationship. Be a giver.
2651- log.debug("want to replipush to discovered host %r @ %s",
2652- remote_hostid, addr)
2653- for db_name in couchdb_io.get_database_names_replicatable(
2654- couchdb_io.mkuri("localhost", local_port)):
2655- if not is_running: return
2656- couchdb_io.replicate(db_name, db_name,
2657- target_host=addr, target_port=port,
2658- source_port=local_port)
2659- except Exception, e:
2660- log.exception("replication of discovered hosts aborted")
2661- pass
2662-
2663- try:
2664- # There may be services we send data to. Use the service name (sn)
2665- # to look up what the service needs from us.
2666-
2667- for remote_hostid, sn, to_pull, to_push in \
2668- couchdb_io.get_static_paired_hosts():
2669-
2670- if not sn in dir(replication_services):
2671- if not is_running: return
2672- if sn in known_bad_service_names:
2673- continue # Don't nag.
2674- known_bad_service_names.add(sn)
2675-
2676- remote_oauth_data = oauth_info_for_service(sn)
2677-
2678- # TODO: push all this into service module.
2679- try:
2680- remote_location = db_targetprefix_for_service(sn)
2681- urlinfo = urlparse.urlsplit(str(remote_location))
2682- except ValueError, e:
2683- log.warn("Can't reach service %s. %s", sn, e)
2684- continue
2685- if ":" in urlinfo.netloc:
2686- addr, port = urlinfo.netloc.rsplit(":", 1)
2687- else:
2688- addr = urlinfo.netloc
2689- port = 443 if urlinfo.scheme == "https" else 80
2690- remote_db_name_prefix = urlinfo.path.strip("/")
2691- # ^
2692-
2693- if to_pull:
2694- for db_name in couchdb_io.get_database_names_replicatable(
2695- couchdb_io.mkuri("localhost", int(local_port))):
2696- if not is_running: return
2697-
2698- remote_db_name = remote_db_name_prefix + "/" + db_name
2699-
2700- log.debug("want to replipush %r to static host %r @ %s",
2701- remote_db_name, remote_hostid, addr)
2702- couchdb_io.replicate(db_name, remote_db_name,
2703- target_host=addr, target_port=port,
2704- source_port=local_port, target_ssl=True,
2705- target_oauth=remote_oauth_data)
2706- if to_push:
2707- for remote_db_name in \
2708- couchdb_io.get_database_names_replicatable(
2709- couchdb_io.mkuri(addr, port)):
2710- if not is_running: return
2711- try:
2712- if not remote_db_name.startswith(
2713- str(remote_db_name_prefix + "/")):
2714- continue
2715- except ValueError, e:
2716- log.error("skipping %r on %s. %s", db_name, sn, e)
2717- continue
2718-
2719- db_name = remote_db_name[1+len(str(remote_db_name_prefix)):]
2720- if db_name.strip("/") == "management":
2721- continue # be paranoid about what we accept.
2722- log.debug("want to replipull %r from static host %r @ %s",
2723- db_name, remote_hostid, addr)
2724- couchdb_io.replicate(remote_db_name, db_name,
2725- source_host=addr, source_port=port,
2726- target_port=local_port, source_ssl=True,
2727- source_oauth=remote_oauth_data)
2728-
2729- except Exception, e:
2730- log.exception("replication of services aborted")
2731- pass
2732- finally:
2733- already_replicating = False
2734- log.debug("finished replicating")
2735-
2736-
2737-def replicate_local_databases_to_paired_hosts(local_port):
2738- if already_replicating:
2739- log.warn("haven't finished replicating before next time to start.")
2740- return False
2741-
2742- reactor.callInThread(do_all_replication, local_port)
2743-
2744-def set_up(port_getter):
2745- port = port_getter()
2746- unique_identifiers = couchdb_io.get_my_host_unique_id(
2747- couchdb_io.mkuri("localhost", int(port)), create=True)
2748-
2749- beacons = [dbus_io.LocationAdvertisement(port, "desktopcouch " + i)
2750- for i in unique_identifiers]
2751- for b in beacons:
2752- try:
2753- b.publish()
2754- except dbus.exceptions.DBusException, e:
2755- log.error("We seem to be running already, or can't publish "
2756- "our zeroconf advert. %s", e)
2757- return None
2758-
2759- dbus_io.discover_services(None, None, True)
2760-
2761- dbus_io.maintain_discovered_servers()
2762-
2763- t = task.LoopingCall(replicate_local_databases_to_paired_hosts, port)
2764- t.start(600)
2765-
2766- # TODO: port may change, so every so often, check it and
2767- # perhaps refresh the beacons. We return an array of beacons, so we could
2768- # keep a reference to that array and mutate it when the port-beacons
2769- # change.
2770-
2771- return beacons, t
2772-
2773-
2774-def tear_down(beacons, looping_task):
2775- for b in beacons:
2776- b.unpublish()
2777- try:
2778- is_running = False
2779- looping_task.stop()
2780- except:
2781- pass
2782
2783=== added directory 'desktopcouch/replication_services'
2784=== removed directory 'desktopcouch/replication_services'
2785=== added file 'desktopcouch/replication_services/__init__.py'
2786--- desktopcouch/replication_services/__init__.py 1970-01-01 00:00:00 +0000
2787+++ desktopcouch/replication_services/__init__.py 2009-10-12 14:29:10 +0000
2788@@ -0,0 +1,4 @@
2789+"""Modules imported here are available as services."""
2790+
2791+import ubuntuone
2792+import example
2793
2794=== removed file 'desktopcouch/replication_services/__init__.py'
2795--- desktopcouch/replication_services/__init__.py 2009-09-23 14:22:38 +0000
2796+++ desktopcouch/replication_services/__init__.py 1970-01-01 00:00:00 +0000
2797@@ -1,4 +0,0 @@
2798-"""Modules imported here are available as services."""
2799-
2800-import ubuntuone
2801-import example
2802
2803=== added file 'desktopcouch/replication_services/example.py'
2804--- desktopcouch/replication_services/example.py 1970-01-01 00:00:00 +0000
2805+++ desktopcouch/replication_services/example.py 2009-10-12 14:29:10 +0000
2806@@ -0,0 +1,26 @@
2807+# Note that the __init__.py of this package must import this module for it to
2808+# be found. Plugin logic is not pretty, and not implemented yet.
2809+
2810+# Required
2811+name = "Example"
2812+# Required; should include the words "cloud service" on the end.
2813+description = "Example cloud service"
2814+
2815+# Required
2816+def is_active():
2817+ """Can we deliver information?"""
2818+ return False
2819+
2820+# Required
2821+def oauth_data():
2822+ """OAuth information needed to replicate to a server."""
2823+ return dict(consumer_key="", consumer_secret="", oauth_token="",
2824+ oauth_token_secret="")
2825+ # or to symbolize failure
2826+ return None
2827+
2828+# Access to this as a string fires off functions.
2829+# Required
2830+db_name_prefix = "http://host.required.example.com/a_prefix_if_necessary"
2831+# You can be sure that access to this will always, always be through its
2832+# __str__ method.
2833
2834=== removed file 'desktopcouch/replication_services/example.py'
2835--- desktopcouch/replication_services/example.py 2009-09-28 12:06:08 +0000
2836+++ desktopcouch/replication_services/example.py 1970-01-01 00:00:00 +0000
2837@@ -1,26 +0,0 @@
2838-# Note that the __init__.py of this package must import this module for it to
2839-# be found. Plugin logic is not pretty, and not implemented yet.
2840-
2841-# Required
2842-name = "Example"
2843-# Required; should include the words "cloud service" on the end.
2844-description = "Example cloud service"
2845-
2846-# Required
2847-def is_active():
2848- """Can we deliver information?"""
2849- return False
2850-
2851-# Required
2852-def oauth_data():
2853- """OAuth information needed to replicate to a server."""
2854- return dict(consumer_key="", consumer_secret="", oauth_token="",
2855- oauth_token_secret="")
2856- # or to symbolize failure
2857- return None
2858-
2859-# Access to this as a string fires off functions.
2860-# Required
2861-db_name_prefix = "http://host.required.example.com/a_prefix_if_necessary"
2862-# You can be sure that access to this will always, always be through its
2863-# __str__ method.
2864
2865=== added file 'desktopcouch/replication_services/ubuntuone.py'
2866--- desktopcouch/replication_services/ubuntuone.py 1970-01-01 00:00:00 +0000
2867+++ desktopcouch/replication_services/ubuntuone.py 2009-10-12 14:29:10 +0000
2868@@ -0,0 +1,125 @@
2869+import hashlib
2870+from oauth import oauth
2871+import logging
2872+import httplib2
2873+import simplejson
2874+import gnomekeyring
2875+
2876+name = "Ubuntu One"
2877+description = "The Ubuntu One cloud service"
2878+
2879+oauth_consumer_key = "ubuntuone"
2880+oauth_consumer_secret = "hammertime"
2881+
2882+def is_active():
2883+ """Can we deliver information?"""
2884+ return get_oauth_data() is not None
2885+
2886+oauth_data = None
2887+def get_oauth_data():
2888+ """Information needed to replicate to a server."""
2889+ global oauth_data
2890+ if oauth_data is not None:
2891+ return oauth_data
2892+
2893+ try:
2894+ import gnomekeyring
2895+ matches = gnomekeyring.find_items_sync(
2896+ gnomekeyring.ITEM_GENERIC_SECRET,
2897+ {'ubuntuone-realm': "https://ubuntuone.com",
2898+ 'oauth-consumer-key': oauth_consumer_key})
2899+ if matches:
2900+ # parse "a=b&c=d" to {"a":"b","c":"d"}
2901+ kv_list = [x.split("=", 1) for x in matches[0].secret.split("&")]
2902+ keys, values = zip(*kv_list)
2903+ keys = [k.replace("oauth_", "") for k in keys]
2904+ oauth_data = dict(zip(keys, values))
2905+ oauth_data.update({
2906+ "consumer_key": oauth_consumer_key,
2907+ "consumer_secret": oauth_consumer_secret,
2908+ })
2909+ return oauth_data
2910+ except ImportError, e:
2911+ logging.info("Can't replicate to Ubuntu One cloud without credentials."
2912+ " %s", e)
2913+ except gnomekeyring.NoMatchError:
2914+ logging.info("This machine hasn't authorized itself to Ubuntu One; "
2915+ "replication to the cloud isn't possible until it has. See "
2916+ "'ubuntuone-client-applet'.")
2917+ except gnomekeyring.NoKeyringDaemonError:
2918+ logging.error("No keyring daemon found in this session, so we have "
2919+ "no access to Ubuntu One data.")
2920+
2921+def get_oauth_token(consumer):
2922+ """Get the token from the keyring"""
2923+ import gobject
2924+ gobject.set_application_name("desktopcouch replication to Ubuntu One")
2925+ try:
2926+ items = gnomekeyring.find_items_sync(
2927+ gnomekeyring.ITEM_GENERIC_SECRET,
2928+ {'ubuntuone-realm': "https://one.ubuntu.com",
2929+ 'oauth-consumer-key': consumer.key})
2930+ except gnomekeyring.NoMatchError:
2931+ logging.info("No o.u.c key. Maybe there's uo.c key?")
2932+ items = gnomekeyring.find_items_sync(
2933+ gnomekeyring.ITEM_GENERIC_SECRET,
2934+ {'ubuntuone-realm': "https://ubuntuone.com",
2935+ 'oauth-consumer-key': consumer.key})
2936+ if len(items):
2937+ return oauth.OAuthToken.from_string(items[0].secret)
2938+
2939+def get_oauth_request_header(consumer, access_token, http_url):
2940+ """Get an oauth request header given the token and the url"""
2941+ signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
2942+ assert http_url.startswith("https")
2943+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(
2944+ http_url=http_url,
2945+ http_method="GET",
2946+ oauth_consumer=consumer,
2947+ token=access_token)
2948+ oauth_request.sign_request(signature_method, consumer, access_token)
2949+ return oauth_request.to_header()
2950+
2951+
2952+class PrefixGetter():
2953+ def __init__(self):
2954+ self.str = None
2955+ self.oauth_header = None
2956+
2957+ def __str__(self):
2958+ if self.str is not None:
2959+ return self.str
2960+
2961+ url = "https://one.ubuntu.com/api/account/"
2962+ if self.oauth_header is None:
2963+ consumer = oauth.OAuthConsumer(oauth_consumer_key,
2964+ oauth_consumer_secret)
2965+ try:
2966+ access_token = get_oauth_token(consumer)
2967+ except gnomekeyring.NoKeyringDaemonError:
2968+ logging.info("No keyring daemon is running for this session.")
2969+ raise ValueError("No keyring access")
2970+ if not access_token:
2971+ logging.info("Could not get access token from keyring")
2972+ raise ValueError("No keyring access")
2973+ self.oauth_header = get_oauth_request_header(consumer, access_token, url)
2974+
2975+ client = httplib2.Http()
2976+ resp, content = client.request(url, "GET", headers=self.oauth_header)
2977+ if resp['status'] == "200":
2978+ document = simplejson.loads(content)
2979+ if "couchdb_root" not in document:
2980+ raise ValueError("couchdb_root not found in %s" % (document,))
2981+ self.str = document["couchdb_root"]
2982+ else:
2983+ logging.error("Couldn't talk to %r. Got HTTP %s", url, resp['status'])
2984+ raise ValueError("HTTP %s for %r" % (resp['status'], url))
2985+
2986+ return self.str
2987+
2988+# Access to this as a string fires off functions.
2989+db_name_prefix = PrefixGetter()
2990+
2991+if __name__ == "__main__":
2992+ logging.basicConfig(level=logging.DEBUG, format="%(message)s")
2993+ print str(db_name_prefix)
2994
2995=== removed file 'desktopcouch/replication_services/ubuntuone.py'
2996--- desktopcouch/replication_services/ubuntuone.py 2009-09-28 12:06:08 +0000
2997+++ desktopcouch/replication_services/ubuntuone.py 1970-01-01 00:00:00 +0000
2998@@ -1,125 +0,0 @@
2999-import hashlib
3000-from oauth import oauth
3001-import logging
3002-import httplib2
3003-import simplejson
3004-import gnomekeyring
3005-
3006-name = "Ubuntu One"
3007-description = "The Ubuntu One cloud service"
3008-
3009-oauth_consumer_key = "ubuntuone"
3010-oauth_consumer_secret = "hammertime"
3011-
3012-def is_active():
3013- """Can we deliver information?"""
3014- return get_oauth_data() is not None
3015-
3016-oauth_data = None
3017-def get_oauth_data():
3018- """Information needed to replicate to a server."""
3019- global oauth_data
3020- if oauth_data is not None:
3021- return oauth_data
3022-
3023- try:
3024- import gnomekeyring
3025- matches = gnomekeyring.find_items_sync(
3026- gnomekeyring.ITEM_GENERIC_SECRET,
3027- {'ubuntuone-realm': "https://ubuntuone.com",
3028- 'oauth-consumer-key': oauth_consumer_key})
3029- if matches:
3030- # parse "a=b&c=d" to {"a":"b","c":"d"}
3031- kv_list = [x.split("=", 1) for x in matches[0].secret.split("&")]
3032- keys, values = zip(*kv_list)
3033- keys = [k.replace("oauth_", "") for k in keys]
3034- oauth_data = dict(zip(keys, values))
3035- oauth_data.update({
3036- "consumer_key": oauth_consumer_key,
3037- "consumer_secret": oauth_consumer_secret,
3038- })
3039- return oauth_data
3040- except ImportError, e:
3041- logging.info("Can't replicate to Ubuntu One cloud without credentials."
3042- " %s", e)
3043- except gnomekeyring.NoMatchError:
3044- logging.info("This machine hasn't authorized itself to Ubuntu One; "
3045- "replication to the cloud isn't possible until it has. See "
3046- "'ubuntuone-client-applet'.")
3047- except gnomekeyring.NoKeyringDaemonError:
3048- logging.error("No keyring daemon found in this session, so we have "
3049- "no access to Ubuntu One data.")
3050-
3051-def get_oauth_token(consumer):
3052- """Get the token from the keyring"""
3053- import gobject
3054- gobject.set_application_name("desktopcouch replication to Ubuntu One")
3055- try:
3056- items = gnomekeyring.find_items_sync(
3057- gnomekeyring.ITEM_GENERIC_SECRET,
3058- {'ubuntuone-realm': "https://one.ubuntu.com",
3059- 'oauth-consumer-key': consumer.key})
3060- except gnomekeyring.NoMatchError:
3061- logging.info("No o.u.c key. Maybe there's uo.c key?")
3062- items = gnomekeyring.find_items_sync(
3063- gnomekeyring.ITEM_GENERIC_SECRET,
3064- {'ubuntuone-realm': "https://ubuntuone.com",
3065- 'oauth-consumer-key': consumer.key})
3066- if len(items):
3067- return oauth.OAuthToken.from_string(items[0].secret)
3068-
3069-def get_oauth_request_header(consumer, access_token, http_url):
3070- """Get an oauth request header given the token and the url"""
3071- signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
3072- assert http_url.startswith("https")
3073- oauth_request = oauth.OAuthRequest.from_consumer_and_token(
3074- http_url=http_url,
3075- http_method="GET",
3076- oauth_consumer=consumer,
3077- token=access_token)
3078- oauth_request.sign_request(signature_method, consumer, access_token)
3079- return oauth_request.to_header()
3080-
3081-
3082-class PrefixGetter():
3083- def __init__(self):
3084- self.str = None
3085- self.oauth_header = None
3086-
3087- def __str__(self):
3088- if self.str is not None:
3089- return self.str
3090-
3091- url = "https://one.ubuntu.com/api/account/"
3092- if self.oauth_header is None:
3093- consumer = oauth.OAuthConsumer(oauth_consumer_key,
3094- oauth_consumer_secret)
3095- try:
3096- access_token = get_oauth_token(consumer)
3097- except gnomekeyring.NoKeyringDaemonError:
3098- logging.info("No keyring daemon is running for this session.")
3099- raise ValueError("No keyring access")
3100- if not access_token:
3101- logging.info("Could not get access token from keyring")
3102- raise ValueError("No keyring access")
3103- self.oauth_header = get_oauth_request_header(consumer, access_token, url)
3104-
3105- client = httplib2.Http()
3106- resp, content = client.request(url, "GET", headers=self.oauth_header)
3107- if resp['status'] == "200":
3108- document = simplejson.loads(content)
3109- if "couchdb_root" not in document:
3110- raise ValueError("couchdb_root not found in %s" % (document,))
3111- self.str = document["couchdb_root"]
3112- else:
3113- logging.error("Couldn't talk to %r. Got HTTP %s", url, resp['status'])
3114- raise ValueError("HTTP %s for %r" % (resp['status'], url))
3115-
3116- return self.str
3117-
3118-# Access to this as a string fires off functions.
3119-db_name_prefix = PrefixGetter()
3120-
3121-if __name__ == "__main__":
3122- logging.basicConfig(level=logging.DEBUG, format="%(message)s")
3123- print str(db_name_prefix)
3124
3125=== added file 'po/desktopcouch.pot'
3126--- po/desktopcouch.pot 1970-01-01 00:00:00 +0000
3127+++ po/desktopcouch.pot 2009-10-12 14:29:10 +0000
3128@@ -0,0 +1,102 @@
3129+# Copyright (C) 2009 Canonical Ltd.
3130+# This file is distributed under the same license as the desktopcouch package.
3131+# Ken VanDine <ken.vandine@canonical.com>, 2009.
3132+#
3133+#, fuzzy
3134+msgid ""
3135+msgstr ""
3136+"Project-Id-Version: PACKAGE VERSION\n"
3137+"Report-Msgid-Bugs-To: \n"
3138+"POT-Creation-Date: 2009-07-27 15:06-0400\n"
3139+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
3140+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
3141+"Language-Team: LANGUAGE <LL@li.org>\n"
3142+"MIME-Version: 1.0\n"
3143+"Content-Type: text/plain; charset=CHARSET\n"
3144+"Content-Transfer-Encoding: 8bit\n"
3145+
3146+#: ../desktopcouch-pair.desktop.in.h:1 ../bin/desktopcouch-pair.py:592
3147+msgid "CouchDB Pairing Tool"
3148+msgstr ""
3149+
3150+#: ../desktopcouch-pair.desktop.in.h:2
3151+msgid "Utility for pairing Desktop CouchDB"
3152+msgstr ""
3153+
3154+#: ../bin/desktopcouch-pair.py:153
3155+#, python-format
3156+msgid "Inviting %s to pair for CouchDB Pairing"
3157+msgstr ""
3158+
3159+#: ../bin/desktopcouch-pair.py:167
3160+#, python-format
3161+msgid "We're inviting %s to pair with\n"
3162+msgstr ""
3163+
3164+#: ../bin/desktopcouch-pair.py:223
3165+msgid "Accepting Invitation"
3166+msgstr ""
3167+
3168+#: ../bin/desktopcouch-pair.py:232
3169+#, python-format
3170+msgid "To verify your pairing with %s, enter its secret."
3171+msgstr ""
3172+
3173+#: ../bin/desktopcouch-pair.py:260
3174+msgid "Verify and connect"
3175+msgstr ""
3176+
3177+#: ../bin/desktopcouch-pair.py:355
3178+msgid "Waiting for CouchDB Pairing Invitations"
3179+msgstr ""
3180+
3181+#: ../bin/desktopcouch-pair.py:376
3182+msgid "Add 60 seconds"
3183+msgstr ""
3184+
3185+#: ../bin/desktopcouch-pair.py:390
3186+msgid "We're listening for invitations! From another\n"
3187+msgstr ""
3188+
3189+#: ../bin/desktopcouch-pair.py:414
3190+#, python-format
3191+msgid "%d seconds remaining"
3192+msgstr ""
3193+
3194+#. pylint: disable-msg=W0201
3195+#: ../bin/desktopcouch-pair.py:451 ../bin/desktopcouch-pair.py:452
3196+msgid "service name"
3197+msgstr ""
3198+
3199+#: ../bin/desktopcouch-pair.py:457
3200+msgid "Pick a listening host to invite it to pair with us."
3201+msgstr ""
3202+
3203+#: ../bin/desktopcouch-pair.py:560
3204+msgid "Add this host to the list for others to see?"
3205+msgstr ""
3206+
3207+#: ../bin/desktopcouch-pair.py:564
3208+msgid "Listen for invitations"
3209+msgstr ""
3210+
3211+#: ../bin/desktopcouch-pair.py:576
3212+msgid "I also know of CouchDB sessions here. Pick one "
3213+msgstr ""
3214+
3215+#: ../bin/desktopcouch-pair.py:600
3216+msgid "Copyright 2009 Canonical"
3217+msgstr ""
3218+
3219+#. Some kind of two-phase commit would be nice here, before we say
3220+#. successful.
3221+#. couchdb_io.replicate_to(...)
3222+#: ../bin/desktopcouch-pair.py:620
3223+#, python-format
3224+msgid "Paired with %(host)s"
3225+msgstr ""
3226+
3227+#: ../bin/desktopcouch-pair.py:625
3228+#, python-format
3229+msgid "Successfully paired with %(host)s %(info)s."
3230+msgstr ""
3231
3232=== modified file 'setup.cfg'
3233--- setup.cfg 2009-09-23 14:22:38 +0000
3234+++ setup.cfg 2009-10-12 14:29:10 +0000
3235@@ -1,13 +1,13 @@
3236-[build_i18n]
3237-domain = desktopcouch
3238-desktop_files = [("share/applications", ("desktopcouch-pair.desktop.in",))]
3239-
3240 [egg_info]
3241 tag_build =
3242 tag_date = 0
3243 tag_svn_revision = 0
3244
3245 [build]
3246-i18n = True
3247-icons = True
3248+i18n=True
3249+icons=True
3250+
3251+[build_i18n]
3252+domain=desktopcouch
3253+desktop_files=[("share/applications", ("desktopcouch-pair.desktop.in",))]
3254
3255
3256=== modified file 'setup.py'
3257--- setup.py 2009-09-28 12:06:08 +0000
3258+++ setup.py 2009-10-12 14:29:10 +0000
3259@@ -22,7 +22,7 @@
3260
3261 setup(
3262 name='desktopcouch',
3263- version='0.4.2',
3264+ version='0.4.4',
3265 description='A Desktop CouchDB instance.',
3266 url='https://launchpad.net/desktopcouch',
3267 license='LGPL-3',
3268@@ -32,11 +32,13 @@
3269 scripts=['bin/desktopcouch-pair'],
3270 data_files = [('/usr/lib/desktopcouch/', ['bin/desktopcouch-service',
3271 'bin/desktopcouch-stop']),
3272+ # Be sure all additions are reflected in MANIFEST.in !
3273 ('/usr/share/doc/python-desktopcouch-records/api/',
3274- ['desktopcouch/records/doc/records.txt']),
3275- # System-level XDG_CONFIG_DIRS folder
3276+ ['desktopcouch/records/doc/records.txt',
3277+ 'desktopcouch/records/doc/field_registry.txt',
3278+ 'desktopcouch/contacts/schema.txt']),
3279 ('/etc/xdg/desktop-couch/',
3280- ['config/desktop-couch/compulsory-auth.ini']),
3281+ ['config/desktop-couch/compulsory-auth.ini']),
3282 ('/usr/share/desktopcouch/', ['data/couchdb.tmpl']),
3283 ('/usr/share/dbus-1/services/', ['org.desktopcouch.CouchDB.service']),
3284 ('share/man/man1/', ['docs/man/desktopcouch-pair.1'])],

Subscribers

People subscribed via source and target branches