~ubuntu-branches/ubuntu/quantal/zeitgeist/quantal

« back to all changes in this revision

Viewing changes to zeitgeist/client.py

  • Committer: Package Import Robot
  • Author(s): Didier Roche
  • Date: 2011-11-15 11:15:56 UTC
  • mfrom: (1.1.13)
  • Revision ID: package-import@ubuntu.com-20111115111556-4lmc5wdvjrsdm0ss
Tags: 0.8.99~alpha1-1ubuntu1
Upload to ubuntu the new zeitgeist rewritten in vala

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -.- coding: utf-8 -.-
2
 
 
3
 
# Zeitgeist
4
 
#
5
 
# Copyright © 2009-2011 Siegfried-Angel Gevatter Pujals <siegfried@gevatter.com>
6
 
# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
7
 
# Copyright © 2009-2011 Markus Korn <thekorn@gmx.de>
8
 
# Copyright © 2011 Collabora Ltd.
9
 
#             By Siegfried-Angel Gevatter Pujals <siegfried@gevatter.com>
10
 
#
11
 
# This program is free software: you can redistribute it and/or modify
12
 
# it under the terms of the GNU Lesser General Public License as published by
13
 
# the Free Software Foundation, either version 2.1 of the License, or
14
 
# (at your option) any later version.
15
 
#
16
 
# This program is distributed in the hope that it will be useful,
17
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 
# GNU Lesser General Public License for more details.
20
 
#
21
 
# You should have received a copy of the GNU Lesser General Public License
22
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
 
 
24
 
import dbus
25
 
import dbus.service
26
 
import dbus.mainloop.glib
27
 
import logging
28
 
import os.path
29
 
import sys
30
 
import inspect
31
 
 
32
 
from xml.etree import ElementTree
33
 
 
34
 
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
35
 
 
36
 
from zeitgeist.datamodel import (Event, Subject, TimeRange, StorageState,
37
 
        ResultType)
38
 
 
39
 
SIG_EVENT = "asaasay"
40
 
 
41
 
log = logging.getLogger("zeitgeist.client")
42
 
 
43
 
class _DBusInterface(object):
44
 
        """Wrapper around dbus.Interface adding convenience methods."""
45
 
 
46
 
        # We initialize those as sets in the constructor. Remember that we can't do
47
 
        # that here because otherwise all instances would share their state.
48
 
        _disconnect_callbacks = None
49
 
        _reconnect_callbacks = None
50
 
        _generic_callbacks = None
51
 
 
52
 
        @staticmethod
53
 
        def get_members(introspection_xml):
54
 
                """Parses the XML context returned by Introspect() and returns
55
 
                a tuple, where the first item is a list of all methods and the
56
 
                second one a list of all signals for the related interface
57
 
                """
58
 
                xml = ElementTree.fromstring(introspection_xml)
59
 
                nodes = xml.findall("interface/signal")
60
 
                signals = [node.attrib["name"] for node in nodes]
61
 
                nodes = xml.findall("interface/method")
62
 
                methods = [node.attrib["name"] for node in nodes]
63
 
                try:
64
 
                        methods.remove("Introspect") # Introspect is not part of the API
65
 
                except ValueError:
66
 
                        pass
67
 
                return methods, signals
68
 
 
69
 
        def reconnect(self):
70
 
                if not self._reconnect_when_needed:
71
 
                        return
72
 
                self.__proxy = dbus.SessionBus().get_object(
73
 
                        self.__iface.requested_bus_name, self.__object_path)
74
 
                self.__iface = dbus.Interface(self.__proxy, self.__interface_name)
75
 
                self._load_introspection_data()
76
 
 
77
 
        def _disconnection_safe(self, method_getter, *args, **kwargs):
78
 
                """
79
 
                Executes the method returned by method_getter. If it fails because
80
 
                the D-Bus connection was lost, it attempts to recover it and executes
81
 
                the method again.
82
 
                """
83
 
                
84
 
                custom_error_handler = None
85
 
                original_kwargs = dict(kwargs)
86
 
 
87
 
                def reconnecting_error_handler(e):
88
 
                        error = e.get_dbus_name()
89
 
                        if error == "org.freedesktop.DBus.Error.ServiceUnknown":
90
 
                                self.reconnect()
91
 
                                # We don't use the reconnecting_error_handler here since that'd
92
 
                                # get us into an endless loop if Zeitgeist really isn't there.
93
 
                                return method_getter()(*args, **original_kwargs)
94
 
                        else:
95
 
                                if custom_error_handler is not None:
96
 
                                        custom_error_handler(e)
97
 
                                else:
98
 
                                        raise
99
 
 
100
 
                if 'error_handler' in kwargs:
101
 
                        # If the method is being called asynchronously it'll call the given
102
 
                        # handler on failure instead of directly raising an exception.
103
 
                        custom_error_handler = kwargs['error_handler']
104
 
                        kwargs['error_handler'] = reconnecting_error_handler
105
 
 
106
 
                try:
107
 
                        return method_getter()(*args, **kwargs)
108
 
                except dbus.exceptions.DBusException, e:
109
 
                        return reconnecting_error_handler(e)
110
 
 
111
 
        def __getattr__(self, name):
112
 
                if self.__methods is not None and name not in self.__methods:
113
 
                        raise TypeError("Unknown method name: %s" % name)
114
 
                def _ProxyMethod(*args, **kwargs):
115
 
                        """
116
 
                        Method wrapping around a D-Bus call, which attempts to recover
117
 
                        the connection to Zeitgeist if it got lost.
118
 
                        """
119
 
                        return self._disconnection_safe(
120
 
                                lambda: getattr(self.__iface, name), *args, **kwargs)
121
 
                return _ProxyMethod
122
 
 
123
 
        def get_property(self, property_name):
124
 
                return self._disconnection_safe(
125
 
                        lambda: self.__proxy.get_dbus_method("Get", dbus.PROPERTIES_IFACE),
126
 
                        self.__interface_name, property_name)
127
 
 
128
 
        def connect(self, signal, callback, **kwargs):
129
 
                """Connect a callback to a signal of the current proxy instance."""
130
 
                if self.__signals is None:
131
 
                        self.reconnect()
132
 
                if signal not in self.__signals:
133
 
                        raise TypeError("Unknown signal name: %s" % signal)
134
 
                self._generic_callbacks.add((signal, callback))
