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