Merge lp:~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/snapshots-with-packaging into lp:ubuntu/karmic/desktopcouch
- Karmic (9.10)
- snapshots-with-packaging
- Merge into karmic
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 |
Related bugs: |
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.
Description of the change
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-
Now included these.
desktopcouch-
desktopcouch/
desktopcouch/
James Westby (james-w) wrote : | # |
Looks good now.
Thanks,
James
- 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
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'])], |
The upstream tarball seems to be incomplete as discussed on IRC.
Thanks,
James