135
 
                self.__proxy.connect_to_signal(
136
 
                        signal,
137
 
                        callback,
138
 
                        dbus_interface=self.__interface_name,
139
 
                        **kwargs)
140
 
 
141
 
        def connect_exit(self, callback):
142
 
                """Executes callback when the remote interface disappears from the bus"""
143
 
                self._disconnect_callbacks.add(callback)
144
 
 
145
 
        def connect_join(self, callback):
146
 
                """
147
 
                Executes callback when someone claims the Zeitgeist D-Bus name.
148
 
                This may be used to perform some action if the daemon is restarted while
149
 
                it was being used.
150
 
                """
151
 
                self._reconnect_callbacks.add(callback)
152
 
 
153
 
        @property
154
 
        def proxy(self):
155
 
                return self.__proxy
156
 
 
157
 
        def _load_introspection_data(self):
158
 
                self.__methods, self.__signals = self.get_members(
159
 
                        self.__proxy.Introspect(
160
 
                                dbus_interface='org.freedesktop.DBus.Introspectable'))
161
 
 
162
 
        def __init__(self, proxy, interface_name, object_path, reconnect=True):
163
 
                self.__proxy = proxy
164
 
                self.__interface_name = interface_name
165
 
                self.__object_path = object_path
166
 
                self.__iface = dbus.Interface(proxy, interface_name)
167
 
                self._reconnect_when_needed = reconnect
168
 
                self._load_introspection_data()
169
 
                
170
 
                self._disconnect_callbacks = set()
171
 
                self._reconnect_callbacks = set()
172
 
                self._generic_callbacks = set()
173
 
                
174
 
                # Listen to (dis)connection notifications, for connect_exit and connect_join
175
 
                def name_owner_changed(connection_name):
176
 
                        if connection_name == "":
177
 
                                callbacks = self._disconnect_callbacks
178
 
                                self.__methods = self.__signals = None
179
 
                        else:
180
 
                                if not self._reconnect_when_needed:
181
 
                                        return
182
 
                                self.reconnect()
183
 
                                callbacks = self._reconnect_callbacks
184
 
                                for signal, callback in self._generic_callbacks:
185
 
                                        try:
186
 
                                                self.connect(signal, callback)
187
 
                                        except TypeError:
188
 
                                                log.exception("Failed to reconnect to signal \"%s\" "
189
 
                                                        "after engine disconnection." % signal)
190
 
                        for callback in callbacks:
191
 
                                callback()
192
 
                dbus.SessionBus().watch_name_owner(self.__iface.requested_bus_name,
193
 
                        name_owner_changed)
194
 
 
195
 
class ZeitgeistDBusInterface(object):
196
 
        """ Central DBus interface to the Zeitgeist engine
197
 
        
198
 
        There does not necessarily have to be one single instance of this
199
 
        interface class, but all instances should share the same state
200
 
        (like use the same bus and be connected to the same proxy). This is
201
 
        achieved by extending the `Borg Pattern` as described by Alex Martelli  
202
 
        """
203
 
        __shared_state = {}
204
 
        
205
 
        BUS_NAME = "org.gnome.zeitgeist.Engine"
206
 
        INTERFACE_NAME = "org.gnome.zeitgeist.Log"
207
 
        OBJECT_PATH = "/org/gnome/zeitgeist/log/activity"
208
 
        
209
 
        def __getattr__(self, name):
210
 
                return getattr(self.__shared_state["dbus_interface"], name)
211
 
        
212
 
        def version(self):
213
 
                """Returns the API version"""
214
 
                dbus_interface = self.__shared_state["dbus_interface"]
215
 
                return dbus_interface.get_property("version")
216
 
        
217
 
        def extensions(self):
218
 
                """Returns active extensions"""
219
 
                dbus_interface = self.__shared_state["dbus_interface"]
220
 
                return dbus_interface.get_property("extensions")
221
 
        
222
 
        def get_extension(cls, name, path, busname=None):
223
 
                """ Returns an interface to the given extension.
224
 
                
225
 
                Example usage:
226
 
                        >> reg = get_extension("DataSourceRegistry", "data_source_registry")
227
 
                        >> reg.RegisterDataSource(...)
228
 
                """
229
 
                if busname:
230
 
                        busname = "org.gnome.zeitgeist.%s" % busname
231
 
                else:
232
 
                        busname = cls.BUS_NAME
233
 
                if not name in cls.__shared_state["extension_interfaces"]:
234
 
                        interface_name = "org.gnome.zeitgeist.%s" % name
235
 
                        object_path = "/org/gnome/zeitgeist/%s" % path
236
 
                        proxy = dbus.SessionBus().get_object(busname, object_path)
237
 
                        iface = _DBusInterface(proxy, interface_name, object_path)
238
 
                        iface.BUS_NAME = busname
239
 
                        iface.INTERFACE_NAME = interface_name
240
 
                        iface.OBJECT_PATH = object_path
241
 
                        cls.__shared_state["extension_interfaces"][name] = iface
242
 
                return cls.__shared_state["extension_interfaces"][name]
243
 
        
244
 
        def __init__(self, reconnect=True):
245
 
                if not "dbus_interface" in self.__shared_state:
246
 
                        try:
247
 
                                proxy = dbus.SessionBus().get_object(self.BUS_NAME,
248
 
                                        self.OBJECT_PATH)
249
 
                        except dbus.exceptions.DBusException, e:
250
 
                                if e.get_dbus_name() == "org.freedesktop.DBus.Error.ServiceUnknown":
251
 
                                        raise RuntimeError(
252
 
                                                "Found no running zeitgeist-daemon instance: %s" % \
253
 
                                                e.get_dbus_message())
254
 
                                else:
255
 
                                        raise
256
 
                        self.__shared_state["extension_interfaces"] = {}
257
 
                        self.__shared_state["dbus_interface"] = _DBusInterface(proxy,
258
 
                                self.INTERFACE_NAME, self.OBJECT_PATH, reconnect)
259
 
 
260
 
class Monitor(dbus.service.Object):
261
 
        """
262
 
        DBus interface for monitoring the Zeitgeist log for certain types
263
 
        of events.
264
 
        
265
 
        When using the Python bindings monitors are normally instantiated
266
 
        indirectly by calling :meth:`ZeitgeistClient.install_monitor`.
267
 
        
268
 
        It is important to understand that the Monitor instance lives on the
269
 
        client side, and expose a DBus service there, and the Zeitgeist engine
270
 
        calls back to the monitor when matching events are registered.
271
 
        """
