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