Return-Path: X-Original-To: notmuch@notmuchmail.org Delivered-To: notmuch@notmuchmail.org Received: from localhost (localhost [127.0.0.1]) by olra.theworths.org (Postfix) with ESMTP id 48AAB431FBC for ; Tue, 29 Dec 2009 01:16:49 -0800 (PST) X-Virus-Scanned: Debian amavisd-new at olra.theworths.org Received: from olra.theworths.org ([127.0.0.1]) by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id lZx0k+k4kSYT for ; Tue, 29 Dec 2009 01:16:48 -0800 (PST) Received: from mail-qy0-f202.google.com (mail-qy0-f202.google.com [209.85.221.202]) by olra.theworths.org (Postfix) with ESMTP id 0C4CC431FAE for ; Tue, 29 Dec 2009 01:16:47 -0800 (PST) Received: by qyk40 with SMTP id 40so5264405qyk.22 for ; Tue, 29 Dec 2009 01:16:46 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:received:received:content-type:subject:from:to :date:message-id:user-agent:content-transfer-encoding; bh=cnZ8E1369lPl4czU/3aXQAa3yUO9YDGIOQ6GLJ3KAao=; b=TG3MuSKA2ZYBbmoODzGPKhp1zEyWM7vYaWQjHG9KrbuTXpH6I4tUTVFYJ+T+KaiQh0 c/SUa4ridmXxuR9kp8AiZzhwnWY/aGUaAnii17LQDgZlVy3WQ7BgGnEJpucLyTDb+CDF ugoTw8Lgxtf0V7SprjUFhI5XdOyhARltmHWr4= DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=content-type:subject:from:to:date:message-id:user-agent :content-transfer-encoding; b=KIG/6hxK0Z4QGZbPFIaF9PZL/LzYU2BMr2h2qBBL5JArwwpOeq5E0joyrNqXGA4V60 DBNnPLv7QrLl6aigCZQTEN30GERlmrSmQm8X1OFy5AfbrZolhJM/HCTtaqeDXud9glEN frE34NFuEvIRHNuy2K/5EO2IvUd6iVA/c1li4= Received: by 10.224.26.200 with SMTP id f8mr7947250qac.156.1262078205941; Tue, 29 Dec 2009 01:16:45 -0800 (PST) Received: from localhost (c-24-61-223-13.hsd1.nh.comcast.net [24.61.223.13]) by mx.google.com with ESMTPS id 6sm33608070qwd.26.2009.12.29.01.16.44 (version=TLSv1/SSLv3 cipher=RC4-MD5); Tue, 29 Dec 2009 01:16:44 -0800 (PST) Content-Type: text/plain; charset=UTF-8 From: Ben Gamari To: notmuch Date: Tue, 29 Dec 2009 04:16:43 -0500 Message-Id: <1262078148-sup-7891@ben-laptop> User-Agent: Sup/git Content-Transfer-Encoding: 8bit X-Mailman-Approved-At: Tue, 29 Dec 2009 21:42:08 -0800 Subject: [notmuch] SWIG (and particularly Python) bindings X-BeenThere: notmuch@notmuchmail.org X-Mailman-Version: 2.1.12 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 29 Dec 2009 09:16:49 -0000 Hey all, I've been looking at switching away from sup recently to something with a slightly little less everything-and-the-kitchen-sink philosophy. Notmuch looks excellent, although it appears that its current front-end for my editor of choice (vim) is a little lacking in some ways (operations involving a call to notmuch invariably lock up vi for the duration of the operation). Regardless, I thought it might be nice to have access to the notmuch backend from a language other than C (preferably my high-level language of choice, python). To this end, I took a few hours today acquainting myself with SWIG and produced these bindings for Python. Unfortunately, it doesn't appear that SWIG has particularly good support for object-oriented C, so the approach I took was using SWIG to generate the low-level glue, on top of which I wrote a properly structured set of Python bindings. While I theoretically have full interface coverage, I unfortunately haven't had a chance to use these for anything useful, so who knows whether much of it actually works. Regardless, I thought folks might be interested. While the bindings are currently in the form of a patch to notmuch (creating a top-level swig directory in the source tree), they could certainly be moved out-of-tree if the powers that be don't feel it appropriate to include them. Unfortunately, the build system is currently almost entirely independent from the notmuch build system. If these are to be included in-tree, I would be curious to hear people have to say about how we might integrate it into the sup build system. Hope this helps someone. I'll hopefully have a chance to work with the bindings soon and will send a new set of patches once I think I've found all of the obvious issues. - Ben --- swig/Makefile | 18 ++++ swig/notmuch.py | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++ swig/notmuch_funcs.i | 9 ++ 3 files changed, 249 insertions(+), 0 deletions(-) create mode 100644 swig/Makefile create mode 100644 swig/notmuch.py create mode 100644 swig/notmuch_funcs.i diff --git a/swig/Makefile b/swig/Makefile new file mode 100644 index 0000000..c01c427 --- /dev/null +++ b/swig/Makefile @@ -0,0 +1,18 @@ +include ../Makefile.config + +INCLUDES=-I../lib -I/usr/include/python2.6 +CFLAGS=${INCLUDES} + +all : python + +python : _notmuch_funcs.so notmuch.py + +_notmuch_funcs.so : notmuch_funcs_wrap.o + g++ -shared ${XAPIAN_LDFLAGS} ${GMIME_LDFLAGS} ${TALLOC_LDFLAGS} -o $@ $< ../lib/notmuch.a + +%_wrap.o : %_wrap.c + gcc ${CFLAGS} -fPIC -c -o $@ $< + +%_wrap.c %.py : %.i + swig -python ${INCLUDES} $< + diff --git a/swig/notmuch.py b/swig/notmuch.py new file mode 100644 index 0000000..95b81ad --- /dev/null +++ b/swig/notmuch.py @@ -0,0 +1,222 @@ +import notmuch_funcs as nm +from datetime import datetime + +class Exception(Exception): + def get_message(): + errors = { + nm.NOTMUCH_STATUS_OUT_OF_MEMORY: 'out of memory', + nm.NOTMUCH_STATUS_READONLY_DATABASE: 'database opened as read-only', + nm.NOTMUCH_STATUS_XAPIAN_EXCEPTION: 'xapian error', + nm.NOTMUCH_STATUS_FILE_ERROR: 'file error', + nm.NOTMUCH_STATUS_FILE_NOT_EMAIL: 'file not email message', + nm.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: 'duplicate message id', + nm.NOTMUCH_STATUS_NULL_POINTER: 'null pointer', + nm.NOTMUCH_STATUS_TAG_TOO_LONG: 'tag name too long', + nm.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: 'unbalanced freeze/thaw', + } + return errors.get(self.status, 'unknown error') + + def __init__(self, status): + self.status = status + +def _handle_status(status): + if (status != nm.NOTMUCH_STATUS_SUCCESS): + raise Exception(status) + +class Database(object): + MODE_READ_ONLY = nm.NOTMUCH_DATABASE_MODE_READ_ONLY + MODE_READ_WRITE = nm.NOTMUCH_DATABASE_MODE_READ_WRITE + + def __init__(self, db): + if not db: + raise "Failed to open database" + self.db = db + + @staticmethod + def create(path): + return Database(nm.notmuch_database_create(path)) + + @staticmethod + def open(path, mode): + return Database(nm.notmuch_database_open(path, mode)) + + def close(self): + nm.notmuch_database_close(self.db) + + def get_path(self): + return nm.notmuch_database_get_path(self.db) + + def set_timestamp(self, key, timestamp): + _handle_status(nm.notmuch_database_set_timestamp(self.db, key, timestamp)) + + def get_timestamp(self, key): + return datetime.fromtimestamp(nm.notmuch_database_get_timestamp(self.db, key)) + + def add_message(self, filename, message): + _handle_status(nm.notmuch_database_add_message(self.db, filename, message.message)) + + def find_message(self, message_id): + return Message(nm.notmuch_database_find_message(self.db, message_id)) + + def get_all_tags(self): + return Tags(nm.notmuch_database_get_all_tags(self.db)) + + def __destroy__(self): + self.close() + +class Query(object): + def __init__(self, db, query_string): + self.query = nm.notmuch_query_create(db, query_string) + if not self.query: # This might not work + raise "Failed to create query" + + def set_sort(self, sort): + nm.notmuch_query_set_sort(self.query, sort) + + def search_threads(self): + return Threads(nm.notmuch_query_search_threads(self.query)) + + def search_messages(self): + return Messages(nm.notmuch_query_search_messages(self.query)) + + def count_message(self): + return nm.notmuch_query_count_messages(self.query) + + def __destroy__(self): + nm.notmuch_query_destroy(self.query) + +class Tags(object): + def __init__(self, tags): + self.tags = tags + + def __iter__(self): + return self + + def next(self): + if not nm.notmuch_tags_has_more(self.tags): + raise StopIteration + else: + h = nm.notmuch_tags_get(self.tags) + nm.notmuch_tags_advance(self.tags) + return h + + def __destroy__(self): + nm.notmuch_messages_destroy(self.tags) + +class Thread(object): + def __init__(self, thread): + self.thread = thread + + def get_thread_id(self): + return nm.notmuch_thread_get_thread_id(self.thread) + + def get_total_messages(self): + return nm.notmuch_thread_total_messages(self.thread) + + def get_toplevel_messages(self): + return Messages(nm.notmuch_thread_get_toplevel_messages(self.thread)) + + def get_matched_messages(self): + return nm.notmuch_thread_get_matched_messages(self.thread) + + def get_authors(self): + return nm.notmuch_thread_get_authors(self.thread) + + def get_subject(self): + return nm.notmuch_thread_get_subject(self.thread) + + def get_oldest_date(self): + return nm.notmuch_thread_get_oldest_date(self.thread) + + def get_newest_date(self): + return nm.notmuch_thread_get_newest_date(self.thread) + + def get_tags(self): + return Tags(nm.notmuch_thread_get_tags(self.thread)) + + def __destroy__(self): + nm.notmuch_thread_destroy(self.thread) + +class Threads(object): + def __init__(self, threads): + self.threads = threads + + def __iter__(self): + return self + + def next(self): + if not nm.notmuch_threads_has_more(self.threads): + raise StopIteration + else: + h = Thread(nm.notmuch_threads_get(self.threads)) + nm.notmuch_threads_advance(self.threads) + return h + + def __destroy__(self): + nm.notmuch_threads_destroy(self.threads) + +class Messages(object): + def __init__(self, messages): + self.messages = messages + + def __iter__(self): + return self + + def next(self): + if not nm.notmuch_messages_has_more(self.messages): + raise StopIteration + else: + h = Message(nm.notmuch_messages_get(self.messages)) + nm.notmuch_messages_advance(self.messages) + return h + + def __destroy__(self): + nm.notmuch_messages_destroy(self.messages) + + def collect_tags(self): + return Tags(nm.notmuch_messages_collect_tags(self.messages)) + +class Message(object): + def __init__(self, message): + self.message = message + + def get_replies(self): + return Messages(nm.notmuch_message_get_replies(self.message)) + + def get_filename(self): + return nm.notmuch_message_get_filename(self.message) + + def get_flag(self, flag): + return bool(nm.notmuch_message_get_flag(self.message, flag)) + + def set_flag(self, flag, value): + return nm.notmuch_message_set_flag(self.message, flag, value) + + def get_date(self): + return datetime.fromtimestamp(nm.notmuch_message_get_date(self.message)) + + def get_header(self, header): + return nm.notmuch_message_get_header(self.message, header) + + def get_tags(self): + return Tags(nm.notmuch_message_get_tags(self.message)) + + def add_tag(self, tag): + _handle_status(nm.notmuch_message_add_tag(self.message, tag)) + + def remove_tag(self, tag): + _handle_status(nm.notmuch_message_remove_tag(self.message, tag)) + + def remove_all_tags(self): + nm.notmuch_message_remove_all_tags(self.message) + + def freeze(self): + nm.notmuch_message_freeze(self.message) + + def thaw(self): + nm.notmuch_message_thaw(self.message) + + def __destroy__(self): + nm.notmuch_message_destroy(self.message) + + diff --git a/swig/notmuch_funcs.i b/swig/notmuch_funcs.i new file mode 100644 index 0000000..cc1826e --- /dev/null +++ b/swig/notmuch_funcs.i @@ -0,0 +1,9 @@ +%module notmuch_funcs + +%{ +#define SWIG_FILE_WITH_INIT +#include "notmuch.h" +%} + +%include "notmuch.h" + -- 1.6.3.3