272
 
        
273
 
        # Used in Monitor._next_path() to generate unique path names
274
 
        _last_path_id = 0
275
 
        
276
 
        _event_type = Event
277
 
 
278
 
        def __init__ (self, time_range, event_templates, insert_callback,
279
 
                delete_callback, monitor_path=None, event_type=None):
280
 
                if not monitor_path:
281
 
                        monitor_path = Monitor._next_path()
282
 
                elif isinstance(monitor_path, (str, unicode)):
283
 
                        monitor_path = dbus.ObjectPath(monitor_path)
284
 
                
285
 
                if event_type:
286
 
                        if not issubclass(event_type, Event):
287
 
                                raise TypeError("Event subclass expected.")
288
 
                        self._event_type = event_type
289
 
                
290
 
                self._time_range = time_range
291
 
                self._templates = event_templates
292
 
                self._path = monitor_path
293
 
                self._insert_callback = insert_callback
294
 
                self._delete_callback = delete_callback
295
 
                dbus.service.Object.__init__(self, dbus.SessionBus(), monitor_path)
296
 
        
297
 
        def get_path (self): return self._path
298
 
        path = property(get_path,
299
 
                doc="Read only property with the DBus path of the monitor object")
300
 
        
301
 
        def get_time_range(self): return self._time_range
302
 
        time_range = property(get_time_range,
303
 
                doc="Read only property with the :class:`TimeRange` matched by this monitor")
304
 
        
305
 
        def get_templates (self): return self._templates
306
 
        templates = property(get_templates,
307
 
                doc="Read only property with installed templates")
308
 
        
309
 
        @dbus.service.method("org.gnome.zeitgeist.Monitor",
310
 
                             in_signature="(xx)a("+SIG_EVENT+")")
311
 
        def NotifyInsert(self, time_range, events):
312
 
                """
313
 
                Receive notification that a set of events matching the monitor's
314
 
                templates has been recorded in the log.
315
 
                
316
 
                This method is the raw DBus callback and should normally not be
317
 
                overridden. Events are received via the *insert_callback*
318
 
                argument given in the constructor to this class.
319
 
                
320
 
                :param time_range: A two-tuple of 64 bit integers with the minimum
321
 
                    and maximum timestamps found in *events*. DBus signature (xx)
322
 
                :param events: A list of DBus event structs, signature a(asaasay)
323
 
                    with the events matching the monitor.
324
 
                    See :meth:`ZeitgeistClient.install_monitor`
325
 
                """
326
 
                self._insert_callback(TimeRange(time_range[0], time_range[1]),
327
 
                        map(self._event_type, events))
328
 
        
329
 
        @dbus.service.method("org.gnome.zeitgeist.Monitor",
330
 
                             in_signature="(xx)au")
331
 
        def NotifyDelete(self, time_range, event_ids):
332
 
                """
333
 
                Receive notification that a set of events within the monitor's
334
 
                matched time range has been deleted. Note that this notification
335
 
                will also be emitted for deleted events that doesn't match the
336
 
                event templates of the monitor. It's just the time range which
337
 
                is considered here.
338
 
                
339
 
                This method is the raw DBus callback and should normally not be
340
 
                overridden. Events are received via the *delete_callback*
341
 
                argument given in the constructor to this class.
342
 
                
343
 
                :param time_range: A two-tuple of 64 bit integers with the minimum
344
 
                    and maximum timestamps found in *events*. DBus signature (xx)
345
 
                :param event_ids: A list of event ids. An event id is simply
346
 
                    and unsigned 32 bit integer. DBus signature au.
347
 
                """
348
 
                self._delete_callback(TimeRange(time_range[0], time_range[1]), event_ids)
349
 
        
350
 
        def __hash__ (self):
351
 
                return hash(self._path)
352
 
        
353
 
        @classmethod
354
 
        def _next_path(cls):
355
 
                """
356
 
                Generate a new unique DBus object path for a monitor
357
 
                """
358
 
                cls._last_path_id += 1
359
 
                return dbus.ObjectPath("/org/gnome/zeitgeist/monitor/%s" % \
360
 
                        cls._last_path_id)
361
 
 
362
 
class ZeitgeistClient:
363
 
        """
364
 
        Convenience APIs to have a Pythonic way to call and monitor the running
365
 
        Zeitgeist engine. For raw DBus access use the
366
 
        :class:`ZeitgeistDBusInterface` class.
367
 
        
368
 
        Note that this class only does asynchronous DBus calls. This is almost
369
 
        always the right thing to do. If you really want to do synchronous
370
 
        DBus calls use the raw DBus API found in the ZeitgeistDBusInterface class.
371
 
        """
372
 
        
373
 
        _installed_monitors = []
374
 
        _event_type = Event
375
 
        
376
 
        @staticmethod
377
 
        def get_event_and_extra_arguments(arguments):
378
 
                """ some methods of :class:`ZeitgeistClient` take a variable
379
 
                number of arguments, where one part of the arguments are used
380
 
                to build one :class:`Event` instance and the other part
381
 
                is forwarded to another method. This function returns an event
382
 
                and the remaining arguments."""
383
 
                kwargs = {}
384
 
                for arg in _FIND_EVENTS_FOR_TEMPLATES_ARGS:
385
 
                        if arg in arguments:
386
 
                                kwargs[arg] = arguments.pop(arg)
387
 
                ev = Event.new_for_values(**arguments)
388
 
                return ev, kwargs
389
 
        
390
 
        def __init__ (self):
391
 
                self._iface = ZeitgeistDBusInterface()
392
 
                self._registry = self._iface.get_extension("DataSourceRegistry",
393
 
                        "data_source_registry")
394
 
                
395
 
                # Reconnect all active monitors if the connection is reset.
396
 
                def reconnect_monitors():
397
 
                        log.info("Reconnected to Zeitgeist engine...")
398
 
                        for monitor in self._installed_monitors:
399
 
                                self._iface.InstallMonitor(monitor.path,
400
 
                                        monitor.time_range,
401
 
                                        monitor.templates,
402
 
                                        reply_handler=self._void_reply_handler,
403
 
                                        error_handler=lambda err: log.warn(
404
 
                                                "Error reinstalling monitor: %s" % err))
405
 
                self._iface.connect_join(reconnect_monitors)
406
 
        
407
 
        def register_event_subclass(self, event_type):
408
 
                """
409
 
                Register a subclass of Event with this ZeiteistClient instance. When
410
 
                data received over D-Bus is instantiated into an Event object, the
411
 
                provided subclass will be used.
412
 
                """
