]> git.notmuchmail.org Git - notmuch/blob - cnotmuch/database.py
Improve source documentation
[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     """Represents a notmuch database (wraps notmuch_database_t)
9
10     .. note:: Do note that as soon as we tear down this object, all underlying 
11            derived objects such as queries, threads, messages, tags etc will 
12            be freed by the underlying library as well. Accessing these objects
13            will lead to segfaults and other unexpected behavior.
14
15            We implement reference counting, so that parent objects can be 
16            automatically freed when they are not needed anymore, for example::
17
18             db = Database('path',create=True)
19             msgs = Query(db,'from:myself').search_messages()
20
21            This returns a :class:`Messages` which internally contains
22            a reference to the parent :class:`Query` object. Otherwise
23            the Query() would be immediately freed, taking our *msgs*
24            down with it.
25
26            In this case, the above Query() object will be
27            automatically freed whenever we delete all derived objects,
28            ie in our case: `del (msgs)` would also delete the parent
29            Query (but not the parent Database() as that is still
30            referenced from the variable *db* in which it is stored.
31
32            Pretty much the same is valid for all other objects in the hierarchy,
33            such as :class:`Query`, :class:`Messages`, :class:`Message`,
34            and :class:`Tags`.
35     """
36     MODE = Enum(['READ_ONLY','READ_WRITE'])
37     """Constants: Mode in which to open the database"""
38
39     _std_db_path = None
40     """Class attribute to cache user's default database"""
41
42     """notmuch_database_get_path (notmuch_database_t *database)"""
43     _get_path = nmlib.notmuch_database_get_path
44     _get_path.restype = c_char_p
45
46     """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
47     _open = nmlib.notmuch_database_open 
48     _open.restype = c_void_p
49
50     """ notmuch_database_find_message """
51     _find_message = nmlib.notmuch_database_find_message
52     _find_message.restype = c_void_p
53
54     """notmuch_database_get_all_tags (notmuch_database_t *database)"""
55     _get_all_tags = nmlib.notmuch_database_get_all_tags
56     _get_all_tags.restype = c_void_p
57
58     """ notmuch_database_create(const char *path):"""
59     _create = nmlib.notmuch_database_create
60     _create.restype = c_void_p
61
62     def __init__(self, path=None, create=False, mode= MODE.READ_ONLY):
63         """If *path* is *None*, we will try to read a users notmuch
64         configuration and use his default database. If *create* is `True`,
65         the database will always be created in
66         :attr:`MODE.READ_WRITE` mode as creating an empty
67         database for reading only does not make a great deal of sense.
68
69         :param path:   Directory to open/create the database in (see
70                        above for behavior if `None`)
71         :type path:    `str` or `None`
72         :param create: False to open an existing, True to create a new
73                        database.  
74         :type create:  bool
75         :param mdoe:   Mode to open a database in. Always 
76                        :attr:`MODE`.READ_WRITE when creating a new one.
77         :type mode:    :attr:`MODE`
78         :returns:      Nothing
79         :exception:    :exc:`NotmuchError` in case of failure.
80         """
81         self._db = None
82         if path is None:
83             # no path specified. use a user's default database
84             if Database._std_db_path is None:
85                 #the following line throws a NotmuchError if it fails
86                 Database._std_db_path = self._get_user_default_db()
87             path = Database._std_db_path
88
89         if create == False:
90             self.open(path, status)
91         else:
92             self.create(path)
93
94     def create(self, path):
95         """Creates a new notmuch database
96
97         This function wraps *notmuch_database_create(...)* and creates
98         a new notmuch database at *path*. It will always return a database in
99         :attr:`MODE`.READ_WRITE mode as creating an empty database 
100         for reading only does not make a great deal of sense.
101
102         :param path: A directory in which we should create the database.
103         :type path: str
104         :returns: Nothing
105         :exception: :exc:`NotmuchError` in case of any failure
106                     (after printing an error message on stderr).
107         """
108         if self._db is not None:
109             raise NotmuchError(
110             message="Cannot create db, this Database() already has an open one.")
111
112         res = Database._create(path, MODE.READ_WRITE)
113
114         if res is None:
115             raise NotmuchError(
116                 message="Could not create the specified database")
117         self._db = res
118
119     def open(self, path, status= MODE.READ_ONLY): 
120         """calls notmuch_database_open
121
122         :returns: Raises :exc:`notmuch.NotmuchError` in case
123                   of any failure (after printing an error message on stderr).
124         """
125
126         res = Database._open(path, status)
127
128         if res is None:
129             raise NotmuchError(
130                 message="Could not open the specified database")
131         self._db = res
132
133     def get_path(self):
134         """notmuch_database_get_path (notmuch_database_t *database);  """
135         return Database._get_path(self._db)
136
137     def find_message(self, msgid):
138         """notmuch_database_find_message
139
140         :param msgid: The message id
141         :type msgid: string
142         :returns: :class:`Message` or `None` if no message is found or if an
143                   out-of-memory situation occurs.
144         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
145                   the database was not intitialized.
146         """
147         if self._db is None:
148             raise NotmuchError(STATUS.NOT_INITIALIZED)
149         msg_p = Database._find_message(self._db, msgid)
150         if msg_p is None:
151             return None
152         return Message(msg_p, self)
153
154     def get_all_tags(self):
155         """Returns :class:`Tags` with a list of all tags found in the database
156
157         :returns: :class:`Tags` object or raises :exc:`NotmuchError` with 
158                   STATUS.NULL_POINTER on error
159         """
160         if self._db is None:
161             raise NotmuchError(STATUS.NOT_INITIALIZED)
162
163         tags_p = Database._get_all_tags (self._db)
164         if tags_p == None:
165             raise NotmuchError(STATUS.NULL_POINTER)
166         return Tags(tags_p, self)
167
168     def __repr__(self):
169         return "'Notmuch DB " + self.get_path() + "'"
170
171     def __del__(self):
172         """Close and free the notmuch database if needed"""
173         if self._db is not None:
174             logging.debug("Freeing the database now")
175             nmlib.notmuch_database_close(self._db)
176
177     def _get_user_default_db(self):
178         """ Reads a user's notmuch config and returns his db location
179
180         Throws a NotmuchError if it cannot find it"""
181         from ConfigParser import SafeConfigParser
182         import os.path
183         config = SafeConfigParser()
184         config.read(os.path.expanduser('~/.notmuch-config'))
185         if not config.has_option('database','path'):
186             raise NotmuchError(message=
187                                "No DB path specified and no user default found")
188         return config.get('database','path')
189
190     @property
191     def db_p(self):
192         """Property returning a pointer to the notmuch_database_t or `None`.
193
194         This should normally not be needed by a user."""
195         return self._db
196
197 #------------------------------------------------------------------------------
198 class Query(object):
199     """ Wrapper around a notmuch_query_t
200
201     Do note that as soon as we tear down this object, all derived
202     threads, and messages will be freed as well.
203     """
204     # constants
205     SORT_OLDEST_FIRST = 0
206     SORT_NEWEST_FIRST = 1
207     SORT_MESSAGE_ID = 2
208
209     """notmuch_query_create"""
210     _create = nmlib.notmuch_query_create
211     _create.restype = c_void_p
212
213     """notmuch_query_search_messages"""
214     _search_messages = nmlib.notmuch_query_search_messages
215     _search_messages.restype = c_void_p
216
217     def __init__(self, db, querystr):
218         """TODO: document"""
219         self._db = None
220         self._query = None
221         self.create(db, querystr)
222
223     def create(self, db, querystr):
224         """db is a Database() and querystr a string
225
226         raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and
227         STATUS.NULL_POINTER if the query creation failed (too little memory)
228         """
229         if db.db_p is None:
230             raise NotmuchError(STATUS.NOT_INITIALIZED)            
231         # create reference to parent db to keep it alive
232         self._db = db
233         
234         # create query, return None if too little mem available
235         query_p = Query._create(db.db_p, querystr)
236         if query_p is None:
237             NotmuchError(STATUS.NULL_POINTER)
238         self._query = query_p
239
240     def set_sort(self, sort):
241         """notmuch_query_set_sort
242
243         :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID
244         :returns: Nothing, but raises NotmuchError if query is not inited
245         """
246         if self._query is None:
247             raise NotmuchError(STATUS.NOT_INITIALIZED)
248
249         nmlib.notmuch_query_set_sort(self._query, sort)
250
251     def search_messages(self):
252         """notmuch_query_search_messages
253         Returns Messages() or a raises a NotmuchError()
254         """
255         if self._query is None:
256             raise NotmuchError(STATUS.NOT_INITIALIZED)            
257
258         msgs_p = Query._search_messages(self._query)
259
260         if msgs_p is None:
261             NotmuchError(STATUS.NULL_POINTER)
262
263         return Messages(msgs_p,self)
264
265
266     def __del__(self):
267         """Close and free the Query"""
268         if self._query is not None:
269             logging.debug("Freeing the Query now")
270             nmlib.notmuch_query_destroy (self._query)
271
272 #------------------------------------------------------------------------------
273 class Tags(object):
274     """Wrapper around notmuch_tags_t"""
275
276     #notmuch_tags_get
277     _get = nmlib.notmuch_tags_get
278     _get.restype = c_char_p
279
280     def __init__(self, tags_p, parent=None):
281         """
282         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
283         we will raise an NotmuchError(STATUS.NULL_POINTER).
284
285         Is passed the parent these tags are derived from, and saves a
286         reference to it, so we can automatically delete the db object
287         once all derived objects are dead.
288
289         Tags() provides an iterator over all contained tags. However, you will
290         only be able to iterate over the Tags once, because the underlying C
291         function only allows iterating once.
292         #TODO: make the iterator work more than once and cache the tags in 
293                the Python object.
294         """
295         if tags_p is None:
296             NotmuchError(STATUS.NULL_POINTER)
297
298         self._tags = tags_p
299         #save reference to parent object so we keep it alive
300         self._parent = parent
301         logging.debug("Inited Tags derived from %s" %(repr(parent)))
302     
303     def __iter__(self):
304         """ Make Tags an iterator """
305         return self
306
307     def next(self):
308         if self._tags is None:
309             raise NotmuchError(STATUS.NOT_INITIALIZED)
310
311         if not nmlib.notmuch_tags_valid(self._tags):
312             self._tags = None
313             raise StopIteration
314
315         tag = Tags._get (self._tags)
316         nmlib.notmuch_tags_move_to_next(self._tags)
317         return tag
318
319     def __str__(self):
320         """str() of Tags() is a space separated list of tags
321
322         This iterates over the list of Tags and will therefore 'exhaust' Tags()
323         """
324         return " ".join(self)
325
326     def __del__(self):
327         """Close and free the notmuch tags"""
328         if self._tags is not None:
329             logging.debug("Freeing the Tags now")
330             nmlib.notmuch_tags_destroy (self._tags)
331
332
333 #------------------------------------------------------------------------------
334 class Messages(object):
335     """Wrapper around notmuch_messages_t"""
336
337     #notmuch_tags_get
338     _get = nmlib.notmuch_messages_get
339     _get.restype = c_void_p
340
341     _collect_tags = nmlib.notmuch_messages_collect_tags
342     _collect_tags.restype = c_void_p
343
344     def __init__(self, msgs_p, parent=None):
345         """
346         msg_p is a pointer to an notmuch_messages_t Structure. If it is None,
347         we will raise an NotmuchError(STATUS.NULL_POINTER).
348
349         If passed the parent query this Messages() is derived from, it saves a
350         reference to it, so we can automatically delete the parent query object
351         once all derived objects are dead.
352
353         Messages() provides an iterator over all contained Message()s.
354         However, you will only be able to iterate over it once,
355         because the underlying C function only allows iterating once.
356         #TODO: make the iterator work more than once and cache the tags in 
357                the Python object.
358         """
359         if msgs_p is None:
360             NotmuchError(STATUS.NULL_POINTER)
361
362         self._msgs = msgs_p
363         #store parent, so we keep them alive as long as self  is alive
364         self._parent = parent
365         logging.debug("Inited Messages derived from %s" %(str(parent)))
366
367     def collect_tags(self):
368         """ return the Tags() belonging to the messages
369         
370         Do note that collect_tags will iterate over the messages and
371         therefore will not allow further iterationsl
372         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
373         """
374         if self._msgs is None:
375             raise NotmuchError(STATUS.NOT_INITIALIZED)
376
377         # collect all tags (returns NULL on error)
378         tags_p = Messages._collect_tags (self._msgs)
379         #reset _msgs as we iterated over it and can do so only once
380         self._msgs = None
381
382         if tags_p == None:
383             raise NotmuchError(STATUS.NULL_POINTER)
384         return Tags(tags_p, self)
385
386     def __iter__(self):
387         """ Make Messages an iterator """
388         return self
389
390     def next(self):
391         if self._msgs is None:
392             raise NotmuchError(STATUS.NOT_INITIALIZED)
393
394         if not nmlib.notmuch_messages_valid(self._msgs):
395             self._msgs = None
396             raise StopIteration
397
398         msg = Message(Messages._get (self._msgs), self)
399         nmlib.notmuch_messages_move_to_next(self._msgs)
400         return msg
401
402     def __len__(self):
403         """ Returns the number of contained messages
404
405         :note: As this iterates over the messages, we will not be able to 
406                iterate over them again (as in retrieve them)!
407         """
408         if self._msgs is None:
409             raise NotmuchError(STATUS.NOT_INITIALIZED)
410
411         i=0
412         while nmlib.notmuch_messages_valid(self._msgs):
413             nmlib.notmuch_messages_move_to_next(self._msgs)
414             i += 1
415         self._msgs = None
416         return i
417
418
419
420     def __del__(self):
421         """Close and free the notmuch Messages"""
422         if self._msgs is not None:
423             logging.debug("Freeing the Messages now")
424             nmlib.notmuch_messages_destroy (self._msgs)
425
426
427 #------------------------------------------------------------------------------
428 class Message(object):
429     """Wrapper around notmuch_message_t"""
430
431     """notmuch_message_get_filename (notmuch_message_t *message)"""
432     _get_filename = nmlib.notmuch_message_get_filename
433     _get_filename.restype = c_char_p 
434     """notmuch_message_get_message_id (notmuch_message_t *message)"""
435     _get_message_id = nmlib.notmuch_message_get_message_id
436     _get_message_id.restype = c_char_p 
437
438     """notmuch_message_get_tags (notmuch_message_t *message)"""
439     _get_tags = nmlib.notmuch_message_get_tags
440     _get_tags.restype = c_void_p
441
442     _get_date = nmlib.notmuch_message_get_date
443     _get_date.restype = c_uint64
444
445     _get_header = nmlib.notmuch_message_get_header
446     _get_header.restype = c_char_p
447
448     def __init__(self, msg_p, parent=None):
449         """
450         msg_p is a pointer to an notmuch_message_t Structure. If it is None,
451         we will raise an NotmuchError(STATUS.NULL_POINTER).
452
453         Is a 'parent' object is passed which this message is derived from,
454         we save a reference to it, so we can automatically delete the parent
455         object once all derived objects are dead.
456         """
457         if msg_p is None:
458             NotmuchError(STATUS.NULL_POINTER)
459         self._msg = msg_p
460         #keep reference to parent, so we keep it alive
461         self._parent = parent
462         logging.debug("Inited Message derived from %s" %(str(parent)))
463
464
465     def get_message_id(self):
466         """ return the msg id
467         
468         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
469         """
470         if self._msg is None:
471             raise NotmuchError(STATUS.NOT_INITIALIZED)
472         return Message._get_message_id(self._msg)
473
474     def get_date(self):
475         """returns time_t of the message date
476
477         For the original textual representation of the Date header from the
478         message call notmuch_message_get_header() with a header value of
479         "date".
480         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
481         """
482         if self._msg is None:
483             raise NotmuchError(STATUS.NOT_INITIALIZED)
484         return Message._get_date(self._msg)
485
486     def get_header(self, header):
487         """ TODO document me"""
488         if self._msg is None:
489             raise NotmuchError(STATUS.NOT_INITIALIZED)
490
491         #Returns NULL if any error occurs.
492         header = Message._get_header (self._msg, header)
493         if header == None:
494             raise NotmuchError(STATUS.NULL_POINTER)
495         return header
496
497     def get_filename(self):
498         """ return the msg filename
499         
500         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
501         """
502         if self._msg is None:
503             raise NotmuchError(STATUS.NOT_INITIALIZED)
504         return Message._get_filename(self._msg)
505
506     def get_tags(self):
507         """ return the msg tags
508         
509         Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited
510         Raises NotmuchError(STATUS.NULL_POINTER) on error.
511         """
512         if self._msg is None:
513             raise NotmuchError(STATUS.NOT_INITIALIZED)
514
515         tags_p = Message._get_tags(self._msg)
516         if tags_p == None:
517             raise NotmuchError(STATUS.NULL_POINTER)
518         return Tags(tags_p, self)
519
520     def __str__(self):
521         """A message() is represented by a 1-line summary"""
522         msg = {}
523         msg['from'] = self.get_header('from')
524         msg['tags'] = str(self.get_tags())
525         msg['date'] = date.fromtimestamp(self.get_date())
526         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
527
528     def format_as_text(self):
529         """ Output like notmuch show """
530         return str(self)
531
532     def __del__(self):
533         """Close and free the notmuch Message"""
534         if self._msg is not None:
535             logging.debug("Freeing the Message now")
536             nmlib.notmuch_message_destroy (self._msg)