From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp2 ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms0.migadu.com with LMTPS id KJiWAzIg/WBB3AAAgWs5BA (envelope-from ) for ; Sun, 25 Jul 2021 10:26:26 +0200 Received: from aspmx1.migadu.com ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp2 with LMTPS id GHbXOjEg/WAMWwAAB5/wlQ (envelope-from ) for ; Sun, 25 Jul 2021 08:26:25 +0000 Received: from mail.notmuchmail.org (nmbug.tethera.net [144.217.243.247]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id EF5B8631E for ; Sun, 25 Jul 2021 10:26:24 +0200 (CEST) Received: from nmbug.tethera.net (localhost [127.0.0.1]) by mail.notmuchmail.org (Postfix) with ESMTP id 0E16B26A09; Sun, 25 Jul 2021 04:26:20 -0400 (EDT) X-Greylist: delayed 589 seconds by postgrey-1.36 at nmbug; Sun, 25 Jul 2021 04:25:59 EDT Received: from venus.etour.fr (venus.etour.fr [188.165.44.197]) by mail.notmuchmail.org (Postfix) with ESMTPS id 2448620685 for ; Sun, 25 Jul 2021 04:25:59 -0400 (EDT) Received: from snek.local (astrasbourg-651-1-68-19.w90-40.abo.wanadoo.fr [90.40.155.19]) (authenticated bits=0) by venus.etour.fr (8.15.2/8.15.2) with ESMTPSA id 16P8G2YC009242 (version=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 bits=256 verify=NO); Sun, 25 Jul 2021 10:16:03 +0200 DKIM-Filter: OpenDKIM Filter v2.11.0 venus.etour.fr 16P8G2YC009242 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=lange.nom.fr; s=201709; t=1627200963; bh=jPYh35xP33S4jpBwPvskbCkTKCyzD3QefNTRlEymPpc=; h=From:To:Cc:Subject:Date:From; b=b2Xp9RJKm3Pfbc/NWamVMUDlCEBQK4pTYd0cUULqiANHg6FHt+QmoS3nYrpkkldYZ ev3+H1WatgUyRC2NP/bRGEnccnOtfEqAhAuobamxD5F7HtIM8dw12x1QGzt2KhHgJB BRFc6NasJJcV6ekmB2Z2yKNUntM0FeyJb56BUti0= From: Ludovic LANGE To: notmuch@notmuchmail.org Subject: [PATCH 1/1] python/notmuch2: provide binding for database_get_directory() Date: Sun, 25 Jul 2021 10:16:02 +0200 Message-Id: <20210725081602.81497-1-ll-notmuch@lange.nom.fr> X-Mailer: git-send-email 2.32.0 MIME-Version: 1.0 X-Greylist: Sender succeeded SMTP AUTH, not delayed by milter-greylist-4.6.2 (venus.etour.fr [188.165.44.197]); Sun, 25 Jul 2021 10:16:03 +0200 (CEST) X-lange-nom-fr-MailScanner-Information: Please contact the ISP for more information X-lange-nom-fr-MailScanner-ID: 16P8G2YC009242 X-lange-nom-fr-MailScanner: Not scanned: please contact your Internet E-Mail Service Provider for details X-lange-nom-fr-MailScanner-SpamCheck: not spam, SpamAssassin (not cached, score=-3.099, required 6, autolearn=not spam, ALL_TRUSTED -1.00, BAYES_00 -1.90, DKIM_SIGNED 0.10, DKIM_VALID -0.10, DKIM_VALID_AU -0.10, DKIM_VALID_EF -0.10, URIBL_BLOCKED 0.00) X-lange-nom-fr-MailScanner-From: ll-notmuch@lange.nom.fr X-lange-nom-fr-MailScanner-Watermark: 1627805764.17673@5/TwuqaB81zJ54zWnqhEmg X-Spam-Status: No Message-ID-Hash: HTP7IK4UOCUF335I447BZKCZVBTD2MIS X-Message-ID-Hash: HTP7IK4UOCUF335I447BZKCZVBTD2MIS X-MailFrom: ll-notmuch@lange.nom.fr X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-notmuch.notmuchmail.org-0; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; suspicious-header CC: Ludovic LANGE X-Mailman-Version: 3.2.1 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Help: List-Post: List-Subscribe: List-Unsubscribe: Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit X-Migadu-Flow: FLOW_IN ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1627201585; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding:list-id:list-help: list-unsubscribe:list-subscribe:list-post:dkim-signature; bh=Mlri3Oc0wKqtpicEgIBQS38gZFt1FyRN2WjxlG2Zg0g=; b=r9gVcN+1oBV3f8ooE0zYrrk6s54eavPB/56CbBp2EflfDp5OgsdIP9OANUntgzYdQ5b/59 Yy4vLceSmbOPPC1rqYyztc+ZgVBknLblUMTDT3s2Xb7Y9mxz/Q2YXAbWnHOtbuo1I24XAr v7/PFtyyX33XRobVpPxmh/VyElFE3/kKOr7LbSmqysqDecf+pYQ5ABNwReyusbC7rpv1kZ TQEGOeMEuvakzM7KlSllZWcLlg3rWHtyTNlqrGp73pucdAvA06hGDnZWZBIemfLznBKMny LiUBIUd3OhurhRdzLZZIUq51RiN7xUsFYRYBir4MPcCJEcNLGhgidMy7rzCr4g== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1627201585; a=rsa-sha256; cv=none; b=qbcS7nYKEGW3/ymEmGAseSM8Q51qDRi533uxmV3B0z0A+XLqjKdxAl9u/P50Qe6lzIj3qR T+amD3/N5YXTfLpWM60tXn2epohHQ5lXZgelhRmWZj0DaLqvZy3vX3HN72fCCqYw4WE9m7 S0rXKAufp2hwo1rBDcrfX56WPIX3L9W+JV8maU1PxfL0521JSDdcJhYiZRvH7JyxUQAZiI AlGZ+NY+lij05srRm3xoK74t1usZP1zXz9am90H1CCgy1rj1mOO7Zmcl8E171gh8AhgD8/ bnaHLhCo0YBxURiW65eylUscGtT+pjfxppus5udqAGMyeQs4hKzMQUG8Wqb4cQ== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("body hash did not verify") header.d=lange.nom.fr header.s=201709 header.b=b2Xp9RJK; dmarc=fail reason="SPF not aligned (strict)" header.from=lange.nom.fr (policy=none); spf=pass (aspmx1.migadu.com: domain of notmuch-bounces@notmuchmail.org designates 144.217.243.247 as permitted sender) smtp.mailfrom=notmuch-bounces@notmuchmail.org X-Migadu-Spam-Score: 0.81 Authentication-Results: aspmx1.migadu.com; dkim=fail ("body hash did not verify") header.d=lange.nom.fr header.s=201709 header.b=b2Xp9RJK; dmarc=fail reason="SPF not aligned (strict)" header.from=lange.nom.fr (policy=none); spf=pass (aspmx1.migadu.com: domain of notmuch-bounces@notmuchmail.org designates 144.217.243.247 as permitted sender) smtp.mailfrom=notmuch-bounces@notmuchmail.org X-Migadu-Queue-Id: EF5B8631E X-Spam-Score: 0.81 X-Migadu-Scanner: scn0.migadu.com X-TUID: c5kb+ZV03iup database_get_directory() is accessible in the legacy bindings as a method on the database object. In the cffi bindings, it raises NotImplementedError, so we provide a naive implementation, and the corresponding implementation of Directory object. --- Hello, This a my first try at updating the python-cffi bindings. The motivation for that is related to `alot` which uses those bindings, and for which I'm missing the notmuch_database_get_directory() call. It may not be the cleanest implementation (I had to guess a few things) and I do not have much experience with neither notmuch C API, nor python's CFFI. I'm waiting for your comments in order to improve it, and hope it can be accepted. Regards, Ludovic. bindings/python-cffi/notmuch2/_build.py | 18 +++ bindings/python-cffi/notmuch2/_database.py | 42 ++++- bindings/python-cffi/notmuch2/_directory.py | 164 ++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 bindings/python-cffi/notmuch2/_directory.py diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index f712b6c5..0f0a0a46 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -134,6 +134,10 @@ ffibuilder.cdef( notmuch_database_get_revision (notmuch_database_t *notmuch, const char **uuid); notmuch_status_t + notmuch_database_get_directory (notmuch_database_t *database, + const char *path, + notmuch_directory_t **directory); + notmuch_status_t notmuch_database_index_file (notmuch_database_t *database, const char *filename, notmuch_indexopts_t *indexopts, @@ -303,6 +307,20 @@ ffibuilder.cdef( void notmuch_tags_destroy (notmuch_tags_t *tags); + notmuch_status_t + notmuch_directory_set_mtime (notmuch_directory_t *directory, + time_t mtime); + time_t + notmuch_directory_get_mtime (notmuch_directory_t *directory); + notmuch_filenames_t * + notmuch_directory_get_child_files (notmuch_directory_t *directory); + notmuch_filenames_t * + notmuch_directory_get_child_directories (notmuch_directory_t *directory); + notmuch_status_t + notmuch_directory_delete (notmuch_directory_t *directory); + void + notmuch_directory_destroy (notmuch_directory_t *directory); + notmuch_bool_t notmuch_filenames_valid (notmuch_filenames_t *filenames); const char * diff --git a/bindings/python-cffi/notmuch2/_database.py b/bindings/python-cffi/notmuch2/_database.py index 868f4408..e48fa895 100644 --- a/bindings/python-cffi/notmuch2/_database.py +++ b/bindings/python-cffi/notmuch2/_database.py @@ -13,6 +13,7 @@ import notmuch2._errors as errors import notmuch2._message as message import notmuch2._query as querymod import notmuch2._tags as tags +import notmuch2._directory as directory __all__ = ['Database', 'AtomicContext', 'DbRevision'] @@ -338,7 +339,46 @@ class Database(base.NotmuchObject): return DbRevision(rev, capi.ffi.string(raw_uuid[0])) def get_directory(self, path): - raise NotImplementedError + """Returns a :class:`Directory` from the database for path, + + :param path: An unicode string containing the path relative to the path + of database (see :attr:`path`), or else should be an absolute + path with initial components that match the path of 'database'. + :returns: :class:`Directory` or raises an exception. + :raises: :exc:`FileError` if path is not relative database or absolute + with initial components same as database. + + + :raises XapianError: A Xapian exception occurred. + :raises LookupError: The directory object referred to by ``pathname`` + does not exist in the database. + :raises FileNotEmailError: The file referreed to by + ``pathname`` is not recognised as an email message. + :raises UpgradeRequiredError: The database must be upgraded + first. + """ + if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + directory_pp = capi.ffi.new('notmuch_directory_t **') + ret = capi.lib.notmuch_database_get_directory( + self._db_p, os.fsencode(path), directory_pp) + + if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: + raise errors.NotmuchError(ret) + + directory_p = directory_pp[0] + if directory_p == capi.ffi.NULL: + raise LookupError + + if os.path.isabs(path): + # we got an 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)) + + ret_dir = directory.Directory(abs_dirpath, directory_p, self) + return ret_dir def default_indexopts(self): """Returns default index options for the database. diff --git a/bindings/python-cffi/notmuch2/_directory.py b/bindings/python-cffi/notmuch2/_directory.py new file mode 100644 index 00000000..1d48aa54 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_directory.py @@ -0,0 +1,164 @@ +import os +import pathlib + +import notmuch2._base as base +import notmuch2._capi as capi +import notmuch2._errors as errors +from ._message import FilenamesIter + +__all__ = ["Directory"] + + +class PurePathIter(FilenamesIter): + """Iterator for pathlib.PurePath objects.""" + + def __next__(self): + fname = super().__next__() + return pathlib.PurePath(os.fsdecode(fname)) + + +class Directory(base.NotmuchObject): + """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. + """ + + _msg_p = base.MemoryPointer() + + 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 + + @property + def alive(self): + if not self._parent.alive: + return False + try: + self._dir_p + except errors.ObjectDestroyedError: + return False + else: + return True + + def __del__(self): + """Close and free the Directory""" + self._destroy() + + def _destroy(self): + if self.alive: + capi.lib.notmuch_directory_destroy(self._dir_p) + self._dir_p = None + + 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.index_file` 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 + :raises: :exc:`XapianError` a Xapian exception occurred, mtime + not stored + :raises: :exc:`ReadOnlyDatabaseError` the database was opened + in read-only mode so directory mtime cannot be modified + :raises: :exc:`NotInitializedError` the directory object has not + been initialized + """ + ret = capi.lib.notmuch_directory_set_mtime(self._dir_p, mtime) + + if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: + raise errors.NotmuchError(ret) + + 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 + :raises: :exc:`NotmuchError`: + + :attr:`STATUS`.NOT_INITIALIZED + The directory has not been initialized + """ + return capi.lib.notmuch_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. + """ + fnames_p = capi.lib.notmuch_directory_get_child_files(self._dir_p) + return PurePathIter(self, fnames_p) + + 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. + """ + fnames_p = capi.lib.notmuch_directory_get_child_directories(self._dir_p) + return PurePathIter(self, fnames_p) + + @property + def path(self): + """Returns the absolute path of this Directory (read-only)""" + return self._path + + def __repr__(self): + """Object representation""" + try: + self._dir_p + except errors.ObjectDestroyedError: + return ''.format(self=self) + else: + return ''.format(self=self) -- 2.32.0