~ken-vandine/desktopcouch/spb

« back to all changes in this revision

Viewing changes to desktopcouch/records/server.py

  • Committer: Ken VanDine
  • Date: 2009-09-23 18:23:42 UTC
  • mfrom: (1.1.4 upstream)
  • Revision ID: ken.vandine@canonical.com-20090923182342-5rr1sdr8e9gyyxpt
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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>
21
 
 
 
21
 
22
22
"""The Desktop Couch Records API."""
23
 
 
 
23
 
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
29
 
 
30
 
 
31
 
#DEFAULT_DESIGN_DOCUMENT = "design"
32
 
DEFAULT_DESIGN_DOCUMENT = None  # each view in its own eponymous design doc.
33
 
 
34
 
 
35
 
class NoSuchDatabase(Exception):
36
 
    "Exception for trying to use a non-existent database"
37
 
 
38
 
    def __init__(self, dbname):
39
 
        self.database = dbname
40
 
        super(NoSuchDatabase, self).__init__()
41
 
 
42
 
    def __str__(self):
43
 
        return ("Database %s does not exist on this server. (Create it by "
44
 
                "passing create=True)") % self.database
45
 
 
46
 
 
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
50
 
    required."""
51
 
    try:
52
 
        return row['application_annotations']['Ubuntu One']\
53
 
                ['private_application_annotations']['deleted']
54
 
    except KeyError:
55
 
        return False
56
 
 
57
 
 
58
 
class CouchDatabase(object):
 
26
from desktopcouch.records import server_base
 
27
 
 
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)
 
40
 
 
41
class CouchDatabase(server_base.CouchDatabaseBase):
59
42
    """An small records specific abstraction over a couch db database."""
60
 
 
61
 
    def __init__(self, database, uri=None, record_factory=None, create=False):
 
43
 
 
44
    def __init__(self, database, uri=None, record_factory=None, create=False,
 
45
                 server_class=OAuthCapableServer):
62
46
        if not uri:
63
47
            desktopcouch.find_pid()
64
48
            port = desktopcouch.find_port()
65
 
            self.server_uri = "http://localhost:%s" % port
66
 
        else:
67
 
            self.server_uri = uri
68
 
        self._server = Server(self.server_uri)
69
 
        if database not in self._server:
70
 
            if create:
71
 
                self._server.create(database)
72
 
            else:
73
 
                raise NoSuchDatabase(database)
74
 
        self.db = self._server[database]
75
 
        self.record_factory = record_factory or Record
76
 
 
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,
81
 
              wrapper, **options)
82
 
 
83
 
    def get_record(self, record_id):
84
 
        """Get a record from back end storage."""
85
 
        try:
86
 
            couch_record = self.db[record_id]
87
 
        except ResourceNotFound:
88
 
            return None
89
 
        data = {}
90
 
        data.update(couch_record)
91
 
        record = self.record_factory(data=data)
92
 
        record.record_id = record_id
93
 
        return record
94
 
 
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
99
 
        if record_id:
100
 
            self.db[record_id] = record_data
101
 
        else:
102
 
            record_id = self._add_record(record_data)
103
 
        return record_id
104
 
 
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.
109
 
        """
110
 
        while True:
111
 
            doc = self.db[doc_id]
112
 
            doc.update(fields)
113
 
            try:
114
 
                self.db[doc.id] = doc
115
 
            except ResourceConflict:
116
 
                continue
117
 
            break
118
 
 
119
 
    def _add_record(self, data):
120
 
        """Add a new record to the storage backend."""
121
 
        return self.db.create(data)
122
 
 
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', {})[
128
 
            'deleted'] = True
129
 
        self.db[record_id] = record
130
 
 
131
 
    def record_exists(self, record_id):
132
 
        """Check if record with given id exists."""
133
 
        if record_id not in self.db:
134
 
            return False
135
 
        record = self.db[record_id]
