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