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