136
 
        return 'deleted' not in record.get('application_annotations', {}).get(
137
 
            'Ubuntu One', {}).get('private_application_annotations', {})
138
 
 
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
144
 
 
145
 
        doc_id = "_design/%(design_doc)s" % locals()
146
 
 
147
 
        # No atomic updates.  Only read & mutate & write.  Le sigh.
148
 
        # First, get current contents.
149
 
        try:
150
 
            view_container = self.db[doc_id]["views"]
151
 
        except (KeyError, ResourceNotFound):
152
 
            raise KeyError
153
 
 
154
 
        deleted_data = view_container.pop(view_name)  # Remove target
155
 
 
156
 
        if len(view_container) > 0:
157
 
            # Construct a new list of objects representing all views to have.
158
 
            views = [
159
 
                    ViewDefinition(design_doc, k, v.get("map"), v.get("reduce"))
160
 
                    for k, v
161
 
                    in view_container.iteritems()
162
 
                    ]
163
 
            # Push back a new batch of view.  Pray to Eris that this doesn't
164
 
            # clobber anything we want.
165
 
 
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)
171
 
        else:
172
 
            # There are no views left in this design document.
173
 
 
174
 
            # Remove design document.  This assumes there are only views in
175
 
            # design documents.  :(
176
 
            del self.db[doc_id]
177
 
 
178
 
        assert not self.view_exists(view_name, design_doc)
179
 
 
180
 
        return deleted_data
181
 
 
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
186
 
 
187
 
        view_id_fmt = "_design/%(design_doc)s/_view/%(view_name)s"
188
 
        return self.db.view(view_id_fmt % locals())
189
 
 
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
196
 
 
197
 
        view = ViewDefinition(design_doc, view_name, map_js, reduce_js)
198
 
        view.sync(self.db)
199
 
        assert self.view_exists(view_name, design_doc)
200
 
 
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
203
 
        exist?"""
204
 
        if design_doc is None:
205
 
            design_doc = view_name
206
 
 
207
 
        doc_id = "_design/%(design_doc)s" % locals()
208
 
 
209
 
        try:
210
 
            view_container = self.db[doc_id]["views"]
211
 
            return view_name in view_container
212
 
        except (KeyError, ResourceNotFound):
213
 
            return False
214
 
 
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
218
 
        in it."""
219
 
        doc_id = "_design/%(design_doc)s" % locals()
220
 
        try:
221
 
            return list(self.db[doc_id]["views"])
222
 
        except (KeyError, ResourceNotFound):
223
 
            return []
224
 
 
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 .
232
 
 
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 .
237
 
 
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
243
 
 
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']]
247
 
        """
248
 
        view_name = "get_records_and_type"
249
 
        view_map_js = """
250
 
            function(doc) { 
251
 
                try {
252
 
                    if (! doc['application_annotations']['Ubuntu One']
253
 
                            ['private_application_annotations']['deleted']) {
254
 
                        emit(doc.record_type, doc);
255
 
                    }
256
 
                } catch (e) {
257
 
                    emit(doc.record_type, doc);
258
 
                }
259
 
            }"""
260
 
 
261
 
        if design_doc is None:
262
 
            design_doc = view_name
263
 
 
264
 
        if not version is None:  # versions do not affect design_doc name.
265
 
            view_name = view_name + "__v" + version
266
 
 
267
 
        exists = self.view_exists(view_name, design_doc)
268
 
 
269
 
        if exists:
270
 
            if create_view is None:
271
 
                raise KeyError("Exclusive creation failed.")
272
 
        else:
273
 
            if create_view == False:
274
 
                raise KeyError("View doesn't already exist.")
275
 
 
276
 
        if not exists:
277
 
            self.add_view(view_name, view_map_js, None, design_doc)
278
 
 
279
 
        viewdata = self.execute_view(view_name, design_doc)
280
 
        if record_type is None:
281
 
            return viewdata
282
 
        else:
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)