diff options
| author | Floris Bruynooghe <flub@devork.be> | 2019-11-17 17:41:35 +0100 |
|---|---|---|
| committer | David Bremner <david@tethera.net> | 2019-12-03 08:12:30 -0400 |
| commit | e2df30f7a98f91543d0b3561dbb366eb4b3d812c (patch) | |
| tree | 7b1bb0c60d8723e9e58158b151afbb5d0989c56f /bindings/python-cffi/notdb/_database.py | |
| parent | a950aa28449feef76246ad2b64224fd72e2e574c (diff) | |
Rename package to notmuch2
This is based on a previous discussion on the list where this was more
or less seen as the least-bad option.
Diffstat (limited to 'bindings/python-cffi/notdb/_database.py')
| -rw-r--r-- | bindings/python-cffi/notdb/_database.py | 705 |
1 files changed, 0 insertions, 705 deletions
diff --git a/bindings/python-cffi/notdb/_database.py b/bindings/python-cffi/notdb/_database.py deleted file mode 100644 index d414082a..00000000 --- a/bindings/python-cffi/notdb/_database.py +++ /dev/null @@ -1,705 +0,0 @@ -import collections -import configparser -import enum -import functools -import os -import pathlib -import weakref - -import notdb._base as base -import notdb._capi as capi -import notdb._errors as errors -import notdb._message as message -import notdb._query as querymod -import notdb._tags as tags - - -__all__ = ['Database', 'AtomicContext', 'DbRevision'] - - -def _config_pathname(): - """Return the path of the configuration file. - - :rtype: pathlib.Path - """ - cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config') - return pathlib.Path(os.path.expanduser(cfgfname)) - - -class Mode(enum.Enum): - READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY - READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE - - -class QuerySortOrder(enum.Enum): - OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST - NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST - MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID - UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED - - -class QueryExclude(enum.Enum): - TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE - FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG - FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE - ALL = capi.lib.NOTMUCH_EXCLUDE_ALL - - -class Database(base.NotmuchObject): - """Toplevel access to notmuch. - - A :class:`Database` can be opened read-only or read-write. - Modifications are not atomic by default, use :meth:`begin_atomic` - for atomic updates. If the underlying database has been modified - outside of this class a :exc:`XapianError` will be raised and the - instance must be closed and a new one created. - - You can use an instance of this class as a context-manager. - - :cvar MODE: The mode a database can be opened with, an enumeration - of ``READ_ONLY`` and ``READ_WRITE`` - :cvar SORT: The sort order for search results, ``OLDEST_FIRST``, - ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``. - :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``, - ``FLAG``, ``FALSE`` or ``ALL``. See the query documentation - for details. - :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by - :meth:`add` as return value. - :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items. - This is used to implement the ``ro`` and ``rw`` string - variants. - - :ivar closed: Boolean indicating if the database is closed or - still open. - - :param path: The directory of where the database is stored. If - ``None`` the location will be read from the user's - configuration file, respecting the ``NOTMUCH_CONFIG`` - environment variable if set. - :type path: str, bytes, os.PathLike or pathlib.Path - :param mode: The mode to open the database in. One of - :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`. For - convenience you can also use the strings ``ro`` for - :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`. - :type mode: :attr:`MODE` or str. - - :raises KeyError: if an unknown mode string is used. - :raises OSError: or subclasses if the configuration file can not - be opened. - :raises configparser.Error: or subclasses if the configuration - file can not be parsed. - :raises NotmuchError: or subclasses for other failures. - """ - - MODE = Mode - SORT = QuerySortOrder - EXCLUDE = QueryExclude - AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup']) - _db_p = base.MemoryPointer() - STR_MODE_MAP = { - 'ro': MODE.READ_ONLY, - 'rw': MODE.READ_WRITE, - } - - def __init__(self, path=None, mode=MODE.READ_ONLY): - if isinstance(mode, str): - mode = self.STR_MODE_MAP[mode] - self.mode = mode - if path is None: - path = self.default_path() - if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): - path = bytes(path) - db_pp = capi.ffi.new('notmuch_database_t **') - cmsg = capi.ffi.new('char**') - ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path), - mode.value, db_pp, cmsg) - if cmsg[0]: - msg = capi.ffi.string(cmsg[0]).decode(errors='replace') - capi.lib.free(cmsg[0]) - else: - msg = None - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret, msg) - self._db_p = db_pp[0] - self.closed = False - - @classmethod - def create(cls, path=None): - """Create and open database in READ_WRITE mode. - - This is creates a new notmuch database and returns an opened - instance in :attr:`MODE.READ_WRITE` mode. - - :param path: The directory of where the database is stored. If - ``None`` the location will be read from the user's - configuration file, respecting the ``NOTMUCH_CONFIG`` - environment variable if set. - :type path: str, bytes or os.PathLike - - :raises OSError: or subclasses if the configuration file can not - be opened. - :raises configparser.Error: or subclasses if the configuration - file can not be parsed. - :raises NotmuchError: if the config file does not have the - database.path setting. - :raises FileError: if the database already exists. - - :returns: The newly created instance. - """ - if path is None: - path = cls.default_path() - if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): - path = bytes(path) - db_pp = capi.ffi.new('notmuch_database_t **') - cmsg = capi.ffi.new('char**') - ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path), - db_pp, cmsg) - if cmsg[0]: - msg = capi.ffi.string(cmsg[0]).decode(errors='replace') - capi.lib.free(cmsg[0]) - else: - msg = None - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret, msg) - - # Now close the db and let __init__ open it. Inefficient but - # creating is not a hot loop while this allows us to have a - # clean API. - ret = capi.lib.notmuch_database_destroy(db_pp[0]) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - return cls(path, cls.MODE.READ_WRITE) - - @staticmethod - def default_path(cfg_path=None): - """Return the path of the user's default database. - - This reads the user's configuration file and returns the - default path of the database. - - :param cfg_path: The pathname of the notmuch configuration file. - If not specified tries to use the pathname provided in the - :env:`NOTMUCH_CONFIG` environment variable and falls back - to :file:`~/.notmuch-config. - :type cfg_path: str, bytes, os.PathLike or pathlib.Path. - - :returns: The path of the database, which does not necessarily - exists. - :rtype: pathlib.Path - :raises OSError: or subclasses if the configuration file can not - be opened. - :raises configparser.Error: or subclasses if the configuration - file can not be parsed. - :raises NotmuchError if the config file does not have the - database.path setting. - """ - if not cfg_path: - cfg_path = _config_pathname() - if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path): - cfg_path = bytes(cfg_path) - parser = configparser.ConfigParser() - with open(cfg_path) as fp: - parser.read_file(fp) - try: - return pathlib.Path(parser.get('database', 'path')) - except configparser.Error: - raise errors.NotmuchError( - 'No database.path setting in {}'.format(cfg_path)) - - def __del__(self): - self._destroy() - - @property - def alive(self): - try: - self._db_p - except errors.ObjectDestroyedError: - return False - else: - return True - - def _destroy(self): - try: - ret = capi.lib.notmuch_database_destroy(self._db_p) - except errors.ObjectDestroyedError: - ret = capi.lib.NOTMUCH_STATUS_SUCCESS - else: - self._db_p = None - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - - def close(self): - """Close the notmuch database. - - Once closed most operations will fail. This can still be - useful however to explicitly close a database which is opened - read-write as this would otherwise stop other processes from - reading the database while it is open. - - :raises ObjectDestroyedError: if used after destroyed. - """ - ret = capi.lib.notmuch_database_close(self._db_p) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - self.closed = True - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - @property - def path(self): - """The pathname of the notmuch database. - - This is returned as a :class:`pathlib.Path` instance. - - :raises ObjectDestroyedError: if used after destoryed. - """ - try: - return self._cache_path - except AttributeError: - ret = capi.lib.notmuch_database_get_path(self._db_p) - self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret))) - return self._cache_path - - @property - def version(self): - """The database format version. - - This is a positive integer. - - :raises ObjectDestroyedError: if used after destoryed. - """ - try: - return self._cache_version - except AttributeError: - ret = capi.lib.notmuch_database_get_version(self._db_p) - self._cache_version = ret - return ret - - @property - def needs_upgrade(self): - """Whether the database should be upgraded. - - If *True* the database can be upgraded using :meth:`upgrade`. - Not doing so may result in some operations raising - :exc:`UpgradeRequiredError`. - - A read-only database will never be upgradable. - - :raises ObjectDestroyedError: if used after destoryed. - """ - ret = capi.lib.notmuch_database_needs_upgrade(self._db_p) - return bool(ret) - - def upgrade(self, progress_cb=None): - """Upgrade the database to the latest version. - - Upgrade the database, optionally with a progress callback - which should be a callable which will be called with a - floating point number in the range of [0.0 .. 1.0]. - """ - raise NotImplementedError - - def atomic(self): - """Return a context manager to perform atomic operations. - - The returned context manager can be used to perform atomic - operations on the database. - - .. note:: Unlinke a traditional RDBMS transaction this does - not imply durability, it only ensures the changes are - performed atomically. - - :raises ObjectDestroyedError: if used after destoryed. - """ - ctx = AtomicContext(self, '_db_p') - return ctx - - def revision(self): - """The currently committed revision in the database. - - Returned as a ``(revision, uuid)`` namedtuple. - - :raises ObjectDestroyedError: if used after destoryed. - """ - raw_uuid = capi.ffi.new('char**') - rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid) - return DbRevision(rev, capi.ffi.string(raw_uuid[0])) - - def get_directory(self, path): - raise NotImplementedError - - def add(self, filename, *, sync_flags=False): - """Add a message to the database. - - Add a new message to the notmuch database. The message is - referred to by the pathname of the maildir file. If the - message ID of the new message already exists in the database, - this adds ``pathname`` to the list of list of files for the - existing message. - - :param filename: The path of the file containing the message. - :type filename: str, bytes, os.PathLike or pathlib.Path. - :param sync_flags: Whether to sync the known maildir flags to - notmuch tags. See :meth:`Message.flags_to_tags` for - details. - - :returns: A tuple where the first item is the newly inserted - messages as a :class:`Message` instance, and the second - item is a boolean indicating if the message inserted was a - duplicate. This is the namedtuple ``AddedMessage(msg, - dup)``. - :rtype: Database.AddedMessage - - If an exception is raised, no message was added. - - :raises XapianError: A Xapian exception occurred. - :raises FileError: The file referred to by ``pathname`` could - not be opened. - :raises FileNotEmailError: The file referreed to by - ``pathname`` is not recognised as an email message. - :raises ReadOnlyDatabaseError: The database is opened in - READ_ONLY mode. - :raises UpgradeRequiredError: The database must be upgraded - first. - :raises ObjectDestroyedError: if used after destoryed. - """ - if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path): - filename = bytes(filename) - msg_pp = capi.ffi.new('notmuch_message_t **') - ret = capi.lib.notmuch_database_add_message(self._db_p, - os.fsencode(filename), - msg_pp) - ok = [capi.lib.NOTMUCH_STATUS_SUCCESS, - capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID] - if ret not in ok: - raise errors.NotmuchError(ret) - msg = message.Message(self, msg_pp[0], db=self) - if sync_flags: - msg.tags.from_maildir_flags() - return self.AddedMessage( - msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) - - def remove(self, filename): - """Remove a message from the notmuch database. - - Removing a message which is not in the database is just a - silent nop-operation. - - :param filename: The pathname of the file containing the - message to be removed. - :type filename: str, bytes, os.PathLike or pathlib.Path. - - :returns: True if the message is still in the database. This - can happen when multiple files contain the same message ID. - The true/false distinction is fairly arbitrary, but think - of it as ``dup = db.remove_message(name); if dup: ...``. - :rtype: bool - - :raises XapianError: A Xapian exception ocurred. - :raises ReadOnlyDatabaseError: The database is opened in - READ_ONLY mode. - :raises UpgradeRequiredError: The database must be upgraded - first. - :raises ObjectDestroyedError: if used after destoryed. - """ - if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path): - filename = bytes(filename) - ret = capi.lib.notmuch_database_remove_message(self._db_p, - os.fsencode(filename)) - ok = [capi.lib.NOTMUCH_STATUS_SUCCESS, - capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID] - if ret not in ok: - raise errors.NotmuchError(ret) - if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: - return True - else: - return False - - def find(self, msgid): - """Return the message matching the given message ID. - - If a message with the given message ID is found a - :class:`Message` instance is returned. Otherwise a - :exc:`LookupError` is raised. - - :param msgid: The message ID to look for. - :type msgid: str - - :returns: The message instance. - :rtype: Message - - :raises LookupError: If no message was found. - :raises OutOfMemoryError: When there is no memory to allocate - the message instance. - :raises XapianError: A Xapian exception ocurred. - :raises ObjectDestroyedError: if used after destoryed. - """ - msg_pp = capi.ffi.new('notmuch_message_t **') - ret = capi.lib.notmuch_database_find_message(self._db_p, - msgid.encode(), msg_pp) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - msg_p = msg_pp[0] - if msg_p == capi.ffi.NULL: - raise LookupError - msg = message.Message(self, msg_p, db=self) - return msg - - def get(self, filename): - """Return the :class:`Message` given a pathname. - - If a message with the given pathname exists in the database - return the :class:`Message` instance for the message. - Otherwise raise a :exc:`LookupError` exception. - - :param filename: The pathname of the message. - :type filename: str, bytes, os.PathLike or pathlib.Path - - :returns: The message instance. - :rtype: Message - - :raises LookupError: If no message was found. This is also - a subclass of :exc:`KeyError`. - :raises OutOfMemoryError: When there is no memory to allocate - the message instance. - :raises XapianError: A Xapian exception ocurred. - :raises ObjectDestroyedError: if used after destoryed. - """ - if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path): - filename = bytes(filename) - msg_pp = capi.ffi.new('notmuch_message_t **') - ret = capi.lib.notmuch_database_find_message_by_filename( - self._db_p, os.fsencode(filename), msg_pp) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - msg_p = msg_pp[0] - if msg_p == capi.ffi.NULL: - raise LookupError - msg = message.Message(self, msg_p, db=self) - return msg - - @property - def tags(self): - """Return an immutable set with all tags used in this database. - - This returns an immutable set-like object implementing the - collections.abc.Set Abstract Base Class. Due to the - underlying libnotmuch implementation some operations have - different performance characteristics then plain set objects. - Mainly any lookup operation is O(n) rather then O(1). - - Normal usage treats tags as UTF-8 encoded unicode strings so - they are exposed to Python as normal unicode string objects. - If you need to handle tags stored in libnotmuch which are not - valid unicode do check the :class:`ImmutableTagSet` docs for - how to handle this. - - :rtype: ImmutableTagSet - - :raises ObjectDestroyedError: if used after destoryed. - """ - try: - ref = self._cached_tagset - except AttributeError: - tagset = None - else: - tagset = ref() - if tagset is None: - tagset = tags.ImmutableTagSet( - self, '_db_p', capi.lib.notmuch_database_get_all_tags) - self._cached_tagset = weakref.ref(tagset) - return tagset - - def _create_query(self, query, *, - omit_excluded=EXCLUDE.TRUE, - sort=SORT.UNSORTED, # Check this default - exclude_tags=None): - """Create an internal query object. - - :raises OutOfMemoryError: if no memory is available to - allocate the query. - """ - if isinstance(query, str): - query = query.encode('utf-8') - query_p = capi.lib.notmuch_query_create(self._db_p, query) - if query_p == capi.ffi.NULL: - raise errors.OutOfMemoryError() - capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value) - capi.lib.notmuch_query_set_sort(query_p, sort.value) - if exclude_tags is not None: - for tag in exclude_tags: - if isinstance(tag, str): - tag = str.encode('utf-8') - capi.lib.notmuch_query_add_tag_exclude(query_p, tag) - return querymod.Query(self, query_p) - - def messages(self, query, *, - omit_excluded=EXCLUDE.TRUE, - sort=SORT.UNSORTED, # Check this default - exclude_tags=None): - """Search the database for messages. - - :returns: An iterator over the messages found. - :rtype: MessageIter - - :raises OutOfMemoryError: if no memory is available to - allocate the query. - :raises ObjectDestroyedError: if used after destoryed. - """ - query = self._create_query(query, - omit_excluded=omit_excluded, - sort=sort, - exclude_tags=exclude_tags) - return query.messages() - - def count_messages(self, query, *, - omit_excluded=EXCLUDE.TRUE, - sort=SORT.UNSORTED, # Check this default - exclude_tags=None): - """Search the database for messages. - - :returns: An iterator over the messages found. - :rtype: MessageIter - - :raises ObjectDestroyedError: if used after destoryed. - """ - query = self._create_query(query, - omit_excluded=omit_excluded, - sort=sort, - exclude_tags=exclude_tags) - return query.count_messages() - - def threads(self, query, *, - omit_excluded=EXCLUDE.TRUE, - sort=SORT.UNSORTED, # Check this default - exclude_tags=None): - query = self._create_query(query, - omit_excluded=omit_excluded, - sort=sort, - exclude_tags=exclude_tags) - return query.threads() - - def count_threads(self, query, *, - omit_excluded=EXCLUDE.TRUE, - sort=SORT.UNSORTED, # Check this default - exclude_tags=None): - query = self._create_query(query, - omit_excluded=omit_excluded, - sort=sort, - exclude_tags=exclude_tags) - return query.count_threads() - - def status_string(self): - raise NotImplementedError - - def __repr__(self): - return 'Database(path={self.path}, mode={self.mode})'.format(self=self) - - -class AtomicContext: - """Context manager for atomic support. - - This supports the notmuch_database_begin_atomic and - notmuch_database_end_atomic API calls. The object can not be - directly instantiated by the user, only via ``Database.atomic``. - It does keep a reference to the :class:`Database` instance to keep - the C memory alive. - - :raises XapianError: When this is raised at enter time the atomic - section is not active. When it is raised at exit time the - atomic section is still active and you may need to try using - :meth:`force_end`. - :raises ObjectDestroyedError: if used after destoryed. - """ - - def __init__(self, db, ptr_name): - self._db = db - self._ptr = lambda: getattr(db, ptr_name) - - def __del__(self): - self._destroy() - - @property - def alive(self): - return self.parent.alive - - def _destroy(self): - pass - - def __enter__(self): - ret = capi.lib.notmuch_database_begin_atomic(self._ptr()) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - return self - - def __exit__(self, exc_type, exc_value, traceback): - ret = capi.lib.notmuch_database_end_atomic(self._ptr()) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - - def force_end(self): - """Force ending the atomic section. - - This can only be called once __exit__ has been called. It - will attept to close the atomic section (again). This is - useful if the original exit raised an exception and the atomic - section is still open. But things are pretty ugly by now. - - :raises XapianError: If exiting fails, the atomic section is - not ended. - :raises UnbalancedAtomicError: If the database was currently - not in an atomic section. - :raises ObjectDestroyedError: if used after destoryed. - """ - ret = capi.lib.notmuch_database_end_atomic(self._ptr()) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - - -@functools.total_ordering -class DbRevision: - """A database revision. - - The database revision number increases monotonically with each - commit to the database. Which means user-visible changes can be - ordered. This object is sortable with other revisions. It - carries the UUID of the database to ensure it is only ever - compared with revisions from the same database. - """ - - def __init__(self, rev, uuid): - self._rev = rev - self._uuid = uuid - - @property - def rev(self): - """The revision number, a positive integer.""" - return self._rev - - @property - def uuid(self): - """The UUID of the database, consider this opaque.""" - return self._uuid - - def __eq__(self, other): - if isinstance(other, self.__class__): - if self.uuid != other.uuid: - return False - return self.rev == other.rev - else: - return NotImplemented - - def __lt__(self, other): - if self.__class__ is other.__class__: - if self.uuid != other.uuid: - return False - return self.rev < other.rev - else: - return NotImplemented - - def __repr__(self): - return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self) |
