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