~ubuntuone-control-tower/ubuntu/karmic/desktopcouch/snapshots-with-packaging

« back to all changes in this revision

Viewing changes to desktopcouch/records/server_base.py

  • Committer: James Westby
  • Date: 2009-10-14 13:43:23 UTC
  • mfrom: (1.1.6 upstream)
  • Revision ID: james.westby@canonical.com-20091014134323-yvov1mrx9xvs1i01
Merging shared upstream rev into target branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
24
from couchdb import Server
 
25
from couchdb.client import ResourceNotFound, ResourceConflict
 
26
from couchdb.design import ViewDefinition
 
27
from record import Record
 
28
import httplib2
 
29
from oauth import oauth
 
30
import urlparse
 
31
import cgi
 
32
 
 
33
#DEFAULT_DESIGN_DOCUMENT = "design"
 
34
DEFAULT_DESIGN_DOCUMENT = None  # each view in its own eponymous design doc.
 
35
 
 
36
 
 
37
class 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
 
 
48
class 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
 
 
84
class 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
 
 
104
def 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
 
 
115
class 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]