1
# -.- coding: utf-8 -.-
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>
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.
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.
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/>.
26
import dbus.mainloop.glib
32
from xml.etree import ElementTree
34
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
36
from zeitgeist.datamodel import (Event, Subject, TimeRange, StorageState,
41
log = logging.getLogger("zeitgeist.client")
43
class _DBusInterface(object):
44
"""Wrapper around dbus.Interface adding convenience methods."""
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
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
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]
64
methods.remove("Introspect") # Introspect is not part of the API
67
return methods, signals
70
if not self._reconnect_when_needed:
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()
77
def _disconnection_safe(self, method_getter, *args, **kwargs):
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
84
custom_error_handler = None
85
original_kwargs = dict(kwargs)
87
def reconnecting_error_handler(e):
88
error = e.get_dbus_name()
89
if error == "org.freedesktop.DBus.Error.ServiceUnknown":
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)
95
if custom_error_handler is not None:
96
custom_error_handler(e)
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
107
return method_getter()(*args, **kwargs)
108
except dbus.exceptions.DBusException, e:
109
return reconnecting_error_handler(e)
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):
116
Method wrapping around a D-Bus call, which attempts to recover
117
the connection to Zeitgeist if it got lost.
119
return self._disconnection_safe(
120
lambda: getattr(self.__iface, name), *args, **kwargs)
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)
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:
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(
138
dbus_interface=self.__interface_name,
141
def connect_exit(self, callback):
142
"""Executes callback when the remote interface disappears from the bus"""
143
self._disconnect_callbacks.add(callback)
145
def connect_join(self, callback):
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
151
self._reconnect_callbacks.add(callback)
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'))
162
def __init__(self, proxy, interface_name, object_path, reconnect=True):
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()
170
self._disconnect_callbacks = set()
171
self._reconnect_callbacks = set()
172
self._generic_callbacks = set()
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
180
if not self._reconnect_when_needed:
183
callbacks = self._reconnect_callbacks
184
for signal, callback in self._generic_callbacks:
186
self.connect(signal, callback)
188
log.exception("Failed to reconnect to signal \"%s\" "
189
"after engine disconnection." % signal)
190
for callback in callbacks:
192
dbus.SessionBus().watch_name_owner(self.__iface.requested_bus_name,
195
class ZeitgeistDBusInterface(object):
196
""" Central DBus interface to the Zeitgeist engine
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
205
BUS_NAME = "org.gnome.zeitgeist.Engine"
206
INTERFACE_NAME = "org.gnome.zeitgeist.Log"
207
OBJECT_PATH = "/org/gnome/zeitgeist/log/activity"
209
def __getattr__(self, name):
210
return getattr(self.__shared_state["dbus_interface"], name)
213
"""Returns the API version"""
214
dbus_interface = self.__shared_state["dbus_interface"]
215
return dbus_interface.get_property("version")
217
def extensions(self):
218
"""Returns active extensions"""
219
dbus_interface = self.__shared_state["dbus_interface"]
220
return dbus_interface.get_property("extensions")
222
def get_extension(cls, name, path, busname=None):
223
""" Returns an interface to the given extension.
226
>> reg = get_extension("DataSourceRegistry", "data_source_registry")
227
>> reg.RegisterDataSource(...)
230
busname = "org.gnome.zeitgeist.%s" % busname
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]
244
def __init__(self, reconnect=True):
245
if not "dbus_interface" in self.__shared_state:
247
proxy = dbus.SessionBus().get_object(self.BUS_NAME,
249
except dbus.exceptions.DBusException, e:
250
if e.get_dbus_name() == "org.freedesktop.DBus.Error.ServiceUnknown":
252
"Found no running zeitgeist-daemon instance: %s" % \
253
e.get_dbus_message())
256
self.__shared_state["extension_interfaces"] = {}
257
self.__shared_state["dbus_interface"] = _DBusInterface(proxy,
258
self.INTERFACE_NAME, self.OBJECT_PATH, reconnect)
260
class Monitor(dbus.service.Object):
262
DBus interface for monitoring the Zeitgeist log for certain types
265
When using the Python bindings monitors are normally instantiated
266
indirectly by calling :meth:`ZeitgeistClient.install_monitor`.
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.
273
# Used in Monitor._next_path() to generate unique path names
278
def __init__ (self, time_range, event_templates, insert_callback,
279
delete_callback, monitor_path=None, event_type=None):
281
monitor_path = Monitor._next_path()
282
elif isinstance(monitor_path, (str, unicode)):
283
monitor_path = dbus.ObjectPath(monitor_path)
286
if not issubclass(event_type, Event):
287
raise TypeError("Event subclass expected.")
288
self._event_type = event_type
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)
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")
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")
305
def get_templates (self): return self._templates
306
templates = property(get_templates,
307
doc="Read only property with installed templates")
309
@dbus.service.method("org.gnome.zeitgeist.Monitor",
310
in_signature="(xx)a("+SIG_EVENT+")")
311
def NotifyInsert(self, time_range, events):
313
Receive notification that a set of events matching the monitor's
314
templates has been recorded in the log.
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.
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`
326
self._insert_callback(TimeRange(time_range[0], time_range[1]),
327
map(self._event_type, events))
329
@dbus.service.method("org.gnome.zeitgeist.Monitor",
330
in_signature="(xx)au")
331
def NotifyDelete(self, time_range, event_ids):
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
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.
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.
348
self._delete_callback(TimeRange(time_range[0], time_range[1]), event_ids)
351
return hash(self._path)
356
Generate a new unique DBus object path for a monitor
358
cls._last_path_id += 1
359
return dbus.ObjectPath("/org/gnome/zeitgeist/monitor/%s" % \
362
class ZeitgeistClient:
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.
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.
373
_installed_monitors = []
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."""
384
for arg in _FIND_EVENTS_FOR_TEMPLATES_ARGS:
386
kwargs[arg] = arguments.pop(arg)
387
ev = Event.new_for_values(**arguments)
391
self._iface = ZeitgeistDBusInterface()
392
self._registry = self._iface.get_extension("DataSourceRegistry",
393
"data_source_registry")
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,
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)
407
def register_event_subclass(self, event_type):
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.
413
if not issubclass(event_type, Event):
414
raise TypeError("Event subclass expected.")
415
self._event_type = event_type
417
def register_subject_subclass(self, subject_type):
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.
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.
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.
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
436
def _safe_error_handler(self, error_handler, *args):
437
if error_handler is not None:
438
if callable(error_handler):
441
"Error handler not callable, found %s" % error_handler)
442
return lambda raw: self._stderr_error_handler(raw, *args)
444
def _safe_reply_handler(self, reply_handler):
445
if reply_handler is not None:
446
if callable(reply_handler):
449
"Reply handler not callable, found %s" % reply_handler)
450
return self._void_reply_handler
454
def get_version(self):
455
return [int(i) for i in self._iface.version()]
457
def get_extensions(self):
458
return [unicode(i) for i in self._iface.extensions()]
462
def insert_event (self, event, ids_reply_handler=None, error_handler=None):
464
Send an event to the Zeitgeist event log. The 'event' parameter
465
must be an instance of the Event class.
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.
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.
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
482
In order to use this method there needs to be a mainloop
483
runnning. Both Qt and GLib mainloops are supported.
485
self.insert_events([event],
486
ids_reply_handler=ids_reply_handler,
487
error_handler=error_handler)
489
def insert_event_for_values (self, **values):
491
Send an event to the Zeitgeist event log. The keyword arguments
492
must match those as provided to Event.new_for_values().
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.
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.
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
509
In order to use this method there needs to be a mainloop
510
runnning. Both Qt and GLib mainloops are supported.
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))
517
def insert_events (self, events, ids_reply_handler=None, error_handler=None):
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>`.
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.
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
534
In order to use this method there needs to be a mainloop
535
runnning. Both Qt and GLib mainloops are supported.
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), []))
545
def find_event_ids_for_templates (self,
549
storage_state = StorageState.Any,
551
result_type = ResultType.MostRecentEvents,
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
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
567
The actual :class:`Events` can be looked up via the
568
:meth:`get_events()` method.
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`.
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
586
In order to use this method there needs to be a mainloop
587
runnning. Both Qt and GLib mainloops are supported.
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
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
606
self._check_list_or_tuple(event_templates)
607
self._check_members(event_templates, Event)
609
if not callable(ids_reply_handler):
611
"Reply handler not callable, found %s" % ids_reply_handler)
613
if timerange is None:
614
timerange = TimeRange.until_now()
616
self._iface.FindEventIds(timerange,
621
reply_handler=self._safe_reply_handler(ids_reply_handler),
622
error_handler=self._safe_error_handler(error_handler,
623
ids_reply_handler, []))
625
def find_event_ids_for_template (self, event_template, ids_reply_handler,
628
Alias for :meth:`find_event_ids_for_templates`, for use when only
629
one template is needed.
631
self.find_event_ids_for_templates([event_template],
635
def find_event_ids_for_values(self, ids_reply_handler, **kwargs):
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>`.
644
ev, arguments = self.get_event_and_extra_arguments(kwargs)
645
self.find_event_ids_for_templates([ev],
649
def find_events_for_templates (self,
651
events_reply_handler,
653
storage_state = StorageState.Any,
655
result_type = ResultType.MostRecentEvents,
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
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
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`.
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
682
In order to use this method there needs to be a mainloop
683
runnning. Both Qt and GLib mainloops are supported.
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
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
702
self._check_list_or_tuple(event_templates)
703
self._check_members(event_templates, Event)
705
if not callable(events_reply_handler):
707
"Reply handler not callable, found %s" % events_reply_handler)
709
if timerange is None:
710
timerange = TimeRange.until_now()
712
self._iface.FindEvents(timerange,
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, []))
722
def find_events_for_template (self, event_template, events_reply_handler,
725
Alias for :meth:`find_events_for_templates`, for use when only
726
one template is needed.
728
self.find_events_for_templates([event_template],
729
events_reply_handler,
732
def find_events_for_values(self, events_reply_handler, **kwargs):
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>`.
741
ev, arguments = self.get_event_and_extra_arguments(kwargs)
742
self.find_events_for_templates([ev],
743
events_reply_handler,
746
def get_events (self, event_ids, events_reply_handler, error_handler=None):
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.
753
Each event which is not found in the event log is represented
754
by `None` in the resulting collection.
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.
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
767
In order to use this method there needs to be a mainloop
768
runnning. Both Qt and GLib mainloops are supported.
771
if not callable(events_reply_handler):
773
"Reply handler not callable, found %s" % events_reply_handler)
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, []))
783
def delete_events(self, event_ids, reply_handler=None, error_handler=None):
785
Warning: This API is EXPERIMENTAL and is not fully supported yet.
787
Delete a collection of events from the zeitgeist log given their
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.
796
With custom handlers any errors will be printed to stderr.
798
In order to use this method there needs to be a mainloop
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))
806
self._iface.DeleteEvents(event_ids,
807
reply_handler=self._safe_reply_handler(reply_handler),
808
error_handler=self._safe_error_handler(error_handler))
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):
814
Warning: This API is EXPERIMENTAL and is not fully supported yet.
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`.
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>`
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
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.
841
if not callable(uris_reply_handler):
843
"Reply handler not callable, found %s" % uris_reply_handler)
845
if time_range is None:
846
time_range = TimeRange.until_now()
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,
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):
860
Warning: This API is EXPERIMENTAL and is not fully supported yet.
862
Same as :meth:`find_related_uris_for_events`, but taking a list
863
of subject URIs instead of event templates.
866
event_template = Event.new_for_values(subjects=
867
[Subject.new_for_values(uri=uri) for uri in subject_uris])
869
self.find_related_uris_for_events([event_template],
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)
878
def install_monitor (self, time_range, event_templates,
879
notify_insert_handler, notify_delete_handler, monitor_path=None):
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
888
To remove a monitor call :meth:`remove_monitor` on the returned
889
:class:`Monitor` instance.
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.
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`
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)
924
if not callable(notify_delete_handler):
925
raise TypeError("notify_delete_handler not callable, found %s" % \
926
notify_reply_handler)
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,
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)
941
def remove_monitor (self, monitor, monitor_removed_handler=None):
943
Remove a :class:`Monitor` installed with :meth:`install_monitor`
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.
951
if isinstance(monitor, (str,unicode)):
952
path = dbus.ObjectPath(monitor)
953
elif isinstance(monitor, Monitor):
957
"Monitor, str, or unicode expected. Found %s" % type(monitor))
959
if callable(monitor_removed_handler):
961
def dispatch_handler (error=None):
963
log.warn("Error removing monitor %s: %s" % (monitor, error))
964
monitor_removed_handler(0)
965
else: monitor_removed_handler(1)
967
reply_handler = dispatch_handler
968
error_handler = dispatch_handler
970
reply_handler = self._void_reply_handler
971
error_handler = lambda err: log.warn(
972
"Error removing monitor %s: %s" % (monitor, err))
974
self._iface.RemoveMonitor(path,
975
reply_handler=reply_handler,
976
error_handler=error_handler)
977
self._installed_monitors.remove(monitor)
979
# Data-source related class variables
981
_data_sources_callback_installed = False
983
def register_data_source(self, unique_id, name, description,
984
event_templates, enabled_callback=None):
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.
990
If the data-source registry isn't enabled, do nothing.
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.
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
1007
self._data_sources[unique_id] = {'enabled': None, 'callback': None}
1009
if enabled_callback is not None:
1010
self.set_data_source_enabled_callback(unique_id, enabled_callback)
1012
def _data_source_enabled_cb(unique_id, enabled):
1013
if unique_id not in self._data_sources:
1015
self._data_sources[unique_id]['enabled'] = enabled
1016
callback = self._data_sources[unique_id]['callback']
1017
if callback is not None:
1020
def _data_source_register_cb(enabled):
1021
_data_source_enabled_cb(unique_id, enabled)
1023
if not self._data_sources_callback_installed:
1024
self._registry.connect('DataSourceEnabled', _data_source_enabled_cb)
1025
self._data_sources_callback_installed = True
1027
self._registry.RegisterDataSource(unique_id, name, description,
1029
reply_handler=_data_source_register_cb,
1030
error_handler=self._void_reply_handler) # Errors are ignored
1032
def set_data_source_enabled_callback(self, unique_id, enabled_callback):
1034
This method may only be used after having registered the given unique_id
1035
with register_data_source before.
1037
It registers a method to be called whenever the `enabled' status of
1038
the previously registered data-source changes.
1040
Remember that on some systems the DataSourceRegistry extension may be
1041
disabled, in which case this method will have no effect.
1044
if unique_id not in self._data_sources:
1045
raise ValueError, 'set_data_source_enabled_callback() called before ' \
1046
'register_data_source()'
1048
if not callable(enabled_callback):
1049
raise TypeError, 'enabled_callback: expected a callable method'
1051
self._data_sources[unique_id]['callback'] = enabled_callback
1053
def _check_list_or_tuple(self, collection):
1055
Raise a ValueError unless 'collection' is a list or tuple
1057
if not (isinstance(collection, list) or isinstance(collection, tuple)):
1058
raise TypeError("Expected list or tuple, found %s" % type(collection))
1060
def _check_members (self, collection, member_class):
1062
Raise a ValueError unless all of the members of 'collection'
1063
are of class 'member_class'
1065
for m in collection:
1066
if not isinstance(m, member_class):
1068
"Collection contains member of invalid type %s. Expected %s" % \
1069
(m.__class__, member_class))
1071
def _void_reply_handler(self, *args, **kwargs):
1073
Reply handler for async DBus calls that simply ignores the response
1077
def _stderr_error_handler(self, exception, normal_reply_handler=None,
1078
normal_reply_data=None):
1080
Error handler for async DBus calls that prints the error
1083
print >> sys.stderr, "Error from Zeitgeist engine:", exception
1085
if callable(normal_reply_handler):
1086
normal_reply_handler(normal_reply_data)
1088
_FIND_EVENTS_FOR_TEMPLATES_ARGS = inspect.getargspec(
1089
ZeitgeistClient.find_events_for_templates)[0]