413
 
                if not issubclass(event_type, Event):
414
 
                        raise TypeError("Event subclass expected.")
415
 
                self._event_type = event_type
416
 
        
417
 
        def register_subject_subclass(self, subject_type):
418
 
                """
419
 
                Register a subclass of Subject with this ZeiteistClient instance. When
420
 
                data received over D-Bus is instantiated into a Subject object, the
421
 
                provided subclass will be used.
422
 
                
423
 
                Note that this method works by changing the Event type associated with
424
 
                this ZeitgeistClient instance, so it should always be called *after*
425
 
                any register_event_subclass calls.
426
 
                
427
 
                Even better, if you also have a custom Event subclass, you may directly
428
 
                override the Subject type by changing its _subject_type class variable.
429
 
                """
430
 
                if not issubclass(subject_type, Subject):
431
 
                        raise TypeError("Subject subclass expected.")
432
 
                class EventWithCustomSubject(self._event_type):
433
 
                        _subject_type = subject_type
434
 
                self._event_type = EventWithCustomSubject
435
 
        
436
 
        def _safe_error_handler(self, error_handler, *args):
437
 
                if error_handler is not None:
438
 
                        if callable(error_handler):
439
 
                                return error_handler
440
 
                        raise TypeError(
441
 
                                "Error handler not callable, found %s" % error_handler)
442
 
                return lambda raw: self._stderr_error_handler(raw, *args)
443
 
        
444
 
        def _safe_reply_handler(self, reply_handler):
445
 
                if reply_handler is not None:
446
 
                        if callable(reply_handler):
447
 
                                return reply_handler
448
 
                        raise TypeError(
449
 
                                "Reply handler not callable, found %s" % reply_handler)
450
 
                return self._void_reply_handler
451
 
        
452
 
        # Properties
453
 
        
454
 
        def get_version(self):
455
 
                return [int(i) for i in self._iface.version()]
456
 
                
457
 
        def get_extensions(self):
458
 
                return [unicode(i) for i in self._iface.extensions()]
459
 
        
460
 
        # Methods
461
 
        
462
 
        def insert_event (self, event, ids_reply_handler=None, error_handler=None):
463
 
                """
464
 
                Send an event to the Zeitgeist event log. The 'event' parameter
465
 
                must be an instance of the Event class.
466
 
                
467
 
                The insertion will be done via an asynchronous DBus call and
468
 
                this method will return immediately. This means that the
469
 
                Zeitgeist engine will most likely not have inserted the event
470
 
                when this method returns. There will be a short delay.
471
 
                
472
 
                If the ids_reply_handler argument is set to a callable it will
473
 
                be invoked with a list containing the ids of the inserted events
474
 
                when they have been registered in Zeitgeist.
475
 
                
476
 
                In case of errors a message will be printed on stderr, and
477
 
                an empty result passed to ids_reply_handler (if set).
478
 
                To override this default set the error_handler named argument
479
 
                to a callable that takes a single exception as its sole
480
 
                argument.
481
 
                
482
 
                In order to use this method there needs to be a mainloop
483
 
                runnning. Both Qt and GLib mainloops are supported.
484
 
                """
485
 
                self.insert_events([event],
486
 
                                ids_reply_handler=ids_reply_handler,
487
 
                                error_handler=error_handler)
488
 
        
489
 
        def insert_event_for_values (self, **values):
490
 
                """
491
 
                Send an event to the Zeitgeist event log. The keyword arguments
492
 
                must match those as provided to Event.new_for_values().
493
 
                
494
 
                The insertion will be done via an asynchronous DBus call and
495
 
                this method will return immediately. This means that the
496
 
                Zeitgeist engine will most likely not have inserted the event
497
 
                when this method returns. There will be a short delay.
498
 
                
499
 
                If the ids_reply_handler argument is set to a callable it will
500
 
                be invoked with a list containing the ids of the inserted events
501
 
                when they have been registered in Zeitgeist.
502
 
                
503
 
                In case of errors a message will be printed on stderr, and
504
 
                an empty result passed to ids_reply_handler (if set).
505
 
                To override this default set the error_handler named argument
506
 
                to a callable that takes a single exception as its sole
507
 
                argument.
508
 
                
509
 
                In order to use this method there needs to be a mainloop
510
 
                runnning. Both Qt and GLib mainloops are supported.
511
 
                """
512
 
                ev = Event.new_for_values(**values)
513
 
                self.insert_events([ev],
514
 
                                values.get("ids_reply_handler", None),
515
 
                                values.get("error_handler", None))
516
 
        
517
 
        def insert_events (self, events, ids_reply_handler=None, error_handler=None):
518
 
                """
519
 
                Send a collection of events to the Zeitgeist event log. The
520
 
                *events* parameter must be a list or tuple containing only
521
 
                members of of type :class:`Event <zeitgeist.datamodel.Event>`.
522
 
                
523
 
                The insertion will be done via an asynchronous DBus call and
524
 
                this method will return immediately. This means that the
525
 
                Zeitgeist engine will most likely not have inserted the events
526
 
                when this method returns. There will be a short delay.
527
 
                
528
 
                In case of errors a message will be printed on stderr, and
529
 
                an empty result passed to *ids_reply_handler* (if set).
530
 
                To override this default set the *error_handler* named argument
531
 
                to a callable that takes a single exception as its sole
532
 
                argument.
533
 
                
534
 
                In order to use this method there needs to be a mainloop
535
 
                runnning. Both Qt and GLib mainloops are supported.
536
 
                """
537
 
                
538
 
                self._check_list_or_tuple(events)
539
 
                self._check_members(events, Event)
540
 
                self._iface.InsertEvents(events,
541
 
                                        reply_handler=self._safe_reply_handler(ids_reply_handler),
542
 
                                        error_handler=self._safe_error_handler(error_handler,
543
 
                                                self._safe_reply_handler(ids_reply_handler), []))
544
 
        
545
 
        def find_event_ids_for_templates (self,
546
 
                                        event_templates,
547
 
                                        ids_reply_handler,
548
 
                                        timerange = None,
549
 
                                        storage_state = StorageState.Any,
550
 
                                        num_events = 20,
551
 
                                        result_type = ResultType.MostRecentEvents,
552
 
                                        error_handler=None):
