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