]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
python: implement the context manager protocol for database objects
[notmuch] / bindings / python / notmuch / database.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18 """
19
20 import os
21 import codecs
22 from ctypes import c_char_p, c_void_p, c_uint, c_long, byref, POINTER
23 from notmuch.globals import (nmlib, STATUS, NotmuchError, NotInitializedError,
24      NullPointerError, Enum, _str,
25      NotmuchDatabaseP, NotmuchDirectoryP, NotmuchMessageP, NotmuchTagsP,
26      NotmuchQueryP, NotmuchMessagesP, NotmuchThreadsP, NotmuchFilenamesP)
27 from notmuch.thread import Threads
28 from notmuch.message import Messages, Message
29 from notmuch.tag import Tags
30
31
32 class Database(object):
33     """The :class:`Database` is the highest-level object that notmuch
34     provides. It references a notmuch database, and can be opened in
35     read-only or read-write mode. A :class:`Query` can be derived from
36     or be applied to a specific database to find messages. Also adding
37     and removing messages to the database happens via this
38     object. Modifications to the database are not atmic by default (see
39     :meth:`begin_atomic`) and once a database has been modified, all
40     other database objects pointing to the same data-base will throw an
41     :exc:`XapianError` as the underlying database has been
42     modified. Close and reopen the database to continue working with it.
43
44     :class:`Database` objects implement the context manager protocol
45     so you can use the :keyword:`with` statement to ensure that the
46     database is properly closed.
47
48     .. note::
49
50         Any function in this class can and will throw an
51         :exc:`NotInitializedError` if the database was not intitialized
52         properly.
53
54     .. note::
55
56         Do remember that as soon as we tear down (e.g. via `del db`) this
57         object, all underlying derived objects such as queries, threads,
58         messages, tags etc will be freed by the underlying library as well.
59         Accessing these objects will lead to segfaults and other unexpected
60         behavior. See above for more details.
61     """
62     _std_db_path = None
63     """Class attribute to cache user's default database"""
64
65     MODE = Enum(['READ_ONLY', 'READ_WRITE'])
66     """Constants: Mode in which to open the database"""
67
68     """notmuch_database_get_directory"""
69     _get_directory = nmlib.notmuch_database_get_directory
70     _get_directory.argtypes = [NotmuchDatabaseP, c_char_p]
71     _get_directory.restype = NotmuchDirectoryP
72
73     """notmuch_database_get_path"""
74     _get_path = nmlib.notmuch_database_get_path
75     _get_path.argtypes = [NotmuchDatabaseP]
76     _get_path.restype = c_char_p
77
78     """notmuch_database_get_version"""
79     _get_version = nmlib.notmuch_database_get_version
80     _get_version.argtypes = [NotmuchDatabaseP]
81     _get_version.restype = c_uint
82
83     """notmuch_database_open"""
84     _open = nmlib.notmuch_database_open
85     _open.argtypes = [c_char_p, c_uint]
86     _open.restype = NotmuchDatabaseP
87
88     """notmuch_database_upgrade"""
89     _upgrade = nmlib.notmuch_database_upgrade
90     _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
91     _upgrade.restype = c_uint
92
93     """ notmuch_database_find_message"""
94     _find_message = nmlib.notmuch_database_find_message
95     _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
96                               POINTER(NotmuchMessageP)]
97     _find_message.restype = c_uint
98
99     """notmuch_database_find_message_by_filename"""
100     _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
101     _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
102                                           POINTER(NotmuchMessageP)]
103     _find_message_by_filename.restype = c_uint
104
105     """notmuch_database_get_all_tags"""
106     _get_all_tags = nmlib.notmuch_database_get_all_tags
107     _get_all_tags.argtypes = [NotmuchDatabaseP]
108     _get_all_tags.restype = NotmuchTagsP
109
110     """notmuch_database_create"""
111     _create = nmlib.notmuch_database_create
112     _create.argtypes = [c_char_p]
113     _create.restype = NotmuchDatabaseP
114
115     def __init__(self, path=None, create=False, mode=0):
116         """If *path* is `None`, we will try to read a users notmuch
117         configuration and use his configured database. The location of the
118         configuration file can be specified through the environment variable
119         *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
120
121         If *create* is `True`, the database will always be created in
122         :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
123
124         :param path:   Directory to open/create the database in (see
125                        above for behavior if `None`)
126         :type path:    `str` or `None`
127         :param create: Pass `False` to open an existing, `True` to create a new
128                        database.
129         :type create:  bool
130         :param mode:   Mode to open a database in. Is always
131                        :attr:`MODE`.READ_WRITE when creating a new one.
132         :type mode:    :attr:`MODE`
133         :exception: :exc:`NotmuchError` or derived exception in case of
134             failure.
135         """
136         self._db = None
137         if path is None:
138             # no path specified. use a user's default database
139             if Database._std_db_path is None:
140                 #the following line throws a NotmuchError if it fails
141                 Database._std_db_path = self._get_user_default_db()
142             path = Database._std_db_path
143
144         if create == False:
145             self.open(path, mode)
146         else:
147             self.create(path)
148
149     def __del__(self):
150         self.close()
151
152     def _assert_db_is_initialized(self):
153         """Raises :exc:`NotInitializedError` if self._db is `None`"""
154         if self._db is None:
155             raise NotInitializedError()
156
157     def create(self, path):
158         """Creates a new notmuch database
159
160         This function is used by __init__() and usually does not need
161         to be called directly. It wraps the underlying
162         *notmuch_database_create* function and creates a new notmuch
163         database at *path*. It will always return a database in :attr:`MODE`
164         .READ_WRITE mode as creating an empty database for
165         reading only does not make a great deal of sense.
166
167         :param path: A directory in which we should create the database.
168         :type path: str
169         :returns: Nothing
170         :exception: :exc:`NotmuchError` in case of any failure
171                     (possibly after printing an error message on stderr).
172         """
173         if self._db is not None:
174             raise NotmuchError(message="Cannot create db, this Database() "
175                                        "already has an open one.")
176
177         res = Database._create(_str(path), Database.MODE.READ_WRITE)
178
179         if not res:
180             raise NotmuchError(
181                 message="Could not create the specified database")
182         self._db = res
183
184     def open(self, path, mode=0):
185         """Opens an existing database
186
187         This function is used by __init__() and usually does not need
188         to be called directly. It wraps the underlying
189         *notmuch_database_open* function.
190
191         :param status: Open the database in read-only or read-write mode
192         :type status:  :attr:`MODE`
193         :returns: Nothing
194         :exception: Raises :exc:`NotmuchError` in case of any failure
195                     (possibly after printing an error message on stderr).
196         """
197         res = Database._open(_str(path), mode)
198
199         if not res:
200             raise NotmuchError(message="Could not open the specified database")
201         self._db = res
202
203     _close = nmlib.notmuch_database_close
204     _close.argtypes = [NotmuchDatabaseP]
205     _close.restype = None
206
207     def close(self):
208         """Close and free the notmuch database if needed"""
209         if self._db is not None:
210             self._close(self._db)
211             self._db = None
212
213     def __enter__(self):
214         '''
215         Implements the context manager protocol.
216         '''
217         return self
218
219     def __exit__(self, exc_type, exc_value, traceback):
220         '''
221         Implements the context manager protocol.
222         '''
223         self.close()
224
225     def get_path(self):
226         """Returns the file path of an open database"""
227         self._assert_db_is_initialized()
228         return Database._get_path(self._db).decode('utf-8')
229
230     def get_version(self):
231         """Returns the database format version
232
233         :returns: The database version as positive integer
234         """
235         self._assert_db_is_initialized()
236         return Database._get_version(self._db)
237
238     _needs_upgrade = nmlib.notmuch_database_needs_upgrade
239     _needs_upgrade.argtypes = [NotmuchDatabaseP]
240     _needs_upgrade.restype = bool
241
242     def needs_upgrade(self):
243         """Does this database need to be upgraded before writing to it?
244
245         If this function returns `True` then no functions that modify the
246         database (:meth:`add_message`,
247         :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
248         etc.) will work unless :meth:`upgrade` is called successfully first.
249
250         :returns: `True` or `False`
251         """
252         self._assert_db_is_initialized()
253         return self._needs_upgrade(self._db)
254
255     def upgrade(self):
256         """Upgrades the current database
257
258         After opening a database in read-write mode, the client should
259         check if an upgrade is needed (notmuch_database_needs_upgrade) and
260         if so, upgrade with this function before making any modifications.
261
262         NOT IMPLEMENTED: The optional progress_notify callback can be
263         used by the caller to provide progress indication to the
264         user. If non-NULL it will be called periodically with
265         'progress' as a floating-point value in the range of [0.0..1.0]
266         indicating the progress made so far in the upgrade process.
267
268         :TODO: catch exceptions, document return values and etc...
269         """
270         self._assert_db_is_initialized()
271         status = Database._upgrade(self._db, None, None)
272         #TODO: catch exceptions, document return values and etc
273         return status
274
275     _begin_atomic = nmlib.notmuch_database_begin_atomic
276     _begin_atomic.argtypes = [NotmuchDatabaseP]
277     _begin_atomic.restype = c_uint
278
279     def begin_atomic(self):
280         """Begin an atomic database operation
281
282         Any modifications performed between a successful
283         :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
284         the database atomically.  Note that, unlike a typical database
285         transaction, this only ensures atomicity, not durability;
286         neither begin nor end necessarily flush modifications to disk.
287
288         :returns: :attr:`STATUS`.SUCCESS or raises
289         :exception: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
290                     Xapian exception occurred; atomic section not entered.
291
292         *Added in notmuch 0.9*"""
293         self._assert_db_is_initialized()
294         status = self._begin_atomic(self._db)
295         if status != STATUS.SUCCESS:
296             raise NotmuchError(status)
297         return status
298
299     _end_atomic = nmlib.notmuch_database_end_atomic
300     _end_atomic.argtypes = [NotmuchDatabaseP]
301     _end_atomic.restype = c_uint
302
303     def end_atomic(self):
304         """Indicate the end of an atomic database operation
305
306         See :meth:`begin_atomic` for details.
307
308         :returns: :attr:`STATUS`.SUCCESS or raises
309
310         :exception:
311             :exc:`NotmuchError`:
312                 :attr:`STATUS`.XAPIAN_EXCEPTION
313                     A Xapian exception occurred; atomic section not
314                     ended.
315                 :attr:`STATUS`.UNBALANCED_ATOMIC:
316                     end_atomic has been called more times than begin_atomic.
317
318         *Added in notmuch 0.9*"""
319         self._assert_db_is_initialized()
320         status = self._end_atomic(self._db)
321         if status != STATUS.SUCCESS:
322             raise NotmuchError(status)
323         return status
324
325     def get_directory(self, path):
326         """Returns a :class:`Directory` of path,
327         (creating it if it does not exist(?))
328
329         .. warning::
330
331             This call needs a writeable database in
332             :attr:`Database.MODE`.READ_WRITE mode. The underlying library will
333             exit the program if this method is used on a read-only database!
334
335         :param path: An unicode string containing the path relative to the path
336               of database (see :meth:`get_path`), or else should be an absolute
337               path with initial components that match the path of 'database'.
338         :returns: :class:`Directory` or raises an exception.
339         :exception:
340             :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR
341                     If path is not relative database or absolute with initial
342                     components same as database.
343         """
344         self._assert_db_is_initialized()
345         # sanity checking if path is valid, and make path absolute
346         if path[0] == os.sep:
347             # we got an absolute path
348             if not path.startswith(self.get_path()):
349                 # but its initial components are not equal to the db path
350                 raise NotmuchError(STATUS.FILE_ERROR,
351                                    message="Database().get_directory() called "
352                                            "with a wrong absolute path.")
353             abs_dirpath = path
354         else:
355             #we got a relative path, make it absolute
356             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
357
358         dir_p = Database._get_directory(self._db, _str(path))
359
360         # return the Directory, init it with the absolute path
361         return Directory(_str(abs_dirpath), dir_p, self)
362
363     _add_message = nmlib.notmuch_database_add_message
364     _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
365                              POINTER(NotmuchMessageP)]
366     _add_message.restype = c_uint
367
368     def add_message(self, filename, sync_maildir_flags=False):
369         """Adds a new message to the database
370
371         :param filename: should be a path relative to the path of the
372             open database (see :meth:`get_path`), or else should be an
373             absolute filename with initial components that match the
374             path of the database.
375
376             The file should be a single mail message (not a
377             multi-message mbox) that is expected to remain at its
378             current location, since the notmuch database will reference
379             the filename, and will not copy the entire contents of the
380             file.
381
382         :param sync_maildir_flags: If the message contains Maildir
383             flags, we will -depending on the notmuch configuration- sync
384             those tags to initial notmuch tags, if set to `True`. It is
385             `False` by default to remain consistent with the libnotmuch
386             API. You might want to look into the underlying method
387             :meth:`Message.maildir_flags_to_tags`.
388
389         :returns: On success, we return
390
391            1) a :class:`Message` object that can be used for things
392               such as adding tags to the just-added message.
393            2) one of the following :attr:`STATUS` values:
394
395               :attr:`STATUS`.SUCCESS
396                   Message successfully added to database.
397               :attr:`STATUS`.DUPLICATE_MESSAGE_ID
398                   Message has the same message ID as another message already
399                   in the database. The new filename was successfully added
400                   to the list of the filenames for the existing message.
401
402         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
403
404         :exception: Raises a :exc:`NotmuchError` with the following meaning.
405               If such an exception occurs, nothing was added to the database.
406
407               :attr:`STATUS`.FILE_ERROR
408                       An error occurred trying to open the file, (such as
409                       permission denied, or file not found, etc.).
410               :attr:`STATUS`.FILE_NOT_EMAIL
411                       The contents of filename don't look like an email
412                       message.
413               :attr:`STATUS`.READ_ONLY_DATABASE
414                       Database was opened in read-only mode so no message can
415                       be added.
416         """
417         self._assert_db_is_initialized()
418         msg_p = NotmuchMessageP()
419         status = self._add_message(self._db, _str(filename), byref(msg_p))
420
421         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
422             raise NotmuchError(status)
423
424         #construct Message() and return
425         msg = Message(msg_p, self)
426         #automatic sync initial tags from Maildir flags
427         if sync_maildir_flags:
428             msg.maildir_flags_to_tags()
429         return (msg, status)
430
431     _remove_message = nmlib.notmuch_database_remove_message
432     _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
433     _remove_message.restype = c_uint
434
435     def remove_message(self, filename):
436         """Removes a message (filename) from the given notmuch database
437
438         Note that only this particular filename association is removed from
439         the database. If the same message (as determined by the message ID)
440         is still available via other filenames, then the message will
441         persist in the database for those filenames. When the last filename
442         is removed for a particular message, the database content for that
443         message will be entirely removed.
444
445         :returns: A :attr:`STATUS` value with the following meaning:
446
447              :attr:`STATUS`.SUCCESS
448                The last filename was removed and the message was removed
449                from the database.
450              :attr:`STATUS`.DUPLICATE_MESSAGE_ID
451                This filename was removed but the message persists in the
452                database with at least one other filename.
453
454         :exception: Raises a :exc:`NotmuchError` with the following meaning.
455              If such an exception occurs, nothing was removed from the
456              database.
457
458              :attr:`STATUS`.READ_ONLY_DATABASE
459                Database was opened in read-only mode so no message can be
460                removed.
461         """
462         self._assert_db_is_initialized()
463         return self._remove_message(self._db, _str(filename))
464
465     def find_message(self, msgid):
466         """Returns a :class:`Message` as identified by its message ID
467
468         Wraps the underlying *notmuch_database_find_message* function.
469
470         :param msgid: The message ID
471         :type msgid: unicode or str
472         :returns: :class:`Message` or `None` if no message is found.
473         :exception:
474             :exc:`OutOfMemoryError`
475                   If an Out-of-memory occured while constructing the message.
476             :exc:`XapianError`
477                   In case of a Xapian Exception. These exceptions
478                   include "Database modified" situations, e.g. when the
479                   notmuch database has been modified by another program
480                   in the meantime. In this case, you should close and
481                   reopen the database and retry.
482             :exc:`NotInitializedError` if
483                     the database was not intitialized.
484         """
485         self._assert_db_is_initialized()
486         msg_p = NotmuchMessageP()
487         status = Database._find_message(self._db, _str(msgid), byref(msg_p))
488         if status != STATUS.SUCCESS:
489             raise NotmuchError(status)
490         return msg_p and Message(msg_p, self) or None
491
492     def find_message_by_filename(self, filename):
493         """Find a message with the given filename
494
495         .. warning::
496
497             This call needs a writeable database in
498             :attr:`Database.MODE`.READ_WRITE mode. The underlying library will
499             exit the program if this method is used on a read-only database!
500
501         :returns: If the database contains a message with the given
502             filename, then a class:`Message:` is returned.  This
503             function returns None if no message is found with the given
504             filename.
505
506         :exception:
507             :exc:`OutOfMemoryError`
508                   If an Out-of-memory occured while constructing the message.
509             :exc:`XapianError`
510                   In case of a Xapian Exception. These exceptions
511                   include "Database modified" situations, e.g. when the
512                   notmuch database has been modified by another program
513                   in the meantime. In this case, you should close and
514                   reopen the database and retry.
515             :exc:`NotInitializedError` if
516                     the database was not intitialized.
517
518         *Added in notmuch 0.9*"""
519         self._assert_db_is_initialized()
520         msg_p = NotmuchMessageP()
521         status = Database._find_message_by_filename(self._db, _str(filename),
522                                                     byref(msg_p))
523         if status != STATUS.SUCCESS:
524             raise NotmuchError(status)
525         return msg_p and Message(msg_p, self) or None
526
527     def get_all_tags(self):
528         """Returns :class:`Tags` with a list of all tags found in the database
529
530         :returns: :class:`Tags`
531         :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
532                     on error
533         """
534         self._assert_db_is_initialized()
535         tags_p = Database._get_all_tags(self._db)
536         if tags_p == None:
537             raise NotmuchError(STATUS.NULL_POINTER)
538         return Tags(tags_p, self)
539
540     def create_query(self, querystring):
541         """Returns a :class:`Query` derived from this database
542
543         This is a shorthand method for doing::
544
545           # short version
546           # Automatically frees the Database() when 'q' is deleted
547
548           q  = Database(dbpath).create_query('from:"Biene Maja"')
549
550           # long version, which is functionally equivalent but will keep the
551           # Database in the 'db' variable around after we delete 'q':
552
553           db = Database(dbpath)
554           q  = Query(db,'from:"Biene Maja"')
555
556         This function is a python extension and not in the underlying C API.
557         """
558         return Query(self, querystring)
559
560     def __repr__(self):
561         return "'Notmuch DB " + self.get_path() + "'"
562
563     def _get_user_default_db(self):
564         """ Reads a user's notmuch config and returns his db location
565
566         Throws a NotmuchError if it cannot find it"""
567         try:
568             # python3.x
569             from configparser import SafeConfigParser
570         except ImportError:
571             # python2.x
572             from ConfigParser import SafeConfigParser
573
574         config = SafeConfigParser()
575         conf_f = os.getenv('NOTMUCH_CONFIG',
576                            os.path.expanduser('~/.notmuch-config'))
577         config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
578         if not config.has_option('database', 'path'):
579             raise NotmuchError(message="No DB path specified"
580                                        " and no user default found")
581         return config.get('database', 'path')
582
583     @property
584     def db_p(self):
585         """Property returning a pointer to `notmuch_database_t` or `None`
586
587         This should normally not be needed by a user (and is not yet
588         guaranteed to remain stable in future versions).
589         """
590         return self._db
591
592
593 class Query(object):
594     """Represents a search query on an opened :class:`Database`.
595
596     A query selects and filters a subset of messages from the notmuch
597     database we derive from.
598
599     :class:`Query` provides an instance attribute :attr:`sort`, which
600     contains the sort order (if specified via :meth:`set_sort`) or
601     `None`.
602
603     Any function in this class may throw an :exc:`NotInitializedError`
604     in case the underlying query object was not set up correctly.
605
606     .. note:: Do remember that as soon as we tear down this object,
607            all underlying derived objects such as threads,
608            messages, tags etc will be freed by the underlying library
609            as well. Accessing these objects will lead to segfaults and
610            other unexpected behavior. See above for more details.
611     """
612     # constants
613     SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
614     """Constants: Sort order in which to return results"""
615
616     """notmuch_query_create"""
617     _create = nmlib.notmuch_query_create
618     _create.argtypes = [NotmuchDatabaseP, c_char_p]
619     _create.restype = NotmuchQueryP
620
621     """notmuch_query_search_threads"""
622     _search_threads = nmlib.notmuch_query_search_threads
623     _search_threads.argtypes = [NotmuchQueryP]
624     _search_threads.restype = NotmuchThreadsP
625
626     """notmuch_query_search_messages"""
627     _search_messages = nmlib.notmuch_query_search_messages
628     _search_messages.argtypes = [NotmuchQueryP]
629     _search_messages.restype = NotmuchMessagesP
630
631     """notmuch_query_count_messages"""
632     _count_messages = nmlib.notmuch_query_count_messages
633     _count_messages.argtypes = [NotmuchQueryP]
634     _count_messages.restype = c_uint
635
636     def __init__(self, db, querystr):
637         """
638         :param db: An open database which we derive the Query from.
639         :type db: :class:`Database`
640         :param querystr: The query string for the message.
641         :type querystr: utf-8 encoded str or unicode
642         """
643         self._db = None
644         self._query = None
645         self.sort = None
646         self.create(db, querystr)
647
648     def _assert_query_is_initialized(self):
649         """Raises :exc:`NotInitializedError` if self._query is `None`"""
650         if self._query is None:
651             raise NotInitializedError()
652
653     def create(self, db, querystr):
654         """Creates a new query derived from a Database
655
656         This function is utilized by __init__() and usually does not need to
657         be called directly.
658
659         :param db: Database to create the query from.
660         :type db: :class:`Database`
661         :param querystr: The query string
662         :type querystr: utf-8 encoded str or unicode
663         :returns: Nothing
664         :exception:
665             :exc:`NullPointerError` if the query creation failed
666                 (e.g. too little memory).
667             :exc:`NotInitializedError` if the underlying db was not
668                 intitialized.
669         """
670         db._assert_db_is_initialized()
671         # create reference to parent db to keep it alive
672         self._db = db
673         # create query, return None if too little mem available
674         query_p = Query._create(db.db_p, _str(querystr))
675         if not query_p:
676             raise NullPointerError
677         self._query = query_p
678
679     _set_sort = nmlib.notmuch_query_set_sort
680     _set_sort.argtypes = [NotmuchQueryP, c_uint]
681     _set_sort.argtypes = None
682
683     def set_sort(self, sort):
684         """Set the sort order future results will be delivered in
685
686         :param sort: Sort order (see :attr:`Query.SORT`)
687         """
688         self._assert_query_is_initialized()
689         self.sort = sort
690         self._set_sort(self._query, sort)
691
692     def search_threads(self):
693         """Execute a query for threads
694
695         Execute a query for threads, returning a :class:`Threads` iterator.
696         The returned threads are owned by the query and as such, will only be
697         valid until the Query is deleted.
698
699         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
700         match the query. The method :meth:`Message.get_flag` allows us
701         to get the value of this flag.
702
703         :returns: :class:`Threads`
704         :exception: :exc:`NullPointerError` if search_threads failed
705         """
706         self._assert_query_is_initialized()
707         threads_p = Query._search_threads(self._query)
708
709         if not threads_p:
710             raise NullPointerError
711         return Threads(threads_p, self)
712
713     def search_messages(self):
714         """Filter messages according to the query and return
715         :class:`Messages` in the defined sort order
716
717         :returns: :class:`Messages`
718         :exception: :exc:`NullPointerError` if search_messages failed
719         """
720         self._assert_query_is_initialized()
721         msgs_p = Query._search_messages(self._query)
722
723         if not msgs_p:
724             raise NullPointerError
725         return Messages(msgs_p, self)
726
727     def count_messages(self):
728         """Estimate the number of messages matching the query
729
730         This function performs a search and returns Xapian's best
731         guess as to the number of matching messages. It is much faster
732         than performing :meth:`search_messages` and counting the
733         result with `len()` (although it always returned the same
734         result in my tests). Technically, it wraps the underlying
735         *notmuch_query_count_messages* function.
736
737         :returns: :class:`Messages`
738         """
739         self._assert_query_is_initialized()
740         return Query._count_messages(self._query)
741
742     _destroy = nmlib.notmuch_query_destroy
743     _destroy.argtypes = [NotmuchQueryP]
744     _destroy.restype = None
745
746     def __del__(self):
747         """Close and free the Query"""
748         if self._query is not None:
749             self._destroy(self._query)
750
751
752 class Directory(object):
753     """Represents a directory entry in the notmuch directory
754
755     Modifying attributes of this object will modify the
756     database, not the real directory attributes.
757
758     The Directory object is usually derived from another object
759     e.g. via :meth:`Database.get_directory`, and will automatically be
760     become invalid whenever that parent is deleted. You should
761     therefore initialized this object handing it a reference to the
762     parent, preventing the parent from automatically being garbage
763     collected.
764     """
765
766     """notmuch_directory_get_mtime"""
767     _get_mtime = nmlib.notmuch_directory_get_mtime
768     _get_mtime.argtypes = [NotmuchDirectoryP]
769     _get_mtime.restype = c_long
770
771     """notmuch_directory_set_mtime"""
772     _set_mtime = nmlib.notmuch_directory_set_mtime
773     _set_mtime.argtypes = [NotmuchDirectoryP, c_long]
774     _set_mtime.restype = c_uint
775
776     """notmuch_directory_get_child_files"""
777     _get_child_files = nmlib.notmuch_directory_get_child_files
778     _get_child_files.argtypes = [NotmuchDirectoryP]
779     _get_child_files.restype = NotmuchFilenamesP
780
781     """notmuch_directory_get_child_directories"""
782     _get_child_directories = nmlib.notmuch_directory_get_child_directories
783     _get_child_directories.argtypes = [NotmuchDirectoryP]
784     _get_child_directories.restype = NotmuchFilenamesP
785
786     def _assert_dir_is_initialized(self):
787         """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
788         if dir_p is None"""
789         if not self._dir_p:
790             raise NotmuchError(STATUS.NOT_INITIALIZED)
791
792     def __init__(self, path, dir_p, parent):
793         """
794         :param path:   The absolute path of the directory object as unicode.
795         :param dir_p:  The pointer to an internal notmuch_directory_t object.
796         :param parent: The object this Directory is derived from
797                        (usually a :class:`Database`). We do not directly use
798                        this, but store a reference to it as long as
799                        this Directory object lives. This keeps the
800                        parent object alive.
801         """
802         assert isinstance(path, unicode), "Path needs to be an UNICODE object"
803         self._path = path
804         self._dir_p = dir_p
805         self._parent = parent
806
807     def set_mtime(self, mtime):
808         """Sets the mtime value of this directory in the database
809
810         The intention is for the caller to use the mtime to allow efficient
811         identification of new messages to be added to the database. The
812         recommended usage is as follows:
813
814         * Read the mtime of a directory from the filesystem
815
816         * Call :meth:`Database.add_message` for all mail files in
817           the directory
818
819         * Call notmuch_directory_set_mtime with the mtime read from the
820           filesystem.  Then, when wanting to check for updates to the
821           directory in the future, the client can call :meth:`get_mtime`
822           and know that it only needs to add files if the mtime of the
823           directory and files are newer than the stored timestamp.
824
825           .. note::
826
827                 :meth:`get_mtime` function does not allow the caller to
828                 distinguish a timestamp of 0 from a non-existent timestamp. So
829                 don't store a timestamp of 0 unless you are comfortable with
830                 that.
831
832           :param mtime: A (time_t) timestamp
833           :returns: Nothing on success, raising an exception on failure.
834           :exception: :exc:`NotmuchError`:
835
836                         :attr:`STATUS`.XAPIAN_EXCEPTION
837                           A Xapian exception occurred, mtime not stored.
838                         :attr:`STATUS`.READ_ONLY_DATABASE
839                           Database was opened in read-only mode so directory
840                           mtime cannot be modified.
841                         :attr:`STATUS`.NOT_INITIALIZED
842                           The directory has not been initialized
843         """
844         self._assert_dir_is_initialized()
845         #TODO: make sure, we convert the mtime parameter to a 'c_long'
846         status = Directory._set_mtime(self._dir_p, mtime)
847
848         #return on success
849         if status == STATUS.SUCCESS:
850             return
851         #fail with Exception otherwise
852         raise NotmuchError(status)
853
854     def get_mtime(self):
855         """Gets the mtime value of this directory in the database
856
857         Retrieves a previously stored mtime for this directory.
858
859         :param mtime: A (time_t) timestamp
860         :returns: Nothing on success, raising an exception on failure.
861         :exception: :exc:`NotmuchError`:
862
863                         :attr:`STATUS`.NOT_INITIALIZED
864                           The directory has not been initialized
865         """
866         self._assert_dir_is_initialized()
867         return Directory._get_mtime(self._dir_p)
868
869     # Make mtime attribute a property of Directory()
870     mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
871                      and setting of the Directory *mtime* (read-write)
872
873                      See :meth:`get_mtime` and :meth:`set_mtime` for usage and
874                      possible exceptions.""")
875
876     def get_child_files(self):
877         """Gets a Filenames iterator listing all the filenames of
878         messages in the database within the given directory.
879
880         The returned filenames will be the basename-entries only (not
881         complete paths.
882         """
883         self._assert_dir_is_initialized()
884         files_p = Directory._get_child_files(self._dir_p)
885         return Filenames(files_p, self)
886
887     def get_child_directories(self):
888         """Gets a :class:`Filenames` iterator listing all the filenames of
889         sub-directories in the database within the given directory
890
891         The returned filenames will be the basename-entries only (not
892         complete paths.
893         """
894         self._assert_dir_is_initialized()
895         files_p = Directory._get_child_directories(self._dir_p)
896         return Filenames(files_p, self)
897
898     @property
899     def path(self):
900         """Returns the absolute path of this Directory (read-only)"""
901         return self._path
902
903     def __repr__(self):
904         """Object representation"""
905         return "<notmuch Directory object '%s'>" % self._path
906
907     _destroy = nmlib.notmuch_directory_destroy
908     _destroy.argtypes = [NotmuchDirectoryP]
909     _destroy.argtypes = None
910
911     def __del__(self):
912         """Close and free the Directory"""
913         if self._dir_p is not None:
914             self._destroy(self._dir_p)
915
916
917 class Filenames(object):
918     """An iterator over File- or Directory names stored in the database"""
919
920     #notmuch_filenames_get
921     _get = nmlib.notmuch_filenames_get
922     _get.argtypes = [NotmuchFilenamesP]
923     _get.restype = c_char_p
924
925     def __init__(self, files_p, parent):
926         """
927         :param files_p: The pointer to an internal notmuch_filenames_t object.
928         :param parent: The object this Directory is derived from
929                        (usually a Directory()). We do not directly use
930                        this, but store a reference to it as long as
931                        this Directory object lives. This keeps the
932                        parent object alive.
933         """
934         self._files_p = files_p
935         self._parent = parent
936
937     def __iter__(self):
938         """ Make Filenames an iterator """
939         return self
940
941     _valid = nmlib.notmuch_filenames_valid
942     _valid.argtypes = [NotmuchFilenamesP]
943     _valid.restype = bool
944
945     _move_to_next = nmlib.notmuch_filenames_move_to_next
946     _move_to_next.argtypes = [NotmuchFilenamesP]
947     _move_to_next.restype = None
948
949     def __next__(self):
950         if not self._files_p:
951             raise NotmuchError(STATUS.NOT_INITIALIZED)
952
953         if not self._valid(self._files_p):
954             self._files_p = None
955             raise StopIteration
956
957         file_ = Filenames._get(self._files_p)
958         self._move_to_next(self._files_p)
959         return file_.decode('utf-8', 'ignore')
960     next = __next__ # python2.x iterator protocol compatibility
961
962     def __len__(self):
963         """len(:class:`Filenames`) returns the number of contained files
964
965         .. note::
966
967             As this iterates over the files, we will not be able to
968             iterate over them again! So this will fail::
969
970                  #THIS FAILS
971                  files = Database().get_directory('').get_child_files()
972                  if len(files) > 0:  # this 'exhausts' msgs
973                      # next line raises
974                      # NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
975                      for file in files: print file
976         """
977         if not self._files_p:
978             raise NotmuchError(STATUS.NOT_INITIALIZED)
979
980         i = 0
981         while self._valid(self._files_p):
982             self._move_to_next(self._files_p)
983             i += 1
984         self._files_p = None
985         return i
986
987     _destroy = nmlib.notmuch_filenames_destroy
988     _destroy.argtypes = [NotmuchFilenamesP]
989     _destroy.restype = None
990
991     def __del__(self):
992         """Close and free Filenames"""
993         if self._files_p is not None:
994             self._destroy(self._files_p)