10fb6dcce4d3b6450386c8008a46d882b243cbd9
[notmuch] / bindings / python / notmuch / query.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
18 """
19
20 from ctypes import c_char_p, c_uint, POINTER, byref
21 from .globals import (
22     nmlib,
23     Enum,
24     _str,
25     NotmuchQueryP,
26     NotmuchThreadsP,
27     NotmuchDatabaseP,
28     NotmuchMessagesP,
29 )
30 from .errors import (
31     NotmuchError,
32     NullPointerError,
33     NotInitializedError,
34 )
35 from .threads import Threads
36 from .messages import Messages
37
38
39 class Query(object):
40     """Represents a search query on an opened :class:`Database`.
41
42     A query selects and filters a subset of messages from the notmuch
43     database we derive from.
44
45     :class:`Query` provides an instance attribute :attr:`sort`, which
46     contains the sort order (if specified via :meth:`set_sort`) or
47     `None`.
48
49     Any function in this class may throw an :exc:`NotInitializedError`
50     in case the underlying query object was not set up correctly.
51
52     .. note:: Do remember that as soon as we tear down this object,
53            all underlying derived objects such as threads,
54            messages, tags etc will be freed by the underlying library
55            as well. Accessing these objects will lead to segfaults and
56            other unexpected behavior. See above for more details.
57     """
58     # constants
59     SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
60     """Constants: Sort order in which to return results"""
61
62     def __init__(self, db, querystr):
63         """
64         :param db: An open database which we derive the Query from.
65         :type db: :class:`Database`
66         :param querystr: The query string for the message.
67         :type querystr: utf-8 encoded str or unicode
68         """
69         self._db = None
70         self._query = None
71         self.sort = None
72         self.create(db, querystr)
73
74     def _assert_query_is_initialized(self):
75         """Raises :exc:`NotInitializedError` if self._query is `None`"""
76         if not self._query:
77             raise NotInitializedError()
78
79     """notmuch_query_create"""
80     _create = nmlib.notmuch_query_create
81     _create.argtypes = [NotmuchDatabaseP, c_char_p]
82     _create.restype = NotmuchQueryP
83
84     def create(self, db, querystr):
85         """Creates a new query derived from a Database
86
87         This function is utilized by __init__() and usually does not need to
88         be called directly.
89
90         :param db: Database to create the query from.
91         :type db: :class:`Database`
92         :param querystr: The query string
93         :type querystr: utf-8 encoded str or unicode
94         :raises:
95             :exc:`NullPointerError` if the query creation failed
96                 (e.g. too little memory).
97             :exc:`NotInitializedError` if the underlying db was not
98                 intitialized.
99         """
100         db._assert_db_is_initialized()
101         # create reference to parent db to keep it alive
102         self._db = db
103         # create query, return None if too little mem available
104         query_p = Query._create(db._db, _str(querystr))
105         if not query_p:
106             raise NullPointerError
107         self._query = query_p
108
109     _set_sort = nmlib.notmuch_query_set_sort
110     _set_sort.argtypes = [NotmuchQueryP, c_uint]
111     _set_sort.argtypes = None
112
113     def set_sort(self, sort):
114         """Set the sort order future results will be delivered in
115
116         :param sort: Sort order (see :attr:`Query.SORT`)
117         """
118         self._assert_query_is_initialized()
119         self.sort = sort
120         self._set_sort(self._query, sort)
121
122     _exclude_tag = nmlib.notmuch_query_add_tag_exclude
123     _exclude_tag.argtypes = [NotmuchQueryP, c_char_p]
124     _exclude_tag.resttype = None
125
126     def exclude_tag(self, tagname):
127         """Add a tag that will be excluded from the query results by default.
128
129         This exclusion will be overridden if this tag appears explicitly in the
130         query.
131
132         :param tagname: Name of the tag to be excluded
133         """
134         self._assert_query_is_initialized()
135         self._exclude_tag(self._query, _str(tagname))
136
137     """notmuch_query_search_threads"""
138     _search_threads = nmlib.notmuch_query_search_threads
139     _search_threads.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)]
140     _search_threads.restype = c_uint
141
142     def search_threads(self):
143         """Execute a query for threads
144
145         Execute a query for threads, returning a :class:`Threads` iterator.
146         The returned threads are owned by the query and as such, will only be
147         valid until the Query is deleted.
148
149         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
150         match the query. The method :meth:`Message.get_flag` allows us
151         to get the value of this flag.
152
153         :returns: :class:`Threads`
154         :raises: :exc:`NullPointerError` if search_threads failed
155         """
156         self._assert_query_is_initialized()
157         threads_p = NotmuchThreadsP() # == NULL
158         status = Query._search_threads(self._query, byref(threads_p))
159         if status != 0:
160             raise NotmuchError(status)
161
162         if not threads_p:
163             raise NullPointerError
164         return Threads(threads_p, self)
165
166     """notmuch_query_search_messages_st"""
167     _search_messages = nmlib.notmuch_query_search_messages
168     _search_messages.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)]
169     _search_messages.restype = c_uint
170
171     def search_messages(self):
172         """Filter messages according to the query and return
173         :class:`Messages` in the defined sort order
174
175         :returns: :class:`Messages`
176         :raises: :exc:`NullPointerError` if search_messages failed
177         """
178         self._assert_query_is_initialized()
179         msgs_p = NotmuchMessagesP() # == NULL
180         status = Query._search_messages(self._query, byref(msgs_p))
181         if status != 0:
182             raise NotmuchError(status)
183
184         if not msgs_p:
185             raise NullPointerError
186         return Messages(msgs_p, self)
187
188     _count_messages = nmlib.notmuch_query_count_messages
189     _count_messages.argtypes = [NotmuchQueryP, POINTER(c_uint)]
190     _count_messages.restype = c_uint
191
192     def count_messages(self):
193         '''
194         This function performs a search and returns Xapian's best
195         guess as to the number of matching messages.
196
197         :returns: the estimated number of messages matching this query
198         :rtype:   int
199         '''
200         self._assert_query_is_initialized()
201         count = c_uint(0)
202         status = Query._count_messages(self._query, byref(count))
203         if status != 0:
204             raise NotmuchError(status)
205         return count.value
206
207     _count_threads = nmlib.notmuch_query_count_threads_st
208     _count_threads.argtypes = [NotmuchQueryP, POINTER(c_uint)]
209     _count_threads.restype = c_uint
210
211     def count_threads(self):
212         '''
213         This function performs a search and returns the number of
214         unique thread IDs in the matching messages. This is the same
215         as number of threads matching a search.
216
217         Note that this is a significantly heavier operation than
218         meth:`Query.count_messages`.
219
220         :returns: the number of threads returned by this query
221         :rtype:   int
222         '''
223         self._assert_query_is_initialized()
224         count = c_uint(0)
225         status = Query._count_threads(self._query, byref(count))
226         if status != 0:
227             raise NotmuchError(status)
228         return count.value
229
230     _destroy = nmlib.notmuch_query_destroy
231     _destroy.argtypes = [NotmuchQueryP]
232     _destroy.restype = None
233
234     def __del__(self):
235         """Close and free the Query"""
236         if self._query:
237             self._destroy(self._query)