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