]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_database.py
a47049bae4f8d4a888beae111007a15904b865ec
[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', 'IndexOptions']
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 reopen(self, mode=None):
289         """Reopen an opened notmuch database.
290
291         :param mode: Mode to reopen the database with. When None, the previously
292            active mode is preserved.
293         :type mode: :attr:`MODE`, str, or None.
294
295         This is useful e.g. for:
296         - switching the mode between read-only and read-write
297         - recovering from OperationInvalidatedError
298         """
299         if isinstance(mode, str):
300             try:
301                 mode = self.STR_MODE_MAP[mode]
302             except KeyError:
303                 raise ValueError('Invalid mode: %s' % mode)
304         else:
305             mode = mode or self.mode
306         self.mode = mode
307
308         ret = capi.lib.notmuch_database_reopen(self._db_p, mode.value)
309         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
310             raise errors.NotmuchError(ret)
311
312     def __enter__(self):
313         return self
314
315     def __exit__(self, exc_type, exc_value, traceback):
316         self.close()
317
318     @property
319     def path(self):
320         """The pathname of the notmuch database.
321
322         This is returned as a :class:`pathlib.Path` instance.
323
324         :raises ObjectDestroyedError: if used after destroyed.
325         """
326         try:
327             return self._cache_path
328         except AttributeError:
329             ret = capi.lib.notmuch_database_get_path(self._db_p)
330             self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
331             return self._cache_path
332
333     @property
334     def version(self):
335         """The database format version.
336
337         This is a positive integer.
338
339         :raises ObjectDestroyedError: if used after destroyed.
340         """
341         try:
342             return self._cache_version
343         except AttributeError:
344             ret = capi.lib.notmuch_database_get_version(self._db_p)
345             self._cache_version = ret
346             return ret
347
348     @property
349     def needs_upgrade(self):
350         """Whether the database should be upgraded.
351
352         If *True* the database can be upgraded using :meth:`upgrade`.
353         Not doing so may result in some operations raising
354         :exc:`UpgradeRequiredError`.
355
356         A read-only database will never be upgradable.
357
358         :raises ObjectDestroyedError: if used after destroyed.
359         """
360         ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
361         return bool(ret)
362
363     def upgrade(self, progress_cb=None):
364         """Upgrade the database to the latest version.
365
366         Upgrade the database, optionally with a progress callback
367         which should be a callable which will be called with a
368         floating point number in the range of [0.0 .. 1.0].
369         """
370         raise NotImplementedError
371
372     def atomic(self):
373         """Return a context manager to perform atomic operations.
374
375         The returned context manager can be used to perform atomic
376         operations on the database.
377
378         .. note:: Unlike a traditional RDBMS transaction this does
379            not imply durability, it only ensures the changes are
380            performed atomically.
381
382         :raises ObjectDestroyedError: if used after destroyed.
383         """
384         ctx = AtomicContext(self, '_db_p')
385         return ctx
386
387     def revision(self):
388         """The currently committed revision in the database.
389
390         Returned as a ``(revision, uuid)`` namedtuple.
391
392         :raises ObjectDestroyedError: if used after destroyed.
393         """
394         raw_uuid = capi.ffi.new('char**')
395         rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
396         return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
397
398     def get_directory(self, path):
399         raise NotImplementedError
400
401     def default_indexopts(self):
402         """Returns default index options for the database.
403
404         :raises ObjectDestroyedError: if used after destroyed.
405
406         :returns: :class:`IndexOptions`.
407         """
408         opts = capi.lib.notmuch_database_get_default_indexopts(self._db_p)
409         return IndexOptions(self, opts)
410
411     def add(self, filename, *, sync_flags=False, indexopts=None):
412         """Add a message to the database.
413
414         Add a new message to the notmuch database.  The message is
415         referred to by the pathname of the maildir file.  If the
416         message ID of the new message already exists in the database,
417         this adds ``pathname`` to the list of list of files for the
418         existing message.
419
420         :param filename: The path of the file containing the message.
421         :type filename: str, bytes, os.PathLike or pathlib.Path.
422         :param sync_flags: Whether to sync the known maildir flags to
423            notmuch tags.  See :meth:`Message.flags_to_tags` for
424            details.
425         :type sync_flags: bool
426         :param indexopts: The indexing options, see
427            :meth:`default_indexopts`.  Leave as `None` to use the
428            default options configured in the database.
429         :type indexopts: :class:`IndexOptions` or `None`
430
431         :returns: A tuple where the first item is the newly inserted
432            messages as a :class:`Message` instance, and the second
433            item is a boolean indicating if the message inserted was a
434            duplicate.  This is the namedtuple ``AddedMessage(msg,
435            dup)``.
436         :rtype: Database.AddedMessage
437
438         If an exception is raised, no message was added.
439
440         :raises XapianError: A Xapian exception occurred.
441         :raises FileError: The file referred to by ``pathname`` could
442            not be opened.
443         :raises FileNotEmailError: The file referred to by
444            ``pathname`` is not recognised as an email message.
445         :raises ReadOnlyDatabaseError: The database is opened in
446            READ_ONLY mode.
447         :raises UpgradeRequiredError: The database must be upgraded
448            first.
449         :raises ObjectDestroyedError: if used after destroyed.
450         """
451         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
452             filename = bytes(filename)
453         msg_pp = capi.ffi.new('notmuch_message_t **')
454         opts_p = indexopts._opts_p if indexopts else capi.ffi.NULL
455         ret = capi.lib.notmuch_database_index_file(
456             self._db_p, os.fsencode(filename), opts_p, msg_pp)
457         ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
458               capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
459         if ret not in ok:
460             raise errors.NotmuchError(ret)
461         msg = message.Message(self, msg_pp[0], db=self)
462         if sync_flags:
463             msg.tags.from_maildir_flags()
464         return self.AddedMessage(
465             msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
466
467     def remove(self, filename):
468         """Remove a message from the notmuch database.
469
470         Removing a message which is not in the database is just a
471         silent nop-operation.
472
473         :param filename: The pathname of the file containing the
474            message to be removed.
475         :type filename: str, bytes, os.PathLike or pathlib.Path.
476
477         :returns: True if the message is still in the database.  This
478            can happen when multiple files contain the same message ID.
479            The true/false distinction is fairly arbitrary, but think
480            of it as ``dup = db.remove_message(name); if dup: ...``.
481         :rtype: bool
482
483         :raises XapianError: A Xapian exception occurred.
484         :raises ReadOnlyDatabaseError: The database is opened in
485            READ_ONLY mode.
486         :raises UpgradeRequiredError: The database must be upgraded
487            first.
488         :raises ObjectDestroyedError: if used after destroyed.
489         """
490         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
491             filename = bytes(filename)
492         ret = capi.lib.notmuch_database_remove_message(self._db_p,
493                                                        os.fsencode(filename))
494         ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
495               capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
496         if ret not in ok:
497             raise errors.NotmuchError(ret)
498         if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
499             return True
500         else:
501             return False
502
503     def find(self, msgid):
504         """Return the message matching the given message ID.
505
506         If a message with the given message ID is found a
507         :class:`Message` instance is returned.  Otherwise a
508         :exc:`LookupError` is raised.
509
510         :param msgid: The message ID to look for.
511         :type msgid: str
512
513         :returns: The message instance.
514         :rtype: Message
515
516         :raises LookupError: If no message was found.
517         :raises OutOfMemoryError: When there is no memory to allocate
518            the message instance.
519         :raises XapianError: A Xapian exception occurred.
520         :raises ObjectDestroyedError: if used after destroyed.
521         """
522         msg_pp = capi.ffi.new('notmuch_message_t **')
523         ret = capi.lib.notmuch_database_find_message(self._db_p,
524                                                      msgid.encode(), msg_pp)
525         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
526             raise errors.NotmuchError(ret)
527         msg_p = msg_pp[0]
528         if msg_p == capi.ffi.NULL:
529             raise LookupError
530         msg = message.Message(self, msg_p, db=self)
531         return msg
532
533     def get(self, filename):
534         """Return the :class:`Message` given a pathname.
535
536         If a message with the given pathname exists in the database
537         return the :class:`Message` instance for the message.
538         Otherwise raise a :exc:`LookupError` exception.
539
540         :param filename: The pathname of the message.
541         :type filename: str, bytes, os.PathLike or pathlib.Path
542
543         :returns: The message instance.
544         :rtype: Message
545
546         :raises LookupError: If no message was found.  This is also
547            a subclass of :exc:`KeyError`.
548         :raises OutOfMemoryError: When there is no memory to allocate
549            the message instance.
550         :raises XapianError: A Xapian exception occurred.
551         :raises ObjectDestroyedError: if used after destroyed.
552         """
553         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
554             filename = bytes(filename)
555         msg_pp = capi.ffi.new('notmuch_message_t **')
556         ret = capi.lib.notmuch_database_find_message_by_filename(
557             self._db_p, os.fsencode(filename), msg_pp)
558         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
559             raise errors.NotmuchError(ret)
560         msg_p = msg_pp[0]
561         if msg_p == capi.ffi.NULL:
562             raise LookupError
563         msg = message.Message(self, msg_p, db=self)
564         return msg
565
566     @property
567     def tags(self):
568         """Return an immutable set with all tags used in this database.
569
570         This returns an immutable set-like object implementing the
571         collections.abc.Set Abstract Base Class.  Due to the
572         underlying libnotmuch implementation some operations have
573         different performance characteristics then plain set objects.
574         Mainly any lookup operation is O(n) rather then O(1).
575
576         Normal usage treats tags as UTF-8 encoded unicode strings so
577         they are exposed to Python as normal unicode string objects.
578         If you need to handle tags stored in libnotmuch which are not
579         valid unicode do check the :class:`ImmutableTagSet` docs for
580         how to handle this.
581
582         :rtype: ImmutableTagSet
583
584         :raises ObjectDestroyedError: if used after destroyed.
585         """
586         try:
587             ref = self._cached_tagset
588         except AttributeError:
589             tagset = None
590         else:
591             tagset = ref()
592         if tagset is None:
593             tagset = tags.ImmutableTagSet(
594                 self, '_db_p', capi.lib.notmuch_database_get_all_tags)
595             self._cached_tagset = weakref.ref(tagset)
596         return tagset
597
598     @property
599     def config(self):
600         """Return a mutable mapping with the settings stored in this database.
601
602         This returns an mutable dict-like object implementing the
603         collections.abc.MutableMapping Abstract Base Class.
604
605         :rtype: Config
606
607         :raises ObjectDestroyedError: if used after destroyed.
608         """
609         try:
610             ref = self._cached_config
611         except AttributeError:
612             config_mapping = None
613         else:
614             config_mapping = ref()
615         if config_mapping is None:
616             config_mapping = config.ConfigMapping(self, '_db_p')
617             self._cached_config = weakref.ref(config_mapping)
618         return config_mapping
619
620     def _create_query(self, query, *,
621                       omit_excluded=EXCLUDE.TRUE,
622                       sort=SORT.UNSORTED,  # Check this default
623                       exclude_tags=None):
624         """Create an internal query object.
625
626         :raises OutOfMemoryError: if no memory is available to
627            allocate the query.
628         """
629         if isinstance(query, str):
630             query = query.encode('utf-8')
631         query_p = capi.lib.notmuch_query_create(self._db_p, query)
632         if query_p == capi.ffi.NULL:
633             raise errors.OutOfMemoryError()
634         capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
635         capi.lib.notmuch_query_set_sort(query_p, sort.value)
636         if exclude_tags is not None:
637             for tag in exclude_tags:
638                 if isinstance(tag, str):
639                     tag = tag.encode('utf-8')
640                 capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
641         return querymod.Query(self, query_p)
642
643     def messages(self, query, *,
644                  omit_excluded=EXCLUDE.TRUE,
645                  sort=SORT.UNSORTED,  # Check this default
646                  exclude_tags=None):
647         """Search the database for messages.
648
649         :returns: An iterator over the messages found.
650         :rtype: MessageIter
651
652         :raises OutOfMemoryError: if no memory is available to
653            allocate the query.
654         :raises ObjectDestroyedError: if used after destroyed.
655         """
656         query = self._create_query(query,
657                                    omit_excluded=omit_excluded,
658                                    sort=sort,
659                                    exclude_tags=exclude_tags)
660         return query.messages()
661
662     def count_messages(self, query, *,
663                        omit_excluded=EXCLUDE.TRUE,
664                        sort=SORT.UNSORTED,  # Check this default
665                        exclude_tags=None):
666         """Search the database for messages.
667
668         :returns: An iterator over the messages found.
669         :rtype: MessageIter
670
671         :raises ObjectDestroyedError: if used after destroyed.
672         """
673         query = self._create_query(query,
674                                    omit_excluded=omit_excluded,
675                                    sort=sort,
676                                    exclude_tags=exclude_tags)
677         return query.count_messages()
678
679     def threads(self,  query, *,
680                 omit_excluded=EXCLUDE.TRUE,
681                 sort=SORT.UNSORTED,  # Check this default
682                 exclude_tags=None):
683         query = self._create_query(query,
684                                    omit_excluded=omit_excluded,
685                                    sort=sort,
686                                    exclude_tags=exclude_tags)
687         return query.threads()
688
689     def count_threads(self, query, *,
690                       omit_excluded=EXCLUDE.TRUE,
691                       sort=SORT.UNSORTED,  # Check this default
692                       exclude_tags=None):
693         query = self._create_query(query,
694                                    omit_excluded=omit_excluded,
695                                    sort=sort,
696                                    exclude_tags=exclude_tags)
697         return query.count_threads()
698
699     def status_string(self):
700         raise NotImplementedError
701
702     def __repr__(self):
703         return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
704
705
706 class AtomicContext:
707     """Context manager for atomic support.
708
709     This supports the notmuch_database_begin_atomic and
710     notmuch_database_end_atomic API calls.  The object can not be
711     directly instantiated by the user, only via ``Database.atomic``.
712     It does keep a reference to the :class:`Database` instance to keep
713     the C memory alive.
714
715     :raises XapianError: When this is raised at enter time the atomic
716        section is not active.  When it is raised at exit time the
717        atomic section is still active and you may need to try using
718        :meth:`force_end`.
719     :raises ObjectDestroyedError: if used after destroyed.
720     """
721
722     def __init__(self, db, ptr_name):
723         self._db = db
724         self._ptr = lambda: getattr(db, ptr_name)
725         self._exit_fn = lambda: None
726
727     def __del__(self):
728         self._destroy()
729
730     @property
731     def alive(self):
732         return self.parent.alive
733
734     def _destroy(self):
735         pass
736
737     def __enter__(self):
738         ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
739         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
740             raise errors.NotmuchError(ret)
741         self._exit_fn = self._end_atomic
742         return self
743
744     def _end_atomic(self):
745         ret = capi.lib.notmuch_database_end_atomic(self._ptr())
746         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
747             raise errors.NotmuchError(ret)
748
749     def __exit__(self, exc_type, exc_value, traceback):
750         self._exit_fn()
751
752     def force_end(self):
753         """Force ending the atomic section.
754
755         This can only be called once __exit__ has been called.  It
756         will attempt to close the atomic section (again).  This is
757         useful if the original exit raised an exception and the atomic
758         section is still open.  But things are pretty ugly by now.
759
760         :raises XapianError: If exiting fails, the atomic section is
761            not ended.
762         :raises UnbalancedAtomicError: If the database was currently
763            not in an atomic section.
764         :raises ObjectDestroyedError: if used after destroyed.
765         """
766         ret = capi.lib.notmuch_database_end_atomic(self._ptr())
767         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
768             raise errors.NotmuchError(ret)
769
770     def abort(self):
771         """Abort the transaction.
772
773         Aborting a transaction will not commit any of the changes, but
774         will also implicitly close the database.
775         """
776         self._exit_fn = lambda: None
777         self._db.close()
778
779
780 @functools.total_ordering
781 class DbRevision:
782     """A database revision.
783
784     The database revision number increases monotonically with each
785     commit to the database.  Which means user-visible changes can be
786     ordered.  This object is sortable with other revisions.  It
787     carries the UUID of the database to ensure it is only ever
788     compared with revisions from the same database.
789     """
790
791     def __init__(self, rev, uuid):
792         self._rev = rev
793         self._uuid = uuid
794
795     @property
796     def rev(self):
797         """The revision number, a positive integer."""
798         return self._rev
799
800     @property
801     def uuid(self):
802         """The UUID of the database, consider this opaque."""
803         return self._uuid
804
805     def __eq__(self, other):
806         if isinstance(other, self.__class__):
807             if self.uuid != other.uuid:
808                 return False
809             return self.rev == other.rev
810         else:
811             return NotImplemented
812
813     def __lt__(self, other):
814         if self.__class__ is other.__class__:
815             if self.uuid != other.uuid:
816                 return False
817             return self.rev < other.rev
818         else:
819             return NotImplemented
820
821     def __repr__(self):
822         return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
823
824
825 class IndexOptions(base.NotmuchObject):
826     """Indexing options.
827
828     This represents the indexing options which can be used to index a
829     message.  See :meth:`Database.default_indexopts` to create an
830     instance of this.  It can be used e.g. when indexing a new message
831     using :meth:`Database.add`.
832     """
833     _opts_p = base.MemoryPointer()
834
835     def __init__(self, parent, opts_p):
836         self._parent = parent
837         self._opts_p = opts_p
838
839     @property
840     def alive(self):
841         if not self._parent.alive:
842             return False
843         try:
844             self._opts_p
845         except errors.ObjectDestroyedError:
846             return False
847         else:
848             return True
849
850     def _destroy(self):
851         if self.alive:
852             capi.lib.notmuch_indexopts_destroy(self._opts_p)
853         self._opts_p = None
854
855     @property
856     def decrypt_policy(self):
857         """The decryption policy.
858
859         This is an enum from the :class:`DecryptionPolicy`.  See the
860         `index.decrypt` section in :any:`notmuch-config(1)` for details
861         on the options.  **Do not set this to
862         :attr:`DecryptionPolicy.TRUE`** without considering the
863         security of your index.
864
865         You can change this policy by assigning a new
866         :class:`DecryptionPolicy` to this property.
867
868         :raises ObjectDestroyedError: if used after destroyed.
869
870         :returns: A :class:`DecryptionPolicy` enum instance.
871         """
872         raw = capi.lib.notmuch_indexopts_get_decrypt_policy(self._opts_p)
873         return DecryptionPolicy(raw)
874
875     @decrypt_policy.setter
876     def decrypt_policy(self, val):
877         ret = capi.lib.notmuch_indexopts_set_decrypt_policy(
878             self._opts_p, val.value)
879         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
880             raise errors.NotmuchError(ret, msg)