X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=cnotmuch%2Fdatabase.py;h=714290347b38377c5070e92ea2825c0545f2c042;hp=2b51e5ea34a9fd0a0291f3eac57b3b6260c704fd;hb=a27c480048127c744909022ed8f633f8b2896ba6;hpb=61bef71a126efb02b477209a02789cc194a5b784 diff --git a/cnotmuch/database.py b/cnotmuch/database.py index 2b51e5ea..71429034 100644 --- a/cnotmuch/database.py +++ b/cnotmuch/database.py @@ -1,8 +1,9 @@ -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) @@ -13,16 +14,24 @@ class Database(object): 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)""" _get_path = nmlib.notmuch_database_get_path _get_path.restype = c_char_p + """notmuch_database_get_version""" + _get_version = nmlib.notmuch_database_get_version + _get_version.restype = c_uint + """notmuch_database_open (const char *path, notmuch_database_mode_t mode)""" _open = nmlib.notmuch_database_open _open.restype = c_void_p @@ -39,19 +48,22 @@ 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`) :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 mode: 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 @@ -70,10 +82,15 @@ 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 - This function is used by __init__() usually does not need + 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 @@ -90,24 +107,24 @@ 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__() usually does not need + This function is used by __init__() and usually does not need to be called directly. It wraps the underlying *notmuch_database_open* function. :param status: Open the database in read-only or read-write mode :type status: :attr:`MODE` :returns: Nothing - :exception: Raises :exc:`notmuch.NotmuchError` in case + :exception: Raises :exc:`NotmuchError` in case of any failure (after printing an error message on stderr). """ @@ -122,22 +139,189 @@ 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): + """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:`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 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): """Returns a :class:`Message` as identified by its message ID - wraps *notmuch_database_find_message* + Wraps the underlying *notmuch_database_find_message* function. - :param msgid: The message id + :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 @@ -146,24 +330,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): @@ -171,9 +377,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") @@ -181,42 +388,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. + + :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` - raises NotmuchError STATUS.NOT_INITIALIZED if db is not inited and - STATUS.NULL_POINTER if the query creation failed (too little memory) + * 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) @@ -230,299 +476,247 @@ 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 - :param sort: one of Query.SORT_OLDEST_FIRST|SORT_NEWEST_FIRST|SORT_MESSAGE_ID - :returns: Nothing, but raises NotmuchError if query is not inited + 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.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() - """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - msgs_p = Query._search_messages(self._query) + def search_threads(self): + """Execute a query for threads - if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) + 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. - return Messages(msgs_p,self) + 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. - 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) + :returns: :class:`Threads` + :exception: :exc:`NotmuchError` -#------------------------------------------------------------------------------ -class Tags(object): - """Wrapper around notmuch_tags_t""" + * 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) - #notmuch_tags_get - _get = nmlib.notmuch_tags_get - _get.restype = c_char_p + threads_p = Query._search_threads(self._query) - 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. - """ - if tags_p is None: + if threads_p is None: NotmuchError(STATUS.NULL_POINTER) - 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 - - def next(self): - if self._tags is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + return Threads(threads_p,self) - if not nmlib.notmuch_tags_valid(self._tags): - self._tags = None - raise StopIteration + def search_messages(self): + """Filter messages according to the query and return + :class:`Messages` in the defined sort order - tag = Tags._get (self._tags) - nmlib.notmuch_tags_move_to_next(self._tags) - return tag + Technically, it wraps the underlying + *notmuch_query_search_messages* function. - def __str__(self): - """str() of Tags() is a space separated list of tags + :returns: :class:`Messages` + :exception: :exc:`NotmuchError` - This iterates over the list of Tags and will therefore 'exhaust' Tags() + * STATUS.NOT_INITIALIZED if query is not inited + * STATUS.NULL_POINTER if search_messages failed """ - return " ".join(self) - - 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) - - -#------------------------------------------------------------------------------ -class Messages(object): - """Wrapper around notmuch_messages_t""" - - #notmuch_tags_get - _get = nmlib.notmuch_messages_get - _get.restype = c_void_p + if self._query is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) - _collect_tags = nmlib.notmuch_messages_collect_tags - _collect_tags.restype = c_void_p + msgs_p = Query._search_messages(self._query) - def __init__(self, msgs_p, parent=None): - """ - 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. - """ 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._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 - """ - if self._msgs is None: - raise NotmuchError(STATUS.NOT_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 - - if tags_p == None: - raise NotmuchError(STATUS.NULL_POINTER) - return Tags(tags_p, self) - - def __iter__(self): - """ Make Messages an iterator """ - return self - - def next(self): - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + return Messages(msgs_p,self) - if not nmlib.notmuch_messages_valid(self._msgs): - self._msgs = None - raise StopIteration + def count_messages(self): + """Estimate the number of messages matching the query - msg = Message(Messages._get (self._msgs), self) - nmlib.notmuch_messages_move_to_next(self._msgs) - return msg + 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. - def __len__(self): - """ Returns the number of contained messages + :returns: :class:`Messages` + :exception: :exc:`NotmuchError` - :note: As this iterates over the messages, we will not be able to - iterate over them again (as in retrieve them)! + * STATUS.NOT_INITIALIZED if query is not inited """ - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - i=0 - while nmlib.notmuch_messages_valid(self._msgs): - nmlib.notmuch_messages_move_to_next(self._msgs) - i += 1 - self._msgs = None - return i - + if self._query is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + return Query._count_messages(self._query) 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) + """Close and free the Query""" + if self._query is not None: + nmlib.notmuch_query_destroy (self._query) #------------------------------------------------------------------------------ -class Message(object): - """Wrapper around notmuch_message_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_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_directory_get_mtime""" + _get_mtime = nmlib.notmuch_directory_get_mtime + _get_mtime.restype = c_long - """notmuch_message_get_tags (notmuch_message_t *message)""" - _get_tags = nmlib.notmuch_message_get_tags - _get_tags.restype = c_void_p + """notmuch_directory_set_mtime""" + _set_mtime = nmlib.notmuch_directory_set_mtime + _set_mtime.argtypes = [c_char_p, c_long] - _get_date = nmlib.notmuch_message_get_date - _get_date.restype = c_uint64 + """notmuch_directory_get_child_directories""" + _get_child_directories = nmlib.notmuch_directory_get_child_directories + _get_child_directories.restype = c_void_p - _get_header = nmlib.notmuch_message_get_header - _get_header.restype = c_char_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, msg_p, parent=None): + def __init__(self, path, dir_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 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 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 msg_p is None: - NotmuchError(STATUS.NULL_POINTER) - self._msg = msg_p - #keep reference to parent, so we keep it alive + #TODO, sanity checking that the path is really absolute? + self._path = path + self._dir_p = dir_p self._parent = parent - logging.debug("Inited Message derived from %s" %(str(parent))) - def get_message_id(self): - """ return the msg id - - 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._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_message_id(self._msg) + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None + self._verify_dir_initialized() - def get_date(self): - """returns time_t of the message date + #TODO: make sure, we convert the mtime parameter to a 'c_long' + status = Directory._set_mtime(self._dir_p, mtime) - 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) + #return on success + if status == STATUS.SUCCESS: + return + #fail with Exception otherwise + raise NotmuchError(status) - def get_header(self, header): - """ TODO document me""" - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + def get_mtime (self): + """Gets the mtime value of this directory in the database - #Returns NULL if any error occurs. - header = Message._get_header (self._msg, header) - if header == None: - raise NotmuchError(STATUS.NULL_POINTER) - return header + Retrieves a previously stored mtime for this directory. - def get_filename(self): - """ return the msg filename - - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited + :param mtime: A (time_t) timestamp + :returns: Nothing on success, raising an exception on failure. + :exception: :exc:`NotmuchError`: + + STATUS.NOT_INITIALIZED + The directory has not been initialized """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_filename(self._msg) + #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None + self._verify_dir_initialized() + + return Directory._get_mtime (self._dir_p) + - def get_tags(self): - """ return the msg tags + # Make mtime attribute a property of Directory() + mtime = property(get_mtime, set_mtime, doc="""Property that allows getting + and setting of the Directory *mtime*""") + + 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. + """ + pass + #notmuch_filenames_t * notmuch_directory_get_child_files (notmuch_directory_t *directory); + + def get_child_directories(self): + """Gets a Filenams iterator listing all the filenames of + sub-directories in the database within the given directory - Raises NotmuchError(STATUS.NOT_INITIALIZED) if not inited - Raises NotmuchError(STATUS.NULL_POINTER) on error. + The returned filenames will be the basename-entries only (not + complete paths. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + #notmuch_filenames_t * notmuch_directory_get_child_directories (notmuch_directory_t *directory); + pass - 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) + @property + def path(self): + """Returns the absolute path of this Directory""" + return self._path - def format_as_text(self): - """ Output like notmuch show """ - return str(self) + def __repr__(self): + """Object representation""" + return "" % self._path 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 the Directory""" + if self._dir_p is not None: + nmlib.notmuch_directory_destroy(self._dir_p)