]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/database.py
Implement an Enum class and make the STATUS object one
[notmuch] / cnotmuch / database.py
1 import ctypes
2 from ctypes import c_int, c_char_p, c_void_p, c_uint64
3 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
4 import logging
5 from datetime import date
6
7 class Database(object):
8     """ Wrapper around a notmuch_database_t
9
10     Do note that as soon as we tear down this object, all derived queries,
11     threads, and messages will be freed as well.
12     """
13     MODE = Enum(['READ_ONLY','READ_WRITE'])
14     """Constants: Mode in which to open the database"""
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             logging.debug("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     def set_sort(self, sort):
189         """notmuch_query_set_sort
190
191         :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID
192         :returns: Nothing, but raises NotmuchError if query is not inited
193         """
194         if self._query is None:
195             raise NotmuchError(STATUS.NOT_INITIALIZED)
196
197         nmlib.notmuch_query_set_sort(self._query, sort)
198
199     def search_messages(self):
200         """notmuch_query_search_messages
201         Returns Messages() or a raises a NotmuchError()
202         """
203         if self._query is None:
204             raise NotmuchError(STATUS.NOT_INITIALIZED)            
205
206         msgs_p = Query._search_messages(self._query)
207
208         if msgs_p is None:
209             NotmuchError(STATUS.NULL_POINTER)
210
211         return Messages(msgs_p,self)
212
213
214     def __del__(self):
215         """Close and free the Query"""
216         if self._query is not None:
217             logging.debug("Freeing the Query now")
218             nmlib.notmuch_query_destroy (self._query)
219
220 #------------------------------------------------------------------------------
221 class Tags(object):
222     """Wrapper around notmuch_tags_t"""
223
224     #notmuch_tags_get
225     _get = nmlib.notmuch_tags_get
226     _get.restype = c_char_p
227
228     def __init__(self, tags_p, parent=None):
229         """
230         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
231         we will raise an NotmuchError(STATUS.NULL_POINTER).
232
233         Is passed the parent these tags are derived from, and saves a
234         reference to it, so we can automatically delete the db object
235         once all derived objects are dead.
236
237         Tags() provides an iterator over all contained tags. However, you will
238         only be able to iterate over the Tags once, because the underlying C
239         function only allows iterating once.
240         #TODO: make the iterator work more than once and cache the tags in 
241                the Python object.
242         """
243         if tags_p is None:
244             NotmuchError(STATUS.NULL_POINTER)
245
246         self._tags = tags_p
247         #save reference to parent object so we keep it alive
248         self._parent = parent
249         logging.debug("Inited Tags derived from %s" %(repr(parent)))
250     
251     def __iter__(self):
252         """ Make Tags an iterator """
253         return self
254
255     def next(self):
256         if self._tags is None:
257             raise NotmuchError(STATUS.NOT_INITIALIZED)
258
259         if not nmlib.notmuch_tags_valid(self._tags):
260             self._tags = None
261             raise StopIteration
262
263         tag = Tags._get (self._tags)
264         nmlib.notmuch_tags_move_to_next(self._tags)
265         return tag
266
267     def __str__(self):
268         """str() of Tags() is a space separated list of tags
269
270         This iterates over the list of Tags and will therefore 'exhaust' Tags()
271         """
272         return " ".join(self)
273
274     def __del__(self):
275         """Close and free the notmuch tags"""
276         if self._tags is not None:
277             logging.debug("Freeing the Tags now")
278             nmlib.notmuch_tags_destroy (self._tags)
279
280
281 #------------------------------------------------------------------------------
282 class Messages(object):
283     """Wrapper around notmuch_messages_t"""
284
285     #notmuch_tags_get
286     _get = nmlib.notmuch_messages_get
287     _get.restype = c_void_p
288
289     _collect_tags = nmlib.notmuch_messages_collect_tags
290     _collect_tags.restype = c_void_p
291
292     def __init__(self, msgs_p, parent=None):
293         """
294         msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
295         we will raise an NotmuchError(STATUS.NULL_POINTER).
296
297         If passed the parent query this Messages() is derived from, it saves a
298         reference to it, so we can automatically delete the parent query object
299         once all derived objects are dead.
300
301         Messages() provides an iterator over all contained Message()s.
302         However, you will only be able to iterate over it once,
303         because the underlying C function only allows iterating once.
304         #TODO: make the iterator work more than once and cache the tags in 
305                the Python object.
306         """
307         if msgs_p is None:
308             NotmuchError(STATUS.NULL_POINTER)
309
310         self._msgs = msgs_p
311         #store parent, so we keep them alive as long as self  is alive
312         self._parent = parent
313         logging.debug("Inited Messages derived from %s" %(str(parent)))
314
315     def collect_tags(self):
316         """ return the Tags() belonging to the messages
317         
318         Do note that collect_tags will iterate over the messages and
319         therefore will not allow further iterationsl
320         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
321         """
322         if self._msgs is None:
323             raise NotmuchError(STATUS.NOT_INITIALIZED)
324
325         # collect all tags (returns NULL on error)
326         tags_p = Messages._collect_tags (self._msgs)
327         #reset _msgs as we iterated over it and can do so only once
328         self._msgs = None
329
330         if tags_p == None:
331             raise NotmuchError(STATUS.NULL_POINTER)
332         return Tags(tags_p, self)
333
334     def __iter__(self):
335         """ Make Messages an iterator """
336         return self
337
338     def next(self):
339         if self._msgs is None:
340             raise NotmuchError(STATUS.NOT_INITIALIZED)
341
342         if not nmlib.notmuch_messages_valid(self._msgs):
343             self._msgs = None
344             raise StopIteration
345
346         msg = Message(Messages._get (self._msgs), self)
347         nmlib.notmuch_messages_move_to_next(self._msgs)
348         return msg
349
350     def __len__(self):
351         """ Returns the number of contained messages
352
353         :note: As this iterates over the messages, we will not be able to 
354                iterate over them again (as in retrieve them)!
355         """
356         if self._msgs is None:
357             raise NotmuchError(STATUS.NOT_INITIALIZED)
358
359         i=0
360         while nmlib.notmuch_messages_valid(self._msgs):
361             nmlib.notmuch_messages_move_to_next(self._msgs)
362             i += 1
363         self._msgs = None
364         return i
365
366
367
368     def __del__(self):
369         """Close and free the notmuch Messages"""
370         if self._msgs is not None:
371             logging.debug("Freeing the Messages now")
372             nmlib.notmuch_messages_destroy (self._msgs)
373
374
375 #------------------------------------------------------------------------------
376 class Message(object):
377     """Wrapper around notmuch_message_t"""
378
379     """notmuch_message_get_filename (notmuch_message_t *message)"""
380     _get_filename = nmlib.notmuch_message_get_filename
381     _get_filename.restype = c_char_p 
382     """notmuch_message_get_message_id (notmuch_message_t *message)"""
383     _get_message_id = nmlib.notmuch_message_get_message_id
384     _get_message_id.restype = c_char_p 
385
386     """notmuch_message_get_tags (notmuch_message_t *message)"""
387     _get_tags = nmlib.notmuch_message_get_tags
388     _get_tags.restype = c_void_p
389
390     _get_date = nmlib.notmuch_message_get_date
391     _get_date.restype = c_uint64
392
393     _get_header = nmlib.notmuch_message_get_header
394     _get_header.restype = c_char_p
395
396     def __init__(self, msg_p, parent=None):
397         """
398         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
399         we will raise an NotmuchError(STATUS.NULL_POINTER).
400
401         Is a 'parent' object is passed which this message is derived from,
402         we save a reference to it, so we can automatically delete the parent
403         object once all derived objects are dead.
404         """
405         if msg_p is None:
406             NotmuchError(STATUS.NULL_POINTER)
407         self._msg = msg_p
408         #keep reference to parent, so we keep it alive
409         self._parent = parent
410         logging.debug("Inited Message derived from %s" %(str(parent)))
411
412
413     def get_message_id(self):
414         """ return the msg id
415         
416         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
417         """
418         if self._msg is None:
419             raise NotmuchError(STATUS.NOT_INITIALIZED)
420         return Message._get_message_id(self._msg)
421
422     def get_date(self):
423         """returns time_t of the message date
424
425         For the original textual representation of the Date header from the
426         message call notmuch_message_get_header() with a header value of
427         "date".
428         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
429         """
430         if self._msg is None:
431             raise NotmuchError(STATUS.NOT_INITIALIZED)
432         return Message._get_date(self._msg)
433
434     def get_header(self, header):
435         """ TODO document me"""
436         if self._msg is None:
437             raise NotmuchError(STATUS.NOT_INITIALIZED)
438
439         #Returns NULL if any error occurs.
440         header = Message._get_header (self._msg, header)
441         if header == None:
442             raise NotmuchError(STATUS.NULL_POINTER)
443         return header
444
445     def get_filename(self):
446         """ return the msg filename
447         
448         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
449         """
450         if self._msg is None:
451             raise NotmuchError(STATUS.NOT_INITIALIZED)
452         return Message._get_filename(self._msg)
453
454     def get_tags(self):
455         """ return the msg tags
456         
457         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
458         Raises NotmuchError(STATUS.NULL_POINTER) on error.
459         """
460         if self._msg is None:
461             raise NotmuchError(STATUS.NOT_INITIALIZED)
462
463         tags_p = Message._get_tags(self._msg)
464         if tags_p == None:
465             raise NotmuchError(STATUS.NULL_POINTER)
466         return Tags(tags_p, self)
467
468     def __str__(self):
469         """A message() is represented by a 1-line summary"""
470         msg = {}
471         msg['from'] = self.get_header('from')
472         msg['tags'] = str(self.get_tags())
473         msg['date'] = date.fromtimestamp(self.get_date())
474         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
475
476     def format_as_text(self):
477         """ Output like notmuch show """
478         return str(self)
479
480     def __del__(self):
481         """Close and free the notmuch Message"""
482         if self._msg is not None:
483             logging.debug("Freeing the Message now")
484             nmlib.notmuch_message_destroy (self._msg)