X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=bindings%2Fpython%2Fnotmuch%2Fdatabase.py;h=25b4b1b1ca33a3202f9f8042ff2cad05d8412823;hp=7f0091de583044b91d696f5646a15075a69c1d88;hb=3434d194026ff65217d9342ffe511f67fd71e79f;hpb=c50524efd523e885f370374e33cd5d5549ed9079 diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py index 7f0091de..25b4b1b1 100644 --- a/bindings/python/notmuch/database.py +++ b/bindings/python/notmuch/database.py @@ -18,62 +18,92 @@ Copyright 2010 Sebastian Spaeth ' """ import os -from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref -from notmuch.globals import nmlib, STATUS, NotmuchError, Enum +from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref, POINTER +from notmuch.globals import (nmlib, STATUS, NotmuchError, NotInitializedError, + NullPointerError, OutOfMemoryError, XapianError, Enum, _str, + NotmuchDatabaseP, NotmuchDirectoryP, NotmuchMessageP, NotmuchTagsP, + NotmuchQueryP, NotmuchMessagesP, NotmuchThreadsP, NotmuchFilenamesP) from notmuch.thread import Threads from notmuch.message import Messages, Message from notmuch.tag import Tags class Database(object): - """Represents a notmuch database (wraps notmuch_database_t) - - .. note:: Do remember that as soon as we tear down this object, - all underlying derived objects such as queries, threads, - messages, tags etc will be freed by the underlying library - as well. Accessing these objects will lead to segfaults and - other unexpected behavior. See above for more details. + """The :class:`Database` is the highest-level object that notmuch + provides. It references a notmuch database, and can be opened in + read-only or read-write mode. A :class:`Query` can be derived from + or be applied to a specific database to find messages. Also adding + and removing messages to the database happens via this + object. Modifications to the database are not atmic by default (see + :meth:`begin_atomic`) and once a database has been modified, all + other database objects pointing to the same data-base will throw an + :exc:`XapianError` as the underlying database has been + modified. Close and reopen the database to continue working with it. + + .. note:: Any function in this class can and will throw an + :exc:`NotInitializedError` if the database was not + intitialized properly. + + .. note:: Do remember that as soon as we tear down (e.g. via `del + db`) this object, all underlying derived objects such as + queries, threads, messages, tags etc will be freed by the + underlying library as well. Accessing these objects will lead + to segfaults and other unexpected behavior. See above for + more details. """ _std_db_path = None """Class attribute to cache user's default database""" - MODE = Enum(['READ_ONLY','READ_WRITE']) + MODE = Enum(['READ_ONLY', 'READ_WRITE']) """Constants: Mode in which to open the database""" """notmuch_database_get_directory""" _get_directory = nmlib.notmuch_database_get_directory - _get_directory.restype = c_void_p + _get_directory.argtypes = [NotmuchDatabaseP, c_char_p] + _get_directory.restype = NotmuchDirectoryP """notmuch_database_get_path""" _get_path = nmlib.notmuch_database_get_path + _get_path.argtypes = [NotmuchDatabaseP] _get_path.restype = c_char_p """notmuch_database_get_version""" _get_version = nmlib.notmuch_database_get_version + _get_version.argtypes = [NotmuchDatabaseP] _get_version.restype = c_uint """notmuch_database_open""" - _open = nmlib.notmuch_database_open - _open.restype = c_void_p + _open = nmlib.notmuch_database_open + _open.argtypes = [c_char_p, c_uint] + _open.restype = NotmuchDatabaseP """notmuch_database_upgrade""" _upgrade = nmlib.notmuch_database_upgrade - _upgrade.argtypes = [c_void_p, c_void_p, c_void_p] + _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p] + _upgrade.restype = c_uint """ notmuch_database_find_message""" _find_message = nmlib.notmuch_database_find_message - _find_message.restype = c_void_p + _find_message.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchMessageP)] + _find_message.restype = c_uint + + """notmuch_database_find_message_by_filename""" + _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename + _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchMessageP)] + _find_message_by_filename.restype = c_uint """notmuch_database_get_all_tags""" _get_all_tags = nmlib.notmuch_database_get_all_tags - _get_all_tags.restype = c_void_p + _get_all_tags.argtypes = [NotmuchDatabaseP] + _get_all_tags.restype = NotmuchTagsP """notmuch_database_create""" _create = nmlib.notmuch_database_create - _create.restype = c_void_p + _create.argtypes = [c_char_p] + _create.restype = NotmuchDatabaseP - def __init__(self, path=None, create=False, mode= 0): - """If *path* is `None`, we will try to read a users notmuch - configuration and use his configured database. The location of the + def __init__(self, path=None, create=False, mode=0): + """If *path* is `None`, we will try to read a users notmuch + configuration and use his configured database. The location of the configuration file can be specified through the environment variable *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`. @@ -84,13 +114,13 @@ class Database(object): above for behavior if `None`) :type path: `str` or `None` :param create: Pass `False` to open an existing, `True` to create a new - database. + database. :type create: bool - :param mode: Mode to open a database in. Is always + :param mode: Mode to open a database in. Is always :attr:`MODE`.READ_WRITE when creating a new one. :type mode: :attr:`MODE` - :returns: Nothing - :exception: :exc:`NotmuchError` in case of failure. + :exception: :exc:`NotmuchError` or derived exception in case of + failure. """ self._db = None if path is None: @@ -105,10 +135,10 @@ class Database(object): else: self.create(path) - def _verify_initialized_db(self): - """Raises a NotmuchError in case self._db is still None""" + def _assert_db_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._db is `None`""" if self._db is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + raise NotInitializedError() def create(self, path): """Creates a new notmuch database @@ -124,20 +154,20 @@ class Database(object): :type path: str :returns: Nothing :exception: :exc:`NotmuchError` in case of any failure - (after printing an error message on stderr). + (possibly after printing an error message on stderr). """ if self._db is not None: - raise NotmuchError( - message="Cannot create db, this Database() already has an open one.") + raise NotmuchError(message="Cannot create db, this Database() " + "already has an open one.") - res = Database._create(path, Database.MODE.READ_WRITE) + res = Database._create(_str(path), Database.MODE.READ_WRITE) if res is None: raise NotmuchError( message="Could not create the specified database") self._db = res - def open(self, path, mode= 0): + def open(self, path, mode=0): """Opens an existing database This function is used by __init__() and usually does not need @@ -145,39 +175,33 @@ class Database(object): *notmuch_database_open* function. :param status: Open the database in read-only or read-write mode - :type status: :attr:`MODE` + :type status: :attr:`MODE` :returns: Nothing :exception: Raises :exc:`NotmuchError` in case - of any failure (after printing an error message on stderr). + of any failure (possibly after printing an error message on stderr). """ - - res = Database._open(path, mode) + res = Database._open(_str(path), mode) if res is None: - raise NotmuchError( - message="Could not open the specified database") + raise NotmuchError(message="Could not open the specified database") self._db = res def get_path(self): - """Returns the file path of an open database - - Wraps *notmuch_database_get_path*.""" - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - return Database._get_path(self._db) + """Returns the file path of an open database""" + self._assert_db_is_initialized() + return Database._get_path(self._db).decode('utf-8') def get_version(self): """Returns the database format version :returns: The database version as positive integer - :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if - the database was not intitialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() + self._assert_db_is_initialized() + return Database._get_version(self._db) - return Database._get_version (self._db) + _needs_upgrade = nmlib.notmuch_database_needs_upgrade + _needs_upgrade.argtypes = [NotmuchDatabaseP] + _needs_upgrade.restype = bool def needs_upgrade(self): """Does this database need to be upgraded before writing to it? @@ -188,13 +212,9 @@ class Database(object): etc.) will work unless :meth:`upgrade` is called successfully first. :returns: `True` or `False` - :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if - the database was not intitialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - return notmuch_database_needs_upgrade(self._db) + self._assert_db_is_initialized() + return self._needs_upgrade(self._db) def upgrade(self): """Upgrades the current database @@ -206,119 +226,177 @@ class Database(object): NOT IMPLEMENTED: The optional progress_notify callback can be used by the caller to provide progress indication to the user. If non-NULL it will be called periodically with - 'progress' as a floating-point value in the range of [0.0..1.0] + 'progress' as a floating-point value in the range of [0.0..1.0] indicating the progress made so far in the upgrade process. :TODO: catch exceptions, document return values and etc... """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - status = Database._upgrade (self._db, None, None) + self._assert_db_is_initialized() + status = Database._upgrade(self._db, None, None) #TODO: catch exceptions, document return values and etc return status + _begin_atomic = nmlib.notmuch_database_begin_atomic + _begin_atomic.argtypes = [NotmuchDatabaseP] + _begin_atomic.restype = c_uint + + def begin_atomic(self): + """Begin an atomic database operation + + Any modifications performed between a successful + :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to + the database atomically. Note that, unlike a typical database + transaction, this only ensures atomicity, not durability; + neither begin nor end necessarily flush modifications to disk. + + :returns: :attr:`STATUS`.SUCCESS or raises + + :exception: :exc:`NotmuchError`: + :attr:`STATUS`.XAPIAN_EXCEPTION + Xapian exception occurred; atomic section not entered. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = self._begin_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + + _end_atomic = nmlib.notmuch_database_end_atomic + _end_atomic.argtypes = [NotmuchDatabaseP] + _end_atomic.restype = c_uint + + def end_atomic(self): + """Indicate the end of an atomic database operation + + See :meth:`begin_atomic` for details. + + :returns: :attr:`STATUS`.SUCCESS or raises + + :exception: + :exc:`NotmuchError`: + :attr:`STATUS`.XAPIAN_EXCEPTION + A Xapian exception occurred; atomic section not + ended. + :attr:`STATUS`.UNBALANCED_ATOMIC: + end_atomic has been called more times than begin_atomic. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = self._end_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + def get_directory(self, path): - """Returns a :class:`Directory` of path, + """Returns a :class:`Directory` of path, (creating it if it does not exist(?)) - .. warning:: This call needs a writeable database in - Database.MODE.READ_WRITE mode. The underlying library will exit the + .. warning:: This call needs a writeable database in + :attr:`Database.MODE`.READ_WRITE mode. The underlying library will exit the program if this method is used on a read-only database! - :param path: A str containing the path relative to the path of database - (see :meth:`get_path`), or else should be an absolute path + :param path: An unicode string containing the path relative to the path + of database (see :meth:`get_path`), or else should be an absolute path with initial components that match the path of 'database'. :returns: :class:`Directory` or raises an exception. - :exception: :exc:`NotmuchError` - - STATUS.NOT_INITIALIZED - If the database was not intitialized. - - STATUS.FILE_ERROR - If path is not relative database or absolute with initial + :exception: + :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR + If path is not relative database or absolute with initial components same as database. - """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - + self._assert_db_is_initialized() # sanity checking if path is valid, and make path absolute if path[0] == os.sep: # we got an absolute path if not path.startswith(self.get_path()): # but its initial components are not equal to the db path - raise NotmuchError(STATUS.FILE_ERROR, - message="Database().get_directory() called with a wrong absolute path.") + raise NotmuchError(STATUS.FILE_ERROR, + message="Database().get_directory() called " + "with a wrong absolute path.") abs_dirpath = path else: #we got a relative path, make it absolute - abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path)) + abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) - dir_p = Database._get_directory(self._db, path); + dir_p = Database._get_directory(self._db, _str(path)) # return the Directory, init it with the absolute path - return Directory(abs_dirpath, dir_p, self) + return Directory(_str(abs_dirpath), dir_p, self) + + _add_message = nmlib.notmuch_database_add_message + _add_message.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchMessageP)] + _add_message.restype = c_uint - def add_message(self, filename): + def add_message(self, filename, sync_maildir_flags=False): """Adds a new message to the database - `filename` should be a path relative to the path of the open - database (see :meth:`get_path`), or else should be an absolute - filename with initial components that match the path of the - database. + :param filename: should be a path relative to the path of the + open database (see :meth:`get_path`), or else should be an + absolute filename with initial components that match the + path of the database. + + The file should be a single mail message (not a + multi-message mbox) that is expected to remain at its + current location, since the notmuch database will reference + the filename, and will not copy the entire contents of the + file. - The file should be a single mail message (not a multi-message mbox) - that is expected to remain at its current location, since the - notmuch database will reference the filename, and will not copy the - entire contents of the file. + :param sync_maildir_flags: If the message contains Maildir + flags, we will -depending on the notmuch configuration- sync + those tags to initial notmuch tags, if set to `True`. It is + `False` by default to remain consistent with the libnotmuch + API. You might want to look into the underlying method + :meth:`Message.maildir_flags_to_tags`. - :returns: On success, we return + :returns: On success, we return 1) a :class:`Message` object that can be used for things such as adding tags to the just-added message. - 2) one of the following STATUS values: + 2) one of the following :attr:`STATUS` values: - STATUS.SUCCESS + :attr:`STATUS`.SUCCESS Message successfully added to database. - STATUS.DUPLICATE_MESSAGE_ID + :attr:`STATUS`.DUPLICATE_MESSAGE_ID Message has the same message ID as another message already in the database. The new filename was successfully added - to the message in the database. + to the list of the filenames for the existing message. - :rtype: 2-tuple(:class:`Message`, STATUS) + :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`) :exception: Raises a :exc:`NotmuchError` with the following meaning. If such an exception occurs, nothing was added to the database. - STATUS.FILE_ERROR - An error occurred trying to open the file, (such as + :attr:`STATUS`.FILE_ERROR + An error occurred trying to open the file, (such as permission denied, or file not found, etc.). - STATUS.FILE_NOT_EMAIL - The contents of filename don't look like an email message. - STATUS.READ_ONLY_DATABASE + :attr:`STATUS`.FILE_NOT_EMAIL + The contents of filename don't look like an email + message. + :attr:`STATUS`.READ_ONLY_DATABASE Database was opened in read-only mode so no message can be added. - STATUS.NOT_INITIALIZED - The database has not been initialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - + self._assert_db_is_initialized() msg_p = c_void_p() - status = nmlib.notmuch_database_add_message(self._db, - filename, - byref(msg_p)) - - if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]: + status = self._add_message(self._db, _str(filename), byref(msg_p)) + + if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: raise NotmuchError(status) #construct Message() and return msg = Message(msg_p, self) + #automatic sync initial tags from Maildir flags + if sync_maildir_flags: + msg.maildir_flags_to_tags() return (msg, status) + _remove_message = nmlib.notmuch_database_remove_message + _remove_message.argtypes = [NotmuchDatabaseP, c_char_p] + _remove_message.restype = c_uint + def remove_message(self, filename): - """Removes a message from the given notmuch database + """Removes a message (filename) from the given notmuch database Note that only this particular filename association is removed from the database. If the same message (as determined by the message ID) @@ -327,29 +405,25 @@ class Database(object): is removed for a particular message, the database content for that message will be entirely removed. - :returns: A STATUS value with the following meaning: + :returns: A :attr:`STATUS` value with the following meaning: - STATUS.SUCCESS - The last filename was removed and the message was removed + :attr:`STATUS`.SUCCESS + The last filename was removed and the message was removed from the database. - STATUS.DUPLICATE_MESSAGE_ID - This filename was removed but the message persists in the + :attr:`STATUS`.DUPLICATE_MESSAGE_ID + This filename was removed but the message persists in the database with at least one other filename. :exception: Raises a :exc:`NotmuchError` with the following meaning. - If such an exception occurs, nothing was removed from the database. + If such an exception occurs, nothing was removed from the + database. - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so no message can be + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so no message can be removed. - STATUS.NOT_INITIALIZED - The database has not been initialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - return nmlib.notmuch_database_remove_message(self._db, - filename) + self._assert_db_is_initialized() + return self._remove_message(self._db, filename) def find_message(self, msgid): """Returns a :class:`Message` as identified by its message ID @@ -357,36 +431,69 @@ class Database(object): Wraps the underlying *notmuch_database_find_message* function. :param msgid: The message ID - :type msgid: string - :returns: :class:`Message` or `None` if no message is found or - if any xapian exception or out-of-memory situation - occurs. Do note that Xapian Exceptions include - "Database modified" situations, e.g. when the - notmuch database has been modified by - another program in the meantime. A return value of - `None` is therefore no guarantee that the message - does not exist. - :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if - the database was not intitialized. + :type msgid: unicode or str + :returns: :class:`Message` or `None` if no message is found. + :exception: + :exc:`OutOfMemoryError` + If an Out-of-memory occured while constructing the message. + :exc:`XapianError` + In case of a Xapian Exception. These exceptions + include "Database modified" situations, e.g. when the + notmuch database has been modified by another program + in the meantime. In this case, you should close and + reopen the database and retry. + :exc:`NotInitializedError` if + the database was not intitialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() + self._assert_db_is_initialized() + msg_p = c_void_p() + status = Database._find_message(self._db, _str(msgid), byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None + + def find_message_by_filename(self, filename): + """Find a message with the given filename + + .. warning:: This call needs a writeable database in + :attr:`Database.MODE`.READ_WRITE mode. The underlying library will + exit the program if this method is used on a read-only + database! + + :returns: If the database contains a message with the given + filename, then a class:`Message:` is returned. This + function returns None if no message is found with the given + filename. + + :exception: + :exc:`OutOfMemoryError` + If an Out-of-memory occured while constructing the message. + :exc:`XapianError` + In case of a Xapian Exception. These exceptions + include "Database modified" situations, e.g. when the + notmuch database has been modified by another program + in the meantime. In this case, you should close and + reopen the database and retry. + :exc:`NotInitializedError` if + the database was not intitialized. - msg_p = Database._find_message(self._db, msgid) - if msg_p is None: - return None - return Message(msg_p, self) + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + msg_p = c_void_p() + status = Database._find_message_by_filename(self._db, _str(filename), + byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None def get_all_tags(self): """Returns :class:`Tags` with a list of all tags found in the database :returns: :class:`Tags` - :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error + :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER on error """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - tags_p = Database._get_all_tags (self._db) + self._assert_db_is_initialized() + tags_p = Database._get_all_tags(self._db) if tags_p == None: raise NotmuchError(STATUS.NULL_POINTER) return Tags(tags_p, self) @@ -409,18 +516,19 @@ class Database(object): This function is a python extension and not in the underlying C API. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - return Query(self, querystring) def __repr__(self): return "'Notmuch DB " + self.get_path() + "'" + _close = nmlib.notmuch_database_close + _close.argtypes = [NotmuchDatabaseP] + _close.restype = None + def __del__(self): """Close and free the notmuch database if needed""" if self._db is not None: - nmlib.notmuch_database_close(self._db) + self._close(self._db) def _get_user_default_db(self): """ Reads a user's notmuch config and returns his db location @@ -431,10 +539,10 @@ class Database(object): conf_f = os.getenv('NOTMUCH_CONFIG', os.path.expanduser('~/.notmuch-config')) config.read(conf_f) - if not config.has_option('database','path'): - raise NotmuchError(message= - "No DB path specified and no user default found") - return config.get('database','path') + if not config.has_option('database', 'path'): + raise NotmuchError(message="No DB path specified" + " and no user default found") + return config.get('database', 'path').decode('utf-8') @property def db_p(self): @@ -445,18 +553,19 @@ class Database(object): """ return self._db -#------------------------------------------------------------------------------ + class Query(object): """Represents a search query on an opened :class:`Database`. A query selects and filters a subset of messages from the notmuch database we derive from. - Query() provides an instance attribute :attr:`sort`, which + :class:`Query` provides an instance attribute :attr:`sort`, which contains the sort order (if specified via :meth:`set_sort`) or `None`. - Technically, it wraps the underlying *notmuch_query_t* struct. + Any function in this class may throw an :exc:`NotInitializedError` + in case the underlying query object was not set up correctly. .. note:: Do remember that as soon as we tear down this object, all underlying derived objects such as threads, @@ -465,24 +574,27 @@ class Query(object): other unexpected behavior. See above for more details. """ # constants - SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID', 'UNSORTED']) + SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED']) """Constants: Sort order in which to return results""" """notmuch_query_create""" _create = nmlib.notmuch_query_create - _create.restype = c_void_p + _create.argtypes = [NotmuchDatabaseP, c_char_p] + _create.restype = NotmuchQueryP """notmuch_query_search_threads""" _search_threads = nmlib.notmuch_query_search_threads - _search_threads.restype = c_void_p + _search_threads.argtypes = [NotmuchQueryP] + _search_threads.restype = NotmuchThreadsP """notmuch_query_search_messages""" _search_messages = nmlib.notmuch_query_search_messages - _search_messages.restype = c_void_p - + _search_messages.argtypes = [NotmuchQueryP] + _search_messages.restype = NotmuchMessagesP """notmuch_query_count_messages""" _count_messages = nmlib.notmuch_query_count_messages + _count_messages.argtypes = [NotmuchQueryP] _count_messages.restype = c_uint def __init__(self, db, querystr): @@ -490,109 +602,91 @@ class Query(object): :param db: An open database which we derive the Query from. :type db: :class:`Database` :param querystr: The query string for the message. - :type querystr: str + :type querystr: utf-8 encoded str or unicode """ self._db = None self._query = None self.sort = None self.create(db, querystr) + def _assert_query_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._query is `None`""" + if self._query is None: + raise NotInitializedError() + def create(self, db, querystr): """Creates a new query derived from a Database - This function is utilized by __init__() and usually does not need to + This function is utilized by __init__() and usually does not need to be called directly. :param db: Database to create the query from. :type db: :class:`Database` :param querystr: The query string - :type querystr: str + :type querystr: utf-8 encoded str or unicode :returns: Nothing - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if db is not inited - * STATUS.NULL_POINTER if the query creation failed - (too little memory) + :exception: + :exc:`NullPointerError` if the query creation failed + (e.g. too little memory). + :exc:`NotInitializedError` if the underlying db was not + intitialized. """ - if db.db_p is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + db._assert_db_is_initialized() # create reference to parent db to keep it alive self._db = db - # create query, return None if too little mem available - query_p = Query._create(db.db_p, querystr) + query_p = Query._create(db.db_p, _str(querystr)) if query_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NullPointerError self._query = query_p + _set_sort = nmlib.notmuch_query_set_sort + _set_sort.argtypes = [NotmuchQueryP, c_uint] + _set_sort.argtypes = None + def set_sort(self, sort): """Set the sort order future results will be delivered in - Wraps the underlying *notmuch_query_set_sort* function. - :param sort: Sort order (see :attr:`Query.SORT`) - :returns: Nothing - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not - been initialized. """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() self.sort = sort - nmlib.notmuch_query_set_sort(self._query, sort) + self._set_sort(self._query, sort) def search_threads(self): """Execute a query for threads Execute a query for threads, returning a :class:`Threads` iterator. - The returned threads are owned by the query and as such, will only be + The returned threads are owned by the query and as such, will only be valid until the Query is deleted. The method sets :attr:`Message.FLAG`\.MATCH for those messages that match the query. The method :meth:`Message.get_flag` allows us to get the value of this flag. - Technically, it wraps the underlying - *notmuch_query_search_threads* function. - :returns: :class:`Threads` - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if query is not inited - * STATUS.NULL_POINTER if search_threads failed + :exception: :exc:`NullPointerError` if search_threads failed """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() threads_p = Query._search_threads(self._query) if threads_p is None: - NotmuchError(STATUS.NULL_POINTER) - - return Threads(threads_p,self) + raise NullPointerError + return Threads(threads_p, self) def search_messages(self): """Filter messages according to the query and return :class:`Messages` in the defined sort order - Technically, it wraps the underlying - *notmuch_query_search_messages* function. - :returns: :class:`Messages` - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if query is not inited - * STATUS.NULL_POINTER if search_messages failed + :exception: :exc:`NullPointerError` if search_messages failed """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() msgs_p = Query._search_messages(self._query) if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) - - return Messages(msgs_p,self) + raise NullPointerError + return Messages(msgs_p, self) def count_messages(self): """Estimate the number of messages matching the query @@ -605,22 +699,20 @@ class Query(object): *notmuch_query_count_messages* function. :returns: :class:`Messages` - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if query is not inited """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() return Query._count_messages(self._query) + _destroy = nmlib.notmuch_query_destroy + _destroy.argtypes = [NotmuchQueryP] + _destroy.restype = None + def __del__(self): """Close and free the Query""" if self._query is not None: - nmlib.notmuch_query_destroy (self._query) + self._destroy(self._query) -#------------------------------------------------------------------------------ class Directory(object): """Represents a directory entry in the notmuch directory @@ -637,28 +729,32 @@ class Directory(object): """notmuch_directory_get_mtime""" _get_mtime = nmlib.notmuch_directory_get_mtime + _get_mtime.argtypes = [NotmuchDirectoryP] _get_mtime.restype = c_long """notmuch_directory_set_mtime""" _set_mtime = nmlib.notmuch_directory_set_mtime - _set_mtime.argtypes = [c_char_p, c_long] + _set_mtime.argtypes = [NotmuchDirectoryP, c_long] + _set_mtime.restype = c_uint """notmuch_directory_get_child_files""" _get_child_files = nmlib.notmuch_directory_get_child_files - _get_child_files.restype = c_void_p + _get_child_files.argtypes = [NotmuchDirectoryP] + _get_child_files.restype = NotmuchFilenamesP """notmuch_directory_get_child_directories""" _get_child_directories = nmlib.notmuch_directory_get_child_directories - _get_child_directories.restype = c_void_p + _get_child_directories.argtypes = [NotmuchDirectoryP] + _get_child_directories.restype = NotmuchFilenamesP - def _verify_dir_initialized(self): - """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None""" + def _assert_dir_is_initialized(self): + """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) if dir_p is None""" if self._dir_p is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + raise NotmuchError(STATUS.NOT_INITIALIZED) def __init__(self, path, dir_p, parent): """ - :param path: The absolute path of the directory object. + :param path: The absolute path of the directory object as unicode. :param dir_p: The pointer to an internal notmuch_directory_t object. :param parent: The object this Directory is derived from (usually a :class:`Database`). We do not directly use @@ -666,12 +762,12 @@ class Directory(object): this Directory object lives. This keeps the parent object alive. """ + assert isinstance(path, unicode), "Path needs to be an UNICODE object" self._path = path self._dir_p = dir_p self._parent = parent - - def set_mtime (self, mtime): + def set_mtime(self, mtime): """Sets the mtime value of this directory in the database The intention is for the caller to use the mtime to allow efficient @@ -679,36 +775,34 @@ class Directory(object): recommended usage is as follows: * Read the mtime of a directory from the filesystem - + * Call :meth:`Database.add_message` for all mail files in the directory - * Call notmuch_directory_set_mtime with the mtime read from the + * Call notmuch_directory_set_mtime with the mtime read from the filesystem. Then, when wanting to check for updates to the directory in the future, the client can call :meth:`get_mtime` - and know that it only needs to add files if the mtime of the + and know that it only needs to add files if the mtime of the directory and files are newer than the stored timestamp. - .. note:: :meth:`get_mtime` function does not allow the caller + .. note:: :meth:`get_mtime` function does not allow the caller to distinguish a timestamp of 0 from a non-existent timestamp. So don't store a timestamp of 0 unless you are - comfortable with that. + comfortable with that. - :param mtime: A (time_t) timestamp + :param mtime: A (time_t) timestamp :returns: Nothing on success, raising an exception on failure. :exception: :exc:`NotmuchError`: - STATUS.XAPIAN_EXCEPTION + :attr:`STATUS`.XAPIAN_EXCEPTION A Xapian exception occurred, mtime not stored. - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so directory + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so directory mtime cannot be modified. - STATUS.NOT_INITIALIZED + :attr:`STATUS`.NOT_INITIALIZED The directory has not been initialized """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None - self._verify_dir_initialized() - + self._assert_dir_is_initialized() #TODO: make sure, we convert the mtime parameter to a 'c_long' status = Directory._set_mtime(self._dir_p, mtime) @@ -718,53 +812,47 @@ class Directory(object): #fail with Exception otherwise raise NotmuchError(status) - def get_mtime (self): + def get_mtime(self): """Gets the mtime value of this directory in the database Retrieves a previously stored mtime for this directory. - :param mtime: A (time_t) timestamp + :param mtime: A (time_t) timestamp :returns: Nothing on success, raising an exception on failure. :exception: :exc:`NotmuchError`: - STATUS.NOT_INITIALIZED + :attr:`STATUS`.NOT_INITIALIZED The directory has not been initialized """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None - self._verify_dir_initialized() - - return Directory._get_mtime (self._dir_p) + self._assert_dir_is_initialized() + return Directory._get_mtime(self._dir_p) # Make mtime attribute a property of Directory() mtime = property(get_mtime, set_mtime, doc="""Property that allows getting and setting of the Directory *mtime* (read-write) - See :meth:`get_mtime` and :meth:`set_mtime` for usage and + See :meth:`get_mtime` and :meth:`set_mtime` for usage and possible exceptions.""") def get_child_files(self): """Gets a Filenames iterator listing all the filenames of messages in the database within the given directory. - + The returned filenames will be the basename-entries only (not complete paths. """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None - self._verify_dir_initialized() - + self._assert_dir_is_initialized() files_p = Directory._get_child_files(self._dir_p) return Filenames(files_p, self) def get_child_directories(self): """Gets a :class:`Filenames` iterator listing all the filenames of sub-directories in the database within the given directory - + The returned filenames will be the basename-entries only (not complete paths. """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None - self._verify_dir_initialized() - + self._assert_dir_is_initialized() files_p = Directory._get_child_directories(self._dir_p) return Filenames(files_p, self) @@ -777,18 +865,22 @@ class Directory(object): """Object representation""" return "" % self._path + _destroy = nmlib.notmuch_directory_destroy + _destroy.argtypes = [NotmuchDirectoryP] + _destroy.argtypes = None + def __del__(self): """Close and free the Directory""" if self._dir_p is not None: - nmlib.notmuch_directory_destroy(self._dir_p) + self._destroy(self._dir_p) + -#------------------------------------------------------------------------------ class Filenames(object): - """An iterator over File- or Directory names that are stored in the database - """ + """An iterator over File- or Directory names stored in the database""" #notmuch_filenames_get _get = nmlib.notmuch_filenames_get + _get.argtypes = [NotmuchFilenamesP] _get.restype = c_char_p def __init__(self, files_p, parent): @@ -807,41 +899,53 @@ class Filenames(object): """ Make Filenames an iterator """ return self + _valid = nmlib.notmuch_filenames_valid + _valid.argtypes = [NotmuchFilenamesP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_filenames_move_to_next + _move_to_next.argtypes = [NotmuchFilenamesP] + _move_to_next.restype = None + def next(self): if self._files_p is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - if not nmlib.notmuch_filenames_valid(self._files_p): + if not self._valid(self._files_p): self._files_p = None raise StopIteration - file = Filenames._get (self._files_p) - nmlib.notmuch_filenames_move_to_next(self._files_p) + file = Filenames._get(self._files_p) + self._move_to_next(self._files_p) return file def __len__(self): """len(:class:`Filenames`) returns the number of contained files - .. note:: As this iterates over the files, we will not be able to + .. note:: As this iterates over the files, we will not be able to iterate over them again! So this will fail:: #THIS FAILS files = Database().get_directory('').get_child_files() if len(files) > 0: #this 'exhausts' msgs - # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!! + # next line raises NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)!!! for file in files: print file """ if self._files_p is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - i=0 - while nmlib.notmuch_filenames_valid(self._files_p): - nmlib.notmuch_filenames_move_to_next(self._files_p) + i = 0 + while self._valid(self._files_p): + self._move_to_next(self._files_p) i += 1 self._files_p = None return i + _destroy = nmlib.notmuch_filenames_destroy + _destroy.argtypes = [NotmuchFilenamesP] + _destroy.restype = None + def __del__(self): """Close and free Filenames""" if self._files_p is not None: - nmlib.notmuch_filenames_destroy(self._files_p) + self._destroy(self._files_p)