18
18
# Mark G. Saye <mark.saye@canonical.com>
19
19
# Stuart Langridge <stuart.langridge@canonical.com>
20
20
# Chad Miller <chad.miller@canonical.com>
22
22
"""The Desktop Couch Records API."""
24
24
from couchdb import Server
25
from couchdb.client import ResourceNotFound, ResourceConflict
26
from couchdb.design import ViewDefinition
27
25
import desktopcouch
28
from record import Record
31
#DEFAULT_DESIGN_DOCUMENT = "design"
32
DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
35
class NoSuchDatabase(Exception):
36
"Exception for trying to use a non-existent database"
38
def __init__(self, dbname):
39
self.database = dbname
40
super(NoSuchDatabase, self).__init__()
43
return ("Database %s does not exist on this server. (Create it by "
44
"passing create=True)") % self.database
47
def row_is_deleted(row):
48
"""Test if a row is marked as deleted. Smart views 'maps' should not
49
return rows that are marked as deleted, so this function is not often
52
return row['application_annotations']['Ubuntu One']\
53
['private_application_annotations']['deleted']
58
class CouchDatabase(object):
26
from desktopcouch.records import server_base
28
class OAuthCapableServer(Server):
29
def __init__(self, uri):
30
"""Subclass of couchdb.client.Server which creates a custom
31
httplib2.Http subclass which understands OAuth"""
32
http = server_base.OAuthCapableHttp()
33
http.force_exception_to_status_code = False
34
oauth_tokens = desktopcouch.local_files.get_oauth_tokens()
35
(consumer_key, consumer_secret, token, token_secret) = (
36
oauth_tokens["consumer_key"], oauth_tokens["consumer_secret"],
37
oauth_tokens["token"], oauth_tokens["token_secret"])
38
http.add_oauth_tokens(consumer_key, consumer_secret, token, token_secret)
39
self.resource = server_base.Resource(http, uri)
41
class CouchDatabase(server_base.CouchDatabaseBase):
59
42
"""An small records specific abstraction over a couch db database."""
61
def __init__(self, database, uri=None, record_factory=None, create=False):
44
def __init__(self, database, uri=None, record_factory=None, create=False,
45
server_class=OAuthCapableServer):
63
47
desktopcouch.find_pid()
64
48
port = desktopcouch.find_port()
65
self.server_uri = "http://localhost:%s" % port
68
self._server = Server(self.server_uri)
69
if database not in self._server:
71
self._server.create(database)
73
raise NoSuchDatabase(database)
74
self.db = self._server[database]
75
self.record_factory = record_factory or Record
77
def _temporary_query(self, map_fun, reduce_fun=None, language='javascript',
78
wrapper=None, **options):
79
"""Pass-through to CouchDB library. Deprecated."""
80
return self.db.query(map_fun, reduce_fun, language,
83
def get_record(self, record_id):
84
"""Get a record from back end storage."""
86
couch_record = self.db[record_id]
87
except ResourceNotFound:
90
data.update(couch_record)
91
record = self.record_factory(data=data)
92
record.record_id = record_id
95
def put_record(self, record):
96
"""Put a record in back end storage."""
97
record_id = record.record_id or record._data.get('_id', '')
98
record_data = record._data
100
self.db[record_id] = record_data
102
record_id = self._add_record(record_data)
105
def update_fields(self, doc_id, fields):
106
"""Safely update a number of fields. 'fields' being a
107
dictionary with fieldname: value for only the fields we want
108
to change the value of.
111
doc = self.db[doc_id]
114
self.db[doc.id] = doc
115
except ResourceConflict:
119
def _add_record(self, data):
120
"""Add a new record to the storage backend."""
121
return self.db.create(data)
123
def delete_record(self, record_id):
124
"""Delete record with given id"""
125
record = self.db[record_id]
126
record.setdefault('application_annotations', {}).setdefault(
127
'Ubuntu One', {}).setdefault('private_application_annotations', {})[
129
self.db[record_id] = record
131
def record_exists(self, record_id):
132
"""Check if record with given id exists."""
133
if record_id not in self.db:
135
record = self.db[record_id]
136
return 'deleted' not in record.get('application_annotations', {}).get(
137
'Ubuntu One', {}).get('private_application_annotations', {})
139
def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
140
"""Remove a view, given its name. Raises a KeyError on a unknown
141
view. Returns a dict of functions the deleted view defined."""
142
if design_doc is None:
143
design_doc = view_name
145
doc_id = "_design/%(design_doc)s" % locals()
147
# No atomic updates. Only read & mutate & write. Le sigh.
148
# First, get current contents.
150
view_container = self.db[doc_id]["views"]
151
except (KeyError, ResourceNotFound):
154
deleted_data = view_container.pop(view_name) # Remove target
156
if len(view_container) > 0:
157
# Construct a new list of objects representing all views to have.
159
ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
161
in view_container.iteritems()
163
# Push back a new batch of view. Pray to Eris that this doesn't
164
# clobber anything we want.
166
# sync_many does nothing if we pass an empty list. It even gets
167
# its design-document from the ViewDefinition items, and if there
168
# are no items, then it has no idea of a design document to
169
# update. This is a serious flaw. Thus, the "else" to follow.
170
ViewDefinition.sync_many(self.db, views, remove_missing=True)
172
# There are no views left in this design document.
174
# Remove design document. This assumes there are only views in
175
# design documents. :(
178
assert not self.view_exists(view_name, design_doc)
182
def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
183
"""Execute view and return results."""
184
if design_doc is None:
185
design_doc = view_name
187
view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
188
return self.db.view(view_id_fmt % locals())
190
def add_view(self, view_name, map_js, reduce_js,
191
design_doc=DEFAULT_DESIGN_DOCUMENT):
192
"""Create a view, given a name and the two parts (map and reduce).
193
Return the document id."""
194
if design_doc is None:
195
design_doc = view_name
197
view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
199
assert self.view_exists(view_name, design_doc)
201
def view_exists(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
202
"""Does a view with a given name, in a optional design document
204
if design_doc is None:
205
design_doc = view_name
207
doc_id = "_design/%(design_doc)s" % locals()
210
view_container = self.db[doc_id]["views"]
211
return view_name in view_container
212
except (KeyError, ResourceNotFound):
215
def list_views(self, design_doc):
216
"""Return a list of view names for a given design document. There is
217
no error if the design document does not exist or if there are no views
219
doc_id = "_design/%(design_doc)s" % locals()
221
return list(self.db[doc_id]["views"])
222
except (KeyError, ResourceNotFound):
225
def get_records(self, record_type=None, create_view=False,
226
design_doc=DEFAULT_DESIGN_DOCUMENT, version="1"):
227
"""A convenience function to get records from a view named
228
C{get_records_and_type}, suffixed with C{__v} and the supplied version
229
string (or default of "1"). We optionally create a view in the design
230
document. C{create_view} may be True or False, and a special value,
231
None, is analogous to O_EXCL|O_CREAT .
233
Set record_type to a string to retrieve records of only that
234
specified type. Otherwise, usse the view to return *all* records.
235
If there is no view to use or we insist on creating a new view
236
and cannot, raise KeyError .
238
You can use index notation on the result to get rows with a
239
particular record type.
240
=>> results = get_records()
241
=>> for foo_document in results["foo"]:
242
... print foo_document
244
Use slice notation to apply start-key and end-key options to the view.
245
=>> results = get_records()
246
=>> people = results[['Person']:['Person','ZZZZ']]
248
view_name = "get_records_and_type"
252
if (! doc['application_annotations']['Ubuntu One']
253
['private_application_annotations']['deleted']) {
254
emit(doc.record_type, doc);
257
emit(doc.record_type, doc);
261
if design_doc is None:
262
design_doc = view_name
264
if not version is None: # versions do not affect design_doc name.
265
view_name = view_name + "__v" + version
267
exists = self.view_exists(view_name, design_doc)
270
if create_view is None:
271
raise KeyError("Exclusive creation failed.")
273
if create_view == False:
274
raise KeyError("View doesn't already exist.")
277
self.add_view(view_name, view_map_js, None, design_doc)
279
viewdata = self.execute_view(view_name, design_doc)
280
if record_type is None:
283
return viewdata[record_type]
49
uri = "http://localhost:%s" % port
50
super(CouchDatabase, self).__init__(
51
database, uri, record_factory=record_factory, create=create,
52
server_class=server_class)