properly raise exceptions in python bindings
[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(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(
161                 message="Could not open the specified database")
162         self._db = res
163
164     def get_path(self):
165         """Returns the file path of an open database
166
167         Wraps *notmuch_database_get_path*."""
168         self._assert_db_is_initialized()
169         return Database._get_path(self._db).decode('utf-8')
170
171     def get_version(self):
172         """Returns the database format version
173
174         :returns: The database version as positive integer
175         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
176                     the database was not intitialized.
177         """
178         self._assert_db_is_initialized()
179         return Database._get_version(self._db)
180
181     def needs_upgrade(self):
182         """Does this database need to be upgraded before writing to it?
183
184         If this function returns `True` then no functions that modify the
185         database (:meth:`add_message`,
186         :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
187         etc.) will work unless :meth:`upgrade` is called successfully first.
188
189         :returns: `True` or `False`
190         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
191                     the database was not intitialized.
192         """
193         self._assert_db_is_initialized()
194         return nmlib.notmuch_database_needs_upgrade(self._db)
195
196     def upgrade(self):
197         """Upgrades the current database
198
199         After opening a database in read-write mode, the client should
200         check if an upgrade is needed (notmuch_database_needs_upgrade) and
201         if so, upgrade with this function before making any modifications.
202
203         NOT IMPLEMENTED: The optional progress_notify callback can be
204         used by the caller to provide progress indication to the
205         user. If non-NULL it will be called periodically with
206         'progress' as a floating-point value in the range of [0.0..1.0]
207         indicating the progress made so far in the upgrade process.
208
209         :TODO: catch exceptions, document return values and etc...
210         """
211         self._assert_db_is_initialized()
212         status = Database._upgrade(self._db, None, None)
213         #TODO: catch exceptions, document return values and etc
214         return status
215
216     def begin_atomic(self):
217         """Begin an atomic database operation
218
219         Any modifications performed between a successful
220         :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
221         the database atomically.  Note that, unlike a typical database
222         transaction, this only ensures atomicity, not durability;
223         neither begin nor end necessarily flush modifications to disk.
224
225         :returns: STATUS.SUCCESS or raises
226
227         :exception: :exc:`NotmuchError` STATUS.XAPIAN_EXCEPTION::
228
229                         A Xapian exception occurred; atomic section not
230                         entered."""
231         self._assert_db_is_initialized()
232         status = nmlib.notmuch_database_begin_atomic(self._db)
233         if status != STATUS.SUCCESS:
234             raise NotmuchError(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: STATUS.SUCCESS or raises
243
244         :exception:
245             :exc:`NotmuchError`:
246                 STATUS.XAPIAN_EXCEPTION
247                     A Xapian exception occurred; atomic section not
248                     ended.
249                 STATUS.UNBALANCED_ATOMIC:
250                     end_atomic has been called more times than begin_atomic."""
251         self._assert_db_is_initialized()
252         status = nmlib.notmuch_database_end_atomic(self._db)
253         if status != STATUS.SUCCESS:
254             raise NotmuchError(status)
255         return status
256
257     def get_directory(self, path):
258         """Returns a :class:`Directory` of path,
259         (creating it if it does not exist(?))
260
261         .. warning:: This call needs a writeable database in
262            Database.MODE.READ_WRITE mode. The underlying library will exit the
263            program if this method is used on a read-only database!
264
265         :param path: An unicode string containing the path relative to the path
266               of database (see :meth:`get_path`), or else should be an absolute path
267               with initial components that match the path of 'database'.
268         :returns: :class:`Directory` or raises an exception.
269         :exception: :exc:`NotmuchError`
270
271                   STATUS.NOT_INITIALIZED
272                     If the database was not intitialized.
273
274                   STATUS.FILE_ERROR
275                     If path is not relative database or absolute with initial
276                     components same as database.
277
278         """
279         self._assert_db_is_initialized()
280         # sanity checking if path is valid, and make path absolute
281         if path[0] == os.sep:
282             # we got an absolute path
283             if not path.startswith(self.get_path()):
284                 # but its initial components are not equal to the db path
285                 raise NotmuchError(STATUS.FILE_ERROR,
286                                    message="Database().get_directory() called "
287                                            "with a wrong absolute path.")
288             abs_dirpath = path
289         else:
290             #we got a relative path, make it absolute
291             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
292
293         dir_p = Database._get_directory(self._db, _str(path))
294
295         # return the Directory, init it with the absolute path
296         return Directory(_str(abs_dirpath), dir_p, self)
297
298     def add_message(self, filename, sync_maildir_flags=False):
299         """Adds a new message to the database
300
301         :param filename: should be a path relative to the path of the open
302         database (see :meth:`get_path`), or else should be an absolute
303         filename with initial components that match the path of the
304         database.
305
306         The file should be a single mail message (not a multi-message mbox)
307         that is expected to remain at its current location, since the
308         notmuch database will reference the filename, and will not copy the
309         entire contents of the file.
310
311         :param sync_maildir_flags: If the message contains Maildir
312             flags, we will -depending on the notmuch configuration- sync
313             those tags to initial notmuch tags, if set to `True`. It is
314             `False` by default to remain consistent with the libnotmuch
315             API. You might want to look into the underlying method
316             :meth:`Message.maildir_flags_to_tags`.
317
318         :returns: On success, we return
319
320            1) a :class:`Message` object that can be used for things
321               such as adding tags to the just-added message.
322            2) one of the following STATUS values:
323
324               STATUS.SUCCESS
325                   Message successfully added to database.
326               STATUS.DUPLICATE_MESSAGE_ID
327                   Message has the same message ID as another message already
328                   in the database. The new filename was successfully added
329                   to the list of the filenames for the existing message.
330
331         :rtype:   2-tuple(:class:`Message`, STATUS)
332
333         :exception: Raises a :exc:`NotmuchError` with the following meaning.
334               If such an exception occurs, nothing was added to the database.
335
336               STATUS.FILE_ERROR
337                       An error occurred trying to open the file, (such as
338                       permission denied, or file not found, etc.).
339               STATUS.FILE_NOT_EMAIL
340                       The contents of filename don't look like an email
341                       message.
342               STATUS.READ_ONLY_DATABASE
343                       Database was opened in read-only mode so no message can
344                       be added.
345               STATUS.NOT_INITIALIZED
346                       The database has not been initialized.
347         """
348         self._assert_db_is_initialized()
349         msg_p = c_void_p()
350         status = nmlib.notmuch_database_add_message(self._db,
351                                                   _str(filename),
352                                                   byref(msg_p))
353
354         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
355             raise NotmuchError(status)
356
357         #construct Message() and return
358         msg = Message(msg_p, self)
359         #automatic sync initial tags from Maildir flags
360         if sync_maildir_flags:
361             msg.maildir_flags_to_tags()
362         return (msg, status)
363
364     def remove_message(self, filename):
365         """Removes a message (filename) from the given notmuch database
366
367         Note that only this particular filename association is removed from
368         the database. If the same message (as determined by the message ID)
369         is still available via other filenames, then the message will
370         persist in the database for those filenames. When the last filename
371         is removed for a particular message, the database content for that
372         message will be entirely removed.
373
374         :returns: A STATUS value with the following meaning:
375
376              STATUS.SUCCESS
377                The last filename was removed and the message was removed
378                from the database.
379              STATUS.DUPLICATE_MESSAGE_ID
380                This filename was removed but the message persists in the
381                database with at least one other filename.
382
383         :exception: Raises a :exc:`NotmuchError` with the following meaning.
384              If such an exception occurs, nothing was removed from the
385              database.
386
387              STATUS.READ_ONLY_DATABASE
388                Database was opened in read-only mode so no message can be
389                removed.
390              STATUS.NOT_INITIALIZED
391                The database has not been initialized.
392         """
393         self._assert_db_is_initialized()
394         return nmlib.notmuch_database_remove_message(self._db,
395                                                        filename)
396
397     def find_message(self, msgid):
398         """Returns a :class:`Message` as identified by its message ID
399
400         Wraps the underlying *notmuch_database_find_message* function.
401
402         :param msgid: The message ID
403         :type msgid: string
404         :returns: :class:`Message` or `None` if no message is found or
405                   if any xapian exception or out-of-memory situation
406                   occurs. Do note that Xapian Exceptions include
407                   "Database modified" situations, e.g. when the
408                   notmuch database has been modified by
409                   another program in the meantime. A return value of
410                   `None` is therefore no guarantee that the message
411                   does not exist.
412         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
413                   the database was not intitialized.
414         """
415         self._assert_db_is_initialized()
416         msg_p = Database._find_message(self._db, _str(msgid))
417         return msg_p and Message(msg_p, self) or None
418
419     def find_message_by_filename(self, filename):
420         """Find a message with the given filename
421
422         :returns: If the database contains a message with the given
423             filename, then a class:`Message:` is returned.  This
424             function returns None in the following situations:
425
426                 * No message is found with the given filename
427                 * An out-of-memory situation occurs
428                 * A Xapian exception occurs"""
429         self._assert_db_is_initialized()
430         msg_p = Database._find_message_by_filename(self._db, _str(filename))
431         return msg_p and Message(msg_p, self) or None
432
433     def get_all_tags(self):
434         """Returns :class:`Tags` with a list of all tags found in the database
435
436         :returns: :class:`Tags`
437         :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
438         """
439         self._assert_db_is_initialized()
440         tags_p = Database._get_all_tags(self._db)
441         if tags_p == None:
442             raise NotmuchError(STATUS.NULL_POINTER)
443         return Tags(tags_p, self)
444
445     def create_query(self, querystring):
446         """Returns a :class:`Query` derived from this database
447
448         This is a shorthand method for doing::
449
450           # short version
451           # Automatically frees the Database() when 'q' is deleted
452
453           q  = Database(dbpath).create_query('from:"Biene Maja"')
454
455           # long version, which is functionally equivalent but will keep the
456           # Database in the 'db' variable around after we delete 'q':
457
458           db = Database(dbpath)
459           q  = Query(db,'from:"Biene Maja"')
460
461         This function is a python extension and not in the underlying C API.
462         """
463         self._assert_db_is_initialized()
464         return Query(self, querystring)
465
466     def __repr__(self):
467         return "'Notmuch DB " + self.get_path() + "'"
468
469     def __del__(self):
470         """Close and free the notmuch database if needed"""
471         if self._db is not None:
472             nmlib.notmuch_database_close(self._db)
473
474     def _get_user_default_db(self):
475         """ Reads a user's notmuch config and returns his db location
476
477         Throws a NotmuchError if it cannot find it"""
478         from ConfigParser import SafeConfigParser
479         config = SafeConfigParser()
480         conf_f = os.getenv('NOTMUCH_CONFIG',
481                            os.path.expanduser('~/.notmuch-config'))
482         config.read(conf_f)
483         if not config.has_option('database', 'path'):
484             raise NotmuchError(message="No DB path specified"
485                                        " and no user default found")
486         return config.get('database', 'path').decode('utf-8')
487
488     @property
489     def db_p(self):
490         """Property returning a pointer to `notmuch_database_t` or `None`
491
492         This should normally not be needed by a user (and is not yet
493         guaranteed to remain stable in future versions).
494         """
495         return self._db
496
497
498 class Query(object):
499     """Represents a search query on an opened :class:`Database`.
500
501     A query selects and filters a subset of messages from the notmuch
502     database we derive from.
503
504     Query() provides an instance attribute :attr:`sort`, which
505     contains the sort order (if specified via :meth:`set_sort`) or
506     `None`.
507
508     Technically, it wraps the underlying *notmuch_query_t* struct.
509
510     .. note:: Do remember that as soon as we tear down this object,
511            all underlying derived objects such as threads,
512            messages, tags etc will be freed by the underlying library
513            as well. Accessing these objects will lead to segfaults and
514            other unexpected behavior. See above for more details.
515     """
516     # constants
517     SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
518     """Constants: Sort order in which to return results"""
519
520     """notmuch_query_create"""
521     _create = nmlib.notmuch_query_create
522     _create.restype = c_void_p
523
524     """notmuch_query_search_threads"""
525     _search_threads = nmlib.notmuch_query_search_threads
526     _search_threads.restype = c_void_p
527
528     """notmuch_query_search_messages"""
529     _search_messages = nmlib.notmuch_query_search_messages
530     _search_messages.restype = c_void_p
531
532     """notmuch_query_count_messages"""
533     _count_messages = nmlib.notmuch_query_count_messages
534     _count_messages.restype = c_uint
535
536     def __init__(self, db, querystr):
537         """
538         :param db: An open database which we derive the Query from.
539         :type db: :class:`Database`
540         :param querystr: The query string for the message.
541         :type querystr: utf-8 encoded str or unicode
542         """
543         self._db = None
544         self._query = None
545         self.sort = None
546         self.create(db, querystr)
547
548     def create(self, db, querystr):
549         """Creates a new query derived from a Database
550
551         This function is utilized by __init__() and usually does not need to
552         be called directly.
553
554         :param db: Database to create the query from.
555         :type db: :class:`Database`
556         :param querystr: The query string
557         :type querystr: utf-8 encoded str or unicode
558         :returns: Nothing
559         :exception: :exc:`NotmuchError`
560
561                       * STATUS.NOT_INITIALIZED if db is not inited
562                       * STATUS.NULL_POINTER if the query creation failed
563                         (too little memory)
564         """
565         if db.db_p is None:
566             raise NotmuchError(STATUS.NOT_INITIALIZED)
567         # create reference to parent db to keep it alive
568         self._db = db
569         # create query, return None if too little mem available
570         query_p = Query._create(db.db_p, _str(querystr))
571         if query_p is None:
572             raise NotmuchError(STATUS.NULL_POINTER)
573         self._query = query_p
574
575     def set_sort(self, sort):
576         """Set the sort order future results will be delivered in
577
578         Wraps the underlying *notmuch_query_set_sort* function.
579
580         :param sort: Sort order (see :attr:`Query.SORT`)
581         :returns: Nothing
582         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
583                     been initialized.
584         """
585         if self._query is None:
586             raise NotmuchError(STATUS.NOT_INITIALIZED)
587
588         self.sort = sort
589         nmlib.notmuch_query_set_sort(self._query, sort)
590
591     def search_threads(self):
592         """Execute a query for threads
593
594         Execute a query for threads, returning a :class:`Threads` iterator.
595         The returned threads are owned by the query and as such, will only be
596         valid until the Query is deleted.
597
598         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
599         match the query. The method :meth:`Message.get_flag` allows us
600         to get the value of this flag.
601
602         Technically, it wraps the underlying
603         *notmuch_query_search_threads* function.
604
605         :returns: :class:`Threads`
606         :exception: :exc:`NotmuchError`
607
608                       * STATUS.NOT_INITIALIZED if query is not inited
609                       * STATUS.NULL_POINTER if search_threads failed
610         """
611         if self._query is None:
612             raise NotmuchError(STATUS.NOT_INITIALIZED)
613
614         threads_p = Query._search_threads(self._query)
615
616         if threads_p is None:
617             raise NotmuchError(STATUS.NULL_POINTER)
618
619         return Threads(threads_p, self)
620
621     def search_messages(self):
622         """Filter messages according to the query and return
623         :class:`Messages` in the defined sort order
624
625         Technically, it wraps the underlying
626         *notmuch_query_search_messages* function.
627
628         :returns: :class:`Messages`
629         :exception: :exc:`NotmuchError`
630
631                       * STATUS.NOT_INITIALIZED if query is not inited
632                       * STATUS.NULL_POINTER if search_messages failed
633         """
634         if self._query is None:
635             raise NotmuchError(STATUS.NOT_INITIALIZED)
636
637         msgs_p = Query._search_messages(self._query)
638
639         if msgs_p is None:
640             raise NotmuchError(STATUS.NULL_POINTER)
641
642         return Messages(msgs_p, self)
643
644     def count_messages(self):
645         """Estimate the number of messages matching the query
646
647         This function performs a search and returns Xapian's best
648         guess as to the number of matching messages. It is much faster
649         than performing :meth:`search_messages` and counting the
650         result with `len()` (although it always returned the same
651         result in my tests). Technically, it wraps the underlying
652         *notmuch_query_count_messages* function.
653
654         :returns: :class:`Messages`
655         :exception: :exc:`NotmuchError`
656
657                       * STATUS.NOT_INITIALIZED if query is not inited
658         """
659         if self._query is None:
660             raise NotmuchError(STATUS.NOT_INITIALIZED)
661
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(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                         STATUS.XAPIAN_EXCEPTION
748                           A Xapian exception occurred, mtime not stored.
749                         STATUS.READ_ONLY_DATABASE
750                           Database was opened in read-only mode so directory
751                           mtime cannot be modified.
752                         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                         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(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)