Merge lp:~thekorn/zeitgeist/zeitgeist-resonance_extensions into lp:zeitgeist/0.1

Proposed by Markus Korn
Status: Merged
Merged at revision: not available
Proposed branch: lp:~thekorn/zeitgeist/zeitgeist-resonance_extensions
Merge into: lp:zeitgeist/0.1
Diff against target: 385 lines (+276/-23)
6 files modified
_zeitgeist/engine/extension.py (+82/-0)
_zeitgeist/engine/relevancy_provider.py (+34/-0)
_zeitgeist/engine/resonance_engine.py (+11/-22)
test/resonance-engine-extension-test.py (+39/-0)
test/resonance-engine-test.py (+6/-1)
test/test-engine-extension.rst (+104/-0)
To merge this branch: bzr merge lp:~thekorn/zeitgeist/zeitgeist-resonance_extensions
Reviewer Review Type Date Requested Status
Zeitgeist Framework Team Pending
Review via email: mp+15143@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Markus Korn (thekorn) wrote :

This branch fixes bug 483556 by adding a framework to load extensions to ZeitgeistEngine.
Extension methods are accessible as
  engine.extensions.some_method_name()
For now there is one such extension (RelevancyProvider) which is not loaded by default. See test/test-engine-extension.rst [0] for more information how this framework is working.

