X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=cnotmuch%2Fdatabase.py;h=44fd31548f7583801827ac9470211332c83f2848;hp=eb54626dccfa8964a98f09202fa5f4768efdaa92;hb=7390c869c71cd7d65a99b3dacb17ca659aa09c8b;hpb=06f627df92b644a73c07096bed716d406d4f649d diff --git a/cnotmuch/database.py b/cnotmuch/database.py index eb54626d..44fd3154 100644 --- a/cnotmuch/database.py +++ b/cnotmuch/database.py @@ -1,78 +1,73 @@ -import ctypes -from ctypes import c_int, c_char_p, c_void_p, c_uint64 +import os +from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum -import logging -from datetime import date +from cnotmuch.thread import Threads +from cnotmuch.message import Messages +from cnotmuch.tag import Tags class Database(object): """Represents a notmuch database (wraps notmuch_database_t) - .. note:: Do note 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. - - We implement reference counting, so that parent objects can be - automatically freed when they are not needed anymore, for example:: - - db = Database('path',create=True) - msgs = Query(db,'from:myself').search_messages() - - This returns a :class:`Messages` which internally contains - a reference to the parent :class:`Query` object. Otherwise - the Query() would be immediately freed, taking our *msgs* - down with it. - - In this case, the above Query() object will be - automatically freed whenever we delete all derived objects, - ie in our case: `del (msgs)` would also delete the parent - Query (but not the parent Database() as that is still - referenced from the variable *db* in which it is stored. - - Pretty much the same is valid for all other objects in the hierarchy, - such as :class:`Query`, :class:`Messages`, :class:`Message`, - and :class:`Tags`. + .. 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. """ + _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""" - _std_db_path = None - """Class attribute to cache user's default database""" + """notmuch_database_get_directory""" + _get_directory = nmlib.notmuch_database_get_directory + _get_directory.restype = c_void_p - """notmuch_database_get_path (notmuch_database_t *database)""" + """notmuch_database_get_path""" _get_path = nmlib.notmuch_database_get_path _get_path.restype = c_char_p - """notmuch_database_open (const char *path, notmuch_database_mode_t mode)""" + """notmuch_database_get_version""" + _get_version = nmlib.notmuch_database_get_version + _get_version.restype = c_uint + + """notmuch_database_open""" _open = nmlib.notmuch_database_open _open.restype = c_void_p - """ notmuch_database_find_message """ + """notmuch_database_upgrade""" + _upgrade = nmlib.notmuch_database_upgrade + _upgrade.argtypes = [c_void_p, c_void_p, c_void_p] + + """ notmuch_database_find_message""" _find_message = nmlib.notmuch_database_find_message _find_message.restype = c_void_p - """notmuch_database_get_all_tags (notmuch_database_t *database)""" + """notmuch_database_get_all_tags""" _get_all_tags = nmlib.notmuch_database_get_all_tags _get_all_tags.restype = c_void_p - """ notmuch_database_create(const char *path):""" + """notmuch_database_create""" _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 as creating an empty - database for reading only does not make a great deal of sense. + 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`) :type path: `str` or `None` - :param create: False to open an existing, True to create a new + :param create: Pass `False` to open an existing, `True` to create a new database. :type create: bool - :param mdoe: Mode to open a database in. 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 @@ -87,17 +82,24 @@ class Database(object): path = Database._std_db_path if create == False: - self.open(path, status) + self.open(path, mode) 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 - This function wraps *notmuch_database_create(...)* and creates - a new notmuch database at *path*. It will always return a database in - :attr:`MODE`.READ_WRITE mode as creating an empty database - for reading only does not make a great deal of sense. + This function is used by __init__() and usually does not need + to be called directly. It wraps the underlying + *notmuch_database_create* function and creates a new notmuch + database at *path*. It will always return a database in :attr:`MODE` + .READ_WRITE mode as creating an empty database for + reading only does not make a great deal of sense. :param path: A directory in which we should create the database. :type path: str @@ -109,21 +111,28 @@ 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, status= MODE.READ_ONLY): - """calls notmuch_database_open + def open(self, path, mode= 0): + """Opens an existing database + + This function is used by __init__() and usually does not need + to be called directly. It wraps the underlying + *notmuch_database_open* function. - :returns: Raises :exc:`notmuch.NotmuchError` in case - of any failure (after printing an error message on stderr). + :param status: Open the database in read-only or read-write mode + :type status: :attr:`MODE` + :returns: Nothing + :exception: Raises :exc:`NotmuchError` in case + of any failure (after printing an error message on stderr). """ - res = Database._open(path, status) + res = Database._open(path, mode) if res is None: raise NotmuchError( @@ -131,21 +140,213 @@ class Database(object): self._db = res def get_path(self): - """notmuch_database_get_path (notmuch_database_t *database); """ + """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): + """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() + + return Database._get_version (self._db) + + def needs_upgrade(self): + """Does this database need to be upgraded before writing to it? + + If this function returns `True` then no functions that modify the + database (:meth:`add_message`, + :meth:`Message.add_tag`, :meth:`Directory.set_mtime`, + 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) + + def upgrade(self): + """Upgrades the current database + + After opening a database in read-write mode, the client should + check if an upgrade is needed (notmuch_database_needs_upgrade) and + if so, upgrade with this function before making any modifications. + + 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] + 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) + #TODO: catch exceptions, document return values and etc + return status + + def get_directory(self, 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 + 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 + 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 + components same as database. + + """ + # Raise a NotmuchError if not initialized + self._verify_initialized_db() + + # 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.") + abs_dirpath = path + else: + #we got a relative path, make it absolute + abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path)) + + dir_p = Database._get_directory(self._db, path); + + # return the Directory, init it with the absolute path + return Directory(abs_dirpath, dir_p, self) + + 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): - """notmuch_database_find_message + """Returns a :class:`Message` as identified by its message ID - :param msgid: The message id + 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 an out-of-memory situation occurs. :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 @@ -154,24 +355,46 @@ class Database(object): def get_all_tags(self): """Returns :class:`Tags` with a list of all tags found in the database - :returns: :class:`Tags` object or raises :exc:`NotmuchError` with - STATUS.NULL_POINTER on error + :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() + "'" def __del__(self): """Close and free the notmuch database if needed""" if self._db is not None: - logging.debug("Freeing the database now") nmlib.notmuch_database_close(self._db) def _get_user_default_db(self): @@ -179,9 +402,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") @@ -189,42 +413,81 @@ 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): - """ Wrapper around a notmuch_query_t + """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 + contains the sort order (if specified via :meth:`set_sort`) or + `None`. - Do note that as soon as we tear down this object, all derived - threads, and messages will be freed as well. + Technically, it wraps the underlying *notmuch_query_t* struct. + + .. note:: Do remember that as soon as we tear down this object, + all underlying derived objects such as 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. """ # constants - SORT_OLDEST_FIRST = 0 - SORT_NEWEST_FIRST = 1 - SORT_MESSAGE_ID = 2 + SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID']) + """Constants: Sort order in which to return results""" """notmuch_query_create""" _create = nmlib.notmuch_query_create _create.restype = c_void_p + """notmuch_query_search_threads""" + _search_threads = nmlib.notmuch_query_search_threads + _search_threads.restype = c_void_p + """notmuch_query_search_messages""" _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): - """TODO: document""" + """ + :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 + """ self._db = None self._query = None + self.sort = None self.create(db, querystr) def create(self, db, querystr): - """db is a Database() and querystr a string + """Creates a new query derived from a Database + + This function is utilized by __init__() and usually does not need to + be called directly. - raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and - STATUS.NULL_POINTER if the query creation failed (too little memory) + :param db: Database to create the query from. + :type db: :class:`Database` + :param querystr: The query string + :type querystr: str + :returns: Nothing + :exception: :exc:`NotmuchError` + + * STATUS.NOT_INITIALIZED if db is not inited + * STATUS.NULL_POINTER if the query creation failed + (too little memory) """ if db.db_p is None: raise NotmuchError(STATUS.NOT_INITIALIZED) @@ -238,299 +501,322 @@ class Query(object): self._query = query_p def set_sort(self, sort): - """notmuch_query_set_sort + """Set the sort order future results will be delivered in + + Wraps the underlying *notmuch_query_set_sort* function. - :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID - :returns: Nothing, but raises NotmuchError if query is not inited + :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.sort = sort nmlib.notmuch_query_set_sort(self._query, sort) - def search_messages(self): - """notmuch_query_search_messages - Returns Messages() or a raises a NotmuchError() + 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 + 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 """ if self._query is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - msgs_p = Query._search_messages(self._query) + threads_p = Query._search_threads(self._query) - if msgs_p is None: + if threads_p is None: NotmuchError(STATUS.NULL_POINTER) - return Messages(msgs_p,self) - + return Threads(threads_p,self) - def __del__(self): - """Close and free the Query""" - if self._query is not None: - logging.debug("Freeing the Query now") - nmlib.notmuch_query_destroy (self._query) + def search_messages(self): + """Filter messages according to the query and return + :class:`Messages` in the defined sort order -#------------------------------------------------------------------------------ -class Tags(object): - """Wrapper around notmuch_tags_t""" + Technically, it wraps the underlying + *notmuch_query_search_messages* function. - #notmuch_tags_get - _get = nmlib.notmuch_tags_get - _get.restype = c_char_p + :returns: :class:`Messages` + :exception: :exc:`NotmuchError` - def __init__(self, tags_p, parent=None): - """ - msg_p is a pointer to an notmuch_message_t Structure. If it is None, - we will raise an NotmuchError(STATUS.NULL_POINTER). - - Is passed the parent these tags are derived from, and saves a - reference to it, so we can automatically delete the db object - once all derived objects are dead. - - Tags() provides an iterator over all contained tags. However, you will - only be able to iterate over the Tags once, because the underlying C - function only allows iterating once. - #TODO: make the iterator work more than once and cache the tags in - the Python object. + * STATUS.NOT_INITIALIZED if query is not inited + * STATUS.NULL_POINTER if search_messages failed """ - if tags_p is None: - NotmuchError(STATUS.NULL_POINTER) + if self._query is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) - self._tags = tags_p - #save reference to parent object so we keep it alive - self._parent = parent - logging.debug("Inited Tags derived from %s" %(repr(parent))) - - def __iter__(self): - """ Make Tags an iterator """ - return self + msgs_p = Query._search_messages(self._query) - def next(self): - if self._tags is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if msgs_p is None: + NotmuchError(STATUS.NULL_POINTER) - if not nmlib.notmuch_tags_valid(self._tags): - self._tags = None - raise StopIteration + return Messages(msgs_p,self) - tag = Tags._get (self._tags) - nmlib.notmuch_tags_move_to_next(self._tags) - return tag + def count_messages(self): + """Estimate the number of messages matching the query - def __str__(self): - """str() of Tags() is a space separated list of tags + 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. - This iterates over the list of Tags and will therefore 'exhaust' Tags() + :returns: :class:`Messages` + :exception: :exc:`NotmuchError` + + * STATUS.NOT_INITIALIZED if query is not inited """ - return " ".join(self) + if self._query is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + return Query._count_messages(self._query) def __del__(self): - """Close and free the notmuch tags""" - if self._tags is not None: - logging.debug("Freeing the Tags now") - nmlib.notmuch_tags_destroy (self._tags) + """Close and free the Query""" + if self._query is not None: + nmlib.notmuch_query_destroy (self._query) #------------------------------------------------------------------------------ -class Messages(object): - """Wrapper around notmuch_messages_t""" +class Directory(object): + """Represents a directory entry in the notmuch directory + + Modifying attributes of this object will modify the + database, not the real directory attributes. + + The Directory object is usually derived from another object + e.g. via :meth:`Database.get_directory`, and will automatically be + become invalid whenever that parent is deleted. You should + therefore initialized this object handing it a reference to the + parent, preventing the parent from automatically being garbage + collected. + """ - #notmuch_tags_get - _get = nmlib.notmuch_messages_get - _get.restype = c_void_p + """notmuch_directory_get_mtime""" + _get_mtime = nmlib.notmuch_directory_get_mtime + _get_mtime.restype = c_long - _collect_tags = nmlib.notmuch_messages_collect_tags - _collect_tags.restype = c_void_p + """notmuch_directory_set_mtime""" + _set_mtime = nmlib.notmuch_directory_set_mtime + _set_mtime.argtypes = [c_char_p, c_long] - def __init__(self, msgs_p, parent=None): + """notmuch_directory_get_child_files""" + _get_child_files = nmlib.notmuch_directory_get_child_files + _get_child_files.restype = c_void_p + + """notmuch_directory_get_child_directories""" + _get_child_directories = nmlib.notmuch_directory_get_child_directories + _get_child_directories.restype = c_void_p + + def _verify_dir_initialized(self): + """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None""" + if self._dir_p is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + + def __init__(self, path, dir_p, parent): """ - msg_p is a pointer to an notmuch_messages_t Structure. If it is None, - we will raise an NotmuchError(STATUS.NULL_POINTER). - - If passed the parent query this Messages() is derived from, it saves a - reference to it, so we can automatically delete the parent query object - once all derived objects are dead. - - Messages() provides an iterator over all contained Message()s. - However, you will only be able to iterate over it once, - because the underlying C function only allows iterating once. - #TODO: make the iterator work more than once and cache the tags in - the Python object. + :param path: The absolute path of the directory object. + :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 + this, but store a reference to it as long as + this Directory object lives. This keeps the + parent object alive. """ - if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) - - self._msgs = msgs_p - #store parent, so we keep them alive as long as self is alive + self._path = path + self._dir_p = dir_p self._parent = parent - logging.debug("Inited Messages derived from %s" %(str(parent))) - def collect_tags(self): - """ return the Tags() belonging to the messages - - Do note that collect_tags will iterate over the messages and - therefore will not allow further iterationsl - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited + + 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 + identification of new messages to be added to the database. The + 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 + 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 + directory and files are newer than the stored timestamp. + + .. 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. + + :param mtime: A (time_t) timestamp + :returns: Nothing on success, raising an exception on failure. + :exception: :exc:`NotmuchError`: + + STATUS.XAPIAN_EXCEPTION + A Xapian exception occurred, mtime not stored. + STATUS.READ_ONLY_DATABASE + Database was opened in read-only mode so directory + mtime cannot be modified. + STATUS.NOT_INITIALIZED + The directory has not been initialized """ - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None + self._verify_dir_initialized() - # collect all tags (returns NULL on error) - tags_p = Messages._collect_tags (self._msgs) - #reset _msgs as we iterated over it and can do so only once - self._msgs = None + #TODO: make sure, we convert the mtime parameter to a 'c_long' + status = Directory._set_mtime(self._dir_p, mtime) - if tags_p == None: - raise NotmuchError(STATUS.NULL_POINTER) - return Tags(tags_p, self) + #return on success + if status == STATUS.SUCCESS: + return + #fail with Exception otherwise + raise NotmuchError(status) - def __iter__(self): - """ Make Messages an iterator """ - return self + def get_mtime (self): + """Gets the mtime value of this directory in the database - def next(self): - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + Retrieves a previously stored mtime for this directory. - if not nmlib.notmuch_messages_valid(self._msgs): - self._msgs = None - raise StopIteration + :param mtime: A (time_t) timestamp + :returns: Nothing on success, raising an exception on failure. + :exception: :exc:`NotmuchError`: - msg = Message(Messages._get (self._msgs), self) - nmlib.notmuch_messages_move_to_next(self._msgs) - return msg + 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() - def __len__(self): - """ Returns the number of contained messages + return Directory._get_mtime (self._dir_p) - :note: As this iterates over the messages, we will not be able to - iterate over them again (as in retrieve them)! - """ - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + # 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) - i=0 - while nmlib.notmuch_messages_valid(self._msgs): - nmlib.notmuch_messages_move_to_next(self._msgs) - i += 1 - self._msgs = None - return i + 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() + files_p = Directory._get_child_files(self._dir_p) + return Filenames(files_p, self) - def __del__(self): - """Close and free the notmuch Messages""" - if self._msgs is not None: - logging.debug("Freeing the Messages now") - nmlib.notmuch_messages_destroy (self._msgs) + 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() + files_p = Directory._get_child_directories(self._dir_p) + return Filenames(files_p, self) -#------------------------------------------------------------------------------ -class Message(object): - """Wrapper around notmuch_message_t""" + @property + def path(self): + """Returns the absolute path of this Directory (read-only)""" + return self._path - """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 + def __repr__(self): + """Object representation""" + return "" % self._path - """notmuch_message_get_tags (notmuch_message_t *message)""" - _get_tags = nmlib.notmuch_message_get_tags - _get_tags.restype = c_void_p + def __del__(self): + """Close and free the Directory""" + if self._dir_p is not None: + nmlib.notmuch_directory_destroy(self._dir_p) - _get_date = nmlib.notmuch_message_get_date - _get_date.restype = c_uint64 +#------------------------------------------------------------------------------ +class Filenames(object): + """An iterator over File- or Directory names that are stored in the database + """ - _get_header = nmlib.notmuch_message_get_header - _get_header.restype = c_char_p + #notmuch_filenames_get + _get = nmlib.notmuch_filenames_get + _get.restype = c_char_p - def __init__(self, msg_p, parent=None): + def __init__(self, files_p, parent): """ - msg_p is a pointer to an notmuch_message_t Structure. If it is None, - we will raise an NotmuchError(STATUS.NULL_POINTER). - - Is a 'parent' object is passed which this message is derived from, - we save a reference to it, so we can automatically delete the parent - object once all derived objects are dead. + :param files_p: The pointer to an internal notmuch_filenames_t object. + :param parent: The object this Directory is derived from + (usually a Directory()). We do not directly use + this, but store a reference to it as long as + this Directory object lives. This keeps the + parent object alive. """ - if msg_p is None: - NotmuchError(STATUS.NULL_POINTER) - self._msg = msg_p - #keep reference to parent, so we keep it alive + self._files_p = files_p self._parent = parent - logging.debug("Inited Message derived from %s" %(str(parent))) + def __iter__(self): + """ Make Filenames an iterator """ + return self - def get_message_id(self): - """ return the msg id - - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited - """ - if self._msg is None: + def next(self): + if self._files_p is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_message_id(self._msg) - def get_date(self): - """returns time_t of the message date - - For the original textual representation of the Date header from the - message call notmuch_message_get_header() with a header value of - "date". - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited - """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_date(self._msg) + if not nmlib.notmuch_filenames_valid(self._files_p): + self._files_p = None + raise StopIteration - def get_header(self, header): - """ TODO document me""" - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + file = Filenames._get (self._files_p) + nmlib.notmuch_filenames_move_to_next(self._files_p) + return file - #Returns NULL if any error occurs. - header = Message._get_header (self._msg, header) - if header == None: - raise NotmuchError(STATUS.NULL_POINTER) - return header + def __len__(self): + """len(:class:`Filenames`) returns the number of contained files - def get_filename(self): - """ return the msg filename - - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited - """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_filename(self._msg) + .. note:: As this iterates over the files, we will not be able to + iterate over them again! So this will fail:: - def get_tags(self): - """ return the msg tags - - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited - Raises NotmuchError(STATUS.NULL_POINTER) on error. + #THIS FAILS + files = Database().get_directory('').get_child_files() + if len(files) > 0: #this 'exhausts' msgs + # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!! + for file in files: print file """ - if self._msg is None: + if self._files_p is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - tags_p = Message._get_tags(self._msg) - if tags_p == None: - raise NotmuchError(STATUS.NULL_POINTER) - return Tags(tags_p, self) - - def __str__(self): - """A message() is represented by a 1-line summary""" - msg = {} - 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) - - def format_as_text(self): - """ Output like notmuch show """ - return str(self) + i=0 + while nmlib.notmuch_filenames_valid(self._files_p): + nmlib.notmuch_filenames_move_to_next(self._files_p) + i += 1 + self._files_p = None + return i def __del__(self): - """Close and free the notmuch Message""" - if self._msg is not None: - logging.debug("Freeing the Message now") - nmlib.notmuch_message_destroy (self._msg) + """Close and free Filenames""" + if self._files_p is not None: + nmlib.notmuch_filenames_destroy(self._files_p)