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