[0] http://bazaar.launchpad.net/~thekorn/zeitgeist/zeitgeist-resonance_extensions/annotate/head%3A/test/test-engine-extension.rst

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '_zeitgeist/engine/extension.py'
2--- _zeitgeist/engine/extension.py 1970-01-01 00:00:00 +0000
3+++ _zeitgeist/engine/extension.py 2009-11-23 08:25:19 +0000
4@@ -0,0 +1,82 @@
5+# -.- coding: utf-8 -.-
6+
7+# Zeitgeist
8+#
9+# Copyright © 2009 Markus Korn <thekorn@gmx.de>
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 3 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+class Extension(object):
25+ """ Base class for all extensions
26+
27+ Every extension has to define a list of accessible methods as
28+ 'PUBLIC_METHODS'. The constructor of an Extension object takes the
29+ engine object it extends as the only argument
30+ """
31+ PUBLIC_METHODS = None
32+
33+ def __init__(self, engine):
34+ self.engine = engine
35+
36+
37+class ExtensionsCollection(object):
38+ """ Collection to manage all extensions """
39+
40+ def __init__(self, engine, defaults=None):
41+ self.__extensions = dict()
42+ self.__engine = engine
43+ self.__methods = dict()
44+ if defaults is not None:
45+ for extension in defaults:
46+ self.load(extension)
47+
48+ def __repr__(self):
49+ return "%s(%r)" %(self.__class__.__name__, sorted(self.__methods.keys()))
50+
51+ def load(self, extension):
52+ if not issubclass(extension, Extension):
53+ raise TypeError(
54+ "Unable to load %r, all extensions have to be subclasses of %r" %(extension, Extension)
55+ )
56+ if getattr(extension, "PUBLIC_METHODS", None) is None:
57+ raise ValueError("Unable to load %r, this extension has not defined any methods" %extension)
58+ obj = extension(self.__engine)
59+ for method in obj.PUBLIC_METHODS:
60+ self._register_method(method, getattr(obj, method))
61+ self.__extensions[obj.__class__.__name__] = obj
62+
63+ def unload(self, extension):
64+ obj = self.__extensions[extension.__name__]
65+ for method in obj.PUBLIC_METHODS:
66+ del self.methods[method]
67+ del self.__extensions[extension.__name__]
68+
69+ def __len__(self):
70+ return len(self.__extensions)
71+
72+ @property
73+ def methods(self):
74+ return self.__methods
75+
76+ def _register_method(self, name, method):
77+ if name in self.methods:
78+ raise ValueError("There is already an extension which provides a method called %r" %name)
79+ self.methods[name] = method
80+
81+ def __getattr__(self, name):
82+ try:
83+ return self.methods[name]
84+ except KeyError:
85+ raise AttributeError("%s instance has no attribute %r" %(self.__class__.__name__, name))
86+
87
88=== modified file '_zeitgeist/engine/relevancy_provider.py'
89--- _zeitgeist/engine/relevancy_provider.py 2009-11-16 22:05:31 +0000
90+++ _zeitgeist/engine/relevancy_provider.py 2009-11-23 08:25:19 +0000
91@@ -27,6 +27,8 @@
92 from dbutils import *
93 from __init__ import DB_PATH
94
95+from extension import Extension
96+
97 class FocusSwitchRegister(object):
98
99 def __init__(self, cursor):
100@@ -235,3 +237,35 @@
101 for row in self.cursor:
102 apps.append(self.app_table.lookup_by_id(row[0]))
103 return apps
104+
105+class RelevancyProvider(Extension):
106+ PUBLIC_METHODS = [
107+ "get_subject_focus_duration",
108+ "get_actor_focus_duration",
109+ "get_longest_used_actors",
110+ "get_longest_used_subjects",
111+ "register_focus"
112+ ]
113+
114+ def __init__(self, engine):
115+ super(RelevancyProvider, self).__init__(engine)
116+ self.focus_vertices = FocusSwitchRegister(self.engine._cursor)
117+ self.focus_duration = FocusDurationRegister(self.engine._cursor)
118+
119+ def get_subject_focus_duration(self, subject, start, end):
120+ return self.focus_duration.get_subject_focus_duration(subject, start, end)
121+
122+ def get_actor_focus_duration(self, actor, start, end):
123+ return self.focus_duration.get_actor_focus_duration(actor, start, end)
124+
125+ def get_longest_used_actors(self, number, start, end):
126+ return self.focus_duration.get_longest_used_actors(number, start, end)
127+
128+ def get_longest_used_subjects(self, number, start, end):
129+ return self.focus_duration.get_longest_used_subjects(number, start, end)
130+
131+ def register_focus(self, actor, subject):
132+ now = time.time()
133+ self.focus_duration.focus_change(now, actor, subject)
134+ self.focus_vertices.focus_change(now, actor, subject)
135+
136
137=== modified file '_zeitgeist/engine/resonance_engine.py'
138--- _zeitgeist/engine/resonance_engine.py 2009-11-19 14:22:14 +0000
139+++ _zeitgeist/engine/resonance_engine.py 2009-11-23 08:25:19 +0000
140@@ -30,13 +30,14 @@
141 from xdg import BaseDirectory
142 from xdg.DesktopEntry import DesktopEntry
143
144+from extension import ExtensionsCollection
145+
146 from zeitgeist.datamodel import Subject as _Subject, Event as _Event
147 from zeitgeist.datamodel import Interpretation, Manifestation, Mimetype, Category, StorageState, TimeRange
148 import _zeitgeist.engine
149 from _zeitgeist.engine.dbutils import *
150 from _zeitgeist.engine.querymancer import *
151 from _zeitgeist.lrucache import *
152-from _zeitgeist.engine.relevancy_provider import FocusSwitchRegister, FocusDurationRegister
153
154 logging.basicConfig(level=logging.DEBUG)
155 log = logging.getLogger("zeitgeist.engine")
156@@ -373,7 +374,6 @@
157 global _event
158 self._cursor = get_default_cursor()
159
160- #Load extensions
161 # Find the last event id we used, and start generating
162 # new ids from that offset
163 row = _event.find("max(id)").fetchone()
164@@ -381,9 +381,14 @@
165 self._last_event_id = row[0]
166 else:
167 self._last_event_id = 0
168-
169- self.focus_switch = FocusSwitchRegister(_cursor)
170- self.focus_duration = FocusDurationRegister(_cursor)
171+
172+ #Load extensions
173+ # right now we don't load any default extension
174+ self.__extensions = ExtensionsCollection(self)
175+
176+ @property
177+ def extensions(self):
178+ return self.__extensions
179
180 def close(self):
181 global _cursor
182@@ -603,23 +608,7 @@
183 ORDER BY timestamp DESC LIMIT 1
184 """, (actor,)).fetchone()
185 return query["timestamp"] if query else 0
186-
187- def get_subject_focus_duration(self, subject, start, end):
188- return self.focus_duration.get_subject_focus_duration(subject, start, end)
189-
190- def get_actor_focus_duration(self, actor, start, end):
191- return self.focus_duration.get_actor_focus_duration(actor, start, end)
192-
193- def get_longest_used_actors(self, number, start, end):
194- return self.focus_duration.get_longest_used_actors(number, start, end)
195-
196- def get_longest_used_subjects(self, number, start, end):
197- return self.focus_duration.get_longest_used_subjects(number, start, end)
198-
199- def register_focus(self, actor, subject):
200- now = time.time()
201- self.focus_duration.focus_change(now, actor, subject)
202- self.focus_switch.focus_change(now, actor, subject)
203+
204
205 class WhereClause:
206
207
208=== added file 'test/resonance-engine-extension-test.py'
209--- test/resonance-engine-extension-test.py 1970-01-01 00:00:00 +0000
210+++ test/resonance-engine-extension-test.py 2009-11-23 08:25:19 +0000
211@@ -0,0 +1,39 @@
212+#!/usr/bin/python
213+# -.- coding: utf-8 -.-
214+
215+# Update python path to use local zeitgeist module
216+import sys
217+import os
218+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
219+
220+from _zeitgeist.engine.resonance_engine import ZeitgeistEngine
221+from _zeitgeist.engine.extension import Extension
222+
223+import unittest
224+
225+class _Extension1(Extension):
226+ PUBLIC_METHODS = ["return_hallo", "return_engine"]
227+
228+ def return_hallo(self):
229+ return "Hallo"
230+
231+ def return_boo(self):
232+ return "boo"
233+
234+ def return_engine(self):
235+ return self.engine
236+
237+
238+class TestExtensions(unittest.TestCase):
239+
240+ def testCreateEngine(self):
241+ engine = ZeitgeistEngine()
242+ self.assertEqual(len(engine.extensions), 0)
243+ self.assertRaises(AttributeError, engine.extensions.__getattr__, "return_hallo")
244+ engine.extensions.load(_Extension1)
245+ self.assertEqual(engine.extensions.return_hallo(), "Hallo")
246+ self.assertRaises(AttributeError, engine.extensions.__getattr__, "return_boo")
247+ self.assertEqual(engine.extensions.return_engine(), engine)
248+
249+if __name__ == "__main__":
250+ unittest.main()
251
252=== modified file 'test/resonance-engine-test.py'
253--- test/resonance-engine-test.py 2009-11-19 14:22:14 +0000
254+++ test/resonance-engine-test.py 2009-11-23 08:25:19 +0000
255@@ -9,6 +9,7 @@
256 import _zeitgeist.engine
257 from _zeitgeist.engine import create_engine
258 from zeitgeist.datamodel import *
259+from _zeitgeist.engine.relevancy_provider import RelevancyProvider
260 from testutils import import_events
261
262 import unittest
263@@ -335,8 +336,12 @@
264
265 class ZeitgeistRelevancyProviderTest(_engineTestClass):
266
267+ def setUp(self):
268+ super(ZeitgeistRelevancyProviderTest, self).setUp()
269+ self.engine.extensions.load(RelevancyProvider)
270+
271 def testInsertFocusEvent(self):
272- self.engine.register_focus("boo","boo")
273+ self.engine.extensions.register_focus("boo","boo")
274 #self.assertRaises(ValueError, self.engine.insert_events, [ev])
275
276 if __name__ == "__main__":
277
278=== added file 'test/test-engine-extension.rst'
279--- test/test-engine-extension.rst 1970-01-01 00:00:00 +0000
280+++ test/test-engine-extension.rst 2009-11-23 08:25:19 +0000
281@@ -0,0 +1,104 @@
282+How do extensions to the engine work?
283+=====================================
284+
285+Extensions are objects which are maintained in external python modules
286+with optional methods which are accessible from a ZeitgeistEngine object.
287+There is only one extension right now called RelevancyProvider, which
288+is not enable by default (for now).
289+
290+Per default there is no extension enabled
291+
292+ >>> from _zeitgeist.engine.resonance_engine import ZeitgeistEngine
293+ >>> engine = ZeitgeistEngine()
294+ >>> len(engine.extensions)
295+ 0
296+
297+To create a new extension you have to subclass the Extension class and
298+provide a list of accessible methods in PUBLIC_METHODS
299+
300+ >>> from _zeitgeist.engine.extension import Extension
301+ >>> class SampleExtension(Extension):
302+ ... PUBLIC_METHODS = ["add_value", "get_engine"]
303+ ...
304+ ... def __init__(self, engine):
305+ ... super(SampleExtension, self).__init__(engine)
306+ ... self.counter = 0
307+ ...
308+ ... def add_value(self, value):
309+ ... self.counter += value
310+ ... return self.counter
311+ ...
312+ ... def get_engine(self):
313+ ... return self.engine
314+ ...
315+ ... def internal_method(self):
316+ ... return 0
317+ ...
318+
319+This example adds to new methods to the engine 'add_value' and 'get_engine'.
320+On the other hand the method called 'internal_method' is not available as
321+a method of the engine object. The constructor of an Extension object takes
322+one parameter, the engine object. Per default this engine object is accessible
323+as the 'engine' attribute of the extension object, like 'self.engine'.
324+Now we have to load this extension to the engine
325+
326+ >>> engine.extensions.load(SampleExtension)
327+ >>> len(engine.extensions)
328+ 1
329+ >>> print engine.extensions
330+ ExtensionsCollection(['add_value', 'get_engine'])
331+ >>> sorted(engine.extensions.methods)
332+ ['add_value', 'get_engine']
333+
334+In the last line you can see all method which are added to the engine by
335+an extension.
336+This methods are now accessible like
337+
338+ >>> engine.extensions.add_value(5)
339+ 5
340+ >>> engine.extensions.add_value(1)
341+ 6
342+ >>> engine.extensions.get_engine() # doctest:+ELLIPSIS
343+ <_zeitgeist.engine.resonance_engine.ZeitgeistEngine instance at 0x...>
344+
345+However, there is also a private method which is not accessible as a member
346+of the engine
347+
348+ >>> engine.extensions.internal_method()
349+ Traceback (most recent call last):
350+ ...
351+ AttributeError: ExtensionsCollection instance has no attribute 'internal_method'
352+
353+It is also possible to unload an extension
354+
355+ >>> engine.extensions.unload(SampleExtension)
356+ >>> sorted(engine.extensions.methods)
357+ []
358+
359+Now its methods are not accessible anymore
360+
361+ >>> engine.extensions.add_value(5)
362+ Traceback (most recent call last):
363+ ...
364+ AttributeError: ExtensionsCollection instance has no attribute 'add_value'
365+
366+If you try to load an extension which is not a subclass if `Extension` a
367+TypeError is raised
368+
369+ >>> engine.extensions.load(set) # doctest:+ELLIPSIS
370+ Traceback (most recent call last):
371+ ...
372+ TypeError: Unable to load <type 'set'>, all extensions have to be subclasses of <...Extension'>
373+
374+Also, if an extension does not define any public method a ValueErro is raised
375+
376+ >>> class FailExtension(Extension):
377+ ...
378+ ... def get_boo(self):
379+ ... return "boo"
380+ ...
381+ >>> engine.extensions.load(FailExtension) # doctest:+ELLIPSIS
382+ Traceback (most recent call last):
383+ ...
384+ ValueError: Unable to load <...FailExtension'>, this extension has not defined any methods
385+

Subscribers

People subscribed via source and target branches