implement Query() and search_messages()
[notmuch] / cnotmuch / database.py
1 import ctypes
2 from ctypes import c_int, c_char_p, c_void_p
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError
4
5
6 class Database(object):
7     """ Wrapper around a notmuch_database_t
8
9     Do note that as soon as we tear down this object, all derived queries,
10     threads, and messages will be freed as well.
11     """
12     #constants
13     MODE_READ_ONLY = 0
14     MODE_READ_WRITE = 1
15
16     _std_db_path = None
17     """Class attribute of users default database"""
18
19     """notmuch_database_get_path (notmuch_database_t *database)"""
20     _get_path = nmlib.notmuch_database_get_path
21     _get_path.restype = c_char_p
22
23     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
24     _open = nmlib.notmuch_database_open 
25     _open.restype = c_void_p
26
27     """ notmuch_database_find_message """
28     _find_message = nmlib.notmuch_database_find_message
29     _find_message.restype = c_void_p
30
31     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
32     _get_all_tags = nmlib.notmuch_database_get_all_tags
33     _get_all_tags.restype = c_void_p
34
35     """ notmuch_database_create(const char *path):"""
36     _create = nmlib.notmuch_database_create
37     _create.restype = c_void_p
38
39     def __init__(self, path=None, create=False, status= MODE_READ_ONLY):
40         """ Open or create a notmuch database
41
42         If path is None, we will try to read a users notmuch configuration and
43         use his default database.
44         Throws a NotmuchError in case of failure.
45         """
46         self._db = None
47         if path is None:
48             # no path specified. use a user's default database
49             if Database._std_db_path is None:
50                 #the following line throws a NotmuchError if it fails
51                 Database._std_db_path = self._get_user_default_db()
52             path = Database._std_db_path
53
54         if create == False:
55             self.open(path, status)
56         else:
57             self.create(path, status)
58
59     def create(self, path, status=MODE_READ_ONLY):
60         """ notmuch_database_create(const char *path)
61
62         :returns: Raises :exc:`notmuch.NotmuchError` in case
63                   of any failure (after printing an error message on stderr).
64         """
65         res = Database._create(path, status)
66
67         if res is None:
68             raise NotmuchError(
69                 message="Could not create the specified database")
70         self._db = res
71
72     def open(self, path, status= MODE_READ_ONLY): 
73         """calls notmuch_database_open
74
75         :returns: Raises :exc:`notmuch.NotmuchError` in case
76                   of any failure (after printing an error message on stderr).
77         """
78         res = Database._open(path, status)
79
80         if res is None:
81             raise NotmuchError(
82                 message="Could not open the specified database")
83         self._db = res
84
85     def get_path(self):
86         """notmuch_database_get_path (notmuch_database_t *database);  """
87         return Database._get_path(self._db)
88
89     def find_message(self, msgid):
90         """notmuch_database_find_message
91         :param msgid: The message id
92         :ptype msgid: string
93
94         :returns: Message() or None if no message is found or if an
95                   out-of-memory situation occurs.
96         """
97         if self._db is None:
98             raise NotmuchError(STATUS.NOT_INITIALIZED)
99         msg_p = Database._find_message(self._db, msgid)
100         if msg_p is None:
101             return None
102         return Message(msg_p, self)
103
104     def get_all_tags(self):
105         """Return a Tags() object (list of all tags found in the database)
106
107         :returns: Tags() object or raises :exc:`NotmuchError` with 
108                   STATUS.NULL_POINTER on error
109         """
110         if self._db is None:
111             raise NotmuchError(STATUS.NOT_INITIALIZED)
112
113         tags_p = Database._get_all_tags (self._db)
114         if tags_p == None:
115             raise NotmuchError(STATUS.NULL_POINTER)
116         return Tags(tags_p, self)
117
118     def __repr__(self):
119         return "'Notmuch DB " + self.get_path() + "'"
120
121     def __del__(self):
122         """Close and free the notmuch database if needed"""
123         if self._db is not None:
124             print("Freeing the database now")
125             nmlib.notmuch_database_close(self._db)
126
127     def _get_user_default_db(self):
128         """ Reads a user's notmuch config and returns his db location
129
130         Throws a NotmuchError if it cannot find it"""
131         from ConfigParser import SafeConfigParser
132         import os.path
133         config = SafeConfigParser()
134         config.read(os.path.expanduser('~/.notmuch-config'))
135         if not config.has_option('database','path'):
136             raise NotmuchError(message=
137                                "No DB path specified and no user default found")
138         return config.get('database','path')
139
140     @property
141     def db_p(self):
142         """Returns a pointer to the current notmuch_database_t or None"""
143         return self._db
144
145 #------------------------------------------------------------------------------
146 class Query(object):
147     """ Wrapper around a notmuch_query_t
148
149     Do note that as soon as we tear down this object, all derived
150     threads, and messages will be freed as well.
151     """
152     # constants
153     SORT_OLDEST_FIRST = 0
154     SORT_NEWEST_FIRST = 1
155     SORT_MESSAGE_ID = 2
156
157     """notmuch_query_create"""
158     _create = nmlib.notmuch_query_create
159     _create.restype = c_void_p
160
161     """notmuch_query_search_messages"""
162     _search_messages = nmlib.notmuch_query_search_messages
163     _search_messages.restype = c_void_p
164
165     def __init__(self, db, querystr):
166         """TODO: document"""
167         self._db = None
168         self._query = None
169         self.create(db, querystr)
170
171     def create(self, db, querystr):
172         """db is a Database() and querystr a string
173
174         raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and
175         STATUS.NULL_POINTER if the query creation failed (too little memory)
176         """
177         if db.db_p is None:
178             raise NotmuchError(STATUS.NOT_INITIALIZED)            
179         # create reference to parent db to keep it alive
180         self._db = db
181         
182         # create query, return None if too little mem available
183         query_p = Query._create(db.db_p, querystr)
184         if query_p is None:
185             NotmuchError(STATUS.NULL_POINTER)
186         self._query = query_p
187
188
189     def search_messages(self):
190         """notmuch_query_search_messages
191         Returns Messages() or a raises a NotmuchError()
192         """
193         if self._query is None:
194             raise NotmuchError(STATUS.NOT_INITIALIZED)            
195
196         msgs_p = Query._search_messages(self._query)
197
198         if msgs_p is None:
199             NotmuchError(STATUS.NULL_POINTER)
200
201         return Messages(msgs_p,self)
202
203
204     def __del__(self):
205         """Close and free the Query"""
206         if self._query is not None:
207             print("Freeing the Query now")
208             nmlib.notmuch_query_destroy (self._query)
209
210 #------------------------------------------------------------------------------
211 class Tags(object):
212     """Wrapper around notmuch_tags_t"""
213
214     #notmuch_tags_get
215     _get = nmlib.notmuch_tags_get
216     _get.restype = c_char_p
217
218     def __init__(self, tags_p, db=None):
219         """
220         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
221         we will raise an NotmuchError(STATUS.NULL_POINTER).
222
223         Is passed the db these tags are derived from, and saves a
224         reference to it, so we can automatically delete the db object
225         once all derived objects are dead.
226
227         Tags() provides an iterator over all contained tags. However, you will
228         only be able to iterate over the Tags once, because the underlying C
229         function only allows iterating once.
230         #TODO: make the iterator work more than once and cache the tags in 
231                the Python object.
232         """
233         if tags_p is None:
234             NotmuchError(STATUS.NULL_POINTER)
235
236         self._tags = tags_p
237         self._db = db
238         print "Inited Tags derived from %s" %(str(db))
239     
240     def __iter__(self):
241         """ Make Tags an iterator """
242         if self._tags is None:
243             raise NotmuchError(STATUS.NOT_INITIALIZED)
244         return self
245
246     def next(self):
247         nmlib.notmuch_tags_move_to_next(self._tags)
248         if not nmlib.notmuch_tags_valid(self._tags):
249             print("Freeing the Tags now")
250             nmlib.notmuch_tags_destroy (self._tags)
251             raise StopIteration
252         return Tags._get (self._tags)
253
254     def __del__(self):
255         """Close and free the notmuch tags"""
256         if self._tags is not None:
257             print("Freeing the Tags now")
258             nmlib.notmuch_tags_destroy (self._tags)
259
260
261 #------------------------------------------------------------------------------
262 class Messages(object):
263     """Wrapper around notmuch_messages_t"""
264
265     #notmuch_tags_get
266     _get = nmlib.notmuch_messages_get
267     _get.restype = c_void_p
268
269     def __init__(self, msgs_p, parent=None):
270         """
271         msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
272         we will raise an NotmuchError(STATUS.NULL_POINTER).
273
274         If passed the parent query this Messages() is derived from, it saves a
275         reference to it, so we can automatically delete the parent query object
276         once all derived objects are dead.
277
278         Messages() provides an iterator over all contained Message()s.
279         However, you will only be able to iterate over it once,
280         because the underlying C function only allows iterating once.
281         #TODO: make the iterator work more than once and cache the tags in 
282                the Python object.
283         """
284         if msgs_p is None:
285             NotmuchError(STATUS.NULL_POINTER)
286
287         self._msgs = msgs_p
288         #store parent, so we keep them alive as long as self  is alive
289         self._parent = parent
290         print "Inited Messages derived from %s" %(str(parent))
291     
292     def __iter__(self):
293         """ Make Messages an iterator """
294         return self
295
296     def next(self):
297         if self._msgs is None:
298             raise NotmuchError(STATUS.NOT_INITIALIZED)
299
300         nmlib.notmuch_messages_move_to_next(self._msgs)
301         if not nmlib.notmuch_messages_valid(self._msgs):
302             print("Freeing the Messages now")
303             nmlib.notmuch_messages_destroy (self._msgs)
304             self._msgs = None
305             raise StopIteration
306         return Message(Messages._get (self._msgs), self)
307
308     def __del__(self):
309         """Close and free the notmuch Messages"""
310         if self._msgs is not None:
311             print("Freeing the Messages now")
312             nmlib.notmuch_messages_destroy (self._msgs)
313
314
315 #------------------------------------------------------------------------------
316 class Message(object):
317     """Wrapper around notmuch_message_t"""
318
319     """notmuch_message_get_filename (notmuch_message_t *message)"""
320     _get_filename = nmlib.notmuch_message_get_filename
321     _get_filename.restype = c_char_p 
322     """notmuch_message_get_message_id (notmuch_message_t *message)"""
323     _get_message_id = nmlib.notmuch_message_get_message_id
324     _get_message_id.restype = c_char_p 
325
326     """notmuch_message_get_tags (notmuch_message_t *message)"""
327     _get_tags = nmlib.notmuch_message_get_tags
328     _get_tags.restype = c_void_p
329
330     def __init__(self, msg_p, parent=None):
331         """
332         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
333         we will raise an NotmuchError(STATUS.NULL_POINTER).
334
335         Is a 'parent' object is passed which this message is derived from,
336         we save a reference to it, so we can automatically delete the parent
337         object once all derived objects are dead.
338         """
339         if msg_p is None:
340             NotmuchError(STATUS.NULL_POINTER)
341         self._msg = msg_p
342         self._parent = parent
343         print "Inited Message derived from %s" %(str(parent))
344
345
346     def get_message_id(self):
347         """ return the msg id
348         
349         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
350         """
351         if self._msg is None:
352             raise NotmuchError(STATUS.NOT_INITIALIZED)
353         return Message._get_message_id(self._msg)
354
355
356     def get_filename(self):
357         """ return the msg filename
358         
359         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
360         """
361         if self._msg is None:
362             raise NotmuchError(STATUS.NOT_INITIALIZED)
363         return Message._get_filename(self._msg)
364
365     def get_tags(self):
366         """ return the msg tags
367         
368         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
369         Raises NotmuchError(STATUS.NULL_POINTER) on error.
370         """
371         if self._msg is None:
372             raise NotmuchError(STATUS.NOT_INITIALIZED)
373
374         tags_p = Message._get_tags(self._msg)
375         if tags_p == None:
376             raise NotmuchError(STATUS.NULL_POINTER)
377         return Tags(tags_p, self)
378
379     def __del__(self):
380         """Close and free the notmuch Message"""
381         if self._msg is not None:
382             print("Freeing the Message now")
383             nmlib.notmuch_message_destroy (self._msg)