python: move Query class to its own file
[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 <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18 """
19
20 from ctypes import c_char_p, c_uint
21 from notmuch.globals import (
22     nmlib,
23     Enum,
24     _str,
25     NotmuchQueryP,
26     NotmuchThreadsP,
27     NotmuchDatabaseP,
28     NotmuchMessagesP,
29     NullPointerError,
30     NotInitializedError,
31 )
32 from notmuch.thread import Threads
33 from notmuch.message import Messages
34
35
36 class Query(object):
37     """Represents a search query on an opened :class:`Database`.
38
39     A query selects and filters a subset of messages from the notmuch
40     database we derive from.
41
42     :class:`Query` provides an instance attribute :attr:`sort`, which
43     contains the sort order (if specified via :meth:`set_sort`) or
44     `None`.
45
46     Any function in this class may throw an :exc:`NotInitializedError`
47     in case the underlying query object was not set up correctly.
48
49     .. note:: Do remember that as soon as we tear down this object,
50            all underlying derived objects such as threads,
51            messages, tags etc will be freed by the underlying library
52            as well. Accessing these objects will lead to segfaults and
53            other unexpected behavior. See above for more details.
54     """
55     # constants
56     SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
57     """Constants: Sort order in which to return results"""
58
59     """notmuch_query_create"""
60     _create = nmlib.notmuch_query_create
61     _create.argtypes = [NotmuchDatabaseP, c_char_p]
62     _create.restype = NotmuchQueryP
63
64     """notmuch_query_search_threads"""
65     _search_threads = nmlib.notmuch_query_search_threads
66     _search_threads.argtypes = [NotmuchQueryP]
67     _search_threads.restype = NotmuchThreadsP
68
69     """notmuch_query_search_messages"""
70     _search_messages = nmlib.notmuch_query_search_messages
71     _search_messages.argtypes = [NotmuchQueryP]
72     _search_messages.restype = NotmuchMessagesP
73
74     """notmuch_query_count_messages"""
75     _count_messages = nmlib.notmuch_query_count_messages
76     _count_messages.argtypes = [NotmuchQueryP]
77     _count_messages.restype = c_uint
78
79     def __init__(self, db, querystr):
80         """
81         :param db: An open database which we derive the Query from.
82         :type db: :class:`Database`
83         :param querystr: The query string for the message.
84         :type querystr: utf-8 encoded str or unicode
85         """
86         self._db = None
87         self._query = None
88         self.sort = None
89         self.create(db, querystr)
90
91     def _assert_query_is_initialized(self):
92         """Raises :exc:`NotInitializedError` if self._query is `None`"""
93         if self._query is None:
94             raise NotInitializedError()
95
96     def create(self, db, querystr):
97         """Creates a new query derived from a Database
98
99         This function is utilized by __init__() and usually does not need to
100         be called directly.
101
102         :param db: Database to create the query from.
103         :type db: :class:`Database`
104         :param querystr: The query string
105         :type querystr: utf-8 encoded str or unicode
106         :returns: Nothing
107         :exception:
108             :exc:`NullPointerError` if the query creation failed
109                 (e.g. too little memory).
110             :exc:`NotInitializedError` if the underlying db was not
111                 intitialized.
112         """
113         db._assert_db_is_initialized()
114         # create reference to parent db to keep it alive
115         self._db = db
116         # create query, return None if too little mem available
117         query_p = Query._create(db.db_p, _str(querystr))
118         if not query_p:
119             raise NullPointerError
120         self._query = query_p
121
122     _set_sort = nmlib.notmuch_query_set_sort
123     _set_sort.argtypes = [NotmuchQueryP, c_uint]
124     _set_sort.argtypes = None
125
126     def set_sort(self, sort):
127         """Set the sort order future results will be delivered in
128
129         :param sort: Sort order (see :attr:`Query.SORT`)
130         """
131         self._assert_query_is_initialized()
132         self.sort = sort
133         self._set_sort(self._query, sort)
134
135     def search_threads(self):
136         """Execute a query for threads
137
138         Execute a query for threads, returning a :class:`Threads` iterator.
139         The returned threads are owned by the query and as such, will only be
140         valid until the Query is deleted.
141
142         The method sets :attr:`Message.FLAG`\.MATCH for those messages that
143         match the query. The method :meth:`Message.get_flag` allows us
144         to get the value of this flag.
145
146         :returns: :class:`Threads`
147         :exception: :exc:`NullPointerError` if search_threads failed
148         """
149         self._assert_query_is_initialized()
150         threads_p = Query._search_threads(self._query)
151
152         if not threads_p:
153             raise NullPointerError
154         return Threads(threads_p, self)
155
156     def search_messages(self):
157         """Filter messages according to the query and return
158         :class:`Messages` in the defined sort order
159
160         :returns: :class:`Messages`
161         :exception: :exc:`NullPointerError` if search_messages failed
162         """
163         self._assert_query_is_initialized()
164         msgs_p = Query._search_messages(self._query)
165
166         if not msgs_p:
167             raise NullPointerError
168         return Messages(msgs_p, self)
169
170     def count_messages(self):
171         """Estimate the number of messages matching the query
172
173         This function performs a search and returns Xapian's best
174         guess as to the number of matching messages. It is much faster
175         than performing :meth:`search_messages` and counting the
176         result with `len()` (although it always returned the same
177         result in my tests). Technically, it wraps the underlying
178         *notmuch_query_count_messages* function.
179
180         :returns: :class:`Messages`
181         """
182         self._assert_query_is_initialized()
183         return Query._count_messages(self._query)
184
185     _destroy = nmlib.notmuch_query_destroy
186     _destroy.argtypes = [NotmuchQueryP]
187     _destroy.restype = None
188
189     def __del__(self):
190         """Close and free the Query"""
191         if self._query is not None:
192             self._destroy(self._query)