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