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

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

Commit message

New upstream version 0.4.4.

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

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

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

Thanks,

James

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

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

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

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

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

Looks good now.

Thanks,

James

review: Approve
9. By James Westby

Merging shared upstream rev into target branch.

10. By James Westby

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

11. By James Westby

Upload to karmic.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'MANIFEST.in'
--- MANIFEST.in 2009-09-23 14:22:38 +0000
+++ MANIFEST.in 2009-10-12 14:29:10 +0000
@@ -1,10 +1,12 @@
1include COPYING COPYING.LESSER README1include COPYING COPYING.LESSER README
2recursive-include data *.tmpl2recursive-include data *.tmpl
3include desktopcouch-pair.desktop.in3include desktopcouch-pair.desktop.in
4include setup.cfg
4include po/POTFILES.in5include po/POTFILES.in
5include start-desktop-couchdb.sh6include start-desktop-couchdb.sh
6include stop-desktop-couchdb.sh7include stop-desktop-couchdb.sh
7include desktopcouch/records/doc/records.txt8recursive-include desktopcouch *.txt
9recursive-include po *.pot
8include bin/*10include bin/*
9include docs/man/*11include docs/man/*
10include MANIFEST.in MANIFEST12include MANIFEST.in MANIFEST
1113
=== removed file 'PKG-INFO'
--- PKG-INFO 2009-09-28 12:06:08 +0000
+++ PKG-INFO 1970-01-01 00:00:00 +0000
@@ -1,10 +0,0 @@
1Metadata-Version: 1.0
2Name: desktopcouch
3Version: 0.4.2
4Summary: A Desktop CouchDB instance.
5Home-page: https://launchpad.net/desktopcouch
6Author: Stuart Langridge
7Author-email: stuart.langridge@canonical.com
8License: LGPL-3
9Description: UNKNOWN
10Platform: UNKNOWN
110
=== added directory 'config'
=== removed directory 'config'
=== added directory 'config/desktop-couch'
=== removed directory 'config/desktop-couch'
=== added file 'config/desktop-couch/compulsory-auth.ini'
--- config/desktop-couch/compulsory-auth.ini 1970-01-01 00:00:00 +0000
+++ config/desktop-couch/compulsory-auth.ini 2009-10-12 14:29:10 +0000
@@ -0,0 +1,3 @@
1[couch_httpd_auth]
2require_valid_user = true
3
04
=== removed file 'config/desktop-couch/compulsory-auth.ini'
--- config/desktop-couch/compulsory-auth.ini 2009-09-23 14:22:38 +0000
+++ config/desktop-couch/compulsory-auth.ini 1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
1[couch_httpd_auth]
2require_valid_user = true
3
40
=== modified file 'debian/changelog'
--- debian/changelog 2009-09-28 12:06:08 +0000
+++ debian/changelog 2009-10-12 14:29:10 +0000
@@ -1,3 +1,29 @@
1desktopcouch (0.4.4-0ubuntu1) UNRELEASED; urgency=low
2
3 * New upstream release.
4 + Include doc "txt" and translation files in sources.
5 + couchgrid does not correctly retrieve record id (LP: #447512)
6 + couchgrid selected_records property is buggy and should be removed for
7 karmic if possible (LP: #448357)
8
9 -- Chad MILLER <chad.miller@canonical.com> Mon, 12 Oct 2009 10:17:50 -0400
10
11desktopcouch (0.4.3-0ubuntu1) karmic; urgency=low
12
13 * Include compulsory-auth INI file to be secure by default.
14 (LP: #438800)
15 * Make debhelper warn about files not installed to some package.
16 * Shorten debhelper install paths using dh_install exlusions.
17 * New upstream release:
18 + couchgrid did not correctly retrieve record id (LP: #447512)
19 + HTTP 401 for valid auth information when talking to couchdb over SSL
20 (LP: #446516)
21 + Support headless apps. (LP: #428681)
22 + desktopcouch-service "ValueError: dictionary update sequence..." on
23 stdout(LP: #446511)
24
25 -- Chad Miller <chad.miller@canonical.com> Mon, 12 Oct 2009 07:02:07 -0400
26
1desktopcouch (0.4.2-0ubuntu1) karmic; urgency=low27desktopcouch (0.4.2-0ubuntu1) karmic; urgency=low
228
3 * Include missing 0.4.0 changelog entry.29 * Include missing 0.4.0 changelog entry.
430
=== modified file 'debian/desktopcouch-tools.install'
--- debian/desktopcouch-tools.install 2009-07-31 13:44:45 +0000
+++ debian/desktopcouch-tools.install 2009-10-12 14:29:10 +0000
@@ -1,4 +1,3 @@
1debian/tmp/usr/share/applications/desktopcouch-pair.desktop1debian/tmp/usr/share/applications/desktopcouch-pair.desktop
2debian/tmp/usr/bin/desktopcouch-pair2debian/tmp/usr/bin/desktopcouch-pair
3debian/tmp/usr/share/man/man1/desktopcouch-pair.13debian/tmp/usr/share/man/man1/desktopcouch-pair.1
4#debian/tmp/usr/share/locale/*/LC_MESSAGES/desktopcouch.mo
54
=== modified file 'debian/desktopcouch.install'
--- debian/desktopcouch.install 2009-07-31 13:44:45 +0000
+++ debian/desktopcouch.install 2009-10-12 14:29:10 +0000
@@ -1,3 +1,5 @@
1debian/tmp/usr/share/desktopcouch1debian/tmp/etc/xdg/desktop-couch/
2debian/tmp/usr/lib/desktopcouch/desktopcouch-{stop,service}2debian/tmp/usr/share/desktopcouch/
3debian/tmp/usr/lib/desktopcouch/desktopcouch-service
4debian/tmp/usr/lib/desktopcouch/desktopcouch-stop
3debian/tmp/usr/share/dbus-1/services/org.desktopcouch.CouchDB.service5debian/tmp/usr/share/dbus-1/services/org.desktopcouch.CouchDB.service
46
=== modified file 'debian/python-desktopcouch-records.install'
--- debian/python-desktopcouch-records.install 2009-09-28 12:06:08 +0000
+++ debian/python-desktopcouch-records.install 2009-10-12 14:29:10 +0000
@@ -1,5 +1,5 @@
1debian/tmp/usr/share/doc/python-desktopcouch-records/api1debian/tmp/usr/share/doc/python-desktopcouch-records/api/
2debian/tmp/usr/lib/*/*/desktopcouch/records/*2debian/tmp/usr/lib/*/*/desktopcouch/records/
3debian/tmp/usr/lib/*/*/desktopcouch/contacts/*3debian/tmp/usr/lib/*/*/desktopcouch/contacts/
4debian/tmp/usr/lib/*/*/desktopcouch/notes/*4debian/tmp/usr/lib/*/*/desktopcouch/notes/
5debian/tmp/usr/lib/*/*/desktopcouch/replication_services/*5debian/tmp/usr/lib/*/*/desktopcouch/replication_services/
66
=== modified file 'debian/python-desktopcouch.install'
--- debian/python-desktopcouch.install 2009-07-31 13:44:45 +0000
+++ debian/python-desktopcouch.install 2009-10-12 14:29:10 +0000
@@ -1,2 +1,2 @@
1debian/tmp/usr/lib/*/*/desktopcouch/*.py1debian/tmp/usr/lib/*/*/desktopcouch/*.py
2debian/tmp/usr/lib/*/*/desktopcouch/pair/{couchdb_pairing,__init__.py}2debian/tmp/usr/lib/*/*/desktopcouch/pair/
33
=== modified file 'debian/rules'
--- debian/rules 2009-07-31 13:44:45 +0000
+++ debian/rules 2009-10-12 14:29:10 +0000
@@ -1,6 +1,7 @@
1#!/usr/bin/make -f1#!/usr/bin/make -f
22
3DEB_PYTHON_SYSTEM := pycentral3DEB_PYTHON_SYSTEM := pycentral
4DEB_DH_INSTALL_ARGS := --list-missing --exclude=/tests/ --exclude=egg-info/
45
5include /usr/share/cdbs/1/rules/debhelper.mk6include /usr/share/cdbs/1/rules/debhelper.mk
6include /usr/share/cdbs/1/class/python-distutils.mk7include /usr/share/cdbs/1/class/python-distutils.mk
78
=== removed directory 'desktopcouch.egg-info'
=== removed file 'desktopcouch.egg-info/PKG-INFO'
--- desktopcouch.egg-info/PKG-INFO 2009-09-28 12:06:08 +0000
+++ desktopcouch.egg-info/PKG-INFO 1970-01-01 00:00:00 +0000
@@ -1,10 +0,0 @@
1Metadata-Version: 1.0
2Name: desktopcouch
3Version: 0.4.2
4Summary: A Desktop CouchDB instance.
5Home-page: https://launchpad.net/desktopcouch
6Author: Stuart Langridge
7Author-email: stuart.langridge@canonical.com
8License: LGPL-3
9Description: UNKNOWN
10Platform: UNKNOWN
110
=== removed file 'desktopcouch.egg-info/SOURCES.txt'
--- desktopcouch.egg-info/SOURCES.txt 2009-09-23 14:22:38 +0000
+++ desktopcouch.egg-info/SOURCES.txt 1970-01-01 00:00:00 +0000
@@ -1,64 +0,0 @@
1COPYING
2COPYING.LESSER
3MANIFEST.in
4README
5desktopcouch-pair.desktop.in
6org.desktopcouch.CouchDB.service
7setup.cfg
8setup.py
9start-desktop-couchdb.sh
10stop-desktop-couchdb.sh
11bin/desktopcouch-pair
12bin/desktopcouch-service
13bin/desktopcouch-stop
14config/desktop-couch/compulsory-auth.ini
15contrib/mocker.py
16data/couchdb.tmpl
17desktopcouch/__init__.py
18desktopcouch/local_files.py
19desktopcouch/replication.py
20desktopcouch/start_local_couchdb.py
21desktopcouch/stop_local_couchdb.py
22desktopcouch.egg-info/PKG-INFO
23desktopcouch.egg-info/SOURCES.txt
24desktopcouch.egg-info/dependency_links.txt
25desktopcouch.egg-info/top_level.txt
26desktopcouch/contacts/__init__.py
27desktopcouch/contacts/contactspicker.py
28desktopcouch/contacts/record.py
29desktopcouch/contacts/testing/__init__.py
30desktopcouch/contacts/testing/create.py
31desktopcouch/contacts/tests/__init__.py
32desktopcouch/contacts/tests/test_contactspicker.py
33desktopcouch/contacts/tests/test_create.py
34desktopcouch/contacts/tests/test_record.py
35desktopcouch/notes/__init__.py
36desktopcouch/notes/record.py
37desktopcouch/pair/__init__.py
38desktopcouch/pair/couchdb_pairing/__init__.py
39desktopcouch/pair/couchdb_pairing/couchdb_io.py
40desktopcouch/pair/couchdb_pairing/dbus_io.py
41desktopcouch/pair/couchdb_pairing/network_io.py
42desktopcouch/pair/tests/__init__.py
43desktopcouch/pair/tests/test_couchdb_io.py
44desktopcouch/pair/tests/test_network_io.py
45desktopcouch/records/__init__.py
46desktopcouch/records/couchgrid.py
47desktopcouch/records/field_registry.py
48desktopcouch/records/record.py
49desktopcouch/records/server.py
50desktopcouch/records/server_base.py
51desktopcouch/records/doc/records.txt
52desktopcouch/records/tests/__init__.py
53desktopcouch/records/tests/test_couchgrid.py
54desktopcouch/records/tests/test_field_registry.py
55desktopcouch/records/tests/test_record.py
56desktopcouch/records/tests/test_server.py
57desktopcouch/replication_services/__init__.py
58desktopcouch/replication_services/example.py
59desktopcouch/replication_services/ubuntuone.py
60desktopcouch/tests/__init__.py
61desktopcouch/tests/test_local_files.py
62desktopcouch/tests/test_start_local_couchdb.py
63docs/man/desktopcouch-pair.1
64po/POTFILES.in
65\ No newline at end of file0\ No newline at end of file
661
=== removed file 'desktopcouch.egg-info/dependency_links.txt'
--- desktopcouch.egg-info/dependency_links.txt 2009-09-23 14:22:38 +0000
+++ desktopcouch.egg-info/dependency_links.txt 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
1
20
=== removed file 'desktopcouch.egg-info/top_level.txt'
--- desktopcouch.egg-info/top_level.txt 2009-09-23 14:22:38 +0000
+++ desktopcouch.egg-info/top_level.txt 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
1desktopcouch
20
=== added file 'desktopcouch/contacts/schema.txt'
--- desktopcouch/contacts/schema.txt 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/schema.txt 2009-10-12 14:29:10 +0000
@@ -0,0 +1,50 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16
17Schema
18
19The proposed CouchDB contact schema is as follows:
20
21Core fields
22
23 * record_type 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/contact'
24 * first_name (string)
25 * last_name (string)
26 * birth_date (string, "YYYY-MM-DD")
27 * addresses (MergeableList of "address" dictionaries)
28 o city (string)
29 o address1 (string)
30 o address2 (string)
31 o pobox (string)
32 o state (string)
33 o country (string)
34 o postalcode (string)
35 o description (string, e.g., "Home")
36 * email_addresses (MergeableList of "emailaddress" dictionaries)
37 o address (string),
38 o description (string)
39 * phone_numbers (MergeableList of "phone number" dictionaries)
40 o number (string)
41 o description (string)
42 * application_annotations Everything else, organized per application.
43
44Note: None of the core fields are mandatory, but applications should
45not add any other fields at the top level of the record. Any fields
46needed not defined here should be put under application_annotations in
47the namespace of the application there. So for Ubuntu One:
48
49 "application_annotations": {
50 "Ubuntu One": {<Ubuntu One specific fields here>}}
051
=== added file 'desktopcouch/contacts/tests/test_create.py'
--- desktopcouch/contacts/tests/test_create.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/contacts/tests/test_create.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,62 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Nicola Larosa <nicola.larosa@canonical.com>
18
19"""
20Tests for the random contacts creation testing support code.
21
22These tests depend on the specific random generation algorithm used in the
23"random" stdlib module.
24"""
25
26import random
27
28import testtools
29
30from desktopcouch.contacts.testing import create as create
31
32class TestCreate(testtools.TestCase):
33 """Test the random creation testing support code."""
34
35 def test_head_or_tails(self):
36 """
37 Test the head_or_tails function.
38 Once the rndgen algo is seeded, the first four calls to
39 create.head_or_tails will yield True, True, False, False.
40 """
41 random.seed(0)
42 self.assert_(create.head_or_tails())
43 self.assert_(create.head_or_tails())
44 self.assertFalse(create.head_or_tails())
45 self.assertFalse(create.head_or_tails())
46
47 def test_random_bools(self):
48 """
49 Test the random_bools function. See the doc for the head_or_tails test.
50 """
51 self.assertRaises(RuntimeError, create.random_bools, 1)
52 random.seed(0)
53 self.assertEqual(len(create.random_bools(2)), 2) # [True, True]
54 self.assert_(any(create.random_bools(2))) # orig.: [False, False]
55 random.seed(0)
56 create.random_bools(2) # [True, True]
57 self.assertFalse(any(
58 create.random_bools(2, at_least_one_true=False))) # [False, False]
59
60 def test_create_many_contacts(self):
61 """Run the create_many_contacts function."""
62 create.create_many_contacts()
063
=== removed file 'desktopcouch/contacts/tests/test_create.py'
--- desktopcouch/contacts/tests/test_create.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/contacts/tests/test_create.py 1970-01-01 00:00:00 +0000
@@ -1,62 +0,0 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-contacts.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Nicola Larosa <nicola.larosa@canonical.com>
18
19"""
20Tests for the random contacts creation testing support code.
21
22These tests depend on the specific random generation algorithm used in the
23"random" stdlib module.
24"""
25
26import random
27
28import testtools
29
30from desktopcouch.contacts.testing import create as create
31
32class TestCreate(testtools.TestCase):
33 """Test the random creation testing support code."""
34
35 def test_head_or_tails(self):
36 """
37 Test the head_or_tails function.
38 Once the rndgen algo is seeded, the first four calls to
39 create.head_or_tails will yield True, True, False, False.
40 """
41 random.seed(0)
42 self.assert_(create.head_or_tails())
43 self.assert_(create.head_or_tails())
44 self.assertFalse(create.head_or_tails())
45 self.assertFalse(create.head_or_tails())
46
47 def test_random_bools(self):
48 """
49 Test the random_bools function. See the doc for the head_or_tails test.
50 """
51 self.assertRaises(RuntimeError, create.random_bools, 1)
52 random.seed(0)
53 self.assertEqual(len(create.random_bools(2)), 2) # [True, True]
54 self.assert_(any(create.random_bools(2))) # orig.: [False, False]
55 random.seed(0)
56 create.random_bools(2) # [True, True]
57 self.assertFalse(any(
58 create.random_bools(2, at_least_one_true=False))) # [False, False]
59
60 def test_create_many_contacts(self):
61 """Run the create_many_contacts function."""
62 create.create_many_contacts()
630
=== modified file 'desktopcouch/local_files.py'
--- desktopcouch/local_files.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/local_files.py 2009-10-12 14:29:10 +0000
@@ -136,6 +136,8 @@
136136
137def set_bind_address(address, config_file_name=FILE_INI):137def set_bind_address(address, config_file_name=FILE_INI):
138 c = configparser.SafeConfigParser()138 c = configparser.SafeConfigParser()
139 # monkeypatch ConfigParser to stop it lower-casing option names
140 c.optionxform = lambda s: s
139 c.read(config_file_name)141 c.read(config_file_name)
140 if not c.has_section("httpd"):142 if not c.has_section("httpd"):
141 c.add_section("httpd")143 c.add_section("httpd")
@@ -147,3 +149,13 @@
147# You will need to add -b or -k on the end of this149# You will need to add -b or -k on the end of this
148COUCH_EXEC_COMMAND = [COUCH_EXE, couch_chain_ini_files(), '-p', FILE_PID,150COUCH_EXEC_COMMAND = [COUCH_EXE, couch_chain_ini_files(), '-p', FILE_PID,
149 '-o', FILE_STDOUT, '-e', FILE_STDERR]151 '-o', FILE_STDOUT, '-e', FILE_STDERR]
152
153
154# Set appropriate permissions on relevant files and folders
155for fn in [FILE_PID, FILE_STDOUT, FILE_STDERR, FILE_INI, FILE_LOG]:
156 if os.path.exists(fn):
157 os.chmod(fn, 0600)
158for dn in [rootdir, config_dir, DIR_DB]:
159 if os.path.isdir(dn):
160 os.chmod(dn, 0700)
161
150162
=== added directory 'desktopcouch/notes'
=== removed directory 'desktopcouch/notes'
=== added file 'desktopcouch/notes/__init__.py'
--- desktopcouch/notes/__init__.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/notes/__init__.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,19 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-notes.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
18
19"""UbuntuOne Notes API"""
020
=== removed file 'desktopcouch/notes/__init__.py'
--- desktopcouch/notes/__init__.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/notes/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-notes.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
18
19"""UbuntuOne Notes API"""
200
=== added file 'desktopcouch/notes/record.py'
--- desktopcouch/notes/record.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/notes/record.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,31 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-notes.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
18
19
20"""A dictionary based note record representation."""
21
22from desktopcouch.records.record import Record
23
24NOTE_RECORD_TYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/note'
25
26class Note(Record):
27 """An Ubuntuone Note Record."""
28
29 def __init__(self, data=None, record_id=None):
30 super(Note, self).__init__(
31 record_id=record_id, data=data, record_type=NOTE_RECORD_TYPE)
032
=== removed file 'desktopcouch/notes/record.py'
--- desktopcouch/notes/record.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/notes/record.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch-notes.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Rodrigo Moya <rodrigo.moya@canonical.com>
18
19
20"""A dictionary based note record representation."""
21
22from desktopcouch.records.record import Record
23
24NOTE_RECORD_TYPE = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/note'
25
26class Note(Record):
27 """An Ubuntuone Note Record."""
28
29 def __init__(self, data=None, record_id=None):
30 super(Note, self).__init__(
31 record_id=record_id, data=data, record_type=NOTE_RECORD_TYPE)
320
=== modified file 'desktopcouch/pair/couchdb_pairing/couchdb_io.py'
--- desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/pair/couchdb_pairing/couchdb_io.py 2009-10-12 14:29:10 +0000
@@ -18,6 +18,7 @@
18"""Communicate with CouchDB."""18"""Communicate with CouchDB."""
1919
20import logging20import logging
21import urllib
2122
22from desktopcouch import find_port as desktopcouch_find_port23from desktopcouch import find_port as desktopcouch_find_port
23from desktopcouch.records import server24from desktopcouch.records import server
@@ -25,6 +26,7 @@
25import socket26import socket
26import uuid27import uuid
27import datetime28import datetime
29import urllib
2830
29RECTYPE_BASE = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/"31RECTYPE_BASE = "http://www.freedesktop.org/wiki/Specifications/desktopcouch/"
30PAIRED_SERVER_RECORD_TYPE = RECTYPE_BASE + "paired_server"32PAIRED_SERVER_RECORD_TYPE = RECTYPE_BASE + "paired_server"
@@ -33,10 +35,15 @@
33def mkuri(hostname, port, has_ssl=False, path="", auth_pair=None):35def mkuri(hostname, port, has_ssl=False, path="", auth_pair=None):
34 """Create a URI from parts."""36 """Create a URI from parts."""
35 protocol = "https" if has_ssl else "http"37 protocol = "https" if has_ssl else "http"
36 auth = (":".join(map(urllib.quote, auth_pair) + "@")) if auth_pair else ""38 if auth_pair:
37 port = int(port)39 auth = (":".join(map(urllib.quote, auth_pair)) + "@")
38 uri = "%(protocol)s://%(auth)s%(hostname)s:%(port)d/%(path)s" % locals()40 else:
39 return uri41 auth = ""
42 if (protocol, port) in (("http", 80), ("https", 443)):
43 return "%s://%s%s/%s" % (protocol, auth, hostname, path)
44 else:
45 port = str(port)
46 return "%s://%s%s:%s/%s" % (protocol, auth, hostname, port, path)
4047
41def _get_db(name, create=True, uri=None):48def _get_db(name, create=True, uri=None):
42 """Get (and create?) a database."""49 """Get (and create?) a database."""
@@ -115,6 +122,7 @@
115122
116 excluded = set()123 excluded = set()
117 excluded.add("management")124 excluded.add("management")
125 excluded.add("users")
118 excluded_msets = _get_management_data(PAIRED_SERVER_RECORD_TYPE,126 excluded_msets = _get_management_data(PAIRED_SERVER_RECORD_TYPE,
119 "excluded_names", uri=uri)127 "excluded_names", uri=uri)
120 for excluded_mset in excluded_msets:128 for excluded_mset in excluded_msets:
@@ -158,6 +166,8 @@
158 v = dict()166 v = dict()
159 v["record_id"] = row.id167 v["record_id"] = row.id
160 v["active"] = True168 v["active"] = True
169 if "oauth" in row.value:
170 v["oauth"] = row.value["oauth"]
161 if "unpaired" in row.value:171 if "unpaired" in row.value:
162 v["active"] = not row.value["unpaired"]172 v["active"] = not row.value["unpaired"]
163 hostid = row.value["pairing_identifier"]173 hostid = row.value["pairing_identifier"]
@@ -193,15 +203,27 @@
193 target_oauth=None):203 target_oauth=None):
194 """This replication is instant and blocking, and does not persist. """204 """This replication is instant and blocking, and does not persist. """
195205
206 try:
207 if target_host:
208 # Target databases must exist before replicating to them.
209 logging.debug("creating %r %s:%d %s", target_database, target_host,
210 target_port, target_oauth)
211 create_database(target_host, target_port, target_database,
212 target_ssl, target_oauth)
213 logging.debug("db exists, and we're ready to replicate")
214 except:
215 logging.exception("can't create/verify %r %s:%d oauth=%s",
216 target_database, target_host, target_port, target_oauth)
217
196 if source_host:218 if source_host:
197 source = mkuri(source_host, source_port, source_ssl, source_database)219 source = mkuri(source_host, source_port, source_ssl, urllib.quote(source_database, safe=""))
198 else:220 else:
199 source = source_database221 source = urllib.quote(source_database, safe="")
200222
201 if target_host:223 if target_host:
202 target = mkuri(target_host, target_port, target_ssl, target_database)224 target = mkuri(target_host, target_port, target_ssl, urllib.quote(target_database, safe=""))
203 else:225 else:
204 target = target_database226 target = urllib.quote(target_database, safe="")
205227
206 if source_oauth:228 if source_oauth:
207 assert "consumer_secret" in source_oauth229 assert "consumer_secret" in source_oauth
@@ -212,35 +234,24 @@
212 target = dict(url=target, auth=dict(oauth=target_oauth))234 target = dict(url=target, auth=dict(oauth=target_oauth))
213235
214 record = dict(source=source, target=target)236 record = dict(source=source, target=target)
215 try:237
216
217 if target_host:
218 # Target databases must exist before replicating to them.
219 logging.debug("creating %r %s:%d", target_database, target_host,
220 target_port)
221 create_database(target_host, target_port, target_database,
222 target_ssl, target_oauth)
223 except:
224 logging.exception("can't talk to couchdb. %r %s:%d oauth=%s",
225 target_database, target_host, target_port, target_oauth)
226
227 logging.debug("db exists, and we're ready to replicate")
228 try:238 try:
229 # regardless of source and target, we talk to our local couchdb :(239 # regardless of source and target, we talk to our local couchdb :(
230 port = int(desktopcouch_find_port())240 port = int(desktopcouch_find_port())
231 url = mkuri("localhost", port,)241 url = mkuri("localhost", port,)
232242
233 logging.debug("asking %r to send %s to %s", url, source, target)243 logging.debug("asking %r to replicate %s to %s, using record %s", url, source, target, record)
234244
235 ### All until python-couchdb gets a Server.replicate() function245 ### All until python-couchdb gets a Server.replicate() function
236 local_server = server.OAuthCapableServer(url)246 local_server = server.OAuthCapableServer(url)
237 resp, data = local_server.resource.post(path='/_replicate', content=record)247 resp, data = local_server.resource.post(path='/_replicate',
248 content=record)
238249
239 logging.debug("replicate result: %r %r", resp, data)250 logging.debug("replicate result: %r %r", resp, data)
240 ###251 ###
241 except:252 except:
242 logging.error("can't talk to couchdb. %r <== %r", url, record)253 logging.exception("can't replicate %r %r <== %r", source_database,
243 raise254 url, record)
244255
245def get_pairings(uri=None):256def get_pairings(uri=None):
246 """Get a list of paired servers."""257 """Get a list of paired servers."""
247258
=== modified file 'desktopcouch/pair/couchdb_pairing/dbus_io.py'
--- desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-10-12 14:29:10 +0000
@@ -103,18 +103,14 @@
103class LocationAdvertisement(Advertisement):103class LocationAdvertisement(Advertisement):
104 """An advertised couchdb location. See Advertisement class."""104 """An advertised couchdb location. See Advertisement class."""
105 def __init__(self, *args, **kwargs):105 def __init__(self, *args, **kwargs):
106 if "stype" in kwargs:106 kwargs["stype"] = location_discovery_service_type
107 kwargs.pop(stype)107 super(LocationAdvertisement, self).__init__(*args, **kwargs)
108 super(LocationAdvertisement, self).__init__(
109 stype=location_discovery_service_type, *args, **kwargs)
110108
111class PairAdvertisement(Advertisement):109class PairAdvertisement(Advertisement):
112 """An advertised couchdb pairing opportunity. See Advertisement class."""110 """An advertised couchdb pairing opportunity. See Advertisement class."""
113 def __init__(self, *args, **kwargs):111 def __init__(self, *args, **kwargs):
114 if "stype" in kwargs:112 kwargs["stype"] = invitations_discovery_service_type
115 kwargs.pop(stype)113 super(PairAdvertisement, self).__init__(*args, **kwargs)
116 super(PairAdvertisement, self).__init__(
117 stype=invitations_discovery_service_type, *args, **kwargs)
118114
119def avahitext_to_dict(avahitext):115def avahitext_to_dict(avahitext):
120 text = {}116 text = {}
@@ -141,7 +137,13 @@
141def get_seen_paired_hosts():137def get_seen_paired_hosts():
142 pairing_encyclopedia = couchdb_io.get_all_known_pairings()138 pairing_encyclopedia = couchdb_io.get_all_known_pairings()
143 return (139 return (
144 (uuid, addr, port, pairing_encyclopedia[uuid]["active"]) 140 (
141 uuid,
142 addr,
143 port,
144 not pairing_encyclopedia[uuid]["active"],
145 pairing_encyclopedia[uuid]["oauth"],
146 )
145 for uuid, (addr, port) 147 for uuid, (addr, port)
146 in nearby_desktop_couch_instances.items() 148 in nearby_desktop_couch_instances.items()
147 if uuid in pairing_encyclopedia)149 if uuid in pairing_encyclopedia)
@@ -149,51 +151,39 @@
149def maintain_discovered_servers(add_cb=cb_found_desktopcouch_server, 151def maintain_discovered_servers(add_cb=cb_found_desktopcouch_server,
150 del_cb=cb_lost_desktopcouch_server):152 del_cb=cb_lost_desktopcouch_server):
151153
152 def remove_item_handler(interface, protocol, name, stype, domain, flags):154 def remove_item_handler(cb, interface, protocol, name, stype, domain,
155 flags):
153 """A service disappeared."""156 """A service disappeared."""
154157
155 def handle_error(*args):158 if name.startswith("desktopcouch "):
156 """An error in resolving a new service."""159 hostid = name[13:]
157 logging.error("zeroconf ItemNew error for services, %s", args)160 logging.debug("lost sight of %r", hostid)
158161 cb(hostid)
159 def handle_resolved(*args):162 else:
160 """Successfully resolved a new service, which we decode and send163 logging.error("annc doesn't look like one of ours. %r", name)
161 back to our calling environment with the callback function."""164
162165 def new_item_handler(cb, interface, protocol, name, stype, domain, flags):
163 name, host, port = args[2], args[5], args[8]
164 if name.startswith("desktopcouch "):
165 hostid = name[13:]
166 logging.debug("lost sight of %r", hostid)
167 del_cb(hostid)
168 else:
169 logging.error("no UUID in zeroconf message, %r", args)
170
171 server.ResolveService(interface, protocol, name, stype,
172 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
173 reply_handler=handle_resolved, error_handler=handle_error)
174
175 def new_item_handler(interface, protocol, name, stype, domain, flags):
176 """A service appeared."""166 """A service appeared."""
177167
178 def handle_error(*args):168 def handle_error(*args):
179 """An error in resolving a new service."""169 """An error in resolving a new service."""
180 logging.error("zeroconf ItemNew error for services, %s", args)170 logging.error("zeroconf ItemNew error for services, %s", args)
181171
182 def handle_resolved(*args):172 def handle_resolved(cb, *args):
183 """Successfully resolved a new service, which we decode and send173 """Successfully resolved a new service, which we decode and send
184 back to our calling environment with the callback function."""174 back to our calling environment with the callback function."""
185175
186 name, host, port = args[2], args[5], args[8]176 name, host, port = args[2], args[5], args[8]
187 # FIXME strip off "desktopcouch "
188 if name.startswith("desktopcouch "):177 if name.startswith("desktopcouch "):
189 add_cb(name[13:], host, port)178 cb(name[13:], host, port)
190 else:179 else:
191 logging.error("no UUID in zeroconf message, %r", name)180 logging.error("annc doesn't look like one of ours. %r", name)
192 return True181 return True
193182
194 server.ResolveService(interface, protocol, name, stype, 183 server.ResolveService(interface, protocol, name, stype,
195 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), 184 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
196 reply_handler=handle_resolved, error_handler=handle_error)185 reply_handler=lambda *a: handle_resolved(cb, *a),
186 error_handler=handle_error)
197187
198 bus, server = get_dbus_bus_server()188 bus, server = get_dbus_bus_server()
199 domain_name = get_local_hostname()[1]189 domain_name = get_local_hostname()[1]
@@ -203,8 +193,10 @@
203193
204 sbrowser = dbus.Interface(browser_name,194 sbrowser = dbus.Interface(browser_name,
205 avahi.DBUS_INTERFACE_SERVICE_BROWSER)195 avahi.DBUS_INTERFACE_SERVICE_BROWSER)
206 sbrowser.connect_to_signal("ItemNew", new_item_handler)196 sbrowser.connect_to_signal("ItemNew",
207 sbrowser.connect_to_signal("ItemRemove", remove_item_handler)197 lambda *a: new_item_handler(add_cb, *a))
198 sbrowser.connect_to_signal("ItemRemove",
199 lambda *a: remove_item_handler(del_cb, *a))
208 sbrowser.connect_to_signal("Failure", 200 sbrowser.connect_to_signal("Failure",
209 lambda *a: logging.error("avahi error %r", a))201 lambda *a: logging.error("avahi error %r", a))
210202
@@ -214,27 +206,26 @@
214 """Start looking for services. Use two callbacks to handle seeing206 """Start looking for services. Use two callbacks to handle seeing
215 new services and seeing services disappear."""207 new services and seeing services disappear."""
216208
217 def remove_item_handler(interface, protocol, name, stype, domain, flags):209 def remove_item_handler(cb, interface, protocol, name, stype, domain, flags):
218 """A service disappeared."""210 """A service disappeared."""
219211
220 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:212 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:
221 return213 return
222214 cb(name)
223 del_commport_name_cb(name)215
224216 def new_item_handler(cb, interface, protocol, name, stype, domain, flags):
225 def new_item_handler(interface, protocol, name, stype, domain, flags):
226 """A service appeared."""217 """A service appeared."""
227218
228 def handle_error(*args):219 def handle_error(*args):
229 """An error in resolving a new service."""220 """An error in resolving a new service."""
230 logging.error("zeroconf ItemNew error for services, %s", args)221 logging.error("zeroconf ItemNew error for services, %s", args)
231222
232 def handle_resolved(*args):223 def handle_resolved(cb, *args):
233 """Successfully resolved a new service, which we decode and send224 """Successfully resolved a new service, which we decode and send
234 back to our calling environment with the callback function."""225 back to our calling environment with the callback function."""
235 text = avahitext_to_dict(args[9])226 text = avahitext_to_dict(args[9])
236 name, host, port = args[2], args[5], args[8]227 name, host, port = args[2], args[5], args[8]
237 add_commport_name_cb(name, text.get("description", "?"),228 cb(name, text.get("description", "?"),
238 host, port, text.get("version", None))229 host, port, text.get("version", None))
239230
240 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:231 if not show_local and flags & avahi.LOOKUP_RESULT_LOCAL:
@@ -242,8 +233,8 @@
242233
243 server.ResolveService(interface, protocol, name, stype, 234 server.ResolveService(interface, protocol, name, stype,
244 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), 235 domain, avahi.PROTO_UNSPEC, dbus.UInt32(0),
245 reply_handler=handle_resolved, error_handler=handle_error)236 reply_handler=lambda *a: handle_resolved(cb, *a),
246237 error_handler=handle_error)
247238
248 bus, server = get_dbus_bus_server()239 bus, server = get_dbus_bus_server()
249 domain_name = get_local_hostname()[1]240 domain_name = get_local_hostname()[1]
@@ -254,7 +245,9 @@
254245
255 sbrowser = dbus.Interface(browser_name,246 sbrowser = dbus.Interface(browser_name,
256 avahi.DBUS_INTERFACE_SERVICE_BROWSER)247 avahi.DBUS_INTERFACE_SERVICE_BROWSER)
257 sbrowser.connect_to_signal("ItemNew", new_item_handler)248 sbrowser.connect_to_signal("ItemNew",
258 sbrowser.connect_to_signal("ItemRemove", remove_item_handler)249 lambda *a: new_item_handler(add_commport_name_cb, *a))
250 sbrowser.connect_to_signal("ItemRemove",
251 lambda *a: remove_item_handler(del_commport_name_cb, *a))
259 sbrowser.connect_to_signal("Failure",252 sbrowser.connect_to_signal("Failure",
260 lambda *a: logging.error("avahi error %r", a))253 lambda *a: logging.error("avahi error %r", a))
261254
=== added file 'desktopcouch/pair/tests/test_couchdb_io.py'
--- desktopcouch/pair/tests/test_couchdb_io.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/pair/tests/test_couchdb_io.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,140 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16
17
18import pygtk
19pygtk.require('2.0')
20
21import desktopcouch.tests as dctests
22
23from desktopcouch.pair.couchdb_pairing import couchdb_io
24from desktopcouch.records.server import CouchDatabase
25from desktopcouch.records.record import Record
26import unittest
27import uuid
28import os
29import httplib2
30URI = None # use autodiscovery that desktopcouch.tests permits.
31
32class TestCouchdbIo(unittest.TestCase):
33
34 def setUp(self):
35 """setup each test"""
36 self.mgt_database = CouchDatabase('management', create=True, uri=URI)
37 self.foo_database = CouchDatabase('foo', create=True, uri=URI)
38 #create some records to pull out and test
39 self.foo_database.put_record(Record({
40 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
41 "record_type": "test.com"}))
42 self.foo_database.put_record(Record({
43 "key2_1": "val2_1", "key2_2": "val2_2", "key2_3": "val2_3",
44 "record_type": "test.com"}))
45 self.foo_database.put_record(Record({
46 "key13_1": "va31_1", "key3_2": "val3_2", "key3_3": "val3_3",
47 "record_type": "test.com"}))
48
49 def tearDown(self):
50 """tear down each test"""
51 del self.mgt_database._server['management']
52 del self.mgt_database._server['foo']
53
54 def test_put_static_paired_service(self):
55 service_name = "dummyfortest"
56 oauth_data = {
57 "consumer_key": str("abcdef"),
58 "consumer_secret": str("ghighjklm"),
59 "token": str("opqrst"),
60 "token_secret": str("uvwxyz"),
61 }
62 couchdb_io.put_static_paired_service(oauth_data, service_name, uri=URI)
63 pairings = list(couchdb_io.get_pairings())
64
65 def test_put_dynamic_paired_host(self):
66 hostname = "host%d" % (os.getpid(),)
67 remote_uuid = str(uuid.uuid4())
68 oauth_data = {
69 "consumer_key": str("abcdef"),
70 "consumer_secret": str("ghighjklm"),
71 "token": str("opqrst"),
72 "token_secret": str("uvwxyz"),
73 }
74
75 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
76 uri=URI)
77 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
78 uri=URI)
79 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
80 uri=URI)
81
82 pairings = list(couchdb_io.get_pairings())
83 self.assertEqual(3, len(pairings))
84 self.assertEqual(pairings[0].value["oauth"], oauth_data)
85 self.assertEqual(pairings[0].value["server"], hostname)
86 self.assertEqual(pairings[0].value["pairing_identifier"], remote_uuid)
87
88 for i, row in enumerate(pairings):
89 couchdb_io.remove_pairing(row.id, i == 1)
90
91 pairings = list(couchdb_io.get_pairings())
92 self.assertEqual(0, len(pairings))
93
94
95 def test_get_database_names_replicatable_bad_server(self):
96 # If this resolves, FIRE YOUR DNS PROVIDER.
97
98 try:
99 names = couchdb_io.get_database_names_replicatable(
100 uri='http://test.desktopcouch.example.com:9/')
101 self.assertEqual(set(), names)
102 except httplib2.ServerNotFoundError:
103 pass
104
105 def test_get_database_names_replicatable(self):
106 names = couchdb_io.get_database_names_replicatable(uri=URI)
107 self.assertFalse('management' in names)
108 self.assertTrue('foo' in names)
109
110 def test_get_my_host_unique_id(self):
111 got = couchdb_io.get_my_host_unique_id(uri=URI)
112 again = couchdb_io.get_my_host_unique_id(uri=URI)
113 self.assertEquals(len(got), 1)
114 self.assertEquals(got, again)
115
116 def test_mkuri(self):
117 uri = couchdb_io.mkuri(
118 'fnord.org', 55241, has_ssl=True, path='a/b/c',
119 auth_pair=('f o o', 'b=a=r'))
120 self.assertEquals(
121 'https://f%20o%20o:b%3Da%3Dr@fnord.org:55241/a/b/c', uri)
122
123 def Xtest_replication_good(self):
124 pass
125
126 def Xtest_replication_no_oauth_remote(self):
127 pass
128
129 def Xtest_replication_bad_oauth_remote(self):
130 pass
131
132 def Xtest_replication_no_oauth_local(self):
133 pass
134
135 def Xtest_replication_bad_oauth_local(self):
136 pass
137
138
139if __name__ == "__main__":
140 unittest.main()
0141
=== removed file 'desktopcouch/pair/tests/test_couchdb_io.py'
--- desktopcouch/pair/tests/test_couchdb_io.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/pair/tests/test_couchdb_io.py 1970-01-01 00:00:00 +0000
@@ -1,133 +0,0 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16
17
18import pygtk
19pygtk.require('2.0')
20
21import desktopcouch.tests as dctests
22
23from desktopcouch.pair.couchdb_pairing import couchdb_io
24from desktopcouch.records.server import CouchDatabase
25from desktopcouch.records.record import Record
26import unittest
27import uuid
28import os
29import httplib2
30URI = None # use autodiscovery that desktopcouch.tests permits.
31
32class TestCouchdbIo(unittest.TestCase):
33
34 def setUp(self):
35 """setup each test"""
36 self.mgt_database = CouchDatabase('management', create=True, uri=URI)
37 self.foo_database = CouchDatabase('foo', create=True, uri=URI)
38 #create some records to pull out and test
39 self.foo_database.put_record(Record({
40 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
41 "record_type": "test.com"}))
42 self.foo_database.put_record(Record({
43 "key2_1": "val2_1", "key2_2": "val2_2", "key2_3": "val2_3",
44 "record_type": "test.com"}))
45 self.foo_database.put_record(Record({
46 "key13_1": "va31_1", "key3_2": "val3_2", "key3_3": "val3_3",
47 "record_type": "test.com"}))
48
49 def tearDown(self):
50 """tear down each test"""
51 del self.mgt_database._server['management']
52 del self.mgt_database._server['foo']
53
54 def test_put_static_paired_service(self):
55 service_name = "dummyfortest"
56 oauth_data = {
57 "consumer_key": str("abcdef"),
58 "consumer_secret": str("ghighjklm"),
59 "token": str("opqrst"),
60 "token_secret": str("uvwxyz"),
61 }
62 couchdb_io.put_static_paired_service(oauth_data, service_name, uri=URI)
63 pairings = list(couchdb_io.get_pairings())
64
65 def test_put_dynamic_paired_host(self):
66 hostname = "host%d" % (os.getpid(),)
67 remote_uuid = str(uuid.uuid4())
68 oauth_data = {
69 "consumer_key": str("abcdef"),
70 "consumer_secret": str("ghighjklm"),
71 "token": str("opqrst"),
72 "token_secret": str("uvwxyz"),
73 }
74
75 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
76 uri=URI)
77 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
78 uri=URI)
79 couchdb_io.put_dynamic_paired_host(hostname, remote_uuid, oauth_data,
80 uri=URI)
81
82 pairings = list(couchdb_io.get_pairings())
83 self.assertEqual(3, len(pairings))
84 self.assertEqual(pairings[0].value["oauth"], oauth_data)
85 self.assertEqual(pairings[0].value["server"], hostname)
86 self.assertEqual(pairings[0].value["pairing_identifier"], remote_uuid)
87
88 for i, row in enumerate(pairings):
89 couchdb_io.remove_pairing(row.id, i == 1)
90
91 pairings = list(couchdb_io.get_pairings())
92 self.assertEqual(0, len(pairings))
93
94
95 def test_get_database_names_replicatable_bad_server(self):
96 # If this resolves, FIRE YOUR DNS PROVIDER.
97
98 try:
99 names = couchdb_io.get_database_names_replicatable(
100 uri='http://test.desktopcouch.example.com:9/')
101 self.assertEqual(set(), names)
102 except httplib2.ServerNotFoundError:
103 pass
104
105 def test_get_database_names_replicatable(self):
106 names = couchdb_io.get_database_names_replicatable(uri=URI)
107 self.assertFalse('management' in names)
108 self.assertTrue('foo' in names)
109
110 def test_get_my_host_unique_id(self):
111 got = couchdb_io.get_my_host_unique_id(uri=URI)
112 again = couchdb_io.get_my_host_unique_id(uri=URI)
113 self.assertEquals(len(got), 1)
114 self.assertEquals(got, again)
115
116 def Xtest_replication_good(self):
117 pass
118
119 def Xtest_replication_no_oauth_remote(self):
120 pass
121
122 def Xtest_replication_bad_oauth_remote(self):
123 pass
124
125 def Xtest_replication_no_oauth_local(self):
126 pass
127
128 def Xtest_replication_bad_oauth_local(self):
129 pass
130
131
132if __name__ == "__main__":
133 unittest.main()
1340
=== modified file 'desktopcouch/records/couchgrid.py'
--- desktopcouch/records/couchgrid.py 2009-08-27 15:32:11 +0000
+++ desktopcouch/records/couchgrid.py 2009-10-12 14:29:10 +0000
@@ -212,7 +212,7 @@
212 pass212 pass
213213
214 #set the last value as the document_id, and append214 #set the last value as the document_id, and append
215 row[-1] = r.key215 row[-1] = r.value["_id"]
216 self.list_store.append(row)216 self.list_store.append(row)
217217
218 #apply the model tot he Treeview218 #apply the model tot he Treeview
@@ -341,19 +341,6 @@
341 for r in rows:341 for r in rows:
342 selection.select_path(r)342 selection.select_path(r)
343343
344 @property
345 def selected_records(self):
346 """ selected_records - returns a list of Record objects
347 for those selected in the CouchGrid.
348
349 This property is read only.
350
351 """
352 recs = [] #a list of records to return
353 for id in self.selected_record_ids:
354 #retrieve a record for each id
355 recs.append(Record(record_id = id, record_type = self.record_type))
356 return recs
357344
358 def __reset_model(self):345 def __reset_model(self):
359 """ __reset_model - internal funciton, do not call directly.346 """ __reset_model - internal funciton, do not call directly.
@@ -434,10 +421,6 @@
434 for r in cw.selected_record_ids:421 for r in cw.selected_record_ids:
435 disp += str(r) + "\n"422 disp += str(r) + "\n"
436423
437 disp += "\n\nRecords:\n"
438 for r in cw.selected_records:
439 disp += str(r) + "\n"
440
441 tv.get_buffer().set_text(disp)424 tv.get_buffer().set_text(disp)
442425
443def __select_ids(widget, widgets):426def __select_ids(widget, widgets):
444427
=== added file 'desktopcouch/records/doc/field_registry.txt'
--- desktopcouch/records/doc/field_registry.txt 1970-01-01 00:00:00 +0000
+++ desktopcouch/records/doc/field_registry.txt 2009-10-12 14:29:10 +0000
@@ -0,0 +1,213 @@
1The Field Registry and Transformers
2
3Creating a field registry and/or a custom Transformer object is an
4easy yet flexible way to map data structures between desktopcouch and
5existing applications.
6
7>>> from desktopcouch.records.field_registry import (
8... SimpleFieldMapping, MergeableListFieldMapping, Transformer)
9>>> from desktopcouch.records.record import Record
10
11Say we have a very simple audiofile record type that defines 'artist'
12and 'title' string fields. Now also say we have an application that
13wants to interact with records of this type called 'My Awesome Music
14Player' or MAMP. The developers of MAMP use a data structure that has
15the same fields, but uses slightly different names for them:
16'songtitle' and 'songartist'. We can now define a mapping between the
17fields:
18
19>>> my_registry = {
20... 'songartist': SimpleFieldMapping('artist'),
21... 'songtitle': SimpleFieldMapping('title')
22... }
23
24and instantiate a Transformer object:
25
26>>> my_transformer = Transformer('My Awesome Music Player', my_registry)
27
28If MAMP has the following song object (a plain dictionary):
29
30>>> my_song = {
31... 'songartist': 'Thomas Tantrum',
32... 'songtitle': 'Shake It Shake It'
33... }
34
35We can have the transformer transform it into a desktopcouch record
36object:
37
38>>> AUDIO_FILE_RECORD_TYPE = 'http://example.org/record_types/audio_file'
39>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
40>>> my_transformer.from_app(my_song, new_record)
41
42Now we can look at the underlying data:
43
44>>> new_record._data #doctest: +NORMALIZE_WHITESPACE
45{'record_type': 'http://example.org/record_types/audio_file',
46 'title': 'Shake It Shake It',
47 'artist': 'Thomas Tantrum'}
48
49You might think that this doesn't really help all that much and that
50the code you would have had to write to do this yourself would not
51have been all that much bigger than using the Transformer and you'd be
52right, but this is not all the transformers do. Let's say the song in
53MAMP also has a field 'number_of_times_played_in_mamp':
54
55>>> my_song = {
56... 'songartist': 'Thomas Tantrum',
57... 'songtitle': 'Shake It Shake It',
58... 'number_of_times_played_in_mamp': 23
59... }
60
61Obviously that is not a field defined by our record type, since it is
62exceedingly unlikely that any other application would be interested in
63this data. Let's see what happens if we run the transformation with
64this field present, but undefined in the field registry:
65
66>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
67>>> my_transformer.from_app(my_song, new_record)
68
69>>> new_record._data #doctest: +NORMALIZE_WHITESPACE
70{'record_type': 'http://example.org/record_types/audio_file',
71 'title': 'Shake It Shake It',
72 'application_annotations': {'My Awesome Music Player': {'application_fields': {'number_of_times_played_in_mamp': 23}}},
73 'artist': 'Thomas Tantrum'}
74
75The transformer, when it encountered a field it had no knowledge of,
76assumed it was specific to this application, and instead of ignoring
77it, stuffed it in the proper place in application_annotations. That's
78already quite useful.
79
80Let's try something a little trickier and more contrived. Say MAMP
81annotates each song in some other interesting ways: let's say it
82allows three very specific tags on each song:
83
84>>> my_song = {
85... 'songartist': 'Thomas Tantrum',
86... 'songtitle': 'Shake It Shake It',
87... 'number_of_times_played_in_mamp': 23,
88... 'tag_vocals': 'female vocals',
89... 'tag_title': 'shaking',
90... 'tag_subject': 'talking'
91... }
92
93Our record type is a little more enlightened, and allows any number of
94tags, in a field 'tags', where each tag has a field 'tag' and and a
95field 'description'. It would be nice if we could keep a mapping
96between the tags that MAMP cares about, and the ones in our
97record. We'll have to do just a little more work, but we can. We'll
98make a new field_registry, and instantiate a new transformer with it:
99
100>>> my_registry = {
101... 'songartist': SimpleFieldMapping('artist'),
102... 'songtitle': SimpleFieldMapping('title'),
103... 'tag_vocals': MergeableListFieldMapping(
104... 'My Awesome Music Player', 'vocals_tag', 'tags', 'tag',
105... default_values={'description': 'vocals'}),
106... 'tag_title': MergeableListFieldMapping(
107... 'My Awesome Music Player', 'title_tag', 'tags', 'tag',
108... default_values={'description': 'title'}),
109... 'tag_subject': MergeableListFieldMapping(
110... 'My Awesome Music Player', 'subject_tag', 'tags', 'tag',
111... default_values={'description': 'subject'}),
112... }
113
114>>> my_transformer = Transformer('My Awesome Music Player', my_registry)
115>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
116>>> my_transformer.from_app(my_song, new_record)
117
118Since _data will now contain lots of uuids to keep references intact,
119it's less readable, and a less clear example, so I'll show you what
120using the higher level API results in:
121
122>>> [tag['tag'] for tag in new_record['tags']]
123['shaking', 'talking', 'female vocals']
124>>> [tag['description'] for tag in new_record['tags']]
125['title', 'subject', 'vocals']
126
127Let's say we append a tag:
128
129>>> new_record['tags'].append({'tag': 'yeah yeah no'})
130
131and we do the same thing:
132
133>>> [tag['tag'] for tag in new_record['tags']]
134['shaking', 'talking', 'female vocals', 'yeah yeah no']
135>>> [tag.get('description') for tag in new_record['tags']]
136['title', 'subject', 'vocals', None]
137
138and say we change the first tag:
139
140>>> new_record['tags'][0]['tag'] = 'shaking it'
141
142and now look at transforming in the other direction:
143
144>>> new_song = {}
145>>> my_transformer.to_app(new_record, new_song)
146>>> new_song #doctest: +NORMALIZE_WHITESPACE
147{'tag_title': 'shaking it',
148 'tag_subject': 'talking',
149 'tag_vocals': 'female vocals',
150 'songtitle': 'Shake It Shake It',
151 'songartist': 'Thomas Tantrum',
152 'number_of_times_played_in_mamp': 23}
153
154We see that we got the data that was in the original song, except with
155the tag_title value changed to 'shaking it', exactly as we'd expect'.
156
157Many more things are possible by creating new Transformers and/or
158FieldMapping types. I'll give one last example. Let us say that our
159record_type defines a rating field that's a value between 0 and
160100. Let's also say that MAMP stores a string with anywhere between
161zero and five stars.
162
163>>> class StarIntMapping(SimpleFieldMapping):
164... """Map a five star rating system to a score of 0 to 100 as
165... losslessly as possible.
166... """
167...
168... def getValue(self, record):
169... """Get the value for the registered field."""
170... score = record.get(self._fieldname)
171... stars = score / 20
172... remainder = score % 20
173... if remainder >= 5:
174... stars += 1
175... return "*" * stars
176...
177... def setValue(self, record, value):
178... """Set the value for the registered field."""
179... if value is None:
180... self.deleteValue(record)
181... return
182... star_score = len(value) * 20
183... score = record.get(self._fieldname)
184... if score is None or abs(star_score - score) > 5:
185... record[self._fieldname] = star_score
186... # else we keep the original value, since it was close
187... # enough and more precise
188
189And we make a registry and a transformer:
190
191>>> my_registry = {
192... 'songartist': SimpleFieldMapping('artist'),
193... 'songtitle': SimpleFieldMapping('title'),
194... 'stars': StarIntMapping('score'),
195... }
196>>> my_transformer = Transformer('My Awesome Music Player', my_registry)
197
198Create a song with a rating:
199
200>>> my_song = {
201... 'songartist': 'Thomas Tantrum',
202... 'songtitle': 'Shake It Shake It',
203... 'stars': '*****',
204... 'number_of_times_played_in_mamp': 23
205... }
206
207>>> new_record = Record(record_type=AUDIO_FILE_RECORD_TYPE)
208>>> my_transformer.from_app(my_song, new_record)
209>>> new_record['score']
210100
211
212And, I don't know if you've ever heard the song in question, but that
213is in fact correct! ;)
0214
=== modified file 'desktopcouch/records/doc/records.txt'
--- desktopcouch/records/doc/records.txt 2009-07-31 13:44:45 +0000
+++ desktopcouch/records/doc/records.txt 2009-10-12 14:29:10 +0000
@@ -3,15 +3,16 @@
3>>> from desktopcouch.records.server import CouchDatabase3>>> from desktopcouch.records.server import CouchDatabase
4>>> from desktopcouch.records.record import Record4>>> from desktopcouch.records.record import Record
55
6Create a database object. Your database needs to exist. If it doesn't, you 6Create a database object. Your database needs to exist. If it doesn't, you
7can create it by passing create=True.7can create it by passing create=True.
88
9>>> db = CouchDatabase('testing', create=True)9>>> db = CouchDatabase('testing', create=True)
1010
11Create a Record object. Records have a record type, which should be a URL.11Create a Record object. Records have a record type, which should be a
12The URL should point to a human-readable document which describes your12URL. The URL should point to a human-readable document which
13record type. (This is not checked, though.) You can pass in an initial set13describes your record type. (This is not checked, though.) You can
14of data.14pass in an initial set of data.
15
15>>> r = Record({'a':'b'}, record_type='http://example.com/testrecord')16>>> r = Record({'a':'b'}, record_type='http://example.com/testrecord')
1617
17Records work like Python dicts.18Records work like Python dicts.
@@ -32,6 +33,7 @@
32There is no ad-hoc query functionality.33There is no ad-hoc query functionality.
3334
34For views, you should specify a design document for most all calls.35For views, you should specify a design document for most all calls.
36
35>>> design_doc = "application"37>>> design_doc = "application"
3638
37To create a view:39To create a view:
@@ -41,20 +43,24 @@
41>>> db.add_view("blueberries", map_js, reduce_js, design_doc)43>>> db.add_view("blueberries", map_js, reduce_js, design_doc)
4244
43List views for a given design document:45List views for a given design document:
46
44>>> db.list_views(design_doc)47>>> db.list_views(design_doc)
45['blueberries']48['blueberries']
4649
47Test that a view exists:50Test that a view exists:
51
48>>> db.view_exists("blueberries", design_doc)52>>> db.view_exists("blueberries", design_doc)
49True53True
5054
51Execute a view. Results from execute_view() take list-like syntax to pick one55Execute a view. Results from execute_view() take list-like syntax to
52or more rows to retreive. Use index or slice notation.56pick one or more rows to retrieve. Use index or slice notation.
57
53>>> result = db.execute_view("blueberries", design_doc)58>>> result = db.execute_view("blueberries", design_doc)
54>>> for row in result["idfoo"]:59>>> for row in result["idfoo"]:
55... pass # all rows with id "idfoo". Unlike lists, may be more than one.60... pass # all rows with id "idfoo". Unlike lists, may be more than one.
5661
57Finally, remove a view. It returns a dict containing the deleted view data.62Finally, remove a view. It returns a dict containing the deleted view data.
63
58>>> db.delete_view("blueberries", design_doc)64>>> db.delete_view("blueberries", design_doc)
59{'map': 'function(doc) { emit(doc._id, null) }'}65{'map': 'function(doc) { emit(doc._id, null) }'}
6066
6167
=== modified file 'desktopcouch/records/server.py'
--- desktopcouch/records/server.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/records/server.py 2009-10-12 14:29:10 +0000
@@ -22,6 +22,7 @@
22"""The Desktop Couch Records API."""22"""The Desktop Couch Records API."""
23 23
24from couchdb import Server24from couchdb import Server
25from couchdb.client import Resource
25import desktopcouch26import desktopcouch
26from desktopcouch.records import server_base27from desktopcouch.records import server_base
2728
@@ -37,7 +38,7 @@
37 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"], 38 oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"],
38 oauth_tokens["token"], oauth_tokens["token_secret"])39 oauth_tokens["token"], oauth_tokens["token_secret"])
39 http.add_oauth_tokens(consumer_key, consumer_secret, token, token_secret)40 http.add_oauth_tokens(consumer_key, consumer_secret, token, token_secret)
40 self.resource = server_base.Resource(http, uri)41 self.resource = Resource(http, uri)
41 42
42class CouchDatabase(server_base.CouchDatabaseBase):43class CouchDatabase(server_base.CouchDatabaseBase):
43 """An small records specific abstraction over a couch db database."""44 """An small records specific abstraction over a couch db database."""
4445
=== added file 'desktopcouch/records/server_base.py'
--- desktopcouch/records/server_base.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/records/server_base.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,335 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
18# Mark G. Saye <mark.saye@canonical.com>
19# Stuart Langridge <stuart.langridge@canonical.com>
20# Chad Miller <chad.miller@canonical.com>
21
22"""The Desktop Couch Records API."""
23
24from couchdb import Server
25from couchdb.client import ResourceNotFound, ResourceConflict
26from couchdb.design import ViewDefinition
27from record import Record
28import httplib2
29from oauth import oauth
30import urlparse
31import cgi
32
33#DEFAULT_DESIGN_DOCUMENT = "design"
34DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
35
36
37class NoSuchDatabase(Exception):
38 "Exception for trying to use a non-existent database"
39
40 def __init__(self, dbname):
41 self.database = dbname
42 super(NoSuchDatabase, self).__init__()
43
44 def __str__(self):
45 return ("Database %s does not exist on this server. (Create it by "
46 "passing create=True)") % self.database
47
48class OAuthAuthentication(httplib2.Authentication):
49 """An httplib2.Authentication subclass for OAuth"""
50 def __init__(self, oauth_data, host, request_uri, headers, response,
51 content, http):
52 self.oauth_data = oauth_data
53 httplib2.Authentication.__init__(self, None, host, request_uri,
54 headers, response, content, http)
55
56 def request(self, method, request_uri, headers, content):
57 """Modify the request headers to add the appropriate
58 Authorization header."""
59 consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
60 self.oauth_data['consumer_secret'])
61 access_token = oauth.OAuthToken(self.oauth_data['token'],
62 self.oauth_data['token_secret'])
63 scheme = "http"
64 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
65 if ":" in self.host:
66 trash, port = self.host.split(":", 1)
67 if port == "443":
68 scheme = "https"
69 sig_method = oauth.OAuthSignatureMethod_PLAINTEXT
70 full_http_url = "%s://%s%s" % (scheme, self.host, request_uri)
71 schema, netloc, path, params, query, fragment = \
72 urlparse.urlparse(full_http_url)
73 querystr_as_dict = dict(cgi.parse_qsl(query))
74 req = oauth.OAuthRequest.from_consumer_and_token(
75 consumer,
76 access_token,
77 http_method = method,
78 http_url = full_http_url,
79 parameters = querystr_as_dict
80 )
81 req.sign_request(sig_method(), consumer, access_token)
82 headers.update(httplib2._normalize_headers(req.to_header()))
83
84class OAuthCapableHttp(httplib2.Http):
85 """Subclass of httplib2.Http which specifically uses our OAuth
86 Authentication subclass (because httplib2 doesn't know about it)"""
87 def add_oauth_tokens(self, consumer_key, consumer_secret,
88 token, token_secret):
89 self.oauth_data = {
90 "consumer_key": consumer_key,
91 "consumer_secret": consumer_secret,
92 "token": token,
93 "token_secret": token_secret
94 }
95
96 def _auth_from_challenge(self, host, request_uri, headers, response,
97 content):
98 """Since we know we're talking to desktopcouch, and we know that it
99 requires OAuth, just return the OAuthAuthentication here rather
100 than checking to see which supported auth method is required."""
101 yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
102 response, content, self)
103
104def row_is_deleted(row):
105 """Test if a row is marked as deleted. Smart views 'maps' should not
106 return rows that are marked as deleted, so this function is not often
107 required."""
108 try:
109 return row['application_annotations']['Ubuntu One']\
110 ['private_application_annotations']['deleted']
111 except KeyError:
112 return False
113
114
115class CouchDatabaseBase(object):
116 """An small records specific abstraction over a couch db database."""
117
118 def __init__(self, database, uri, record_factory=None, create=False,
119 server_class=Server, **server_class_extras):
120 self.server_uri = uri
121 self._server = server_class(self.server_uri, **server_class_extras)
122 if database not in self._server:
123 if create:
124 self._server.create(database)
125 else:
126 raise NoSuchDatabase(database)
127 self.db = self._server[database]
128 self.record_factory = record_factory or Record
129
130 def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
131 wrapper=None, **options):
132 """Pass-through to CouchDB library. Deprecated."""
133 return self.db.query(map_fun, reduce_fun, language,
134 wrapper, **options)
135
136 def get_record(self, record_id):
137 """Get a record from back end storage."""
138 try:
139 couch_record = self.db[record_id]
140 except ResourceNotFound:
141 return None
142 data = {}
143 if 'deleted' in couch_record.get('application_annotations', {}).get(
144 'Ubuntu One', {}).get('private_application_annotations', {}):
145 return None
146 data.update(couch_record)
147 record = self.record_factory(data=data)
148 record.record_id = record_id
149 return record
150
151 def put_record(self, record):
152 """Put a record in back end storage."""
153 record_id = record.record_id or record._data.get('_id', '')
154 record_data = record._data
155 if record_id:
156 self.db[record_id] = record_data
157 else:
158 record_id = self._add_record(record_data)
159 return record_id
160
161 def update_fields(self, record_id, fields):
162 """Safely update a number of fields. 'fields' being a
163 dictionary with fieldname: value for only the fields we want
164 to change the value of.
165 """
166 while True:
167 record = self.db[record_id]
168 record.update(fields)
169 try:
170 self.db[record_id] = record
171 except ResourceConflict:
172 continue
173 break
174
175 def _add_record(self, data):
176 """Add a new record to the storage backend."""
177 return self.db.create(data)
178
179 def delete_record(self, record_id):
180 """Delete record with given id"""
181 record = self.db[record_id]
182 record.setdefault('application_annotations', {}).setdefault(
183 'Ubuntu One', {}).setdefault('private_application_annotations', {})[
184 'deleted'] = True
185 self.db[record_id] = record
186
187 def record_exists(self, record_id):
188 """Check if record with given id exists."""
189 if record_id not in self.db:
190 return False
191 record = self.db[record_id]
192 return 'deleted' not in record.get('application_annotations', {}).get(
193 'Ubuntu One', {}).get('private_application_annotations', {})
194
195 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
196 """Remove a view, given its name. Raises a KeyError on a unknown
197 view. Returns a dict of functions the deleted view defined."""
198 if design_doc is None:
199 design_doc = view_name
200
201 doc_id = "_design/%(design_doc)s" % locals()
202
203 # No atomic updates. Only read & mutate & write. Le sigh.
204 # First, get current contents.
205 try:
206 view_container = self.db[doc_id]["views"]
207 except (KeyError, ResourceNotFound):
208 raise KeyError
209
210 deleted_data = view_container.pop(view_name) # Remove target
211
212 if len(view_container) > 0:
213 # Construct a new list of objects representing all views to have.
214 views = [
215 ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
216 for k, v
217 in view_container.iteritems()
218 ]
219 # Push back a new batch of view. Pray to Eris that this doesn't
220 # clobber anything we want.
221
222 # sync_many does nothing if we pass an empty list. It even gets
223 # its design-document from the ViewDefinition items, and if there
224 # are no items, then it has no idea of a design document to
225 # update. This is a serious flaw. Thus, the "else" to follow.
226 ViewDefinition.sync_many(self.db, views, remove_missing=True)
227 else:
228 # There are no views left in this design document.
229
230 # Remove design document. This assumes there are only views in
231 # design documents. :(
232 del self.db[doc_id]
233
234 assert not self.view_exists(view_name, design_doc)
235
236 return deleted_data
237
238 def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
239 """Execute view and return results."""
240 if design_doc is None:
241 design_doc = view_name
242
243 view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
244 return self.db.view(view_id_fmt % locals())
245
246 def add_view(self, view_name, map_js, reduce_js,
247 design_doc=DEFAULT_DESIGN_DOCUMENT):
248 """Create a view, given a name and the two parts (map and reduce).
249 Return the document id."""
250 if design_doc is None:
251 design_doc = view_name
252
253 view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
254 view.sync(self.db)
255 assert self.view_exists(view_name, design_doc)
256
257 def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
258 """Does a view with a given name, in a optional design document
259 exist?"""
260 if design_doc is None:
261 design_doc = view_name
262
263 doc_id = "_design/%(design_doc)s" % locals()
264
265 try:
266 view_container = self.db[doc_id]["views"]
267 return view_name in view_container
268 except (KeyError, ResourceNotFound):
269 return False
270
271 def list_views(self, design_doc):
272 """Return a list of view names for a given design document. There is
273 no error if the design document does not exist or if there are no views
274 in it."""
275 doc_id = "_design/%(design_doc)s" % locals()
276 try:
277 return list(self.db[doc_id]["views"])
278 except (KeyError, ResourceNotFound):
279 return []
280
281 def get_records(self, record_type=None, create_view=False,
282 design_doc=DEFAULT_DESIGN_DOCUMENT):
283 """A convenience function to get records from a view named
284 C{get_records_and_type}. We optionally create a view in the design
285 document. C{create_view} may be True or False, and a special value,
286 None, is analogous to O_EXCL|O_CREAT .
287
288 Set record_type to a string to retrieve records of only that
289 specified type. Otherwise, usse the view to return *all* records.
290 If there is no view to use or we insist on creating a new view
291 and cannot, raise KeyError .
292
293 You can use index notation on the result to get rows with a
294 particular record type.
295 =>> results = get_records()
296 =>> for foo_document in results["foo"]:
297 ... print foo_document
298
299 Use slice notation to apply start-key and end-key options to the view.
300 =>> results = get_records()
301 =>> people = results[['Person']:['Person','ZZZZ']]
302 """
303 view_name = "get_records_and_type"
304 view_map_js = """
305 function(doc) {
306 try {
307 if (! doc['application_annotations']['Ubuntu One']
308 ['private_application_annotations']['deleted']) {
309 emit(doc.record_type, doc);
310 }
311 } catch (e) {
312 emit(doc.record_type, doc);
313 }
314 }"""
315
316 if design_doc is None:
317 design_doc = view_name
318
319 exists = self.view_exists(view_name, design_doc)
320
321 if exists:
322 if create_view is None:
323 raise KeyError("Exclusive creation failed.")
324 else:
325 if create_view == False:
326 raise KeyError("View doesn't already exist.")
327
328 if not exists:
329 self.add_view(view_name, view_map_js, None, design_doc)
330
331 viewdata = self.execute_view(view_name, design_doc)
332 if record_type is None:
333 return viewdata
334 else:
335 return viewdata[record_type]
0336
=== removed file 'desktopcouch/records/server_base.py'
--- desktopcouch/records/server_base.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/records/server_base.py 1970-01-01 00:00:00 +0000
@@ -1,326 +0,0 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Eric Casteleijn <eric.casteleijn@canonical.com>
18# Mark G. Saye <mark.saye@canonical.com>
19# Stuart Langridge <stuart.langridge@canonical.com>
20# Chad Miller <chad.miller@canonical.com>
21
22"""The Desktop Couch Records API."""
23
24from couchdb import Server
25from couchdb.client import ResourceNotFound, ResourceConflict, Resource
26from couchdb.design import ViewDefinition
27from record import Record
28import httplib2
29from oauth import oauth
30import urlparse
31import cgi
32import logging
33
34#DEFAULT_DESIGN_DOCUMENT = "design"
35DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
36
37
38class NoSuchDatabase(Exception):
39 "Exception for trying to use a non-existent database"
40
41 def __init__(self, dbname):
42 self.database = dbname
43 super(NoSuchDatabase, self).__init__()
44
45 def __str__(self):
46 return ("Database %s does not exist on this server. (Create it by "
47 "passing create=True)") % self.database
48
49class OAuthAuthentication(httplib2.Authentication):
50 """An httplib2.Authentication subclass for OAuth"""
51 def __init__(self, oauth_data, host, request_uri, headers, response,
52 content, http):
53 self.oauth_data = oauth_data
54 httplib2.Authentication.__init__(self, None, host, request_uri,
55 headers, response, content, http)
56
57 def request(self, method, request_uri, headers, content):
58 """Modify the request headers to add the appropriate
59 Authorization header."""
60 consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
61 self.oauth_data['consumer_secret'])
62 access_token = oauth.OAuthToken(self.oauth_data['token'],
63 self.oauth_data['token_secret'])
64 full_http_url = "http://%s%s" % (self.host, request_uri)
65 schema, netloc, path, params, query, fragment = urlparse.urlparse(full_http_url)
66 querystr_as_dict = dict(cgi.parse_qsl(query))
67 req = oauth.OAuthRequest.from_consumer_and_token(
68 consumer,
69 access_token,
70 http_method = method,
71 http_url = full_http_url,
72 parameters = querystr_as_dict
73 )
74 req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), consumer, access_token)
75 headers.update(httplib2._normalize_headers(req.to_header()))
76 for header in headers.iteritems():
77 logging.debug("header %s", header)
78
79class OAuthCapableHttp(httplib2.Http):
80 """Subclass of httplib2.Http which specifically uses our OAuth
81 Authentication subclass (because httplib2 doesn't know about it)"""
82 def add_oauth_tokens(self, consumer_key, consumer_secret,
83 token, token_secret):
84 self.oauth_data = {
85 "consumer_key": consumer_key,
86 "consumer_secret": consumer_secret,
87 "token": token,
88 "token_secret": token_secret
89 }
90
91 def _auth_from_challenge(self, host, request_uri, headers, response, content):
92 """Since we know we're talking to desktopcouch, and we know that it
93 requires OAuth, just return the OAuthAuthentication here rather
94 than checking to see which supported auth method is required."""
95 yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
96 response, content, self)
97
98def row_is_deleted(row):
99 """Test if a row is marked as deleted. Smart views 'maps' should not
100 return rows that are marked as deleted, so this function is not often
101 required."""
102 try:
103 return row['application_annotations']['Ubuntu One']\
104 ['private_application_annotations']['deleted']
105 except KeyError:
106 return False
107
108
109class CouchDatabaseBase(object):
110 """An small records specific abstraction over a couch db database."""
111
112 def __init__(self, database, uri, record_factory=None, create=False,
113 server_class=Server, **server_class_extras):
114 self.server_uri = uri
115 self._server = server_class(self.server_uri, **server_class_extras)
116 if database not in self._server:
117 if create:
118 self._server.create(database)
119 else:
120 raise NoSuchDatabase(database)
121 self.db = self._server[database]
122 self.record_factory = record_factory or Record
123
124 def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
125 wrapper=None, **options):
126 """Pass-through to CouchDB library. Deprecated."""
127 return self.db.query(map_fun, reduce_fun, language,
128 wrapper, **options)
129
130 def get_record(self, record_id):
131 """Get a record from back end storage."""
132 try:
133 couch_record = self.db[record_id]
134 except ResourceNotFound:
135 return None
136 data = {}
137 data.update(couch_record)
138 record = self.record_factory(data=data)
139 record.record_id = record_id
140 return record
141
142 def put_record(self, record):
143 """Put a record in back end storage."""
144 record_id = record.record_id or record._data.get('_id', '')
145 record_data = record._data
146 if record_id:
147 self.db[record_id] = record_data
148 else:
149 record_id = self._add_record(record_data)
150 return record_id
151
152 def update_fields(self, doc_id, fields):
153 """Safely update a number of fields. 'fields' being a
154 dictionary with fieldname: value for only the fields we want
155 to change the value of.
156 """
157 while True:
158 doc = self.db[doc_id]
159 doc.update(fields)
160 try:
161 self.db[doc.id] = doc
162 except ResourceConflict:
163 continue
164 break
165
166 def _add_record(self, data):
167 """Add a new record to the storage backend."""
168 return self.db.create(data)
169
170 def delete_record(self, record_id):
171 """Delete record with given id"""
172 record = self.db[record_id]
173 record.setdefault('application_annotations', {}).setdefault(
174 'Ubuntu One', {}).setdefault('private_application_annotations', {})[
175 'deleted'] = True
176 self.db[record_id] = record
177
178 def record_exists(self, record_id):
179 """Check if record with given id exists."""
180 if record_id not in self.db:
181 return False
182 record = self.db[record_id]
183 return 'deleted' not in record.get('application_annotations', {}).get(
184 'Ubuntu One', {}).get('private_application_annotations', {})
185
186 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
187 """Remove a view, given its name. Raises a KeyError on a unknown
188 view. Returns a dict of functions the deleted view defined."""
189 if design_doc is None:
190 design_doc = view_name
191
192 doc_id = "_design/%(design_doc)s" % locals()
193
194 # No atomic updates. Only read & mutate & write. Le sigh.
195 # First, get current contents.
196 try:
197 view_container = self.db[doc_id]["views"]
198 except (KeyError, ResourceNotFound):
199 raise KeyError
200
201 deleted_data = view_container.pop(view_name) # Remove target
202
203 if len(view_container) > 0:
204 # Construct a new list of objects representing all views to have.
205 views = [
206 ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
207 for k, v
208 in view_container.iteritems()
209 ]
210 # Push back a new batch of view. Pray to Eris that this doesn't
211 # clobber anything we want.
212
213 # sync_many does nothing if we pass an empty list. It even gets
214 # its design-document from the ViewDefinition items, and if there
215 # are no items, then it has no idea of a design document to
216 # update. This is a serious flaw. Thus, the "else" to follow.
217 ViewDefinition.sync_many(self.db, views, remove_missing=True)
218 else:
219 # There are no views left in this design document.
220
221 # Remove design document. This assumes there are only views in
222 # design documents. :(
223 del self.db[doc_id]
224
225 assert not self.view_exists(view_name, design_doc)
226
227 return deleted_data
228
229 def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
230 """Execute view and return results."""
231 if design_doc is None:
232 design_doc = view_name
233
234 view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
235 return self.db.view(view_id_fmt % locals())
236
237 def add_view(self, view_name, map_js, reduce_js,
238 design_doc=DEFAULT_DESIGN_DOCUMENT):
239 """Create a view, given a name and the two parts (map and reduce).
240 Return the document id."""
241 if design_doc is None:
242 design_doc = view_name
243
244 view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
245 view.sync(self.db)
246 assert self.view_exists(view_name, design_doc)
247
248 def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
249 """Does a view with a given name, in a optional design document
250 exist?"""
251 if design_doc is None:
252 design_doc = view_name
253
254 doc_id = "_design/%(design_doc)s" % locals()
255
256 try:
257 view_container = self.db[doc_id]["views"]
258 return view_name in view_container
259 except (KeyError, ResourceNotFound):
260 return False
261
262 def list_views(self, design_doc):
263 """Return a list of view names for a given design document. There is
264 no error if the design document does not exist or if there are no views
265 in it."""
266 doc_id = "_design/%(design_doc)s" % locals()
267 try:
268 return list(self.db[doc_id]["views"])
269 except (KeyError, ResourceNotFound):
270 return []
271
272 def get_records(self, record_type=None, create_view=False,
273 design_doc=DEFAULT_DESIGN_DOCUMENT):
274 """A convenience function to get records from a view named
275 C{get_records_and_type}. We optionally create a view in the design
276 document. C{create_view} may be True or False, and a special value,
277 None, is analogous to O_EXCL|O_CREAT .
278
279 Set record_type to a string to retrieve records of only that
280 specified type. Otherwise, usse the view to return *all* records.
281 If there is no view to use or we insist on creating a new view
282 and cannot, raise KeyError .
283
284 You can use index notation on the result to get rows with a
285 particular record type.
286 =>> results = get_records()
287 =>> for foo_document in results["foo"]:
288 ... print foo_document
289
290 Use slice notation to apply start-key and end-key options to the view.
291 =>> results = get_records()
292 =>> people = results[['Person']:['Person','ZZZZ']]
293 """
294 view_name = "get_records_and_type"
295 view_map_js = """
296 function(doc) {
297 try {
298 if (! doc['application_annotations']['Ubuntu One']
299 ['private_application_annotations']['deleted']) {
300 emit(doc.record_type, doc);
301 }
302 } catch (e) {
303 emit(doc.record_type, doc);
304 }
305 }"""
306
307 if design_doc is None:
308 design_doc = view_name
309
310 exists = self.view_exists(view_name, design_doc)
311
312 if exists:
313 if create_view is None:
314 raise KeyError("Exclusive creation failed.")
315 else:
316 if create_view == False:
317 raise KeyError("View doesn't already exist.")
318
319 if not exists:
320 self.add_view(view_name, view_map_js, None, design_doc)
321
322 viewdata = self.execute_view(view_name, design_doc)
323 if record_type is None:
324 return viewdata
325 else:
326 return viewdata[record_type]
3270
=== modified file 'desktopcouch/records/tests/test_couchgrid.py'
--- desktopcouch/records/tests/test_couchgrid.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/records/tests/test_couchgrid.py 2009-10-12 14:29:10 +0000
@@ -128,6 +128,27 @@
128 self.assertEqual(cw.get_model().get_n_columns(),4)128 self.assertEqual(cw.get_model().get_n_columns(),4)
129 self.assertEqual(len(cw.get_model()),2)129 self.assertEqual(len(cw.get_model()),2)
130130
131 def test_selected_id_property(self):
132 #create some records
133 db = CouchDatabase(self.dbname, create=True)
134 id1 = db.put_record(Record({
135 "key1_1": "val1_1", "key1_2": "val1_2", "key1_3": "val1_3",
136 "record_type": self.record_type}))
137 id2 = db.put_record(Record({
138 "key1_1": "val2_1", "key1_2": "val2_2", "key1_3": "val2_3",
139 "record_type": self.record_type}))
140
141 #build the CouchGrid
142 cw = CouchGrid(self.dbname)
143 cw.record_type = self.record_type
144
145 #make sure the record ids are selected properly
146 cw.selected_record_ids = [id1]
147 self.assertEqual(cw.selected_record_ids[0], id1)
148 cw.selected_record_ids = [id2]
149 self.assertEqual(cw.selected_record_ids[0], id2)
150
151
131 def test_single_col_from_database(self):152 def test_single_col_from_database(self):
132 #create some records153 #create some records
133 self.db.put_record(Record({154 self.db.put_record(Record({
134155
=== modified file 'desktopcouch/records/tests/test_field_registry.py'
--- desktopcouch/records/tests/test_field_registry.py 2009-08-27 15:32:11 +0000
+++ desktopcouch/records/tests/test_field_registry.py 2009-10-12 14:29:10 +0000
@@ -17,7 +17,7 @@
1717
18"""Test cases for field mapping"""18"""Test cases for field mapping"""
1919
20import copy20import copy, doctest
21from testtools import TestCase21from testtools import TestCase
22from desktopcouch.records.field_registry import (22from desktopcouch.records.field_registry import (
23 SimpleFieldMapping, MergeableListFieldMapping, Transformer)23 SimpleFieldMapping, MergeableListFieldMapping, Transformer)
@@ -111,3 +111,7 @@
111 self.transformer.to_app(record, data)111 self.transformer.to_app(record, data)
112 self.assertEqual(112 self.assertEqual(
113 {'simpleField': 23, 'strawberryField': 'the value'}, data)113 {'simpleField': 23, 'strawberryField': 'the value'}, data)
114
115 def test_run_doctests(self):
116 results = doctest.testfile('../doc/field_registry.txt')
117 self.assertEqual(0, results.failed)
114118
=== modified file 'desktopcouch/records/tests/test_record.py'
--- desktopcouch/records/tests/test_record.py 2009-08-27 15:32:11 +0000
+++ desktopcouch/records/tests/test_record.py 2009-10-12 14:29:10 +0000
@@ -19,6 +19,7 @@
19"""Tests for the RecordDict object on which the Contacts API is built."""19"""Tests for the RecordDict object on which the Contacts API is built."""
2020
21from testtools import TestCase21from testtools import TestCase
22import doctest
2223
23# pylint does not like relative imports from containing packages24# pylint does not like relative imports from containing packages
24# pylint: disable-msg=F040125# pylint: disable-msg=F0401
@@ -179,6 +180,10 @@
179 self.assertEqual('http://fnord.org/smorgasbord',180 self.assertEqual('http://fnord.org/smorgasbord',
180 self.record.record_type)181 self.record.record_type)
181182
183 def test_run_doctests(self):
184 results = doctest.testfile('../doc/records.txt')
185 self.assertEqual(0, results.failed)
186
182187
183class TestRecordFactory(TestCase):188class TestRecordFactory(TestCase):
184 """Test Record/Mergeable List factories."""189 """Test Record/Mergeable List factories."""
185190
=== modified file 'desktopcouch/records/tests/test_server.py' (properties changed: +x to -x)
--- desktopcouch/records/tests/test_server.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/records/tests/test_server.py 2009-10-12 14:29:10 +0000
@@ -89,6 +89,14 @@
89 self.assert_(deleted_record['application_annotations']['Ubuntu One'][89 self.assert_(deleted_record['application_annotations']['Ubuntu One'][
90 'private_application_annotations']['deleted'])90 'private_application_annotations']['deleted'])
9191
92 def test_get_deleted_record(self):
93 """Test (not) getting a deleted record."""
94 record = Record({'record_number': 0}, record_type="http://example.com/")
95 record_id = self.database.put_record(record)
96 self.database.delete_record(record_id)
97 retrieved_record = self.database.get_record(record_id)
98 self.assertEqual(None, retrieved_record)
99
92 def test_record_exists(self):100 def test_record_exists(self):
93 """Test checking whether a record exists."""101 """Test checking whether a record exists."""
94 record = Record({'record_number': 0}, record_type="http://example.com/")102 record = Record({'record_number': 0}, record_type="http://example.com/")
95103
=== added file 'desktopcouch/replication.py'
--- desktopcouch/replication.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/replication.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,248 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Chad Miller <chad.miller@canonical.com>
18
19import logging
20log = logging.getLogger("replication")
21
22import dbus.exceptions
23
24from desktopcouch.pair.couchdb_pairing import couchdb_io
25from desktopcouch.pair.couchdb_pairing import dbus_io
26from desktopcouch import replication_services
27
28try:
29 import urlparse
30except ImportError:
31 import urllib.parse as urlparse
32
33from twisted.internet import task, reactor
34
35
36known_bad_service_names = set()
37already_replicating = False
38is_running = True
39
40
41def db_targetprefix_for_service(service_name):
42 """Use the service name to look up what the prefix should be on the
43 databases. This gives an egalitarian way for non-UbuntuOne servers to have
44 their own remote-db-name scheme."""
45 try:
46 container = "desktopcouch.replication_services"
47 log.debug("Looking up prefix for service %r", service_name)
48 mod = __import__(container, fromlist=[service_name])
49 return getattr(mod, service_name).db_name_prefix
50 except ImportError, e:
51 log.error("The service %r is unknown. It is not a "
52 "module in the %s package ." % (service_name, container))
53 return ""
54 except Exception, e:
55 log.exception("Not changing remote db name.")
56 return ""
57
58def oauth_info_for_service(service_name):
59 """Use the service name to look up what oauth information we should use
60 when talking to that service."""
61 try:
62 container = "desktopcouch.replication_services"
63 log.debug("Looking up prefix for service %r", service_name)
64 mod = __import__(container, fromlist=[service_name])
65 return getattr(mod, service_name).get_oauth_data()
66 except ImportError, e:
67 log.error("The service %r is unknown. It is not a "
68 "module in the %s package ." % (service_name, container))
69 return None
70
71def do_all_replication(local_port):
72 log.debug("started replicating")
73 try:
74 global already_replicating # Fuzzy, as not really critical,
75 already_replicating = True # just trying to be polite.
76
77 try:
78 # All machines running desktopcouch must advertise themselves with
79 # zeroconf. We collect those elsewhere and filter out the ones
80 # that we have paired with. Now, it's time to send our changes to
81 # all those.
82
83 for remote_hostid, addr, port, is_unpaired, remote_oauth in \
84 dbus_io.get_seen_paired_hosts():
85
86 if is_unpaired:
87 # The far end doesn't know want to break up.
88 count = 0
89 for local_identifier in couchdb_io.get_my_host_unique_id():
90 last_exception = None
91 try:
92 # Tell her gently, using each pseudonym.
93 couchdb_io.expunge_pairing(local_identifier,
94 couchdb_io.mkuri(addr, port), remote_oauth)
95 count += 1
96 except Exception, e:
97 last_exception = e
98 if count == 0:
99 if last_exception is not None:
100 # If she didn't recognize us, something's wrong.
101 try:
102 raise last_exception
103 # push caught exception back...
104 except:
105 # ... so that we log it here.
106 logging.exception(
107 "failed to unpair from other end.")
108 continue
109 else:
110 # Finally, find your inner peace...
111 couchdb_io.expunge_pairing(remote_hostid)
112 # ...and move on.
113 continue
114
115 # Ah, good, this is an active relationship. Be a giver.
116 log.debug("want to replipush to discovered host %r @ %s",
117 remote_hostid, addr)
118 for db_name in couchdb_io.get_database_names_replicatable(
119 couchdb_io.mkuri("localhost", local_port)):
120 if not is_running: return
121 couchdb_io.replicate(db_name, db_name,
122 target_host=addr, target_port=port,
123 source_port=local_port, target_oauth=remote_oauth)
124 log.debug("replication of discovered hosts finished")
125 except Exception, e:
126 log.exception("replication of discovered hosts aborted")
127 pass
128
129 try:
130 # There may be services we send data to. Use the service name (sn)
131 # to look up what the service needs from us.
132
133 for remote_hostid, sn, to_pull, to_push in \
134 couchdb_io.get_static_paired_hosts():
135
136 if not sn in dir(replication_services):
137 if not is_running: return
138 if sn in known_bad_service_names:
139 continue # Don't nag.
140 known_bad_service_names.add(sn)
141
142 remote_oauth_data = oauth_info_for_service(sn)
143
144 # TODO: push all this into service module.
145 try:
146 remote_location = db_targetprefix_for_service(sn)
147 urlinfo = urlparse.urlsplit(str(remote_location))
148 except ValueError, e:
149 log.warn("Can't reach service %s. %s", sn, e)
150 continue
151 if ":" in urlinfo.netloc:
152 addr, port = urlinfo.netloc.rsplit(":", 1)
153 else:
154 addr = urlinfo.netloc
155 port = 443 if urlinfo.scheme == "https" else 80
156 remote_db_name_prefix = urlinfo.path.strip("/")
157 # ^
158
159 if to_pull:
160 for db_name in couchdb_io.get_database_names_replicatable(
161 couchdb_io.mkuri("localhost", int(local_port))):
162 if not is_running: return
163
164 remote_db_name = remote_db_name_prefix + "/" + db_name
165
166 log.debug("want to replipush %r to static host %r @ %s",
167 remote_db_name, remote_hostid, addr)
168 couchdb_io.replicate(db_name, remote_db_name,
169 target_host=addr, target_port=port,
170 source_port=local_port, target_ssl=True,
171 target_oauth=remote_oauth_data)
172 if to_push:
173 for remote_db_name in \
174 couchdb_io.get_database_names_replicatable(
175 couchdb_io.mkuri("localhost",
176 int(local_port))):
177 if not is_running: return
178 try:
179 if not remote_db_name.startswith(
180 str(remote_db_name_prefix + "/")):
181 continue
182 except ValueError, e:
183 log.error("skipping %r on %s. %s", db_name, sn, e)
184 continue
185
186 prefix_len = len(str(remote_db_name_prefix))
187 db_name = remote_db_name[1+prefix_len:]
188 if db_name.strip("/") == "management":
189 continue # be paranoid about what we accept.
190 log.debug(
191 "want to replipull %r from static host %r @ %s",
192 db_name, remote_hostid, addr)
193 couchdb_io.replicate(remote_db_name, db_name,
194 source_host=addr, source_port=port,
195 target_port=local_port, source_ssl=True,
196 source_oauth=remote_oauth_data)
197
198 except Exception, e:
199 log.exception("replication of services aborted")
200 pass
201 finally:
202 already_replicating = False
203 log.debug("finished replicating")
204
205
206def replicate_local_databases_to_paired_hosts(local_port):
207 if already_replicating:
208 log.warn("haven't finished replicating before next time to start.")
209 return False
210
211 reactor.callInThread(do_all_replication, local_port)
212
213def set_up(port_getter):
214 port = port_getter()
215 unique_identifiers = couchdb_io.get_my_host_unique_id(
216 couchdb_io.mkuri("localhost", int(port)), create=True)
217
218 beacons = [dbus_io.LocationAdvertisement(port, "desktopcouch " + i)
219 for i in unique_identifiers]
220 for b in beacons:
221 try:
222 b.publish()
223 except dbus.exceptions.DBusException, e:
224 log.error("We seem to be running already, or can't publish "
225 "our zeroconf advert. %s", e)
226 return None
227
228 dbus_io.maintain_discovered_servers()
229
230 t = task.LoopingCall(replicate_local_databases_to_paired_hosts, port)
231 t.start(600)
232
233 # TODO: port may change, so every so often, check it and
234 # perhaps refresh the beacons. We return an array of beacons, so we could
235 # keep a reference to that array and mutate it when the port-beacons
236 # change.
237
238 return beacons, t
239
240
241def tear_down(beacons, looping_task):
242 for b in beacons:
243 b.unpublish()
244 try:
245 is_running = False
246 looping_task.stop()
247 except:
248 pass
0249
=== removed file 'desktopcouch/replication.py'
--- desktopcouch/replication.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/replication.py 1970-01-01 00:00:00 +0000
@@ -1,242 +0,0 @@
1# Copyright 2009 Canonical Ltd.
2#
3# This file is part of desktopcouch.
4#
5# desktopcouch is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# desktopcouch is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with desktopcouch. If not, see <http://www.gnu.org/licenses/>.
16#
17# Authors: Chad Miller <chad.miller@canonical.com>
18
19import threading
20import logging
21import logging.handlers
22log = logging.getLogger("replication")
23
24import dbus.exceptions
25
26import desktopcouch
27from desktopcouch.pair.couchdb_pairing import couchdb_io
28from desktopcouch.pair.couchdb_pairing import dbus_io
29from desktopcouch import replication_services
30
31try:
32 import urlparse
33except ImportError:
34 import urllib.parse as urlparse
35
36from twisted.internet import task, reactor
37
38
39known_bad_service_names = set()
40already_replicating = False
41is_running = True
42
43
44def db_targetprefix_for_service(service_name):
45 """Use the service name to look up what the prefix should be on the
46 databases. This gives an egalitarian way for non-UbuntuOne servers to have
47 their own remote-db-name scheme."""
48 try:
49 container = "desktopcouch.replication_services"
50 log.debug("Looking up prefix for service %r", service_name)
51 mod = __import__(container, fromlist=[service_name])
52 return getattr(mod, service_name).db_name_prefix
53 except ImportError, e:
54 log.error("The service %r is unknown. It is not a "
55 "module in the %s package ." % (sn, container))
56 return ""
57 except Exception, e:
58 log.exception("Not changing remote db name.")
59 return ""
60
61def oauth_info_for_service(service_name):
62 """Use the service name to look up what oauth information we should use
63 when talking to that service."""
64 try:
65 container = "desktopcouch.replication_services"
66 log.debug("Looking up prefix for service %r", service_name)
67 mod = __import__(container, fromlist=[service_name])
68 return getattr(mod, service_name).get_oauth_data()
69 except ImportError, e:
70 log.error("The service %r is unknown. It is not a "
71 "module in the %s package ." % (sn, container))
72 return None
73
74def do_all_replication(local_port):
75 log.debug("started replicating")
76 try:
77 global already_replicating # Fuzzy, as not really critical,
78 already_replicating = True # just trying to be polite.
79
80 try:
81 # All machines running desktopcouch must advertise themselves with
82 # zeroconf. We collect those elsewhere and filter out the ones
83 # that we have paired with. Now, it's time to send our changes to
84 # all those.
85
86 for remote_hostid, addr, port, is_unpaired in \
87 dbus_io.get_seen_paired_hosts():
88
89 if is_unpaired:
90 # The far end doesn't know want to break up.
91 count = 0
92 for local_identifier in couchdb_io.get_my_host_unique_id():
93 last_exception = None
94 try:
95 # Tell her gently, using each pseudonym.
96 couchdb_io.expunge_pairing(local_identifier,
97 couchdb_io.mkuri(addr, port))
98 count += 1
99 except Exception, e:
100 last_exception = e
101 if count == 0:
102 if last_exception is not None:
103 # If she didn't recognize us, something's wrong.
104 raise last_exception
105 else:
106 # Finally, find your inner peace...
107 couchdb_io.expunge_pairing(remote_identifier)
108 # ...and move on.
109 continue
110
111 # Ah, good, this is an active relationship. Be a giver.
112 log.debug("want to replipush to discovered host %r @ %s",
113 remote_hostid, addr)
114 for db_name in couchdb_io.get_database_names_replicatable(
115 couchdb_io.mkuri("localhost", local_port)):
116 if not is_running: return
117 couchdb_io.replicate(db_name, db_name,
118 target_host=addr, target_port=port,
119 source_port=local_port)
120 except Exception, e:
121 log.exception("replication of discovered hosts aborted")
122 pass
123
124 try:
125 # There may be services we send data to. Use the service name (sn)
126 # to look up what the service needs from us.
127
128 for remote_hostid, sn, to_pull, to_push in \
129 couchdb_io.get_static_paired_hosts():
130
131 if not sn in dir(replication_services):
132 if not is_running: return
133 if sn in known_bad_service_names:
134 continue # Don't nag.
135 known_bad_service_names.add(sn)
136
137 remote_oauth_data = oauth_info_for_service(sn)
138
139 # TODO: push all this into service module.
140 try:
141 remote_location = db_targetprefix_for_service(sn)
142 urlinfo = urlparse.urlsplit(str(remote_location))
143 except ValueError, e:
144 log.warn("Can't reach service %s. %s", sn, e)
145 continue
146 if ":" in urlinfo.netloc:
147 addr, port = urlinfo.netloc.rsplit(":", 1)
148 else:
149 addr = urlinfo.netloc
150 port = 443 if urlinfo.scheme == "https" else 80
151 remote_db_name_prefix = urlinfo.path.strip("/")
152 # ^
153
154 if to_pull:
155 for db_name in couchdb_io.get_database_names_replicatable(
156 couchdb_io.mkuri("localhost", int(local_port))):
157 if not is_running: return
158
159 remote_db_name = remote_db_name_prefix + "/" + db_name
160
161 log.debug("want to replipush %r to static host %r @ %s",
162 remote_db_name, remote_hostid, addr)
163 couchdb_io.replicate(db_name, remote_db_name,
164 target_host=addr, target_port=port,
165 source_port=local_port, target_ssl=True,
166 target_oauth=remote_oauth_data)
167 if to_push:
168 for remote_db_name in \
169 couchdb_io.get_database_names_replicatable(
170 couchdb_io.mkuri(addr, port)):
171 if not is_running: return
172 try:
173 if not remote_db_name.startswith(
174 str(remote_db_name_prefix + "/")):
175 continue
176 except ValueError, e:
177 log.error("skipping %r on %s. %s", db_name, sn, e)
178 continue
179
180 db_name = remote_db_name[1+len(str(remote_db_name_prefix)):]
181 if db_name.strip("/") == "management":
182 continue # be paranoid about what we accept.
183 log.debug("want to replipull %r from static host %r @ %s",
184 db_name, remote_hostid, addr)
185 couchdb_io.replicate(remote_db_name, db_name,
186 source_host=addr, source_port=port,
187 target_port=local_port, source_ssl=True,
188 source_oauth=remote_oauth_data)
189
190 except Exception, e:
191 log.exception("replication of services aborted")
192 pass
193 finally:
194 already_replicating = False
195 log.debug("finished replicating")
196
197
198def replicate_local_databases_to_paired_hosts(local_port):
199 if already_replicating:
200 log.warn("haven't finished replicating before next time to start.")
201 return False
202
203 reactor.callInThread(do_all_replication, local_port)
204
205def set_up(port_getter):
206 port = port_getter()
207 unique_identifiers = couchdb_io.get_my_host_unique_id(
208 couchdb_io.mkuri("localhost", int(port)), create=True)
209
210 beacons = [dbus_io.LocationAdvertisement(port, "desktopcouch " + i)
211 for i in unique_identifiers]
212 for b in beacons:
213 try:
214 b.publish()
215 except dbus.exceptions.DBusException, e:
216 log.error("We seem to be running already, or can't publish "
217 "our zeroconf advert. %s", e)
218 return None
219
220 dbus_io.discover_services(None, None, True)
221
222 dbus_io.maintain_discovered_servers()
223
224 t = task.LoopingCall(replicate_local_databases_to_paired_hosts, port)
225 t.start(600)
226
227 # TODO: port may change, so every so often, check it and
228 # perhaps refresh the beacons. We return an array of beacons, so we could
229 # keep a reference to that array and mutate it when the port-beacons
230 # change.
231
232 return beacons, t
233
234
235def tear_down(beacons, looping_task):
236 for b in beacons:
237 b.unpublish()
238 try:
239 is_running = False
240 looping_task.stop()
241 except:
242 pass
2430
=== added directory 'desktopcouch/replication_services'
=== removed directory 'desktopcouch/replication_services'
=== added file 'desktopcouch/replication_services/__init__.py'
--- desktopcouch/replication_services/__init__.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/replication_services/__init__.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,4 @@
1"""Modules imported here are available as services."""
2
3import ubuntuone
4import example
05
=== removed file 'desktopcouch/replication_services/__init__.py'
--- desktopcouch/replication_services/__init__.py 2009-09-23 14:22:38 +0000
+++ desktopcouch/replication_services/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,4 +0,0 @@
1"""Modules imported here are available as services."""
2
3import ubuntuone
4import example
50
=== added file 'desktopcouch/replication_services/example.py'
--- desktopcouch/replication_services/example.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/replication_services/example.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,26 @@
1# Note that the __init__.py of this package must import this module for it to
2# be found. Plugin logic is not pretty, and not implemented yet.
3
4# Required
5name = "Example"
6# Required; should include the words "cloud service" on the end.
7description = "Example cloud service"
8
9# Required
10def is_active():
11 """Can we deliver information?"""
12 return False
13
14# Required
15def oauth_data():
16 """OAuth information needed to replicate to a server."""
17 return dict(consumer_key="", consumer_secret="", oauth_token="",
18 oauth_token_secret="")
19 # or to symbolize failure
20 return None
21
22# Access to this as a string fires off functions.
23# Required
24db_name_prefix = "http://host.required.example.com/a_prefix_if_necessary"
25# You can be sure that access to this will always, always be through its
26# __str__ method.
027
=== removed file 'desktopcouch/replication_services/example.py'
--- desktopcouch/replication_services/example.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/replication_services/example.py 1970-01-01 00:00:00 +0000
@@ -1,26 +0,0 @@
1# Note that the __init__.py of this package must import this module for it to
2# be found. Plugin logic is not pretty, and not implemented yet.
3
4# Required
5name = "Example"
6# Required; should include the words "cloud service" on the end.
7description = "Example cloud service"
8
9# Required
10def is_active():
11 """Can we deliver information?"""
12 return False
13
14# Required
15def oauth_data():
16 """OAuth information needed to replicate to a server."""
17 return dict(consumer_key="", consumer_secret="", oauth_token="",
18 oauth_token_secret="")
19 # or to symbolize failure
20 return None
21
22# Access to this as a string fires off functions.
23# Required
24db_name_prefix = "http://host.required.example.com/a_prefix_if_necessary"
25# You can be sure that access to this will always, always be through its
26# __str__ method.
270
=== added file 'desktopcouch/replication_services/ubuntuone.py'
--- desktopcouch/replication_services/ubuntuone.py 1970-01-01 00:00:00 +0000
+++ desktopcouch/replication_services/ubuntuone.py 2009-10-12 14:29:10 +0000
@@ -0,0 +1,125 @@
1import hashlib
2from oauth import oauth
3import logging
4import httplib2
5import simplejson
6import gnomekeyring
7
8name = "Ubuntu One"
9description = "The Ubuntu One cloud service"
10
11oauth_consumer_key = "ubuntuone"
12oauth_consumer_secret = "hammertime"
13
14def is_active():
15 """Can we deliver information?"""
16 return get_oauth_data() is not None
17
18oauth_data = None
19def get_oauth_data():
20 """Information needed to replicate to a server."""
21 global oauth_data
22 if oauth_data is not None:
23 return oauth_data
24
25 try:
26 import gnomekeyring
27 matches = gnomekeyring.find_items_sync(
28 gnomekeyring.ITEM_GENERIC_SECRET,
29 {'ubuntuone-realm': "https://ubuntuone.com",
30 'oauth-consumer-key': oauth_consumer_key})
31 if matches:
32 # parse "a=b&c=d" to {"a":"b","c":"d"}
33 kv_list = [x.split("=", 1) for x in matches[0].secret.split("&")]
34 keys, values = zip(*kv_list)
35 keys = [k.replace("oauth_", "") for k in keys]
36 oauth_data = dict(zip(keys, values))
37 oauth_data.update({
38 "consumer_key": oauth_consumer_key,
39 "consumer_secret": oauth_consumer_secret,
40 })
41 return oauth_data
42 except ImportError, e:
43 logging.info("Can't replicate to Ubuntu One cloud without credentials."
44 " %s", e)
45 except gnomekeyring.NoMatchError:
46 logging.info("This machine hasn't authorized itself to Ubuntu One; "
47 "replication to the cloud isn't possible until it has. See "
48 "'ubuntuone-client-applet'.")
49 except gnomekeyring.NoKeyringDaemonError:
50 logging.error("No keyring daemon found in this session, so we have "
51 "no access to Ubuntu One data.")
52
53def get_oauth_token(consumer):
54 """Get the token from the keyring"""
55 import gobject
56 gobject.set_application_name("desktopcouch replication to Ubuntu One")
57 try:
58 items = gnomekeyring.find_items_sync(
59 gnomekeyring.ITEM_GENERIC_SECRET,
60 {'ubuntuone-realm': "https://one.ubuntu.com",
61 'oauth-consumer-key': consumer.key})
62 except gnomekeyring.NoMatchError:
63 logging.info("No o.u.c key. Maybe there's uo.c key?")
64 items = gnomekeyring.find_items_sync(
65 gnomekeyring.ITEM_GENERIC_SECRET,
66 {'ubuntuone-realm': "https://ubuntuone.com",
67 'oauth-consumer-key': consumer.key})
68 if len(items):
69 return oauth.OAuthToken.from_string(items[0].secret)
70
71def get_oauth_request_header(consumer, access_token, http_url):
72 """Get an oauth request header given the token and the url"""
73 signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
74 assert http_url.startswith("https")
75 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
76 http_url=http_url,
77 http_method="GET",
78 oauth_consumer=consumer,
79 token=access_token)
80 oauth_request.sign_request(signature_method, consumer, access_token)
81 return oauth_request.to_header()
82
83
84class PrefixGetter():
85 def __init__(self):
86 self.str = None
87 self.oauth_header = None
88
89 def __str__(self):
90 if self.str is not None:
91 return self.str
92
93 url = "https://one.ubuntu.com/api/account/"
94 if self.oauth_header is None:
95 consumer = oauth.OAuthConsumer(oauth_consumer_key,
96 oauth_consumer_secret)
97 try:
98 access_token = get_oauth_token(consumer)
99 except gnomekeyring.NoKeyringDaemonError:
100 logging.info("No keyring daemon is running for this session.")
101 raise ValueError("No keyring access")
102 if not access_token:
103 logging.info("Could not get access token from keyring")
104 raise ValueError("No keyring access")
105 self.oauth_header = get_oauth_request_header(consumer, access_token, url)
106
107 client = httplib2.Http()
108 resp, content = client.request(url, "GET", headers=self.oauth_header)
109 if resp['status'] == "200":
110 document = simplejson.loads(content)
111 if "couchdb_root" not in document:
112 raise ValueError("couchdb_root not found in %s" % (document,))
113 self.str = document["couchdb_root"]
114 else:
115 logging.error("Couldn't talk to %r. Got HTTP %s", url, resp['status'])
116 raise ValueError("HTTP %s for %r" % (resp['status'], url))
117
118 return self.str
119
120# Access to this as a string fires off functions.
121db_name_prefix = PrefixGetter()
122
123if __name__ == "__main__":
124 logging.basicConfig(level=logging.DEBUG, format="%(message)s")
125 print str(db_name_prefix)
0126
=== removed file 'desktopcouch/replication_services/ubuntuone.py'
--- desktopcouch/replication_services/ubuntuone.py 2009-09-28 12:06:08 +0000
+++ desktopcouch/replication_services/ubuntuone.py 1970-01-01 00:00:00 +0000
@@ -1,125 +0,0 @@
1import hashlib
2from oauth import oauth
3import logging
4import httplib2
5import simplejson
6import gnomekeyring
7
8name = "Ubuntu One"
9description = "The Ubuntu One cloud service"
10
11oauth_consumer_key = "ubuntuone"
12oauth_consumer_secret = "hammertime"
13
14def is_active():
15 """Can we deliver information?"""
16 return get_oauth_data() is not None
17
18oauth_data = None
19def get_oauth_data():
20 """Information needed to replicate to a server."""
21 global oauth_data
22 if oauth_data is not None:
23 return oauth_data
24
25 try:
26 import gnomekeyring
27 matches = gnomekeyring.find_items_sync(
28 gnomekeyring.ITEM_GENERIC_SECRET,
29 {'ubuntuone-realm': "https://ubuntuone.com",
30 'oauth-consumer-key': oauth_consumer_key})
31 if matches:
32 # parse "a=b&c=d" to {"a":"b","c":"d"}
33 kv_list = [x.split("=", 1) for x in matches[0].secret.split("&")]
34 keys, values = zip(*kv_list)
35 keys = [k.replace("oauth_", "") for k in keys]
36 oauth_data = dict(zip(keys, values))
37 oauth_data.update({
38 "consumer_key": oauth_consumer_key,
39 "consumer_secret": oauth_consumer_secret,
40 })
41 return oauth_data
42 except ImportError, e:
43 logging.info("Can't replicate to Ubuntu One cloud without credentials."
44 " %s", e)
45 except gnomekeyring.NoMatchError:
46 logging.info("This machine hasn't authorized itself to Ubuntu One; "
47 "replication to the cloud isn't possible until it has. See "
48 "'ubuntuone-client-applet'.")
49 except gnomekeyring.NoKeyringDaemonError:
50 logging.error("No keyring daemon found in this session, so we have "
51 "no access to Ubuntu One data.")
52
53def get_oauth_token(consumer):
54 """Get the token from the keyring"""
55 import gobject
56 gobject.set_application_name("desktopcouch replication to Ubuntu One")
57 try:
58 items = gnomekeyring.find_items_sync(
59 gnomekeyring.ITEM_GENERIC_SECRET,
60 {'ubuntuone-realm': "https://one.ubuntu.com",
61 'oauth-consumer-key': consumer.key})
62 except gnomekeyring.NoMatchError:
63 logging.info("No o.u.c key. Maybe there's uo.c key?")
64 items = gnomekeyring.find_items_sync(
65 gnomekeyring.ITEM_GENERIC_SECRET,
66 {'ubuntuone-realm': "https://ubuntuone.com",
67 'oauth-consumer-key': consumer.key})
68 if len(items):
69 return oauth.OAuthToken.from_string(items[0].secret)
70
71def get_oauth_request_header(consumer, access_token, http_url):
72 """Get an oauth request header given the token and the url"""
73 signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
74 assert http_url.startswith("https")
75 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
76 http_url=http_url,
77 http_method="GET",
78 oauth_consumer=consumer,
79 token=access_token)
80 oauth_request.sign_request(signature_method, consumer, access_token)
81 return oauth_request.to_header()
82
83
84class PrefixGetter():
85 def __init__(self):
86 self.str = None
87 self.oauth_header = None
88
89 def __str__(self):
90 if self.str is not None:
91 return self.str
92
93 url = "https://one.ubuntu.com/api/account/"
94 if self.oauth_header is None:
95 consumer = oauth.OAuthConsumer(oauth_consumer_key,
96 oauth_consumer_secret)
97 try:
98 access_token = get_oauth_token(consumer)
99 except gnomekeyring.NoKeyringDaemonError:
100 logging.info("No keyring daemon is running for this session.")
101 raise ValueError("No keyring access")
102 if not access_token:
103 logging.info("Could not get access token from keyring")
104 raise ValueError("No keyring access")
105 self.oauth_header = get_oauth_request_header(consumer, access_token, url)
106
107 client = httplib2.Http()
108 resp, content = client.request(url, "GET", headers=self.oauth_header)
109 if resp['status'] == "200":
110 document = simplejson.loads(content)
111 if "couchdb_root" not in document:
112 raise ValueError("couchdb_root not found in %s" % (document,))
113 self.str = document["couchdb_root"]
114 else:
115 logging.error("Couldn't talk to %r. Got HTTP %s", url, resp['status'])
116 raise ValueError("HTTP %s for %r" % (resp['status'], url))
117
118 return self.str
119
120# Access to this as a string fires off functions.
121db_name_prefix = PrefixGetter()
122
123if __name__ == "__main__":
124 logging.basicConfig(level=logging.DEBUG, format="%(message)s")
125 print str(db_name_prefix)
1260
=== added file 'po/desktopcouch.pot'
--- po/desktopcouch.pot 1970-01-01 00:00:00 +0000
+++ po/desktopcouch.pot 2009-10-12 14:29:10 +0000
@@ -0,0 +1,102 @@
1# Copyright (C) 2009 Canonical Ltd.
2# This file is distributed under the same license as the desktopcouch package.
3# Ken VanDine <ken.vandine@canonical.com>, 2009.
4#
5#, fuzzy
6msgid ""
7msgstr ""
8"Project-Id-Version: PACKAGE VERSION\n"
9"Report-Msgid-Bugs-To: \n"
10"POT-Creation-Date: 2009-07-27 15:06-0400\n"
11"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13"Language-Team: LANGUAGE <LL@li.org>\n"
14"MIME-Version: 1.0\n"
15"Content-Type: text/plain; charset=CHARSET\n"
16"Content-Transfer-Encoding: 8bit\n"
17
18#: ../desktopcouch-pair.desktop.in.h:1 ../bin/desktopcouch-pair.py:592
19msgid "CouchDB Pairing Tool"
20msgstr ""
21
22#: ../desktopcouch-pair.desktop.in.h:2
23msgid "Utility for pairing Desktop CouchDB"
24msgstr ""
25
26#: ../bin/desktopcouch-pair.py:153
27#, python-format
28msgid "Inviting %s to pair for CouchDB Pairing"
29msgstr ""
30
31#: ../bin/desktopcouch-pair.py:167
32#, python-format
33msgid "We're inviting %s to pair with\n"
34msgstr ""
35
36#: ../bin/desktopcouch-pair.py:223
37msgid "Accepting Invitation"
38msgstr ""
39
40#: ../bin/desktopcouch-pair.py:232
41#, python-format
42msgid "To verify your pairing with %s, enter its secret."
43msgstr ""
44
45#: ../bin/desktopcouch-pair.py:260
46msgid "Verify and connect"
47msgstr ""
48
49#: ../bin/desktopcouch-pair.py:355
50msgid "Waiting for CouchDB Pairing Invitations"
51msgstr ""
52
53#: ../bin/desktopcouch-pair.py:376
54msgid "Add 60 seconds"
55msgstr ""
56
57#: ../bin/desktopcouch-pair.py:390
58msgid "We're listening for invitations! From another\n"
59msgstr ""
60
61#: ../bin/desktopcouch-pair.py:414
62#, python-format
63msgid "%d seconds remaining"
64msgstr ""
65
66#. pylint: disable-msg=W0201
67#: ../bin/desktopcouch-pair.py:451 ../bin/desktopcouch-pair.py:452
68msgid "service name"
69msgstr ""
70
71#: ../bin/desktopcouch-pair.py:457
72msgid "Pick a listening host to invite it to pair with us."
73msgstr ""
74
75#: ../bin/desktopcouch-pair.py:560
76msgid "Add this host to the list for others to see?"
77msgstr ""
78
79#: ../bin/desktopcouch-pair.py:564
80msgid "Listen for invitations"
81msgstr ""
82
83#: ../bin/desktopcouch-pair.py:576
84msgid "I also know of CouchDB sessions here. Pick one "
85msgstr ""
86
87#: ../bin/desktopcouch-pair.py:600
88msgid "Copyright 2009 Canonical"
89msgstr ""
90
91#. Some kind of two-phase commit would be nice here, before we say
92#. successful.
93#. couchdb_io.replicate_to(...)
94#: ../bin/desktopcouch-pair.py:620
95#, python-format
96msgid "Paired with %(host)s"
97msgstr ""
98
99#: ../bin/desktopcouch-pair.py:625
100#, python-format
101msgid "Successfully paired with %(host)s %(info)s."
102msgstr ""
0103
=== modified file 'setup.cfg'
--- setup.cfg 2009-09-23 14:22:38 +0000
+++ setup.cfg 2009-10-12 14:29:10 +0000
@@ -1,13 +1,13 @@
1[build_i18n]
2domain = desktopcouch
3desktop_files = [("share/applications", ("desktopcouch-pair.desktop.in",))]
4
5[egg_info]1[egg_info]
6tag_build = 2tag_build =
7tag_date = 03tag_date = 0
8tag_svn_revision = 04tag_svn_revision = 0
95
10[build]6[build]
11i18n = True7i18n=True
12icons = True8icons=True
9
10[build_i18n]
11domain=desktopcouch
12desktop_files=[("share/applications", ("desktopcouch-pair.desktop.in",))]
1313
1414
=== modified file 'setup.py'
--- setup.py 2009-09-28 12:06:08 +0000
+++ setup.py 2009-10-12 14:29:10 +0000
@@ -22,7 +22,7 @@
2222
23setup(23setup(
24 name='desktopcouch',24 name='desktopcouch',
25 version='0.4.2',25 version='0.4.4',
26 description='A Desktop CouchDB instance.',26 description='A Desktop CouchDB instance.',
27 url='https://launchpad.net/desktopcouch',27 url='https://launchpad.net/desktopcouch',
28 license='LGPL-3',28 license='LGPL-3',
@@ -32,11 +32,13 @@
32 scripts=['bin/desktopcouch-pair'],32 scripts=['bin/desktopcouch-pair'],
33 data_files = [('/usr/lib/desktopcouch/', ['bin/desktopcouch-service',33 data_files = [('/usr/lib/desktopcouch/', ['bin/desktopcouch-service',
34 'bin/desktopcouch-stop']),34 'bin/desktopcouch-stop']),
35 # Be sure all additions are reflected in MANIFEST.in !
35 ('/usr/share/doc/python-desktopcouch-records/api/',36 ('/usr/share/doc/python-desktopcouch-records/api/',
36 ['desktopcouch/records/doc/records.txt']),37 ['desktopcouch/records/doc/records.txt',
37 # System-level XDG_CONFIG_DIRS folder38 'desktopcouch/records/doc/field_registry.txt',
39 'desktopcouch/contacts/schema.txt']),
38 ('/etc/xdg/desktop-couch/',40 ('/etc/xdg/desktop-couch/',
39 ['config/desktop-couch/compulsory-auth.ini']),41 ['config/desktop-couch/compulsory-auth.ini']),
40 ('/usr/share/desktopcouch/', ['data/couchdb.tmpl']),42 ('/usr/share/desktopcouch/', ['data/couchdb.tmpl']),
41 ('/usr/share/dbus-1/services/', ['org.desktopcouch.CouchDB.service']),43 ('/usr/share/dbus-1/services/', ['org.desktopcouch.CouchDB.service']),
42 ('share/man/man1/', ['docs/man/desktopcouch-pair.1'])],44 ('share/man/man1/', ['docs/man/desktopcouch-pair.1'])],

Subscribers

People subscribed via source and target branches