1
# Copyright 2009 Canonical Ltd.
3
# This file is part of desktopcouch.
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.
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.
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/>.
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>
22
"""The Desktop Couch Records API."""
24
from couchdb import Server
25
from couchdb.client import ResourceNotFound, ResourceConflict
26
from couchdb.design import ViewDefinition
27
from record import Record
29
from oauth import oauth
33
#DEFAULT_DESIGN_DOCUMENT = "design"
34
DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
37
class NoSuchDatabase(Exception):
38
"Exception for trying to use a non-existent database"
40
def __init__(self, dbname):
41
self.database = dbname
42
super(NoSuchDatabase, self).__init__()
45
return ("Database %s does not exist on this server. (Create it by "
46
"passing create=True)") % self.database
48
class OAuthAuthentication(httplib2.Authentication):
49
"""An httplib2.Authentication subclass for OAuth"""
50
def __init__(self, oauth_data, host, request_uri, headers, response,
52
self.oauth_data = oauth_data
53
httplib2.Authentication.__init__(self, None, host, request_uri,
54
headers, response, content, http)
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'])
64
sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
66
trash, port = self.host.split(":", 1)
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(
78
http_url = full_http_url,
79
parameters = querystr_as_dict
81
req.sign_request(sig_method(), consumer, access_token)
82
headers.update(httplib2._normalize_headers(req.to_header()))
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,
90
"consumer_key": consumer_key,
91
"consumer_secret": consumer_secret,
93
"token_secret": token_secret
96
def _auth_from_challenge(self, host, request_uri, headers, response,
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)
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
109
return row['application_annotations']['Ubuntu One']\
110
['private_application_annotations']['deleted']
115
class CouchDatabaseBase(object):
116
"""An small records specific abstraction over a couch db database."""
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:
124
self._server.create(database)
126
raise NoSuchDatabase(database)
127
self.db = self._server[database]
128
self.record_factory = record_factory or Record
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,
136
def get_record(self, record_id):
137
"""Get a record from back end storage."""
139
couch_record = self.db[record_id]
140
except ResourceNotFound:
143
if 'deleted' in couch_record.get('application_annotations', {}).get(
144
'Ubuntu One', {}).get('private_application_annotations', {}):
146
data.update(couch_record)
147
record = self.record_factory(data=data)
148
record.record_id = record_id
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
156
self.db[record_id] = record_data
158
record_id = self._add_record(record_data)
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.
167
record = self.db[record_id]
168
record.update(fields)
170
self.db[record_id] = record
171
except ResourceConflict:
175
def _add_record(self, data):
176
"""Add a new record to the storage backend."""
177
return self.db.create(data)
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', {})[
185
self.db[record_id] = record
187
def record_exists(self, record_id):
188
"""Check if record with given id exists."""
189
if record_id not in self.db:
191
record = self.db[record_id]
192
return 'deleted' not in record.get('application_annotations', {}).get(
193
'Ubuntu One', {}).get('private_application_annotations', {})
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
201
doc_id = "_design/%(design_doc)s" % locals()
203
# No atomic updates. Only read & mutate & write. Le sigh.
204
# First, get current contents.
206
view_container = self.db[doc_id]["views"]
207
except (KeyError, ResourceNotFound):
210
deleted_data = view_container.pop(view_name) # Remove target
212
if len(view_container) > 0:
213
# Construct a new list of objects representing all views to have.
215
ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
217
in view_container.iteritems()
219
# Push back a new batch of view. Pray to Eris that this doesn't
220
# clobber anything we want.
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)
228
# There are no views left in this design document.
230
# Remove design document. This assumes there are only views in
231
# design documents. :(
234
assert not self.view_exists(view_name, design_doc)
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
243
view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
244
return self.db.view(view_id_fmt % locals())
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
253
view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
255
assert self.view_exists(view_name, design_doc)
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
260
if design_doc is None:
261
design_doc = view_name
263
doc_id = "_design/%(design_doc)s" % locals()
266
view_container = self.db[doc_id]["views"]
267
return view_name in view_container
268
except (KeyError, ResourceNotFound):
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
275
doc_id = "_design/%(design_doc)s" % locals()
277
return list(self.db[doc_id]["views"])
278
except (KeyError, ResourceNotFound):
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 .
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 .
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
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']]
303
view_name = "get_records_and_type"
307
if (! doc['application_annotations']['Ubuntu One']
308
['private_application_annotations']['deleted']) {
309
emit(doc.record_type, doc);
312
emit(doc.record_type, doc);
316
if design_doc is None:
317
design_doc = view_name
319
exists = self.view_exists(view_name, design_doc)
322
if create_view is None:
323
raise KeyError("Exclusive creation failed.")
325
if create_view == False:
326
raise KeyError("View doesn't already exist.")
329
self.add_view(view_name, view_map_js, None, design_doc)
331
viewdata = self.execute_view(view_name, design_doc)
332
if record_type is None:
335
return viewdata[record_type]