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