]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_database.py
python/notmuch2: fix typo for "destroyed"
[notmuch] / bindings / python-cffi / notmuch2 / _database.py
1 import collections
2 import configparser
3 import enum
4 import functools
5 import os
6 import pathlib
7 import weakref
8
9 import notmuch2._base as base
10 import notmuch2._capi as capi
11 import notmuch2._errors as errors
12 import notmuch2._message as message
13 import notmuch2._query as querymod
14 import notmuch2._tags as tags
15
16
17 __all__ = ['Database', 'AtomicContext', 'DbRevision']
18
19
20 def _config_pathname():
21     """Return the path of the configuration file.
22
23     :rtype: pathlib.Path
24     """
25     cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
26     return pathlib.Path(os.path.expanduser(cfgfname))
27
28
29 class Mode(enum.Enum):
30     READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
31     READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
32
33
34 class QuerySortOrder(enum.Enum):
35     OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
36     NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
37     MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
38     UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
39
40
41 class QueryExclude(enum.Enum):
42     TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
43     FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
44     FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
45     ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
46
47
48 class DecryptionPolicy(enum.Enum):
49     FALSE = capi.lib.NOTMUCH_DECRYPT_FALSE
50     TRUE = capi.lib.NOTMUCH_DECRYPT_TRUE
51     AUTO = capi.lib.NOTMUCH_DECRYPT_AUTO
52     NOSTASH = capi.lib.NOTMUCH_DECRYPT_NOSTASH
53
54
55 class Database(base.NotmuchObject):
56     """Toplevel access to notmuch.
57
58     A :class:`Database` can be opened read-only or read-write.
59     Modifications are not atomic by default, use :meth:`begin_atomic`
60     for atomic updates.  If the underlying database has been modified
61     outside of this class a :exc:`XapianError` will be raised and the
62     instance must be closed and a new one created.
63
64     You can use an instance of this class as a context-manager.
65
66     :cvar MODE: The mode a database can be opened with, an enumeration
67        of ``READ_ONLY`` and ``READ_WRITE``
68     :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
69        ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
70     :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
71        ``FLAG``, ``FALSE`` or ``ALL``.  See the query documentation
72        for details.
73     :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
74        :meth:`add` as return value.
75     :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
76        This is used to implement the ``ro`` and ``rw`` string
77        variants.
78
79     :ivar closed: Boolean indicating if the database is closed or
80        still open.
81
82     :param path: The directory of where the database is stored.  If
83        ``None`` the location will be read from the user's
84        configuration file, respecting the ``NOTMUCH_CONFIG``
85        environment variable if set.
86     :type path: str, bytes, os.PathLike or pathlib.Path
87     :param mode: The mode to open the database in.  One of
88        :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`.  For
89        convenience you can also use the strings ``ro`` for
90        :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
91     :type mode: :attr:`MODE` or str.
92
93     :raises KeyError: if an unknown mode string is used.
94     :raises OSError: or subclasses if the configuration file can not
95        be opened.
96     :raises configparser.Error: or subclasses if the configuration
97        file can not be parsed.
98     :raises NotmuchError: or subclasses for other failures.
99     """
100
101     MODE = Mode
102     SORT = QuerySortOrder
103     EXCLUDE = QueryExclude
104     AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
105     _db_p = base.MemoryPointer()
106     STR_MODE_MAP = {
107         'ro': MODE.READ_ONLY,
108         'rw': MODE.READ_WRITE,
109     }
110
111     def __init__(self, path=None, mode=MODE.READ_ONLY):
112         if isinstance(mode, str):
113             mode = self.STR_MODE_MAP[mode]
114         self.mode = mode
115         if path is None:
116             path = self.default_path()
117         if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
118             path = bytes(path)
119         db_pp = capi.ffi.new('notmuch_database_t **')
120         cmsg = capi.ffi.new('char**')
121         ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path),
122                                                      mode.value, db_pp, cmsg)
123         if cmsg[0]:
124             msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
125             capi.lib.free(cmsg[0])
126         else:
127             msg = None
128         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
129             raise errors.NotmuchError(ret, msg)
130         self._db_p = db_pp[0]
131         self.closed = False
132
133     @classmethod
134     def create(cls, path=None):
135         """Create and open database in READ_WRITE mode.
136
137         This is creates a new notmuch database and returns an opened
138         instance in :attr:`MODE.READ_WRITE` mode.
139
140         :param path: The directory of where the database is stored.  If
141            ``None`` the location will be read from the user's
142            configuration file, respecting the ``NOTMUCH_CONFIG``
143            environment variable if set.
144         :type path: str, bytes or os.PathLike
145
146         :raises OSError: or subclasses if the configuration file can not
147            be opened.
148         :raises configparser.Error: or subclasses if the configuration
149            file can not be parsed.
150         :raises NotmuchError: if the config file does not have the
151            database.path setting.
152         :raises FileError: if the database already exists.
153
154         :returns: The newly created instance.
155         """
156         if path is None:
157             path = cls.default_path()
158         if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
159             path = bytes(path)
160         db_pp = capi.ffi.new('notmuch_database_t **')
161         cmsg = capi.ffi.new('char**')
162         ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path),
163                                                        db_pp, cmsg)
164         if cmsg[0]:
165             msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
166             capi.lib.free(cmsg[0])
167         else:
168             msg = None
169         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
170             raise errors.NotmuchError(ret, msg)
171
172         # Now close the db and let __init__ open it.  Inefficient but
173         # creating is not a hot loop while this allows us to have a
174         # clean API.
175         ret = capi.lib.notmuch_database_destroy(db_pp[0])
176         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
177             raise errors.NotmuchError(ret)
178         return cls(path, cls.MODE.READ_WRITE)
179
180     @staticmethod
181     def default_path(cfg_path=None):
182         """Return the path of the user's default database.
183
184         This reads the user's configuration file and returns the
185         default path of the database.
186
187         :param cfg_path: The pathname of the notmuch configuration file.
188            If not specified tries to use the pathname provided in the
189            :env:`NOTMUCH_CONFIG` environment variable and falls back
190            to :file:`~/.notmuch-config.
191         :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
192
193         :returns: The path of the database, which does not necessarily
194            exists.
195         :rtype: pathlib.Path
196         :raises OSError: or subclasses if the configuration file can not
197            be opened.
198         :raises configparser.Error: or subclasses if the configuration
199            file can not be parsed.
200         :raises NotmuchError if the config file does not have the
201            database.path setting.
202         """
203         if not cfg_path:
204             cfg_path = _config_pathname()
205         if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
206             cfg_path = bytes(cfg_path)
207         parser = configparser.ConfigParser()
208         with open(cfg_path) as fp:
209             parser.read_file(fp)
210         try:
211             return pathlib.Path(parser.get('database', 'path'))
212         except configparser.Error:
213             raise errors.NotmuchError(
214                 'No database.path setting in {}'.format(cfg_path))
215
216     def __del__(self):
217         self._destroy()
218
219     @property
220     def alive(self):
221         try:
222             self._db_p
223         except errors.ObjectDestroyedError:
224             return False
225         else:
226             return True
227
228     def _destroy(self):
229         try:
230             ret = capi.lib.notmuch_database_destroy(self._db_p)
231         except errors.ObjectDestroyedError:
232             ret = capi.lib.NOTMUCH_STATUS_SUCCESS
233         else:
234             self._db_p = None
235         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
236             raise errors.NotmuchError(ret)
237
238     def close(self):
239         """Close the notmuch database.
240
241         Once closed most operations will fail.  This can still be
242         useful however to explicitly close a database which is opened
243         read-write as this would otherwise stop other processes from
244         reading the database while it is open.
245
246         :raises ObjectDestroyedError: if used after destroyed.
247         """
248         ret = capi.lib.notmuch_database_close(self._db_p)
249         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
250             raise errors.NotmuchError(ret)
251         self.closed = True
252
253     def __enter__(self):
254         return self
255
256     def __exit__(self, exc_type, exc_value, traceback):
257         self.close()
258
259     @property
260     def path(self):
261         """The pathname of the notmuch database.
262
263         This is returned as a :class:`pathlib.Path` instance.
264
265         :raises ObjectDestroyedError: if used after destroyed.
266         """
267         try:
268             return self._cache_path
269         except AttributeError:
270             ret = capi.lib.notmuch_database_get_path(self._db_p)
271             self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
272             return self._cache_path
273
274     @property
275     def version(self):
276         """The database format version.
277
278         This is a positive integer.
279
280         :raises ObjectDestroyedError: if used after destroyed.
281         """
282         try:
283             return self._cache_version
284         except AttributeError:
285             ret = capi.lib.notmuch_database_get_version(self._db_p)
286             self._cache_version = ret
287             return ret
288
289     @property
290     def needs_upgrade(self):
291         """Whether the database should be upgraded.
292
293         If *True* the database can be upgraded using :meth:`upgrade`.
294         Not doing so may result in some operations raising
295         :exc:`UpgradeRequiredError`.
296
297         A read-only database will never be upgradable.
298
299         :raises ObjectDestroyedError: if used after destroyed.
300         """
301         ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
302         return bool(ret)
303
304     def upgrade(self, progress_cb=None):
305         """Upgrade the database to the latest version.
306
307         Upgrade the database, optionally with a progress callback
308         which should be a callable which will be called with a
309         floating point number in the range of [0.0 .. 1.0].
310         """
311         raise NotImplementedError
312
313     def atomic(self):
314         """Return a context manager to perform atomic operations.
315
316         The returned context manager can be used to perform atomic
317         operations on the database.
318
319         .. note:: Unlinke a traditional RDBMS transaction this does
320            not imply durability, it only ensures the changes are
321            performed atomically.
322
323         :raises ObjectDestroyedError: if used after destroyed.
324         """
325         ctx = AtomicContext(self, '_db_p')
326         return ctx
327
328     def revision(self):
329         """The currently committed revision in the database.
330
331         Returned as a ``(revision, uuid)`` namedtuple.
332
333         :raises ObjectDestroyedError: if used after destroyed.
334         """
335         raw_uuid = capi.ffi.new('char**')
336         rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
337         return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
338
339     def get_directory(self, path):
340         raise NotImplementedError
341
342     def default_indexopts(self):
343         """Returns default index options for the database.
344
345         :raises ObjectDestroyedError: if used after destroyed.
346
347         :returns: :class:`IndexOptions`.
348         """
349         opts = capi.lib.notmuch_database_get_default_indexopts(self._db_p)
350         return IndexOptions(self, opts)
351
352     def add(self, filename, *, sync_flags=False, indexopts=None):
353         """Add a message to the database.
354
355         Add a new message to the notmuch database.  The message is
356         referred to by the pathname of the maildir file.  If the
357         message ID of the new message already exists in the database,
358         this adds ``pathname`` to the list of list of files for the
359         existing message.
360
361         :param filename: The path of the file containing the message.
362         :type filename: str, bytes, os.PathLike or pathlib.Path.
363         :param sync_flags: Whether to sync the known maildir flags to
364            notmuch tags.  See :meth:`Message.flags_to_tags` for
365            details.
366         :type sync_flags: bool
367         :param indexopts: The indexing options, see
368            :meth:`default_indexopts`.  Leave as `None` to use the
369            default options configured in the database.
370         :type indexopts: :class:`IndexOptions` or `None`
371
372         :returns: A tuple where the first item is the newly inserted
373            messages as a :class:`Message` instance, and the second
374            item is a boolean indicating if the message inserted was a
375            duplicate.  This is the namedtuple ``AddedMessage(msg,
376            dup)``.
377         :rtype: Database.AddedMessage
378
379         If an exception is raised, no message was added.
380
381         :raises XapianError: A Xapian exception occurred.
382         :raises FileError: The file referred to by ``pathname`` could
383            not be opened.
384         :raises FileNotEmailError: The file referreed to by
385            ``pathname`` is not recognised as an email message.
386         :raises ReadOnlyDatabaseError: The database is opened in
387            READ_ONLY mode.
388         :raises UpgradeRequiredError: The database must be upgraded
389            first.
390         :raises ObjectDestroyedError: if used after destroyed.
391         """
392         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
393             filename = bytes(filename)
394         msg_pp = capi.ffi.new('notmuch_message_t **')
395         opts_p = indexopts._opts_p if indexopts else capi.ffi.NULL
396         ret = capi.lib.notmuch_database_index_file(
397             self._db_p, os.fsencode(filename), opts_p, msg_pp)
398         ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
399               capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
400         if ret not in ok:
401             raise errors.NotmuchError(ret)
402         msg = message.Message(self, msg_pp[0], db=self)
403         if sync_flags:
404             msg.tags.from_maildir_flags()
405         return self.AddedMessage(
406             msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
407
408     def remove(self, filename):
409         """Remove a message from the notmuch database.
410
411         Removing a message which is not in the database is just a
412         silent nop-operation.
413
414         :param filename: The pathname of the file containing the
415            message to be removed.
416         :type filename: str, bytes, os.PathLike or pathlib.Path.
417
418         :returns: True if the message is still in the database.  This
419            can happen when multiple files contain the same message ID.
420            The true/false distinction is fairly arbitrary, but think
421            of it as ``dup = db.remove_message(name); if dup: ...``.
422         :rtype: bool
423
424         :raises XapianError: A Xapian exception ocurred.
425         :raises ReadOnlyDatabaseError: The database is opened in
426            READ_ONLY mode.
427         :raises UpgradeRequiredError: The database must be upgraded
428            first.
429         :raises ObjectDestroyedError: if used after destroyed.
430         """
431         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
432             filename = bytes(filename)
433         ret = capi.lib.notmuch_database_remove_message(self._db_p,
434                                                        os.fsencode(filename))
435         ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
436               capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
437         if ret not in ok:
438             raise errors.NotmuchError(ret)
439         if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
440             return True
441         else:
442             return False
443
444     def find(self, msgid):
445         """Return the message matching the given message ID.
446
447         If a message with the given message ID is found a
448         :class:`Message` instance is returned.  Otherwise a
449         :exc:`LookupError` is raised.
450
451         :param msgid: The message ID to look for.
452         :type msgid: str
453
454         :returns: The message instance.
455         :rtype: Message
456
457         :raises LookupError: If no message was found.
458         :raises OutOfMemoryError: When there is no memory to allocate
459            the message instance.
460         :raises XapianError: A Xapian exception ocurred.
461         :raises ObjectDestroyedError: if used after destroyed.
462         """
463         msg_pp = capi.ffi.new('notmuch_message_t **')
464         ret = capi.lib.notmuch_database_find_message(self._db_p,
465                                                      msgid.encode(), msg_pp)
466         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
467             raise errors.NotmuchError(ret)
468         msg_p = msg_pp[0]
469         if msg_p == capi.ffi.NULL:
470             raise LookupError
471         msg = message.Message(self, msg_p, db=self)
472         return msg
473
474     def get(self, filename):
475         """Return the :class:`Message` given a pathname.
476
477         If a message with the given pathname exists in the database
478         return the :class:`Message` instance for the message.
479         Otherwise raise a :exc:`LookupError` exception.
480
481         :param filename: The pathname of the message.
482         :type filename: str, bytes, os.PathLike or pathlib.Path
483
484         :returns: The message instance.
485         :rtype: Message
486
487         :raises LookupError: If no message was found.  This is also
488            a subclass of :exc:`KeyError`.
489         :raises OutOfMemoryError: When there is no memory to allocate
490            the message instance.
491         :raises XapianError: A Xapian exception ocurred.
492         :raises ObjectDestroyedError: if used after destroyed.
493         """
494         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
495             filename = bytes(filename)
496         msg_pp = capi.ffi.new('notmuch_message_t **')
497         ret = capi.lib.notmuch_database_find_message_by_filename(
498             self._db_p, os.fsencode(filename), msg_pp)
499         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
500             raise errors.NotmuchError(ret)
501         msg_p = msg_pp[0]
502         if msg_p == capi.ffi.NULL:
503             raise LookupError
504         msg = message.Message(self, msg_p, db=self)
505         return msg
506
507     @property
508     def tags(self):
509         """Return an immutable set with all tags used in this database.
510
511         This returns an immutable set-like object implementing the
512         collections.abc.Set Abstract Base Class.  Due to the
513         underlying libnotmuch implementation some operations have
514         different performance characteristics then plain set objects.
515         Mainly any lookup operation is O(n) rather then O(1).
516
517         Normal usage treats tags as UTF-8 encoded unicode strings so
518         they are exposed to Python as normal unicode string objects.
519         If you need to handle tags stored in libnotmuch which are not
520         valid unicode do check the :class:`ImmutableTagSet` docs for
521         how to handle this.
522
523         :rtype: ImmutableTagSet
524
525         :raises ObjectDestroyedError: if used after destroyed.
526         """
527         try:
528             ref = self._cached_tagset
529         except AttributeError:
530             tagset = None
531         else:
532             tagset = ref()
533         if tagset is None:
534             tagset = tags.ImmutableTagSet(
535                 self, '_db_p', capi.lib.notmuch_database_get_all_tags)
536             self._cached_tagset = weakref.ref(tagset)
537         return tagset
538
539     def _create_query(self, query, *,
540                       omit_excluded=EXCLUDE.TRUE,
541                       sort=SORT.UNSORTED,  # Check this default
542                       exclude_tags=None):
543         """Create an internal query object.
544
545         :raises OutOfMemoryError: if no memory is available to
546            allocate the query.
547         """
548         if isinstance(query, str):
549             query = query.encode('utf-8')
550         query_p = capi.lib.notmuch_query_create(self._db_p, query)
551         if query_p == capi.ffi.NULL:
552             raise errors.OutOfMemoryError()
553         capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
554         capi.lib.notmuch_query_set_sort(query_p, sort.value)
555         if exclude_tags is not None:
556             for tag in exclude_tags:
557                 if isinstance(tag, str):
558                     tag = str.encode('utf-8')
559                 capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
560         return querymod.Query(self, query_p)
561
562     def messages(self, query, *,
563                  omit_excluded=EXCLUDE.TRUE,
564                  sort=SORT.UNSORTED,  # Check this default
565                  exclude_tags=None):
566         """Search the database for messages.
567
568         :returns: An iterator over the messages found.
569         :rtype: MessageIter
570
571         :raises OutOfMemoryError: if no memory is available to
572            allocate the query.
573         :raises ObjectDestroyedError: if used after destroyed.
574         """
575         query = self._create_query(query,
576                                    omit_excluded=omit_excluded,
577                                    sort=sort,
578                                    exclude_tags=exclude_tags)
579         return query.messages()
580
581     def count_messages(self, query, *,
582                        omit_excluded=EXCLUDE.TRUE,
583                        sort=SORT.UNSORTED,  # Check this default
584                        exclude_tags=None):
585         """Search the database for messages.
586
587         :returns: An iterator over the messages found.
588         :rtype: MessageIter
589
590         :raises ObjectDestroyedError: if used after destroyed.
591         """
592         query = self._create_query(query,
593                                    omit_excluded=omit_excluded,
594                                    sort=sort,
595                                    exclude_tags=exclude_tags)
596         return query.count_messages()
597
598     def threads(self,  query, *,
599                 omit_excluded=EXCLUDE.TRUE,
600                 sort=SORT.UNSORTED,  # Check this default
601                 exclude_tags=None):
602         query = self._create_query(query,
603                                    omit_excluded=omit_excluded,
604                                    sort=sort,
605                                    exclude_tags=exclude_tags)
606         return query.threads()
607
608     def count_threads(self, query, *,
609                       omit_excluded=EXCLUDE.TRUE,
610                       sort=SORT.UNSORTED,  # Check this default
611                       exclude_tags=None):
612         query = self._create_query(query,
613                                    omit_excluded=omit_excluded,
614                                    sort=sort,
615                                    exclude_tags=exclude_tags)
616         return query.count_threads()
617
618     def status_string(self):
619         raise NotImplementedError
620
621     def __repr__(self):
622         return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
623
624
625 class AtomicContext:
626     """Context manager for atomic support.
627
628     This supports the notmuch_database_begin_atomic and
629     notmuch_database_end_atomic API calls.  The object can not be
630     directly instantiated by the user, only via ``Database.atomic``.
631     It does keep a reference to the :class:`Database` instance to keep
632     the C memory alive.
633
634     :raises XapianError: When this is raised at enter time the atomic
635        section is not active.  When it is raised at exit time the
636        atomic section is still active and you may need to try using
637        :meth:`force_end`.
638     :raises ObjectDestroyedError: if used after destroyed.
639     """
640
641     def __init__(self, db, ptr_name):
642         self._db = db
643         self._ptr = lambda: getattr(db, ptr_name)
644
645     def __del__(self):
646         self._destroy()
647
648     @property
649     def alive(self):
650         return self.parent.alive
651
652     def _destroy(self):
653         pass
654
655     def __enter__(self):
656         ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
657         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
658             raise errors.NotmuchError(ret)
659         return self
660
661     def __exit__(self, exc_type, exc_value, traceback):
662         ret = capi.lib.notmuch_database_end_atomic(self._ptr())
663         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
664             raise errors.NotmuchError(ret)
665
666     def force_end(self):
667         """Force ending the atomic section.
668
669         This can only be called once __exit__ has been called.  It
670         will attept to close the atomic section (again).  This is
671         useful if the original exit raised an exception and the atomic
672         section is still open.  But things are pretty ugly by now.
673
674         :raises XapianError: If exiting fails, the atomic section is
675            not ended.
676         :raises UnbalancedAtomicError: If the database was currently
677            not in an atomic section.
678         :raises ObjectDestroyedError: if used after destroyed.
679         """
680         ret = capi.lib.notmuch_database_end_atomic(self._ptr())
681         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
682             raise errors.NotmuchError(ret)
683
684
685 @functools.total_ordering
686 class DbRevision:
687     """A database revision.
688
689     The database revision number increases monotonically with each
690     commit to the database.  Which means user-visible changes can be
691     ordered.  This object is sortable with other revisions.  It
692     carries the UUID of the database to ensure it is only ever
693     compared with revisions from the same database.
694     """
695
696     def __init__(self, rev, uuid):
697         self._rev = rev
698         self._uuid = uuid
699
700     @property
701     def rev(self):
702         """The revision number, a positive integer."""
703         return self._rev
704
705     @property
706     def uuid(self):
707         """The UUID of the database, consider this opaque."""
708         return self._uuid
709
710     def __eq__(self, other):
711         if isinstance(other, self.__class__):
712             if self.uuid != other.uuid:
713                 return False
714             return self.rev == other.rev
715         else:
716             return NotImplemented
717
718     def __lt__(self, other):
719         if self.__class__ is other.__class__:
720             if self.uuid != other.uuid:
721                 return False
722             return self.rev < other.rev
723         else:
724             return NotImplemented
725
726     def __repr__(self):
727         return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
728
729
730 class IndexOptions(base.NotmuchObject):
731     """Indexing options.
732
733     This represents the indexing options which can be used to index a
734     message.  See :meth:`Database.default_indexopts` to create an
735     instance of this.  It can be used e.g. when indexing a new message
736     using :meth:`Database.add`.
737     """
738     _opts_p = base.MemoryPointer()
739
740     def __init__(self, parent, opts_p):
741         self._parent = parent
742         self._opts_p = opts_p
743
744     @property
745     def alive(self):
746         if not self._parent.alive:
747             return False
748         try:
749             self._opts_p
750         except errors.ObjectDestroyedError:
751             return False
752         else:
753             return True
754
755     def _destroy(self):
756         if self.alive:
757             capi.lib.notmuch_indexopts_destroy(self._opts_p)
758         self._opts_p = None
759
760     @property
761     def decrypt_policy(self):
762         """The decryption policy.
763
764         This is an enum from the :class:`DecryptionPolicy`.  See the
765         `index.decrypt` section in :man:`notmuch-config` for details
766         on the options.  **Do not set this to
767         :attr:`DecryptionPolicy.TRUE`** without considering the
768         security of your index.
769
770         You can change this policy by assigning a new
771         :class:`DecryptionPolicy` to this property.
772
773         :raises ObjectDestroyedError: if used after destroyed.
774
775         :returns: A :class:`DecryptionPolicy` enum instance.
776         """
777         raw = capi.lib.notmuch_indexopts_get_decrypt_policy(self._opts_p)
778         return DecryptionPolicy(raw)
779
780     @decrypt_policy.setter
781     def decrypt_policy(self, val):
782         ret = capi.lib.notmuch_indexopts_set_decrypt_policy(
783             self._opts_p, val.value)
784         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
785             raise errors.NotmuchError(ret, msg)