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