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