553
 
                """
554
 
                Send a query matching a collection of
555
 
                :class:`Event <zeitgeist.datamodel.Event>` templates to the
556
 
                Zeitgeist event log. The query will match if an event matches
557
 
                any of the templates. If an event template has more
558
 
                than one subject the query will match if any one of the subject
559
 
                templates match.
560
 
                
561
 
                The query will be done via an asynchronous DBus call and
562
 
                this method will return immediately. The return value
563
 
                will be passed to 'ids_reply_handler' as a list
564
 
                of integer event ids. This list must be the sole argument for
565
 
                the callback.
566
 
                
567
 
                The actual :class:`Events` can be looked up via the
568
 
                :meth:`get_events()` method.
569
 
                
570
 
                This method is intended for queries potentially returning a
571
 
                large result set. It is especially useful in cases where only
572
 
                a portion of the results are to be displayed at the same time
573
 
                (eg., by using paging or dynamic scrollbars), as by holding a
574
 
                list of IDs you keep a stable ordering, and you can ask for
575
 
                the details associated to them in batches, when you need them. For
576
 
                queries with a small amount of results, or where you need the
577
 
                information about all results at once no matter how many of them
578
 
                there are, see :meth:`find_events_for_templates`.
579
 
                 
580
 
                In case of errors a message will be printed on stderr, and
581
 
                an empty result passed to ids_reply_handler.
582
 
                To override this default set the error_handler named argument
583
 
                to a callable that takes a single exception as its sole
584
 
                argument.
585
 
                
586
 
                In order to use this method there needs to be a mainloop
587
 
                runnning. Both Qt and GLib mainloops are supported.
588
 
                
589
 
                :param event_templates: List or tuple of
590
 
                    :class:`Event <zeitgeist.datamodel.Event>` instances
591
 
                :param ids_reply_handler: Callable taking a list of integers
592
 
                :param timerange: A
593
 
                    :class:`TimeRange <zeitgeist.datamodel.TimeRange>` instance
594
 
                    that the events must have occured within. Defaults to
595
 
                    :meth:`TimeRange.until_now()`.
596
 
                :param storage_state: A value from the
597
 
                    :class:`StorageState <zeitgeist.datamodel.StorageState>`
598
 
                    enumeration. Defaults to :const:`StorageState.Any`
599
 
                :param num_events: The number of events to return; default is 20
600
 
                :param result_type: A value from the
601
 
                    :class:`ResultType <zeitgeist.datamodel.ResultType>`
602
 
                    enumeration. Defaults to ResultType.MostRecentEvent
603
 
                :param error_handler: Callback to catch error messages.
604
 
                        Read about the default behaviour above
605
 
                """
606
 
                self._check_list_or_tuple(event_templates)
607
 
                self._check_members(event_templates, Event)
608
 
                
609
 
                if not callable(ids_reply_handler):
610
 
                        raise TypeError(
611
 
                                "Reply handler not callable, found %s" % ids_reply_handler)
612
 
                
613
 
                if timerange is None:
614
 
                        timerange = TimeRange.until_now()
615
 
                
616
 
                self._iface.FindEventIds(timerange,
617
 
                                        event_templates,
618
 
                                        storage_state,
619
 
                                        num_events,
620
 
                                        result_type,
621
 
                                        reply_handler=self._safe_reply_handler(ids_reply_handler),
622
 
                                        error_handler=self._safe_error_handler(error_handler,
623
 
                                                ids_reply_handler, []))
624
 
        
625
 
        def find_event_ids_for_template (self, event_template, ids_reply_handler,
626
 
                **kwargs):
627
 
                """
628
 
                Alias for :meth:`find_event_ids_for_templates`, for use when only
629
 
                one template is needed.
630
 
                """
631
 
                self.find_event_ids_for_templates([event_template],
632
 
                                                ids_reply_handler,
633
 
                                                **kwargs)
634
 
        
635
 
        def find_event_ids_for_values(self, ids_reply_handler, **kwargs):
636
 
                """
637
 
                Alias for :meth:`find_event_ids_for_templates`, for when only
638
 
                one template is needed. Instead of taking an already created
639
 
                template, like :meth:`find_event_ids_for_template`, this method
640
 
                will construct the template from the parameters it gets. The
641
 
                allowed keywords are the same as the ones allowed by
642
 
                :meth:`Event.new_for_values() <zeitgeist.datamodel.Event.new_for_values>`.
643
 
                """
644
 
                ev, arguments = self.get_event_and_extra_arguments(kwargs)
645
 
                self.find_event_ids_for_templates([ev],
646
 
                                                ids_reply_handler,
647
 
                                                **arguments)
648
 
        
649
 
        def find_events_for_templates (self,
650
 
                                        event_templates,
651
 
                                        events_reply_handler,
652
 
                                        timerange = None,
653
 
                                        storage_state = StorageState.Any,
654
 
                                        num_events = 20,
655
 
                                        result_type = ResultType.MostRecentEvents,
656
 
                                        error_handler=None):
657
 
                """
658
 
                Send a query matching a collection of
659
 
                :class:`Event <zeitgeist.datamodel.Event>` templates to the
660
 
                Zeitgeist event log. The query will match if an event matches
661
 
                any of the templates. If an event template has more
662
 
                than one subject the query will match if any one of the subject
663
 
                templates match.
664
 
                
665
 
                The query will be done via an asynchronous DBus call and
666
 
                this method will return immediately. The return value
667
 
                will be passed to 'events_reply_handler' as a list
668
 
                of :class:`Event`s. This list must be the sole argument for
669
 
                the callback.
670
 
                
671
 
                If you need to do a query yielding a large (or unpredictable)
672
 
                result set and you only want to show some of the results at the
673
 
                same time (eg., by paging them), consider using
674
 
                :meth:`find_event_ids_for_templates`.
675
 
                 
676
 
                In case of errors a message will be printed on stderr, and
677
 
                an empty result passed to events_reply_handler.
678
 
                To override this default set the error_handler named argument
679
 
                to a callable that takes a single exception as its sole
680
 
                argument.
681
 
                
682
 
                In order to use this method there needs to be a mainloop
683
 
                runnning. Both Qt and GLib mainloops are supported.
684
 
                
685
 
                :param event_templates: List or tuple of
686
 
                    :class:`Event <zeitgeist.datamodel.Event>` instances
687
 
                :param events_reply_handler: Callable taking a list of integers
688
 
                :param timerange: A
689
 
                    :class:`TimeRange <zeitgeist.datamodel.TimeRange>` instance
690
 
                    that the events must have occured within. Defaults to
691
 
                    :meth:`TimeRange.until_now()`.
692
 
                :param storage_state: A value from the
693
 
                    :class:`StorageState <zeitgeist.datamodel.StorageState>`
694
 
                    enumeration. Defaults to :const:`StorageState.Any`
695
 
                :param num_events: The number of events to return; default is 20
696
 
                :param result_type: A value from the
697
 
                    :class:`ResultType <zeitgeist.datamodel.ResultType>`
698
 
                    enumeration. Defaults to ResultType.MostRecentEvent
699
 
                :param error_handler: Callback to catch error messages.
700
 
                        Read about the default behaviour above
701
 
                """
