-import ctypes
+import ctypes, os
from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool
from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
import logging
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
_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`)
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
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
"""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):
: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)
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:`Database.add_message`, :meth:`Database.add_tag`,
+ 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.
: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)
: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
: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:
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")
@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.
_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.
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.
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"""
"""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_tags (notmuch_message_t *message)"""
_get_tags = nmlib.notmuch_message_get_tags
_get_tags.restype = c_void_p
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
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_date(self):
"""Returns time_t of the message date
STATUS.NULL_POINTER
The 'tag' argument is NULL
-
STATUS.TAG_TOO_LONG
The length of 'tag' is too long
(exceeds Message.NOTMUCH_TAG_MAX)
-
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so message cannot be
modified.
-
STATUS.NOT_INITIALIZED
The message has not been initialized.
"""
STATUS.NULL_POINTER
The 'tag' argument is NULL
- NOTMUCH_STATUS_TAG_TOO_LONG
+ STATUS.TAG_TOO_LONG
The length of 'tag' is too long
(exceeds NOTMUCH_TAG_MAX)
- NOTMUCH_STATUS_READ_ONLY_DATABASE
+ STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so message cannot
be modified.
STATUS.NOT_INITIALIZED
The message has not been initialized.
-
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
raise NotmuchError(status)
+ def remove_all_tags(self):
+ """Removes all tags from the given message.
+
+ See :meth:`freeze` for an example showing how to safely
+ replace tag values.
+
+ :returns: STATUS.SUCCESS if the tags were successfully removed.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so message cannot
+ be modified.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_remove_all_tags(self._msg)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+ def freeze(self):
+ """Freezes the current state of 'message' within the database
+
+ This means that changes to the message state, (via :meth:`add_tag`,
+ :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
+ committed to the database until the message is :meth:`thaw`ed.
+
+ Multiple calls to freeze/thaw are valid and these calls will
+ "stack". That is there must be as many calls to thaw as to freeze
+ before a message is actually thawed.
+
+ The ability to do freeze/thaw allows for safe transactions to
+ change tag values. For example, explicitly setting a message to
+ have a given set of tags might look like this::
+
+ msg.freeze()
+ msg.remove_all_tags()
+ for tag in new_tags:
+ msg.add_tag(tag)
+ msg.thaw()
+
+ With freeze/thaw used like this, the message in the database is
+ guaranteed to have either the full set of original tag values, or
+ the full set of new tag values, but nothing in between.
+
+ Imagine the example above without freeze/thaw and the operation
+ somehow getting interrupted. This could result in the message being
+ left with no tags if the interruption happened after
+ :meth:`remove_all_tags` but before :meth:`add_tag`.
+
+ :returns: STATUS.SUCCESS if the message was successfully frozen.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.READ_ONLY_DATABASE
+ Database was opened in read-only mode so message cannot
+ be modified.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_freeze(self._msg)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+ def thaw(self):
+ """Thaws the current 'message'
+
+ Thaw the current 'message', synchronizing any changes that may have
+ occurred while 'message' was frozen into the notmuch database.
+
+ See :meth:`freeze` for an example of how to use this
+ function to safely provide tag changes.
+
+ Multiple calls to freeze/thaw are valid and these calls with
+ "stack". That is there must be as many calls to thaw as to freeze
+ before a message is actually thawed.
+
+ :returns: STATUS.SUCCESS if the message was successfully frozen.
+ Raises an exception otherwise.
+ :exception: :exc:`NotmuchError`. They have the following meaning:
+
+ STATUS.UNBALANCED_FREEZE_THAW
+ An attempt was made to thaw an unfrozen message.
+ That is, there have been an unbalanced number of calls
+ to :meth:`freeze` and :meth:`thaw`.
+ STATUS.NOT_INITIALIZED
+ The message has not been initialized.
+ """
+ if self._msg is None:
+ raise NotmuchError(STATUS.NOT_INITIALIZED)
+
+ status = nmlib.notmuch_message_thaw(self._msg)
+
+ if STATUS.SUCCESS == status:
+ # return on success
+ return status
+
+ raise NotmuchError(status)
+
+
def __str__(self):
"""A message() is represented by a 1-line summary"""
msg = {}