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