]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/database.py
python: Add new functions in API 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`:
228             :attr:`STATUS`.XAPIAN_EXCEPTION
229                 Xapian exception occurred; atomic section not entered.
230
231         *Added in notmuch 0.9*"""
232         self._assert_db_is_initialized()
233         status = nmlib.notmuch_database_begin_atomic(self._db)
234         if status != STATUS.SUCCESS:
235             raise NotmuchError(status)
236         return status
237
238     def end_atomic(self):
239         """Indicate the end of an atomic database operation
240
241         See :meth:`begin_atomic` for details.
242
243         :returns: :attr:`STATUS`.SUCCESS or raises
244
245         :exception:
246             :exc:`NotmuchError`:
247                 :attr:`STATUS`.XAPIAN_EXCEPTION
248                     A Xapian exception occurred; atomic section not
249                     ended.
250                 :attr:`STATUS`.UNBALANCED_ATOMIC:
251                     end_atomic has been called more times than begin_atomic.
252
253         *Added in notmuch 0.9*"""
254         self._assert_db_is_initialized()
255         status = nmlib.notmuch_database_end_atomic(self._db)
256         if status != STATUS.SUCCESS:
257             raise NotmuchError(status)
258         return status
259
260     def get_directory(self, path):
261         """Returns a :class:`Directory` of path,
262         (creating it if it does not exist(?))
263
264         .. warning:: This call needs a writeable database in
265            :attr:`Database.MODE`.READ_WRITE mode. The underlying library will exit the
266            program if this method is used on a read-only database!
267
268         :param path: An unicode string containing the path relative to the path
269               of database (see :meth:`get_path`), or else should be an absolute path
270               with initial components that match the path of 'database'.
271         :returns: :class:`Directory` or raises an exception.
272         :exception: :exc:`NotmuchError`
273
274                   :attr:`STATUS`.NOT_INITIALIZED
275                     If the database was not intitialized.
276
277                   :attr:`STATUS`.FILE_ERROR
278                     If path is not relative database or absolute with initial
279                     components same as database.
280
281         """
282         self._assert_db_is_initialized()
283         # sanity checking if path is valid, and make path absolute
284         if path[0] == os.sep:
285             # we got an absolute path
286             if not path.startswith(self.get_path()):
287                 # but its initial components are not equal to the db path
288                 raise NotmuchError(STATUS.FILE_ERROR,
289                                    message="Database().get_directory() called "
290                                            "with a wrong absolute path.")
291             abs_dirpath = path
292         else:
293             #we got a relative path, make it absolute
294             abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
295
296         dir_p = Database._get_directory(self._db, _str(path))
297
298         # return the Directory, init it with the absolute path
299         return Directory(_str(abs_dirpath), dir_p, self)
300
301     def add_message(self, filename, sync_maildir_flags=False):
302         """Adds a new message to the database
303
304         :param filename: should be a path relative to the path of the
305             open database (see :meth:`get_path`), or else should be an
306             absolute filename with initial components that match the
307             path of the database.
308
309             The file should be a single mail message (not a
310             multi-message mbox) that is expected to remain at its
311             current location, since the notmuch database will reference
312             the filename, and will not copy the entire contents of the
313             file.
314
315         :param sync_maildir_flags: If the message contains Maildir
316             flags, we will -depending on the notmuch configuration- sync
317             those tags to initial notmuch tags, if set to `True`. It is
318             `False` by default to remain consistent with the libnotmuch
319             API. You might want to look into the underlying method
320             :meth:`Message.maildir_flags_to_tags`.
321
322         :returns: On success, we return
323
324            1) a :class:`Message` object that can be used for things
325               such as adding tags to the just-added message.
326            2) one of the following :attr:`STATUS` values:
327
328               :attr:`STATUS`.SUCCESS
329                   Message successfully added to database.
330               :attr:`STATUS`.DUPLICATE_MESSAGE_ID
331                   Message has the same message ID as another message already
332                   in the database. The new filename was successfully added
333                   to the list of the filenames for the existing message.
334
335         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
336
337         :exception: Raises a :exc:`NotmuchError` with the following meaning.
338               If such an exception occurs, nothing was added to the database.
339
340               :attr:`STATUS`.FILE_ERROR
341                       An error occurred trying to open the file, (such as
342                       permission denied, or file not found, etc.).
343               :attr:`STATUS`.FILE_NOT_EMAIL
344                       The contents of filename don't look like an email
345                       message.
346               :attr:`STATUS`.READ_ONLY_DATABASE
347                       Database was opened in read-only mode so no message can
348                       be added.
349               :attr:`STATUS`.NOT_INITIALIZED
350                       The database has not been initialized.
351         """
352         self._assert_db_is_initialized()
353         msg_p = c_void_p()
354         status = nmlib.notmuch_database_add_message(self._db,
355                                                   _str(filename),
356                                                   byref(msg_p))
357
358         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
359             raise NotmuchError(status)
360
361         #construct Message() and return
362         msg = Message(msg_p, self)
363         #automatic sync initial tags from Maildir flags
364         if sync_maildir_flags:
365             msg.maildir_flags_to_tags()
366         return (msg, status)
367
368     def remove_message(self, filename):
369         """Removes a message (filename) from the given notmuch database
370
371         Note that only this particular filename association is removed from
372         the database. If the same message (as determined by the message ID)
373         is still available via other filenames, then the message will
374         persist in the database for those filenames. When the last filename
375         is removed for a particular message, the database content for that
376         message will be entirely removed.
377
378         :returns: A :attr:`STATUS` value with the following meaning:
379
380              :attr:`STATUS`.SUCCESS
381                The last filename was removed and the message was removed
382                from the database.
383              :attr:`STATUS`.DUPLICATE_MESSAGE_ID
384                This filename was removed but the message persists in the
385                database with at least one other filename.
386
387         :exception: Raises a :exc:`NotmuchError` with the following meaning.
388              If such an exception occurs, nothing was removed from the
389              database.
390
391              :attr:`STATUS`.READ_ONLY_DATABASE
392                Database was opened in read-only mode so no message can be
393                removed.
394              :attr:`STATUS`.NOT_INITIALIZED
395                The database has not been initialized.
396         """
397         self._assert_db_is_initialized()
398         return nmlib.notmuch_database_remove_message(self._db,
399                                                        filename)
400
401     def find_message(self, msgid):
402         """Returns a :class:`Message` as identified by its message ID
403
404         Wraps the underlying *notmuch_database_find_message* function.
405
406         :param msgid: The message ID
407         :type msgid: string
408         :returns: :class:`Message` or `None` if no message is found or
409                   if any xapian exception or out-of-memory situation
410                   occurs. Do note that Xapian Exceptions include
411                   "Database modified" situations, e.g. when the
412                   notmuch database has been modified by
413                   another program in the meantime. A return value of
414                   `None` is therefore no guarantee that the message
415                   does not exist.
416         :exception: :exc:`NotmuchError` with :attr:`STATUS`.NOT_INITIALIZED if
417                   the database was not intitialized.
418         """
419         self._assert_db_is_initialized()
420         msg_p = Database._find_message(self._db, _str(msgid))
421         return msg_p and Message(msg_p, self) or None
422
423     def find_message_by_filename(self, filename):
424         """Find a message with the given filename
425
426         .. warning:: This call needs a writeable database in
427            :attr:`Database.MODE`.READ_WRITE mode. The underlying library will
428            exit the program if this method is used on a read-only
429            database!
430
431         :returns: If the database contains a message with the given
432             filename, then a class:`Message:` is returned.  This
433             function returns None in the following situations:
434
435                 * No message is found with the given filename
436                 * An out-of-memory situation occurs
437                 * A Xapian exception occurs
438
439         *Added in notmuch 0.9*"""
440         self._assert_db_is_initialized()
441         msg_p = Database._find_message_by_filename(self._db, _str(filename))
442         return msg_p and Message(msg_p, self) or None
443
444     def get_all_tags(self):
445         """Returns :class:`Tags` with a list of all tags found in the database
446
447         :returns: :class:`Tags`
448         :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER on error
449         """
450         self._assert_db_is_initialized()
451         tags_p = Database._get_all_tags(self._db)
452         if tags_p == None:
453             raise NotmuchError(STATUS.NULL_POINTER)
454         return Tags(tags_p, self)
455
456     def create_query(self, querystring):
457         """Returns a :class:`Query` derived from this database
458
459         This is a shorthand method for doing::
460
461           # short version
462           # Automatically frees the Database() when 'q' is deleted
463
464           q  = Database(dbpath).create_query('from:"Biene Maja"')
465
466           # long version, which is functionally equivalent but will keep the
467           # Database in the 'db' variable around after we delete 'q':
468
469           db = Database(dbpath)
470           q  = Query(db,'from:"Biene Maja"')
471
472         This function is a python extension and not in the underlying C API.
473         """
474         self._assert_db_is_initialized()
475         return Query(self, querystring)
476
477     def __repr__(self):
478         return "'Notmuch DB " + self.get_path() + "'"
479
480     def __del__(self):
481         """Close and free the notmuch database if needed"""
482         if self._db is not None:
483             nmlib.notmuch_database_close(self._db)
484
485     def _get_user_default_db(self):
486         """ Reads a user's notmuch config and returns his db location
487
488         Throws a NotmuchError if it cannot find it"""
489         from ConfigParser import SafeConfigParser
490         config = SafeConfigParser()
491         conf_f = os.getenv('NOTMUCH_CONFIG',
492                            os.path.expanduser('~/.notmuch-config'))
493         config.read(conf_f)
494         if not config.has_option('database', 'path'):
495             raise NotmuchError(message="No DB path specified"
496                                        " and no user default found")
497         return config.get('database', 'path').decode('utf-8')
498
499     @property
500     def db_p(self):
501         """Property returning a pointer to `notmuch_database_t` or `None`
502
503         This should normally not be needed by a user (and is not yet
504         guaranteed to remain stable in future versions).
505         """
506         return self._db
507
508
509 class Query(object):
510     """Represents a search query on an opened :class:`Database`.
511
512     A query selects and filters a subset of messages from the notmuch
513     database we derive from.
514
515     Query() provides an instance attribute :attr:`sort`, which
516     contains the sort order (if specified via :meth:`set_sort`) or
517     `None`.
518
519     Technically, it wraps the underlying *notmuch_query_t* struct.
520
521     .. note:: Do remember that as soon as we tear down this object,
522            all underlying derived objects such as threads,
523            messages, tags etc will be freed by the underlying library
524            as well. Accessing these objects will lead to segfaults and
525            other unexpected behavior. See above for more details.
526     """
527     # constants
528     SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
529     """Constants: Sort order in which to return results"""
530
531     """notmuch_query_create"""
532     _create = nmlib.notmuch_query_create
533     _create.restype = c_void_p
534
535     """notmuch_query_search_threads"""
536     _search_threads = nmlib.notmuch_query_search_threads
537     _search_threads.restype = c_void_p
538
539     """notmuch_query_search_messages"""
540     _search_messages = nmlib.notmuch_query_search_messages
541     _search_messages.restype = c_void_p
542
543     """notmuch_query_count_messages"""
544     _count_messages = nmlib.notmuch_query_count_messages
545     _count_messages.restype = c_uint
546
547     def __init__(self, db, querystr):
548         """
549         :param db: An open database which we derive the Query from.
550         :type db: :class:`Database`
551         :param querystr: The query string for the message.
552         :type querystr: utf-8 encoded str or unicode
553         """
554         self._db = None
555         self._query = None
556         self.sort = None
557         self.create(db, querystr)
558
559     def create(self, db, querystr):
560         """Creates a new query derived from a Database
561
562         This function is utilized by __init__() and usually does not need to
563         be called directly.
564
565         :param db: Database to create the query from.
566         :type db: :class:`Database`
567         :param querystr: The query string
568         :type querystr: utf-8 encoded str or unicode
569         :returns: Nothing
570         :exception: :exc:`NotmuchError`
571
572                       * :attr:`STATUS`.NOT_INITIALIZED if db is not inited
573                       * :attr:`STATUS`.NULL_POINTER if the query creation failed
574                         (too little memory)
575         """
576         if db.db_p is None:
577             raise NotmuchError(STATUS.NOT_INITIALIZED)
578         # create reference to parent db to keep it alive
579         self._db = db
580         # create query, return None if too little mem available
581         query_p = Query._create(db.db_p, _str(querystr))
582         if query_p is None:
583             raise NotmuchError(STATUS.NULL_POINTER)
584         self._query = query_p
585
586     def set_sort(self, sort):
587         """Set the sort order future results will be delivered in
588
589         Wraps the underlying *notmuch_query_set_sort* function.
590
591         :param sort: Sort order (see :attr:`Query.SORT`)
592         :returns: Nothing
593         :exception: :exc:`NotmuchError` :attr:`STATUS`.NOT_INITIALIZED if query has not
594                     been initialized.
595         """
596         if self._query is None:
597             raise NotmuchError(STATUS.NOT_INITIALIZED)
598
599         self.sort = sort
600         nmlib.notmuch_query_set_sort(self._query, sort)
601
602     def search_threads(self):
603         """Execute a query for threads
604
605         Execute a query for threads, returning a :class:`Threads` iterator.
606         The returned threads are owned by the query and as such, will only be
607         valid until the Query is deleted.
608
609         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
610         match the query. The method :meth:`Message.get_flag` allows us
611         to get the value of this flag.
612
613         Technically, it wraps the underlying
614         *notmuch_query_search_threads* function.
615
616         :returns: :class:`Threads`
617         :exception: :exc:`NotmuchError`
618
619                       * :attr:`STATUS`.NOT_INITIALIZED if query is not inited
620                       * :attr:`STATUS`.NULL_POINTER if search_threads failed
621         """
622         if self._query is None:
623             raise NotmuchError(STATUS.NOT_INITIALIZED)
624
625         threads_p = Query._search_threads(self._query)
626
627         if threads_p is None:
628             raise NotmuchError(STATUS.NULL_POINTER)
629
630         return Threads(threads_p, self)
631
632     def search_messages(self):
633         """Filter messages according to the query and return
634         :class:`Messages` in the defined sort order
635
636         Technically, it wraps the underlying
637         *notmuch_query_search_messages* function.
638
639         :returns: :class:`Messages`
640         :exception: :exc:`NotmuchError`
641
642                       * :attr:`STATUS`.NOT_INITIALIZED if query is not inited
643                       * :attr:`STATUS`.NULL_POINTER if search_messages failed
644         """
645         if self._query is None:
646             raise NotmuchError(STATUS.NOT_INITIALIZED)
647
648         msgs_p = Query._search_messages(self._query)
649
650         if msgs_p is None:
651             raise NotmuchError(STATUS.NULL_POINTER)
652
653         return Messages(msgs_p, self)
654
655     def count_messages(self):
656         """Estimate the number of messages matching the query
657
658         This function performs a search and returns Xapian's best
659         guess as to the number of matching messages. It is much faster
660         than performing :meth:`search_messages` and counting the
661         result with `len()` (although it always returned the same
662         result in my tests). Technically, it wraps the underlying
663         *notmuch_query_count_messages* function.
664
665         :returns: :class:`Messages`
666         :exception: :exc:`NotmuchError`
667
668                       * :attr:`STATUS`.NOT_INITIALIZED if query is not inited
669         """
670         if self._query is None:
671             raise NotmuchError(STATUS.NOT_INITIALIZED)
672
673         return Query._count_messages(self._query)
674
675     def __del__(self):
676         """Close and free the Query"""
677         if self._query is not None:
678             nmlib.notmuch_query_destroy(self._query)
679
680
681 class Directory(object):
682     """Represents a directory entry in the notmuch directory
683
684     Modifying attributes of this object will modify the
685     database, not the real directory attributes.
686
687     The Directory object is usually derived from another object
688     e.g. via :meth:`Database.get_directory`, and will automatically be
689     become invalid whenever that parent is deleted. You should
690     therefore initialized this object handing it a reference to the
691     parent, preventing the parent from automatically being garbage
692     collected.
693     """
694
695     """notmuch_directory_get_mtime"""
696     _get_mtime = nmlib.notmuch_directory_get_mtime
697     _get_mtime.restype = c_long
698
699     """notmuch_directory_set_mtime"""
700     _set_mtime = nmlib.notmuch_directory_set_mtime
701     _set_mtime.argtypes = [c_char_p, c_long]
702
703     """notmuch_directory_get_child_files"""
704     _get_child_files = nmlib.notmuch_directory_get_child_files
705     _get_child_files.restype = c_void_p
706
707     """notmuch_directory_get_child_directories"""
708     _get_child_directories = nmlib.notmuch_directory_get_child_directories
709     _get_child_directories.restype = c_void_p
710
711     def _assert_dir_is_initialized(self):
712         """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) if dir_p is None"""
713         if self._dir_p is None:
714             raise NotmuchError(STATUS.NOT_INITIALIZED)
715
716     def __init__(self, path, dir_p, parent):
717         """
718         :param path:   The absolute path of the directory object as unicode.
719         :param dir_p:  The pointer to an internal notmuch_directory_t object.
720         :param parent: The object this Directory is derived from
721                        (usually a :class:`Database`). We do not directly use
722                        this, but store a reference to it as long as
723                        this Directory object lives. This keeps the
724                        parent object alive.
725         """
726         assert isinstance(path, unicode), "Path needs to be an UNICODE object"
727         self._path = path
728         self._dir_p = dir_p
729         self._parent = parent
730
731     def set_mtime(self, mtime):
732         """Sets the mtime value of this directory in the database
733
734         The intention is for the caller to use the mtime to allow efficient
735         identification of new messages to be added to the database. The
736         recommended usage is as follows:
737
738         * Read the mtime of a directory from the filesystem
739
740         * Call :meth:`Database.add_message` for all mail files in
741           the directory
742
743         * Call notmuch_directory_set_mtime with the mtime read from the
744           filesystem.  Then, when wanting to check for updates to the
745           directory in the future, the client can call :meth:`get_mtime`
746           and know that it only needs to add files if the mtime of the
747           directory and files are newer than the stored timestamp.
748
749           .. note:: :meth:`get_mtime` function does not allow the caller
750                  to distinguish a timestamp of 0 from a non-existent
751                  timestamp. So don't store a timestamp of 0 unless you are
752                  comfortable with that.
753
754           :param mtime: A (time_t) timestamp
755           :returns: Nothing on success, raising an exception on failure.
756           :exception: :exc:`NotmuchError`:
757
758                         :attr:`STATUS`.XAPIAN_EXCEPTION
759                           A Xapian exception occurred, mtime not stored.
760                         :attr:`STATUS`.READ_ONLY_DATABASE
761                           Database was opened in read-only mode so directory
762                           mtime cannot be modified.
763                         :attr:`STATUS`.NOT_INITIALIZED
764                           The directory has not been initialized
765         """
766         self._assert_dir_is_initialized()
767         #TODO: make sure, we convert the mtime parameter to a 'c_long'
768         status = Directory._set_mtime(self._dir_p, mtime)
769
770         #return on success
771         if status == STATUS.SUCCESS:
772             return
773         #fail with Exception otherwise
774         raise NotmuchError(status)
775
776     def get_mtime(self):
777         """Gets the mtime value of this directory in the database
778
779         Retrieves a previously stored mtime for this directory.
780
781         :param mtime: A (time_t) timestamp
782         :returns: Nothing on success, raising an exception on failure.
783         :exception: :exc:`NotmuchError`:
784
785                         :attr:`STATUS`.NOT_INITIALIZED
786                           The directory has not been initialized
787         """
788         self._assert_dir_is_initialized()
789         return Directory._get_mtime(self._dir_p)
790
791     # Make mtime attribute a property of Directory()
792     mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
793                      and setting of the Directory *mtime* (read-write)
794
795                      See :meth:`get_mtime` and :meth:`set_mtime` for usage and
796                      possible exceptions.""")
797
798     def get_child_files(self):
799         """Gets a Filenames iterator listing all the filenames of
800         messages in the database within the given directory.
801
802         The returned filenames will be the basename-entries only (not
803         complete paths.
804         """
805         self._assert_dir_is_initialized()
806         files_p = Directory._get_child_files(self._dir_p)
807         return Filenames(files_p, self)
808
809     def get_child_directories(self):
810         """Gets a :class:`Filenames` iterator listing all the filenames of
811         sub-directories in the database within the given directory
812
813         The returned filenames will be the basename-entries only (not
814         complete paths.
815         """
816         self._assert_dir_is_initialized()
817         files_p = Directory._get_child_directories(self._dir_p)
818         return Filenames(files_p, self)
819
820     @property
821     def path(self):
822         """Returns the absolute path of this Directory (read-only)"""
823         return self._path
824
825     def __repr__(self):
826         """Object representation"""
827         return "<notmuch Directory object '%s'>" % self._path
828
829     def __del__(self):
830         """Close and free the Directory"""
831         if self._dir_p is not None:
832             nmlib.notmuch_directory_destroy(self._dir_p)
833
834
835 class Filenames(object):
836     """An iterator over File- or Directory names stored in the database"""
837
838     #notmuch_filenames_get
839     _get = nmlib.notmuch_filenames_get
840     _get.restype = c_char_p
841
842     def __init__(self, files_p, parent):
843         """
844         :param files_p: The pointer to an internal notmuch_filenames_t object.
845         :param parent: The object this Directory is derived from
846                        (usually a Directory()). We do not directly use
847                        this, but store a reference to it as long as
848                        this Directory object lives. This keeps the
849                        parent object alive.
850         """
851         self._files_p = files_p
852         self._parent = parent
853
854     def __iter__(self):
855         """ Make Filenames an iterator """
856         return self
857
858     def next(self):
859         if self._files_p is None:
860             raise NotmuchError(STATUS.NOT_INITIALIZED)
861
862         if not nmlib.notmuch_filenames_valid(self._files_p):
863             self._files_p = None
864             raise StopIteration
865
866         file = Filenames._get(self._files_p)
867         nmlib.notmuch_filenames_move_to_next(self._files_p)
868         return file
869
870     def __len__(self):
871         """len(:class:`Filenames`) returns the number of contained files
872
873         .. note:: As this iterates over the files, we will not be able to
874                iterate over them again! So this will fail::
875
876                  #THIS FAILS
877                  files = Database().get_directory('').get_child_files()
878                  if len(files) > 0:              #this 'exhausts' msgs
879                      # next line raises NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)!!!
880                      for file in files: print file
881         """
882         if self._files_p is None:
883             raise NotmuchError(STATUS.NOT_INITIALIZED)
884
885         i = 0
886         while nmlib.notmuch_filenames_valid(self._files_p):
887             nmlib.notmuch_filenames_move_to_next(self._files_p)
888             i += 1
889         self._files_p = None
890         return i
891
892     def __del__(self):
893         """Close and free Filenames"""
894         if self._files_p is not None:
895             nmlib.notmuch_filenames_destroy(self._files_p)