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