]> git.notmuchmail.org Git - notmuch/blobdiff - cnotmuch/database.py
message.py: small doc changes and unused code removal
[notmuch] / cnotmuch / database.py
index e540bb3ce46dcb2670dcad31b5c11373145ee089..44fd31548f7583801827ac9470211332c83f2848 100644 (file)
@@ -1,5 +1,5 @@
 import os
 import os
-from ctypes import c_int, c_char_p, c_void_p, c_uint, byref
+from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref
 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
 from cnotmuch.thread import Threads
 from cnotmuch.message import Messages
 from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
 from cnotmuch.thread import Threads
 from cnotmuch.message import Messages
@@ -20,7 +20,11 @@ class Database(object):
     MODE = Enum(['READ_ONLY','READ_WRITE'])
     """Constants: Mode in which to open the database"""
 
     MODE = Enum(['READ_ONLY','READ_WRITE'])
     """Constants: Mode in which to open the database"""
 
-    """notmuch_database_get_path (notmuch_database_t *database)"""
+    """notmuch_database_get_directory"""
+    _get_directory = nmlib.notmuch_database_get_directory
+    _get_directory.restype = c_void_p
+
+    """notmuch_database_get_path"""
     _get_path = nmlib.notmuch_database_get_path
     _get_path.restype = c_char_p
 
     _get_path = nmlib.notmuch_database_get_path
     _get_path.restype = c_char_p
 
@@ -28,24 +32,28 @@ class Database(object):
     _get_version = nmlib.notmuch_database_get_version
     _get_version.restype = c_uint
 
     _get_version = nmlib.notmuch_database_get_version
     _get_version.restype = c_uint
 
-    """notmuch_database_open (const char *path, notmuch_database_mode_t mode)"""
+    """notmuch_database_open"""
     _open = nmlib.notmuch_database_open 
     _open.restype = c_void_p
 
     _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
 
     _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
 
     _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= 0):
     _create = nmlib.notmuch_database_create
     _create.restype = c_void_p
 
     def __init__(self, path=None, create=False, mode= 0):
-        """If *path* is *None*, we will try to read a users notmuch 
+        """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`.
         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`.
@@ -89,8 +97,8 @@ class Database(object):
         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
         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
+        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.
         reading only does not make a great deal of sense.
 
         :param path: A directory in which we should create the database.
@@ -134,7 +142,7 @@ class Database(object):
     def get_path(self):
         """Returns the file path of an open database
 
     def get_path(self):
         """Returns the file path of an open database
 
-        Wraps notmuch_database_get_path"""
+        Wraps *notmuch_database_get_path*."""
         # Raise a NotmuchError if not initialized
         self._verify_initialized_db()
 
         # Raise a NotmuchError if not initialized
         self._verify_initialized_db()
 
@@ -155,10 +163,10 @@ class Database(object):
     def needs_upgrade(self):
         """Does this database need to be upgraded before writing to it?
 
     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.
+        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
 
         :returns: `True` or `False`
         :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
@@ -167,7 +175,71 @@ class Database(object):
         # Raise a NotmuchError if not initialized
         self._verify_initialized_db()
 
         # Raise a NotmuchError if not initialized
         self._verify_initialized_db()
 
-        return notmuch_database_needs_upgrade(self.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
 
     def add_message(self, filename):
         """Adds a new message to the database
@@ -236,7 +308,7 @@ class Database(object):
         is removed for a particular message, the database content for that
         message will be entirely removed.
 
         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 STATUS value with the following meaning:
 
              STATUS.SUCCESS
                The last filename was removed and the message was removed 
 
              STATUS.SUCCESS
                The last filename was removed and the message was removed 
@@ -521,3 +593,230 @@ class Query(object):
         """Close and free the Query"""
         if self._query is not None:
             nmlib.notmuch_query_destroy (self._query)
         """Close and free the Query"""
         if self._query is not None:
             nmlib.notmuch_query_destroy (self._query)
+
+
+#------------------------------------------------------------------------------
+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_directory_get_mtime"""
+    _get_mtime = nmlib.notmuch_directory_get_mtime
+    _get_mtime.restype = c_long
+
+    """notmuch_directory_set_mtime"""
+    _set_mtime = nmlib.notmuch_directory_set_mtime
+    _set_mtime.argtypes = [c_char_p, c_long]
+
+    """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):
+        """
+        :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.
+        """
+        self._path = path
+        self._dir_p = dir_p
+        self._parent = parent
+
+
+    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
+        """
+        #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
+        self._verify_dir_initialized()
+
+        #TODO: make sure, we convert the mtime parameter to a 'c_long'
+        status = Directory._set_mtime(self._dir_p, mtime)
+
+        #return on success
+        if status == STATUS.SUCCESS:
+            return
+        #fail with Exception otherwise
+        raise NotmuchError(status)
+
+    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 
+        :returns: Nothing on success, raising an exception on failure.
+        :exception: :exc:`NotmuchError`:
+
+                        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)
+
+    # 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 
+                     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 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)
+
+    @property
+    def path(self):
+        """Returns the absolute path of this Directory (read-only)"""
+        return self._path
+
+    def __repr__(self):
+        """Object representation"""
+        return "<cnotmuch Directory object '%s'>" % self._path
+
+    def __del__(self):
+        """Close and free the Directory"""
+        if self._dir_p is not None:
+            nmlib.notmuch_directory_destroy(self._dir_p)
+
+#------------------------------------------------------------------------------
+class Filenames(object):
+    """An iterator over File- or Directory names that are stored in the database
+    """
+
+    #notmuch_filenames_get
+    _get = nmlib.notmuch_filenames_get
+    _get.restype = c_char_p
+
+    def __init__(self, files_p, parent):
+        """
+        :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.
+        """
+        self._files_p = files_p
+        self._parent = parent
+
+    def __iter__(self):
+        """ Make Filenames an iterator """
+        return self
+
+    def next(self):
+        if self._files_p is None:
+            raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+        if not nmlib.notmuch_filenames_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)
+        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 
+               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)!!!
+                     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 += 1
+        self._files_p = None
+        return i
+
+    def __del__(self):
+        """Close and free Filenames"""
+        if self._files_p is not None:
+            nmlib.notmuch_filenames_destroy(self._files_p)