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