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