1import collections 2import configparser 3import enum 4import functools 5import os 6import pathlib 7import weakref 8 9import notmuch2._base as base 10import notmuch2._config as config 11import notmuch2._capi as capi 12import notmuch2._errors as errors 13import notmuch2._message as message 14import notmuch2._query as querymod 15import notmuch2._tags as tags 16 17 18__all__ = ['Database', 'AtomicContext', 'DbRevision'] 19 20 21def _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 30class Mode(enum.Enum): 31 READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY 32 READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE 33 34class ConfigFile(enum.Enum): 35 EMPTY = b'' 36 SEARCH = capi.ffi.NULL 37 38class 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 45class 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 52class 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 59class 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.EMPTY): 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 682class 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 757class 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 801class 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) 857