xref: /4.6.4/couchstore/python/couchstore.py (revision 0ad474ed)
1# couchstore.py
2# Python interface to CouchStore library
3
4from ctypes import *        # <http://docs.python.org/library/ctypes.html>
5import errno
6
7# Load the couchstore library and customize return types:
8try:
9    _lib = CDLL("libcouchstore.so")         # Linux
10except OSError:
11    try:
12        _lib = CDLL("libcouchstore.dylib")  # Mac OS
13    except OSError:
14        _lib = CDLL("couchstore")           # Windows (?)
15
16_lib.couchstore_strerror.restype = c_char_p
17
18
19class CouchStoreException (Exception):
20    """Exceptions raised by CouchStore APIs."""
21    def __init__ (self, errcode):
22        Exception.__init__(self, _lib.couchstore_strerror(errcode))
23        self.code = errcode
24
25
26### INTERNAL FUNCTIONS:
27
28def _check (err):
29    if err == 0:
30        return
31    elif err == -3:
32        raise MemoryError()
33    elif err == -5:
34        raise KeyError()
35    elif err == -11:
36        raise OSError(errno.ENOENT)
37    else:
38        raise CouchStoreException(err)
39
40
41def _toString (key):
42    if not isinstance(key, basestring):
43        raise TypeError(key)
44    return str(key)
45
46
47### INTERNAL STRUCTS:
48
49class SizedBuf (Structure):
50    _fields_ = [("buf", c_char_p), ("size", c_ulonglong)]
51
52    def __init__(self, string):
53        if string != None:
54            string = _toString(string)
55            Structure.__init__(self, string, len(string))
56        else:
57            Structure.__init__(self, None, 0)
58
59    def __str__(self):
60        return string_at(self.buf, self.size)
61
62class DocStruct (Structure):
63    _fields_ = [("id", SizedBuf), ("data", SizedBuf)]
64
65class DocInfoStruct (Structure):
66    _fields_ = [("id", SizedBuf),
67                ("db_seq", c_ulonglong),
68                ("rev_seq", c_ulonglong),
69                ("rev_meta", SizedBuf),
70                ("deleted", c_int),
71                ("content_meta", c_ubyte),
72                ("bp", c_ulonglong),
73                ("size", c_ulonglong) ]
74
75class LocalDocStruct (Structure):
76    _fields_ = [("id", SizedBuf),
77                ("json", SizedBuf),
78                ("deleted", c_int) ]
79
80
81### DOCUMENT INFO CLASS:
82
83class DocumentInfo (object):
84    """Metadata of a document in a CouchStore database."""
85
86    # Values for contentType:
87    IS_JSON = 0
88    INVALID_JSON = 1
89    INVALID_JSON_KEY = 2
90    NON_JSON = 3
91
92    def __init__ (self, id):
93        self.id = id
94        self.deleted = False
95        self.contentType = DocumentInfo.NON_JSON
96        self.revSequence = 0
97
98    @staticmethod
99    def _fromStruct (info, store = None):
100        self = DocumentInfo(str(info.id))
101        self.store = store
102        self.sequence = info.db_seq
103        self.revSequence = info.rev_seq
104        self.revMeta = str(info.rev_meta)
105        self.deleted = (info.deleted != 0)
106        self.contentType = info.content_meta & 0x0F
107        self.compressed = (info.content_meta & 0x80) != 0
108        self._bp = info.bp
109        self.physSize = info.size
110        return self
111
112    def _asStruct(self):
113        struct = DocInfoStruct(SizedBuf(self.id))
114        if hasattr(self, "sequence"): struct.db_seq = self.sequence
115        if hasattr(self, "revMeta"): struct.rev_meta = SizedBuf(self.revMeta)
116        struct.rev_seq = self.revSequence
117        struct.deleted = self.deleted
118        struct.content_meta = self.contentType & 0x0F
119        if hasattr(self, "compressed") and self.compressed:
120            struct.content_meta |= 0x80
121        if hasattr(self, "_bp"): struct.bp = self._bp
122        if hasattr(self, "physSize"): struct.size = self.physSize
123        return struct
124
125    def __str__ (self):
126        return "DocumentInfo('%s', %d bytes)" % (self.id, self.size)
127
128    def __repr__ (self):
129        return "DocumentInfo('%s', %d bytes)" % (self.id, self.size)
130
131    def dump (self):
132        return "DocumentInfo('%s', %d bytes, seq=%d, revSeq=%d, deleted=%s, contentType=%d, compressed=%d, bp=%d)" % \
133                 (self.id, self.physSize, self.sequence, self.revSequence, self.deleted, \
134                  self.contentType, self.compressed, self._bp)
135
136    def getContents(self, options =0):
137        """Fetches and returns the contents of a DocumentInfo returned from CouchStore's getInfo
138           or getInfoBySequence methods."""
139        if not hasattr(self, "store") or not hasattr(self, "_bp"):
140            raise Exception("Contents unknown")
141        info = self._asStruct()
142        docptr = pointer(DocStruct())
143        _lib.couchstore_open_doc_with_docinfo(self.store, byref(info), byref(docptr), options)
144        contents = str(docptr.contents.data)
145        _lib.couchstore_free_document(docptr)
146        return contents
147
148
149### LOCAL-DOCUMENTS CLASS:
150
151class LocalDocs (object):
152    """Collection that represents the local documents of a CouchStore."""
153    def __init__(self, couchstore):
154        self.couchstore = couchstore
155
156    def __getitem__ (self, key):
157        """Returns the contents of a local document (as a string) given its ID."""
158        id = _toString(key)
159        docptr = pointer(LocalDocStruct())
160        err = _lib.couchstore_open_local_document(self.couchstore, id, len(id), byref(docptr))
161        if err == -5 or (err == 0 and docptr.contents.deleted):
162            raise KeyError(id)
163        _check(err)
164        value = str(docptr.contents.json)
165        _lib.couchstore_free_document(docptr)
166        return value
167
168    def __setitem__ (self, key, value):
169        """Saves a local document with the given ID, or deletes it if the value is None."""
170        idbuf = SizedBuf(key)
171        doc = LocalDocStruct(idbuf)
172        if value != None:
173            doc.json = SizedBuf(value)
174        else:
175            doc.deleted = True
176        _check(_lib.couchstore_save_local_document(self.couchstore, byref(doc)))
177
178    def __delitem__ (self, key):
179        self.__setitem__(key, None)
180
181
182### COUCHSTORE CLASS:
183
184class CouchStore (object):
185    """Interface to a CouchStore database."""
186
187    def __init__ (self, path, mode =None):
188        """Creates a CouchStore at a given path. The option mode parameter can be 'r' for
189           read-only access, or 'c' to create the file if it doesn't already exist."""
190        if mode == 'r':
191            flags = 2 # RDONLY
192        elif mode == 'c':
193            flags = 1 # CREATE
194        else:
195            flags = 0
196
197        db = c_void_p()
198        _check(_lib.couchstore_open_db(path, flags, byref(db)))
199        self._as_parameter_ = db
200        self.path = path
201
202    def __del__(self):
203        self.close()
204
205    def close (self):
206        """Closes the CouchStore."""
207        if hasattr(self, "_as_parameter_"):
208            _lib.couchstore_close_db(self)
209            del self._as_parameter_
210
211    def __str__(self):
212        return "CouchStore(%s)" % self.path
213
214    COMPRESS = 1
215
216    def save (self, id, data, options =0):
217        """Saves a document with the given ID. Returns the sequence number."""
218        if isinstance(id, DocumentInfo):
219            infoStruct = id._asStruct()
220            idbuf = infoStruct.id
221        else:
222            idbuf = SizedBuf(id)
223            infoStruct = DocInfoStruct(idbuf)
224        if data != None:
225            doc = DocStruct(idbuf, SizedBuf(data))
226            docref = byref(doc)
227            if options & CouchStore.COMPRESS:
228                infoStruct.content_meta |= 0x80
229        else:
230            docref = None
231        _check(_lib.couchstore_save_document(self, docref, byref(infoStruct), options))
232        if isinstance(id, DocumentInfo):
233            id.sequence = infoStruct.db_seq
234        return infoStruct.db_seq
235
236    def saveMultiple(self, ids, datas, options =0):
237        """Saves multiple documents. 'ids' is an array of either strings or DocumentInfo objects.
238           'datas' is a parallel array of value strings (or None, in which case the documents
239           will be deleted.) Returns an array of new sequence numbers."""
240        n = len(ids)
241        docStructs = (POINTER(DocStruct) * n)()
242        infoStructs = (POINTER(DocInfoStruct) * n)()
243        for i in xrange(0, n):
244            id = ids[i]
245            if isinstance(id, DocumentInfo):
246                info = id._asStruct()
247            else:
248                info = DocInfoStruct(SizedBuf(id))
249            doc = DocStruct(info.id)
250            if datas and datas[i]:
251                doc.data = SizedBuf(datas[i])
252            else:
253                info.deleted = True
254            infoStructs[i] = pointer(info)
255            docStructs[i] = pointer(doc)
256        _check(_lib.couchstore_save_documents(self, byref(docStructs), byref(infoStructs), n, \
257                                              options))
258        return [info.contents.db_seq for info in infoStructs]
259    pass
260
261    def commit (self):
262        """Ensures all saved data is flushed to disk."""
263        _check(_lib.couchstore_commit(self))
264
265    DECOMPRESS = 1
266
267    def get (self, id, options =0):
268        """Returns the contents of a document (as a string) given its ID."""
269        id = _toString(id)
270        docptr = pointer(DocStruct())
271        err = _lib.couchstore_open_document(self, id, len(id), byref(docptr), options)
272        if err == -5:
273            raise KeyError(id)
274        _check(err)
275        data = str(docptr.contents.data)
276        _lib.couchstore_free_document(docptr)
277        return data
278
279    def __getitem__ (self, key):
280        return self.get(key)
281
282    def __setitem__ (self, key, value):
283        self.save(key, value)
284
285    def __delitem__ (self, key):
286        self.save(key, None)
287
288    # Getting document info:
289
290    def _infoPtrToDoc (self, key, infoptr, err):
291        if err == -5:
292            raise KeyError(key)
293        _check(err)
294        info = infoptr.contents
295        if info == None:
296            return None
297        doc = DocumentInfo._fromStruct(info, self)
298        _lib.couchstore_free_docinfo(infoptr)
299        return doc
300
301    def getInfo (self, id):
302        """Returns the DocumentInfo object with the given ID."""
303        id = _toString(id)
304        infoptr = pointer(DocInfoStruct())
305        err = _lib.couchstore_docinfo_by_id(self, id, len(id), byref(infoptr))
306        return self._infoPtrToDoc(id, infoptr, err)
307
308    def getInfoBySequence (self, sequence):
309        """Returns the DocumentInfo object with the given sequence number."""
310        infoptr = pointer(DocInfoStruct())
311        err = _lib.couchstore_docinfo_by_sequence(self, c_ulonglong(sequence), byref(infoptr))
312        return self._infoPtrToDoc(sequence, infoptr, err)
313
314    # Iterating:
315
316    ITERATORFUNC = CFUNCTYPE(c_int, c_void_p, POINTER(DocInfoStruct), c_void_p)
317
318    def forEachChange(self, since, fn):
319        """Calls the function "fn" once for every document sequence since the "since" parameter.
320           The single parameter to "fn" will be a DocumentInfo object. You can call
321           getContents() on it to get the document contents."""
322        def callback (dbPtr, docInfoPtr, context):
323            fn(DocumentInfo._fromStruct(docInfoPtr.contents, self))
324            return 0
325        _check(_lib.couchstore_changes_since(self, since, 0, \
326               CouchStore.ITERATORFUNC(callback), c_void_p(0)))
327
328    def changesSince (self, since):
329        """Returns an array of DocumentInfo objects, for every document that's changed since the
330           sequence number "since"."""
331        changes = []
332        self.forEachChange(since, lambda docInfo: changes.append(docInfo))
333        return changes
334
335    @property
336    def localDocs(self):
337        """A simple dictionary-like object that accesses the CouchStore's local documents."""
338        return LocalDocs(self)
339