702
 
                self._check_list_or_tuple(event_templates)
703
 
                self._check_members(event_templates, Event)
704
 
                
705
 
                if not callable(events_reply_handler):
706
 
                        raise TypeError(
707
 
                                "Reply handler not callable, found %s" % events_reply_handler)
708
 
                
709
 
                if timerange is None:
710
 
                        timerange = TimeRange.until_now()
711
 
                
712
 
                self._iface.FindEvents(timerange,
713
 
                                        event_templates,
714
 
                                        storage_state,
715
 
                                        num_events,
716
 
                                        result_type,
717
 
                                        reply_handler=lambda raw: events_reply_handler(
718
 
                                                map(self._event_type.new_for_struct, raw)),
719
 
                                        error_handler=self._safe_error_handler(error_handler,
720
 
                                                events_reply_handler, []))
721
 
        
722
 
        def find_events_for_template (self, event_template, events_reply_handler,
723
 
                **kwargs):
724
 
                """
725
 
                Alias for :meth:`find_events_for_templates`, for use when only
726
 
                one template is needed.
727
 
                """
728
 
                self.find_events_for_templates([event_template],
729
 
                                                events_reply_handler,
730
 
                                                **kwargs)
731
 
        
732
 
        def find_events_for_values(self, events_reply_handler, **kwargs):
733
 
                """
734
 
                Alias for :meth:`find_events_for_templates`, for when only
735
 
                one template is needed. Instead of taking an already created
736
 
                template, like :meth:`find_event_ids_for_template`, this method
737
 
                will construct the template from the parameters it gets. The
738
 
                allowed keywords are the same as the ones allowed by
739
 
                :meth:`Event.new_for_values() <zeitgeist.datamodel.Event.new_for_values>`.
740
 
                """
741
 
                ev, arguments = self.get_event_and_extra_arguments(kwargs)
742
 
                self.find_events_for_templates([ev],
743
 
                                                events_reply_handler,
744
 
                                                **arguments)
745
 
        
746
 
        def get_events (self, event_ids, events_reply_handler, error_handler=None):
747
 
                """
748
 
                Look up a collection of :class:`Events <zeitgeist.datamodel.Event>`
749
 
                in the Zeitgeist event log given a collection of event ids.
750
 
                This is useful for looking up the event data for events found
751
 
                with the *find_event_ids_** family of functions.
752
 
                
753
 
                Each event which is not found in the event log is represented
754
 
                by `None` in the resulting collection.
755
 
                
756
 
                The query will be done via an asynchronous DBus call and
757
 
                this method will return immediately. The returned events
758
 
                will be passed to *events_reply_handler* as a list
759
 
                of Events, which must be the only argument of the function.
760
 
                 
761
 
                In case of errors a message will be printed on stderr, and
762
 
                an empty result passed to *events_reply_handler*.
763
 
                To override this default set the *error_handler* named argument
764
 
                to a callable that takes a single exception as its sole
765
 
                argument.
766
 
                
767
 
                In order to use this method there needs to be a mainloop
768
 
                runnning. Both Qt and GLib mainloops are supported.
769
 
                """
770
 
                
771
 
                if not callable(events_reply_handler):
772
 
                        raise TypeError(
773
 
                                "Reply handler not callable, found %s" % events_reply_handler)
774
 
                
775
 
                # Generate a wrapper callback that does automagic conversion of
776
 
                # the raw DBus reply into a list of Event instances
777
 
                self._iface.GetEvents(event_ids,
778
 
                                reply_handler=lambda raw: events_reply_handler(
779
 
                                        map(self._event_type.new_for_struct, raw)),
780
 
                                error_handler=self._safe_error_handler(error_handler,
781
 
                                                events_reply_handler, []))
782
 
        
783
 
        def delete_events(self, event_ids, reply_handler=None, error_handler=None):
784
 
                """
785
 
                Warning: This API is EXPERIMENTAL and is not fully supported yet.
786
 
                
787
 
                Delete a collection of events from the zeitgeist log given their
788
 
                event ids.
789
 
                
790
 
                The deletion will be done asynchronously, and this method returns
791
 
                immediately. To check whether the deletions went well supply
792
 
                the *reply_handler* and/or *error_handler* funtions. The
793
 
                reply handler should not take any argument. The error handler
794
 
                must take a single argument - being the error.
795
 
                
796
 
                With custom handlers any errors will be printed to stderr.
797
 
                
798
 
                In order to use this method there needs to be a mainloop
799
 
                runnning.
800
 
                """
801
 
                self._check_list_or_tuple(event_ids)
802
 
                # we need dbus.UInt32 here as long as dbus.UInt32 is not a subtype
803
 
                # of int, this might change in the future, see docstring of dbus.UInt32
804
 
                self._check_members(event_ids, (int, dbus.UInt32))
805
 
                
806
 
                self._iface.DeleteEvents(event_ids,
807
 
                                        reply_handler=self._safe_reply_handler(reply_handler),
808
 
                                        error_handler=self._safe_error_handler(error_handler))
809
 
        
810
 
        def find_related_uris_for_events(self, event_templates, uris_reply_handler,
811
 
                error_handler=None, time_range = None, result_event_templates=[],
812
 
                storage_state=StorageState.Any, num_events=10, result_type=0):
