1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 """
36 MsgTree
37
38 ClusterShell message tree module. The purpose of MsgTree is to
39 provide a shared message tree for storing message lines received
40 from ClusterShell Workers (for example, from remote cluster
41 commands). It should be efficient, in term of compute power and memory
42 consumption, especially when remote messages are the same.
43 """
44
45 from itertools import ifilterfalse, imap
46 from operator import itemgetter
47
48
50 """
51 Class representing an element of the MsgTree and its associated
52 message. Object of this class are returned by the various MsgTree
53 methods like messages() or walk(). The object can then be used as
54 an iterator over the message lines or casted into a string.
55 """
56 - def __init__(self, msgline=None, parent=None):
57 """
58 Initialize message tree element.
59 """
60
61 self.parent = parent
62 self.children = {}
63
64 self.msgline = msgline
65 self.keys = None
66
68 """Length of whole message string."""
69 return len(str(self))
70
72 """Comparison method compares whole message strings."""
73 return str(self) == str(other)
74
75 - def _shift(self, key, target_elem):
76 """Shift one of our key to specified target element."""
77 if self.keys and len(self.keys) == 1:
78 shifting = self.keys
79 self.keys = None
80 else:
81 shifting = set([ key ])
82 if self.keys:
83 self.keys.difference_update(shifting)
84
85 if not target_elem.keys:
86 target_elem.keys = shifting
87 else:
88 target_elem.keys.update(shifting)
89
90 return target_elem
91
93 return list(self.lines())[i]
94
96 """Iterate over message lines starting from this tree element."""
97
98 if self.msgline is None:
99 return
100
101 path = [self.msgline]
102 parent = self.parent
103 while parent.msgline is not None:
104 path.append(parent.msgline)
105 parent = parent.parent
106
107 while path:
108 yield path.pop()
109
111 """
112 Get the whole message lines iterator from this tree element.
113 """
114 return iter(self)
115
116 splitlines = lines
117
119 """
120 Get the whole message buffer from this tree element.
121 """
122
123 return '\n'.join(self.lines())
124
125 __str__ = message
126
127 - def append(self, key, msgline):
128 """
129 A new message line is coming, append it to the tree element
130 with associated source key. Called by MsgTree.add().
131 Return corresponding newly created MsgTreeElem.
132 """
133
134 return self._shift(key, self.children.setdefault(msgline, \
135 self.__class__(msgline, self)))
136
137
139 """
140 A MsgTree object maps key objects to multi-lines messages.
141 MsgTree's are mutable objects. Keys are almost arbitrary values
142 (must be hashable). Message lines are organized as a tree
143 internally. MsgTree provides low memory consumption especially
144 on a cluster when all nodes return similar messages. Also,
145 the gathering of messages is done automatically.
146 """
147
149
150 self._root = MsgTreeElem()
151
152 self._keys = {}
153
155 """Remove all items from the MsgTree."""
156 self._root = MsgTreeElem()
157 self._keys.clear()
158
160 """Return the number of keys contained in the MsgTree."""
161 return len(self._keys)
162
164 """Return the message of MsgTree with specified key. Raises a
165 KeyError if key is not in the MsgTree."""
166 return self._keys[key]
167
168 - def get(self, key, default=None):
169 """
170 Return the message for key if key is in the MsgTree, else default.
171 If default is not given, it defaults to None, so that this method
172 never raises a KeyError.
173 """
174 return self._keys.get(key, default)
175
176 - def add(self, key, msgline):
177 """
178 Add a message line associated with the given key to the MsgTree.
179 """
180
181
182 e_msg = self._keys.get(key, self._root)
183
184
185 self._keys[key] = e_msg.append(key, msgline)
186
188 """Return an iterator over MsgTree's keys."""
189 return self._keys.iterkeys()
190
191 __iter__ = keys
192
194 """Return an iterator over MsgTree's messages."""
195 return imap(itemgetter(0), self.walk(match))
196
197 - def items(self, match=None, mapper=None):
198 """
199 Return (key, message) for each key of the MsgTree.
200 """
201 if mapper is None:
202 mapper = lambda k: k
203 for key, elem in self._keys.iteritems():
204 if match is None or match(key):
205 yield mapper(key), elem
206
208 """
209 Return the depth of the MsgTree, ie. the max number of lines
210 per message. Added for debugging.
211 """
212 depth = 0
213
214 estack = [ (self._root, depth) ]
215
216 while estack:
217 elem, edepth = estack.pop()
218 if len(elem.children) > 0:
219 estack += [(v, edepth + 1) for v in elem.children.values()]
220 depth = max(depth, edepth)
221
222 return depth
223
224 - def walk(self, match=None, mapper=None):
225 """
226 Walk the tree. Optionally filter keys on match parameter,
227 and optionally map resulting keys with mapper function.
228 Return an iterator of (message, keys) tuples for each
229 different message in the tree.
230 """
231
232 estack = [ self._root ]
233
234 while estack:
235 elem = estack.pop()
236 if len(elem.children) > 0:
237 estack += elem.children.values()
238 if elem.keys:
239 mkeys = filter(match, elem.keys)
240 if len(mkeys):
241 yield elem, map(mapper, mkeys)
242
243 - def remove(self, match=None):
244 """
245 Modify the tree by removing any matching key references from the
246 messages tree.
247
248 Example of use:
249 >>> msgtree.remove(lambda k: k > 3)
250 """
251 estack = [ self._root ]
252
253
254 while estack:
255 elem = estack.pop()
256 if len(elem.children) > 0:
257 estack += elem.children.values()
258 if elem.keys:
259 elem.keys = set(ifilterfalse(match, elem.keys))
260
261
262 for key in filter(match, self._keys.keys()):
263 del self._keys[key]
264