X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=cnotmuch%2Fdatabase.py;h=dde7da16eecea67fec9e513d27c16588a1abccf8;hp=92afa0a047d8f773ec76b1459723bc9f0cdbc1b1;hb=63c5a6d77d2b51104305e91676720099f4667e92;hpb=4ed01d055ac59b182535dfe44a33e52fc271279b diff --git a/cnotmuch/database.py b/cnotmuch/database.py index 92afa0a0..dde7da16 100644 --- a/cnotmuch/database.py +++ b/cnotmuch/database.py @@ -1,5 +1,5 @@ -import ctypes -from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool +import ctypes, os +from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool, byref from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum import logging from datetime import date @@ -13,12 +13,12 @@ class Database(object): as well. Accessing these objects will lead to segfaults and other unexpected behavior. See above for more details. """ - MODE = Enum(['READ_ONLY','READ_WRITE']) - """Constants: Mode in which to open the database""" - _std_db_path = None """Class attribute to cache user's default database""" + MODE = Enum(['READ_ONLY','READ_WRITE']) + """Constants: Mode in which to open the database""" + """notmuch_database_get_path (notmuch_database_t *database)""" _get_path = nmlib.notmuch_database_get_path _get_path.restype = c_char_p @@ -43,11 +43,14 @@ class Database(object): _create = nmlib.notmuch_database_create _create.restype = c_void_p - def __init__(self, path=None, create=False, mode= MODE.READ_ONLY): - """If *path* is *None*, we will try to read a users notmuch - configuration and use his default database. If *create* is `True`, - the database will always be created in - :attr:`MODE`.READ_WRITE mode. + 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`. + + If *create* is `True`, the database will always be created in + :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY. :param path: Directory to open/create the database in (see above for behavior if `None`) @@ -74,6 +77,11 @@ class Database(object): else: self.create(path) + def _verify_initialized_db(self): + """Raises a NotmuchError in case self._db is still None""" + if self._db is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + def create(self, path): """Creates a new notmuch database @@ -94,14 +102,14 @@ class Database(object): raise NotmuchError( message="Cannot create db, this Database() already has an open one.") - res = Database._create(path, MODE.READ_WRITE) + res = Database._create(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= MODE.READ_ONLY): + def open(self, path, mode= 0): """Opens an existing database This function is used by __init__() and usually does not need @@ -126,6 +134,9 @@ class Database(object): """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) def get_version(self): @@ -135,8 +146,8 @@ class Database(object): :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if the database was not intitialized. """ - if self._db is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + # Raise a NotmuchError if not initialized + self._verify_initialized_db() return Database._get_version (self._db) @@ -152,11 +163,102 @@ class Database(object): :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if the database was not intitialized. """ - if self._db is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + # Raise a NotmuchError if not initialized + self._verify_initialized_db() return notmuch_database_needs_upgrade(self.db) + def add_message(self, filename): + """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. + + 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. + + :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: + + STATUS.SUCCESS + Message successfully added to database. + 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. + + :rtype: 2-tuple(:class:`Message`, 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 + 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 + 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() + + 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]: + raise NotmuchError(status) + + #construct Message() and return + msg = Message(msg_p, self) + return (msg, status) + + def remove_message(self, filename): + """Removes a message 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) + is still available via other filenames, then the message will + persist in the database for those filenames. When the last filename + is removed for a particular message, the database content for that + message will be entirely removed. + + :returns: A STATUS.* value with the following meaning: + + 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 + 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. + + 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() + + status = nmlib.notmuch_database_remove_message(self._db, + filename) + def find_message(self, msgid): """Returns a :class:`Message` as identified by its message ID @@ -169,8 +271,9 @@ class Database(object): :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if the database was not intitialized. """ - if self._db is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + # Raise a NotmuchError if not initialized + self._verify_initialized_db() + msg_p = Database._find_message(self._db, msgid) if msg_p is None: return None @@ -182,14 +285,37 @@ class Database(object): :returns: :class:`Tags` :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error """ - if self._db is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + # Raise a NotmuchError if not initialized + self._verify_initialized_db() tags_p = Database._get_all_tags (self._db) if tags_p == None: raise NotmuchError(STATUS.NULL_POINTER) return Tags(tags_p, self) + def create_query(self, querystring): + """Returns a :class:`Query` derived from this database + + This is a shorthand method for doing:: + + # short version + # Automatically frees the Database() when 'q' is deleted + + q = Database(dbpath).create_query('from:"Biene Maja"') + + # long version, which is functionally equivalent but will keep the + # Database in the 'db' variable around after we delete 'q': + + db = Database(dbpath) + q = Query(db,'from:"Biene Maja"') + + 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() + "'" @@ -204,9 +330,10 @@ class Database(object): Throws a NotmuchError if it cannot find it""" from ConfigParser import SafeConfigParser - import os.path config = SafeConfigParser() - config.read(os.path.expanduser('~/.notmuch-config')) + 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") @@ -214,14 +341,16 @@ class Database(object): @property def db_p(self): - """Property returning a pointer to the notmuch_database_t or `None` + """Property returning a pointer to `notmuch_database_t` or `None` - This should normally not be needed by a user.""" + This should normally not be needed by a user (and is not yet + guaranteed to remain stable in future versions). + """ return self._db #------------------------------------------------------------------------------ class Query(object): - """ Represents a search query on an opened :class:`Database`. + """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. @@ -246,6 +375,11 @@ class Query(object): _search_messages = nmlib.notmuch_query_search_messages _search_messages.restype = c_void_p + + """notmuch_query_count_messages""" + _count_messages = nmlib.notmuch_query_count_messages + _count_messages.restype = c_uint + def __init__(self, db, querystr): """ :param db: An open database which we derive the Query from. @@ -258,7 +392,7 @@ class Query(object): self.create(db, querystr) def create(self, db, querystr): - """Creates a new query derived from a Database. + """Creates a new query derived from a Database This function is utilized by __init__() and usually does not need to be called directly. @@ -323,6 +457,25 @@ class Query(object): return Messages(msgs_p,self) + def count_messages(self): + """Estimate the number of messages matching the query + + This function performs a search and returns Xapian's best + guess as to the number of matching messages. It is much faster + than performing :meth:`search_messages` and counting the + result with `len()` (although it always returned the same + result in my tests). Technically, it wraps the underlying + *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) + + return Query._count_messages(self._query) def __del__(self): """Close and free the Query""" @@ -561,7 +714,13 @@ class Messages(object): """len(:class:`Messages`) returns the number of contained messages .. note:: As this iterates over the messages, we will not be able to - iterate over them again (as in retrieve them)! + iterate over them again! So this will fail:: + + #THIS FAILS + msgs = Database().create_query('').search_message() + if len(msgs) > 0: #this 'exhausts' msgs + # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!! + for msg in msgs: print msg """ if self._msgs is None: raise NotmuchError(STATUS.NOT_INITIALIZED) @@ -592,10 +751,19 @@ class Message(object): """notmuch_message_get_filename (notmuch_message_t *message)""" _get_filename = nmlib.notmuch_message_get_filename _get_filename.restype = c_char_p + """notmuch_message_get_message_id (notmuch_message_t *message)""" _get_message_id = nmlib.notmuch_message_get_message_id _get_message_id.restype = c_char_p + """notmuch_message_get_thread_id""" + _get_thread_id = nmlib.notmuch_message_get_thread_id + _get_thread_id.restype = c_char_p + + """notmuch_message_get_replies""" + _get_replies = nmlib.notmuch_message_get_replies + _get_replies.restype = c_void_p + """notmuch_message_get_tags (notmuch_message_t *message)""" _get_tags = nmlib.notmuch_message_get_tags _get_tags.restype = c_void_p @@ -625,7 +793,7 @@ class Message(object): def get_message_id(self): - """Return the message ID + """Returns the message ID :returns: String with a message ID :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message @@ -635,6 +803,51 @@ class Message(object): raise NotmuchError(STATUS.NOT_INITIALIZED) return Message._get_message_id(self._msg) + def get_thread_id(self): + """Returns the thread ID + + The returned string belongs to 'message' will only be valid for as + long as the message is valid. + + This function will not return None since Notmuch ensures that every + message belongs to a single thread. + + :returns: String with a thread ID + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + is not initialized. + """ + if self._msg is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + return Message._get_thread_id (self._msg); + + def get_replies(self): + """Gets all direct replies to this message as :class:`Messages` iterator + + .. note:: This call only makes sense if 'message' was + ultimately obtained from a :class:`Thread` object, (such as + by coming directly from the result of calling + :meth:`Thread.get_toplevel_messages` or by any number of + subsequent calls to :meth:`get_replies`). If this message was + obtained through some non-thread means, (such as by a call + to :meth:`Query.search_messages`), then this function will + return `None`. + + :returns: :class:`Messages` or `None` if there are no replies to + this message. + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + is not initialized. + """ + if self._msg is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + msgs_p = Message._get_replies(self._msg); + + if msgs_p is None: + return None + + return Messages(msgs_p,self) + def get_date(self): """Returns time_t of the message date @@ -895,7 +1108,9 @@ class Message(object): msg['from'] = self.get_header('from') msg['tags'] = str(self.get_tags()) msg['date'] = date.fromtimestamp(self.get_date()) - return "%(from)s (%(date)s) (%(tags)s)" % (msg) + replies = self.get_replies() + msg['replies'] = len(replies) if replies is not None else -1 + return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg) def format_as_text(self): """Output like notmuch show (Not implemented)"""