813
 
                """
814
 
                Warning: This API is EXPERIMENTAL and is not fully supported yet.
815
 
                
816
 
                Get a list of URIs of subjects which frequently occur together
817
 
                with events matching `event_templates`. Possibly restricting to
818
 
                `time_range` or to URIs that occur as subject of events matching
819
 
                `result_event_templates`.
820
 
                
821
 
                :param event_templates: Templates for events that you want to
822
 
                    find URIs that relate to
823
 
                :param uris_reply_handler: A callback that takes a list of strings
824
 
                    with the URIs of the subjects related to the requested events
825
 
                :param time_range: A :class:`TimeRange <zeitgeist.datamodel.TimeRange>`
826
 
                    to restrict to
827
 
                :param result_event_templates: The related URIs must occur
828
 
                    as subjects of events matching these templates
829
 
                :param storage_state: The returned URIs must have this
830
 
                    :class:`storage state <zeitgeist.datamodel.StorageState>`
831
 
                :param num_events: number of related uris you want to have returned
832
 
                :param result_type: sorting of the results by 
833
 
                        0 for relevancy
834
 
                        1 for recency
835
 
                :param error_handler: An optional callback in case of errors.
836
 
                    Must take a single argument being the error raised by the
837
 
                    server. The default behaviour in case of errors is to call
838
 
                    `uris_reply_handler` with an empty list and print an error
839
 
                    message on standard error.
840
 
                """
841
 
                if not callable(uris_reply_handler):
842
 
                        raise TypeError(
843
 
                                "Reply handler not callable, found %s" % uris_reply_handler)
844
 
                
845
 
                if time_range is None:
846
 
                        time_range = TimeRange.until_now()
847
 
                        
848
 
                self._iface.FindRelatedUris(time_range, event_templates,
849
 
                        result_event_templates, storage_state, num_events, result_type,
850
 
                        reply_handler=self._safe_reply_handler(uris_reply_handler),
851
 
                        error_handler=self._safe_error_handler(error_handler,
852
 
                                                               uris_reply_handler,
853
 
                                                               [])
854
 
                        )
855
 
        
856
 
        def find_related_uris_for_uris(self, subject_uris, uris_reply_handler,
857
 
                time_range=None, result_event_templates=[],
858
 
                storage_state=StorageState.Any,  num_events=10, result_type=0, error_handler=None):
859
 
                """
860
 
                Warning: This API is EXPERIMENTAL and is not fully supported yet.
861
 
                
862
 
                Same as :meth:`find_related_uris_for_events`, but taking a list
863
 
                of subject URIs instead of event templates.
864
 
                """
865
 
                
866
 
                event_template = Event.new_for_values(subjects=
867
 
                        [Subject.new_for_values(uri=uri) for uri in subject_uris])
868
 
                
869
 
                self.find_related_uris_for_events([event_template],
870
 
                                                  uris_reply_handler,
871
 
                                                  time_range=time_range,
872
 
                                                  result_event_templates=result_event_templates,
873
 
                                                  storage_state=storage_state,
874
 
                                                  num_events = num_events,
875
 
                                                  result_type = result_type,
876
 
                                                  error_handler=error_handler)
877
 
        
878
 
        def install_monitor (self, time_range, event_templates,
879
 
                notify_insert_handler, notify_delete_handler, monitor_path=None):
880
 
                """
881
 
                Install a monitor in the Zeitgeist engine that calls back
882
 
                when events matching *event_templates* are logged. The matching
883
 
                is done exactly as in the *find_** family of methods and in
884
 
                :meth:`Event.matches_template <zeitgeist.datamodel.Event.matches_template>`.
885
 
                Furthermore matched events must also have timestamps lying in
886
 
                *time_range*.
887
 
                
888
 
                To remove a monitor call :meth:`remove_monitor` on the returned
889
 
                :class:`Monitor` instance.
890
 
                
891
 
                The *notify_insert_handler* will be called when events matching
892
 
                the monitor are inserted into the log. The *notify_delete_handler*
893
 
                function will be called when events lying within the monitored
894
 
                time range are deleted.
895
 
                
896
 
                :param time_range: A :class:`TimeRange <zeitgeist.datamodel.TimeRange>`
897
 
                    that matched events must lie within. To obtain a time range
898
 
                    from now and indefinitely into the future use
899
 
                    :meth:`TimeRange.from_now() <zeitgeist.datamodel.TimeRange.from_now>`
900
 
                :param event_templates: The event templates to look for
901
 
                :param notify_insert_handler: Callback for receiving notifications
902
 
                    about insertions of matching events. The callback should take
903
 
                    a :class:`TimeRange` as first parameter and a list of
904
 
                    :class:`Events` as the second parameter.
905
 
                    The time range will cover the minimum and maximum timestamps
906
 
                    of the inserted events
907
 
                :param notify_delete_handler: Callback for receiving notifications
908
 
                    about deletions of events in the monitored time range.
909
 
                    The callback should take a :class:`TimeRange` as first
910
 
                    parameter and a list of event ids as the second parameter.
911
 
                    Note that an event id is simply an unsigned integer.
912
 
                :param monitor_path: Optional argument specifying the DBus path
913
 
                    to install the client side monitor object on. If none is provided
914
 
                    the client will provide one for you namespaced under
915
 
                    /org/gnome/zeitgeist/monitor/*
916
 
                :returns: a :class:`Monitor`
917
 
                """
918
 
                self._check_list_or_tuple(event_templates)
919
 
                self._check_members(event_templates, Event)
920
 
                if not callable(notify_insert_handler):
921
 
                        raise TypeError("notify_insert_handler not callable, found %s" % \
922
 
                                notify_reply_handler)
923
 
                        
924
 
                if not callable(notify_delete_handler):
925
 
                        raise TypeError("notify_delete_handler not callable, found %s" % \
926
 
                                notify_reply_handler)
927
 
                
928
 
                
929
 
                mon = Monitor(time_range, event_templates, notify_insert_handler,
930
 
                        notify_delete_handler, monitor_path=monitor_path,
931
 
                        event_type=self._event_type)
932
 
                self._iface.InstallMonitor(mon.path,
933
 
                                           mon.time_range,
934
 
                                           mon.templates,
935
 
                                           reply_handler=self._void_reply_handler,
936
 
                                           error_handler=lambda err: log.warn(
937
 
                                                                        "Error installing monitor: %s" % err))
938
 
                self._installed_monitors.append(mon)
939
 
                return mon
940
 
        
941
 
        def remove_monitor (self, monitor, monitor_removed_handler=None):
942
 
                """
943
 
                Remove a :class:`Monitor` installed with :meth:`install_monitor`
944
 
                
945
 
                :param monitor: Monitor to remove. Either as a :class:`Monitor`
946
 
                    instance or a DBus object path to the monitor either as a
947
 
                    string or :class:`dbus.ObjectPath`
948
 
                :param monitor_removed_handler: A callback function taking
949
 
                    one integer argument. 1 on success, 0 on failure.
950
 
                """
951
 
                if isinstance(monitor, (str,unicode)):
952
 
                        path = dbus.ObjectPath(monitor)
953
 
                elif isinstance(monitor, Monitor):
954
 
                        path = monitor.path
955
 
                else:
956
 
                        raise TypeError(
957
 
                                "Monitor, str, or unicode expected. Found %s" % type(monitor))
958
 
                
959
 
                if callable(monitor_removed_handler):
960
 
                        
961
 
                        def dispatch_handler (error=None):
962
 
                                if error :
963
 
                                        log.warn("Error removing monitor %s: %s" % (monitor, error))
964
 
                                        monitor_removed_handler(0)
965
 
                                else: monitor_removed_handler(1)
966
 
                                
967
 
                        reply_handler = dispatch_handler
968
 
                        error_handler = dispatch_handler
969
 
                else:
970
 
                        reply_handler = self._void_reply_handler
971
 
                        error_handler = lambda err: log.warn(
972
 
                                "Error removing monitor %s: %s" % (monitor, err))
973
 
                
974
 
                self._iface.RemoveMonitor(path,
975
 
                                          reply_handler=reply_handler,
976
 
                                          error_handler=error_handler)
977
 
                self._installed_monitors.remove(monitor)
978
 
        
979
 
        # Data-source related class variables
980
 
        _data_sources = {}
981
 
        _data_sources_callback_installed = False
982
 
        
983
 
        def register_data_source(self, unique_id, name, description,
984
 
                event_templates, enabled_callback=None):
985
 
                """
986
 
                Register a data-source as currently running. If the data-source was
987
 
                already in the database, its metadata (name, description and
988
 
                event_templates) are updated.
989
 
                
990
 
                If the data-source registry isn't enabled, do nothing.
991
 
                
992
 
                The optional event_templates is purely informational and serves to
993
 
                let data-source management applications and other data-sources know
994
 
                what sort of information you log.
995
 
                
996
 
                :param unique_id: unique ASCII string identifying the data-source
997
 
                :param name: data-source name (may be translated)
998
 
                :param description: data-source description (may be translated)
999
 
                :param event_templates: list of
1000
 
                        :class:`Event <zeitgeist.datamodel.Event>` templates.
1001
 
                :param enabled_callback: method to call as response with the `enabled'
1002
 
                        status of the data-source, and after that every time said status
1003
 
                        is toggled. See set_data_source_enabled_callback() for more
1004
 
                        information.
1005
 
                """
1006
 
                
1007
 
                self._data_sources[unique_id] = {'enabled': None, 'callback': None}
1008
 
                
1009
 
                if enabled_callback is not None:
1010
 
                        self.set_data_source_enabled_callback(unique_id, enabled_callback)
1011
 
 
1012
 
                def _data_source_enabled_cb(unique_id, enabled):
1013
 
                        if unique_id not in self._data_sources:
1014
 
                                return
1015
 
                        self._data_sources[unique_id]['enabled'] = enabled
1016
 
                        callback = self._data_sources[unique_id]['callback']
1017
 
                        if callback is not None:
1018
 
                                callback(enabled)
1019
 
                
1020
 
                def _data_source_register_cb(enabled):
1021
 
                        _data_source_enabled_cb(unique_id, enabled)
1022
 
 
1023
 
                if not self._data_sources_callback_installed:
1024
 
                        self._registry.connect('DataSourceEnabled', _data_source_enabled_cb)
1025
 
                        self._data_sources_callback_installed = True
1026
 
 
1027
 
                self._registry.RegisterDataSource(unique_id, name, description,
1028
 
                        event_templates,
1029
 
                        reply_handler=_data_source_register_cb,
1030
 
                        error_handler=self._void_reply_handler) # Errors are ignored
1031
 
        
1032
 
        def set_data_source_enabled_callback(self, unique_id, enabled_callback):
1033
 
                """
1034
 
                This method may only be used after having registered the given unique_id
1035
 
                with register_data_source before.
1036
 
                
1037
 
                It registers a method to be called whenever the `enabled' status of
1038
 
                the previously registered data-source changes.
1039
 
                
1040
 
                Remember that on some systems the DataSourceRegistry extension may be
1041
 
                disabled, in which case this method will have no effect.
1042
 
                """
1043
 
                
1044
 
                if unique_id not in self._data_sources:
1045
 
                        raise ValueError, 'set_data_source_enabled_callback() called before ' \
1046
 
                        'register_data_source()'
1047
 
                
1048
 
                if not callable(enabled_callback):
1049
 
                        raise TypeError, 'enabled_callback: expected a callable method'
1050
 
                
1051
 
                self._data_sources[unique_id]['callback'] = enabled_callback
1052
 
        
1053
 
        def _check_list_or_tuple(self, collection):
1054
 
                """
1055
 
                Raise a ValueError unless 'collection' is a list or tuple
1056
 
                """
1057
 
                if not (isinstance(collection, list) or isinstance(collection, tuple)):
1058
 
                        raise TypeError("Expected list or tuple, found %s" % type(collection))
1059
 
        
1060
 
        def _check_members (self, collection, member_class):
1061
 
                """
1062
 
                Raise a ValueError unless all of the members of 'collection'
1063
 
                are of class 'member_class'
1064
 
                """
1065
 
                for m in collection:
1066
 
                        if not isinstance(m, member_class):
1067
 
                                raise TypeError(
1068
 
                                        "Collection contains member of invalid type %s. Expected %s" % \
1069
 
                                        (m.__class__, member_class))
1070
 
        
1071
 
        def _void_reply_handler(self, *args, **kwargs):
1072
 
                """
1073
 
                Reply handler for async DBus calls that simply ignores the response
1074
 
                """
1075
 
                pass
1076
 
                
1077
 
        def _stderr_error_handler(self, exception, normal_reply_handler=None,
1078
 
                normal_reply_data=None):
1079
 
                """
1080
 
                Error handler for async DBus calls that prints the error
1081
 
                to sys.stderr
1082
 
                """
1083
 
                print >> sys.stderr, "Error from Zeitgeist engine:", exception
1084
 
                
1085
 
                if callable(normal_reply_handler):
1086
 
                        normal_reply_handler(normal_reply_data)
1087
 
 
1088
 
_FIND_EVENTS_FOR_TEMPLATES_ARGS = inspect.getargspec(
1089
 
        ZeitgeistClient.find_events_for_templates)[0]