Like other things posted here without notices to the contrary, this code is in the public domain.
This program depends on a recent Python and PyQt. I was experimenting with incremental full-text filesystem search. This program is probably a little too clever about caching, and clearly not clever enough about actual text string searching. There's some stuff in there about storing outline structures in text files (or at least retrieving them from text files) which isn't used at all. I wrote this in November. fsgraph.py: #!/usr/bin/python from __future__ import generators import os, qd, subgraph # provide a graphnot-style interface to a Unix filesystem # (treating file contents as lists of lines) def _walkfs(topdir): for filename in os.listdir(topdir): completepath = os.path.join(topdir, filename) yield filename, walkfs(completepath), completepath def walkfs(topdir): if os.path.isdir(topdir): return subgraph.LazyList(_walkfs(topdir)) else: return subgraph.LazyList(filecontents(topdir)) def filecontents(path): ii = 0 for line in file(path): yield qd.chomp(qd.untabify(line)), [], (path, ii) ii += 1 subgraph.py: #!/usr/bin/python from __future__ import generators import sys sys.path.append('/home/kragen/devel') import graphnot # LazyList: a layer over the top of an iterator that remembers its previous # values. Importantly, allows you to determine whether it's going to return # anything without throwing away any of its results. class LazyList: def __init__(self, seq): self.seq, self.items = iter(seq), [] def __getitem__(self, index): try: while len(self.items) <= index: self.items.append(self.seq.next()) except StopIteration: self.seq = iter([]) raise IndexError("list index out of range") return self.items[index] def __nonzero__(self): try: self[0] except IndexError: return 0 else: return 1 def subgraph(graph, searchstr): for label, child, extra in graph: foundkids = subgraph(child, searchstr) if label.find(searchstr) != -1: yield label, foundkids, extra else: # generators are true even if empty, but we need to see if # foundkids is empty, but without throwing away its contents. # Listifying is too eager, though. # Too bad this doesn't work. foundkids = LazyList(foundkids) if foundkids: yield label, foundkids, extra def _listify(graph): for label, child, extra in graph: yield label, listify(child), extra def listify(graph): return list(_listify(graph)) def ok(a, b): assert a == b, (a, b) def oklsg(graph, searchstr, result): ok(listify(subgraph(graph, searchstr)), listify(result)) def test(): def test_lazylist(): LL = LazyList([1, 2, 3]) if LL: x = 'true' else: x = 'false' ok(x, 'true') ok(list(LL), [1, 2, 3]) ok(list(LL), [1, 2, 3]) LL = LazyList([]) if LL: x = 'true' else: x = 'false' ok(x, 'false') test_lazylist() ok(listify([('a', [], [])]), [('a', [], [])]) a, b = ([('a', (), [])], [('a', [], [])]) assert a != b ok(listify(a), b) oklsg([], 'foo', []) oklsg([('bar', [], 'blort')], 'foo', []) oklsg([('foo', [], 'blort')], 'foo', [('foo', [], 'blort')]) oklsg([('xfoox', [], [])], 'foo', [('xfoox', [], [])]) oklsg([('xbarx', [('xfoox', [], [])], 'blort')], 'foo', [('xbarx', [('xfoox', [], [])], 'blort')]) oklsg([('xbarx', [], []), ('xfoox', [], [])], 'foo', [('xfoox', [], [])]) oklsg([('xbarx', [('xbarx', [], []), ('xfoox', [], [])], [])], 'foo', [('xbarx', [('xfoox', [], [])], [])]) oklsg([('xfoox', [('xbarx', [], []), ('xfoox', [], [])], [])], 'foo', [('xfoox', [('xfoox', [], [])], [])]) oklsg([('xfoox', [('xbarx', [], []), ('xfoox', [], [])], [])], 'bar', [('xfoox', [('xbarx', [], [])], [])]) # verify laziness class Poison(Exception): pass def poisonseq(seq): for item in seq: yield item raise Poison def assert_raises(thunk, exc): try: thunk() except exc: pass else: raise AssertionError("Didn't raise", thunk, exc) # verify LazyList doesn't read past its first item ok(not not LazyList(poisonseq([1])), 1) # instantiating a subgraph doesn't do any iteration x = subgraph(poisonseq([]), 'foo') # but listifying it does assert_raises(lambda: list(x), Poison) # if you find the string in the edge label, you don't need to look # at the kids to return the edge ok(subgraph([('foo', poisonseq([]), [])], 'foo').next()[0], 'foo') # but if you don't find the string there, you have to look at some kids assert_raises(lambda: subgraph([('spo', poisonseq([]), [])], 'foo').next(), Poison) # on the other hand, you also don't need to search all the edges on the # top level to find the first one to return x = subgraph(poisonseq([('foo', [], []), ('bar', [], [])]), 'bar') ok(x.next()[0], 'bar') # but if you keep looking, you may need to look through all of them assert_raises(lambda: x.next(), Poison) # similarly, you don't need to look through all the edges on a deeper level # to find out that you should return the top level x = subgraph([('foo', poisonseq([('bar', [], [])]), [])], 'bar') item = x.next() ok(item[0], 'foo') # but if you insist on looking through the kids you'll have to look at all # of them assert_raises(lambda: listify([item]), Poison) # misc stress test stresstest() def stresstest(): expected = [ ("The colons are part of the syntax. So if you want colons, you have to do this", [], []), ("Quotes can extend a string across multiple lines", [], []), ('Practical applications might include', [ ('a more powerful search utility that allows you to include extra edges', [ ("for example, you might want to see all the email messages with a term", [('you might also want to exclude some edges', [ ("""For example, if you're viewing a program as a graph, you might want to exclude edges indicating references to a routine or variable.""", [], []), ], []), ], []), ], []), ("path-expressions and more powerful query languages", [], []), ], []), ("We'll need adaptors that allow you to read other file formats", [ ("mailbox, of course", [], []), ], []), ] oklsg(graphnot.samplegraph, 'x', expected) test() graphnot.py: #!/usr/bin/python # very unfinished. graph-reading code. # The idea is that nodes are iterable collections of edges; each edge # has a textual label, a node that it points to, and other metadata # that explains how it was stored in the file (e.g. how it was quoted.) samplegraph = [ ("""I want a serialization format for string-edge-labeled graphs like those from UnQL, ideally a format mostly suitable for human reading. I'm going to start with a serialization format for edge-labeled trees, then add backreferences to make it general.""", [], []), ("A string with no colons, quotes, newlines, or leading or trailing whitespace.", [], []), ("Another such string. Both happen to end with periods.", [], []), ("This string labels an edge with children", [("Another string with no leading whitespace.", [], []), ("And another.", [], [])], []), ("The colons are part of the syntax. So if you want colons, you have to do this", [("Put your strings in \" quotes, using \\ to escape quotes inside.", [], []), ("The following are part of this string: lemons and oranges.", [], [])], []), ("You can write more than one edge per line", [ ("From", [("you", [], [])], []), ("To", [("me", [], [])], []), ], [], ), ("This is the same as this", [ ("From", [("you", [], [])], []), ("To", [("me", [], [])], []), ], [], ), ("And you can move it onto the end", [ ("From", [("you", [], [])], []), ("To", [("me", [], [])], []), ], [], ), ("The amount of whitespace on subsequent lines determines indent level.", [], []), ("Therefore, it determines which node you're starting from.", [], []), ("Quotes can extend a string across multiple lines", [ ("""The first line of a quoted string can include leading whitespace. Subsequent lines must be aligned either with the first line, or with the open quote. Leading spaces necessary to indent up to that point are not included in the string, but further indentation there constitutes leading whitespace on those lines. But trailing whitespace is difficult, because it's not easily visible. In order to allow encoding it, you can backslash newlines within the string, and both the newline and any whitespace before the newline is preserved.""", [], []), ], []), ("I'm not sure about the backreferences yet.", [], []), ("As described so far,", [], []), ("the only holes in the scheme", [], []), ("come from incorrect indentation and backslashes in quotes.", [], []), ('', [], []), ('I think sequence of children is important, as in XML.', [], []), ('', [], []), ('Practical applications might include', [ ('an outline viewer', [], []), ('a search utility that', [ ('prunes trees to include only the edges necessary to reach search terms', [], []), ('displays them in outline view', [], []), ('searches incrementally', [], []), ], []), ('a more powerful search utility that allows you to include extra edges', [ ("for example, you might want to see all the email messages with a term", [ ("but you probably want to see the From:, To:, and Subject: too", [], []), ('you might also want to exclude some edges', [ ("""For example, if you're viewing a program as a graph, you might want to exclude edges indicating references to a routine or variable.""", [], []), ], []), ], []), ], []), ("path-expressions and more powerful query languages", [], []), ("a report engine to display tabular data", [], []), ("an editor (perhaps an Emacs mode) --- for outlines of course", [], []), ("a data-entry system", [], []), ], []), ("", [], []), ("We'll need adaptors that allow you to read other file formats", [ ("mailbox, of course", [], []), ("merely a greppable file; one line per string, no hierarchy", [], []), ("a directory tree of files", [], []), ("and all lazily, naturally, so you can view large files", [], []), ("SQL databases, perhaps, although probably not while I'm at AirWave", [], []), ("source code", [], []), ], []), ] def canonreadlabel(thefile, pos): # the if is a dubious efficiency hack if thefile.tell() != pos: thefile.seek(pos) nextch = thefile.read(1) assert nextch == '"', `nextch` labelchars = [] while 1: labelchars.append(thefile.read(1)) if labelchars[-1] == '"': labelchars.pop() return (''.join(labelchars), thefile.tell(), -31337) class canonreaderkids: def __init__(self, thefile, pos, indent): self.thefile, self.pos, self.indent = thefile, pos, indent def __iter__(self): return self def next(self): label, newpos, indent = canonreadlabel(self.thefile, self.pos) return (label, canonreader(self.thefile, newpos, indent), []) class canonreader: def __init__(self, thefile, pos=0, indent=''): self.thefile, self.pos, self.indent = thefile, pos, indent def __iter__(self): return canonreaderkids(self.thefile, self.pos, self.indent) def test(): graphnotfile = file('graph-notation') graphnotrootnode = canonreader(graphnotfile) kids = iter(graphnotrootnode) firstlabel, firstkid, otherinfo = kids.next() assert firstlabel.startswith("I want a serialization format") assert list(firstkid) == [] kids2 = iter(graphnotrootnode) assert kids2.next()[0].startswith("I want a serialization format") assert kids.next()[0] == "" assert kids.next()[0].startswith("A string with no colons") kids2.next() assert kids2.next()[0].startswith("A string with no colons") if __name__ == '__main__': test() fsgraph.py tries to import qd ("quick-and-dirty"). Here's qd.py: def chomp(text): while text[-1:] in ['\r', '\n']: text = text[:-1] return text assert chomp('a\n') == 'a' assert chomp('\n') == '' assert chomp('') == '' def untabify(text): def dumb_tab_exp(char): if char != '\t': return char return ' ' return ''.join(map(dumb_tab_exp, text)) Finally, here's the main program: #!/usr/bin/python import qt, sys, subgraph, fsgraph # interactive incremental grep -r substitute. Works on files, better on # directories. # TODO: # - better feedback on when search is complete # - show partial results while searching; generally be responsive while # searching # - search more efficiently for long strings --- it's obviously somewhat # advantageous to skip over the items you've already decided don't contain # the largest proper prefix of the string, as we do, but once we get beyond # the stuff we've already looked at, it's stupid to go through N levels # of filtering. # - don't remember everything forever in LazyLists. Throw things away! # - navigation control: in and out, up and down. especially "in", # followed by loosening up the search string # - read outlines from actual files (as opposed to viewing # the filesystem as an outline) # - decorate: with quotes, colons, backslashes # - provide expand and collapse. Buttons? Doesn't Qt have some kind of # outline widget? # - alpha blending would be great. QImage and QPixmap have alpha masks. # QColor has an alpha. class outlineview(qt.QWidget): def __init__(self, parent, target): qt.QWidget.__init__(self, parent, None, qt.Qt.WStaticContents | qt.Qt.WRepaintNoErase) self.target = target self.fontSize = 8 self.searchtext = '' self.setEraseColor(self.white) self.setFocusPolicy(qt.QWidget.StrongFocus) self.resetOutline() self.setFont() self.redrawTimer = qt.QTimer(self, 'redraw timer') self.connect(self.redrawTimer, qt.SIGNAL('timeout()'), self.redraw) def setFont(self, delta=0): self.fontSize += delta self.font = qt.QFont('Urw Palladio l', self.fontSize) self.update() def resetOutline(self): self.outlines = [fsgraph.walkfs(self.target)] def keyPressEvent(self, ev): a, b = len(self.outlines), len(self.searchtext) + 1 assert a == b, (a, b) if ev.key() == qt.Qt.Key_Backspace: if self.searchtext: self.searchtext = self.searchtext[:-1] self.outlines.pop() elif ev.key() in (qt.Qt.Key_Plus, qt.Qt.Key_Equal): self.setFont(delta = +2) elif ev.key() == qt.Qt.Key_Minus: self.setFont(delta = -2) elif ev.text(): self.searchtext += str(ev.text()) self.outlines.append(subgraph.LazyList( subgraph.subgraph(self.outlines[-1], self.searchtext))) self.drawSearchString(qt.QPainter(self)) # for rapid response self.redrawTimer.start(0) def drawSearchString(self, pa): pa.save() pa.setPen(self.darkGray) pa.setFont(self.font) pa.drawText(10, 20, self.searchtext) pa.restore() def drawHelp(self, pa): pa.save() pa.setPen(self.gray) pa.setFont(self.font) pa.drawText(110, 20, "Type a search string; + and - zoom") pa.restore() def redraw(self): self.redrawTimer.stop() pix = qt.QPixmap(self.size()) pix.fill(self, 0, 0) pa = qt.QPainter(pix) pa.setFont(self.font) self.drawSearchString(pa) self.drawHelp(pa) self.drawKids(pa, 10, 45, self.outlines[-1]) pa.end() qt.QPainter(self).drawPixmap(0, 0, pix) def paintEvent(self, ev): self.redraw() def drawKids(self, pa, x, y, outline): for text, kids, othercrap in outline: (x, y) = self.drawString(pa, x, y, text) (crap, y) = self.drawKids(pa, x + 20, y, kids) #y += 5 if y > self.height(): return (x, y) return (x, y) def drawString(self, pa, x, y, string): linespacing = pa.fontMetrics().lineSpacing() for line in string.split('\n'): pa.drawText(x, y, line) y += linespacing return (x, y) def main(argv): a = qt.QApplication(argv) if len(argv) < 2: target = '.' else: target = argv[1] w = outlineview(None, target) a.setMainWidget(w) w.show() w.setFocus() a.exec_loop() if __name__ == '__main__': main(sys.argv) Here's a tarfile: begin 664 outlineview-0.tar.gz M'XL(`#8FLS\``^U<X7/;MI+/9_T5J/*!U)16)"=.;M3G>Y/WFKZ7>6Z;7-+I MW3@>/TB$+#Y3!$V0MI2YN;_]?KL`*)*2G$R;I+VK,--&)A:+W<7N8K%84E=E MFF3J-E%W1Z-'#SY+&XV>C)X].\&_H]&SIT]:[EMAIL PROTECTED]">/#U^/*+GX\?CX]$# M<?)YR&FWRI2R$.+!=2&O5+8?[D/]_T>;;JU_XZ]AOOY4<XS&6.^3?>L_'HU/ MGOKU/WY&O\=/1R=/'[EMAIL PROTECTED]'7_^'7SVJ3/%HFF2/\G6YT%DO6>:Z*,5- M&0FS-OA?-;TJ9+Z(Q-SPC]Y#D62E*N2L3&X5?L\*M519*5-Q5:A<'!4TQI1) M695JV`/XVQ^__7&"?X_$5)48*>9*Q5,YNQ8Z$W<+E0FC9#%;B,2(F5[FJ2H5 [EMAIL PROTECTED],H$R`MEJK0T&)"DRHU(LJMO!-8%U*3I&N@)*M>9(<H8$'C$!MAB MM9,M=:&$FL^360+J,7JN"Y'J[$J8L@"L$4='1R(I`R/T]#;1E0&,T4MUMY`E MHY7QK03?5PI]HM3"7">[EMAIL PROTECTED],*5:&K'650!:9%HH&:]%K&9)K&(1ZRPH MP2R&)QDCHR&I+*Z4*45>Z!Q8\D+-DY70<^ZT5$5"[EMAIL PROTECTED]""(QK4I(<*;H[RM5 [EMAIL PROTECTED](:FRFK^1Q]S?E3K:\QO<3J,F>`R9.8B+_2&%3HZFHA?A"INE6I84R8 M?9ZD6#5,/F3Y6=IIT9=34`G0`JJ#;I(@_06=$&?R_?HL,:49"O$6>[EMAIL PROTECTED]@A M[^3Z*\:3R=OD2I8)E(`$4>[EMAIL PROTECTED]&)%A7R(Z7N9]D_8B) MF^LTU7=@:,JL&941&1C(_-MUMG+C&4D&PCDY(^:[EMAIL PROTECTED]<07E`HMX$D*V.L^! MAT5"?M`.M0)EH+7!NM(BR,RC&EBQJ)DN9*DFXBXI%^*FTJ6"_<PT=`K_DKZ; M5)J%,@R.-;[EMAIL PROTECTED]@[EMAIL PROTECTED]<R-`K=_J<H2P_XLQ+=:&9+WZU(L)%:2-%!< M)[EMAIL PROTECTED]:$$?V;D,LT74DQ3E<4DD#M=I3%9!PQ4ED#_^N42:LN3OGZ5 MK)8RM\CMN*4TUV;(R%__%>07Z&1^N1LF/0,?1C1VB_"F'+[^F0D83'H8!VG, MQ>5EDB7EY65H5#J/R)!A:)$H2<L]'+7-X.&>(3_H3$4U_*Y&.,KASV]**-3L MKU`GC#/BO_WS_U`Y#*W\0;\HI,&"^6$TS=`2)$X=9>W..7"]2=XK=/];N\?J M5ZE6-#0(NITE3\7R8VZ&\$=E=VJ`?:=GE7FETV2V;HAQ^`8&D5UQ9V<,7)PJ M?[3"#W?AR\KNXT+%A;Q[FRQAG*<L$_[MI!S8;E'2LZ`S%*:9J5D9=O%$A.;- MR[_]\/PL#&@DU"$<!(,]R]08/[EMAIL PROTECTED],[EMAIL PROTECTED]:D%V>YV/#&N MX*?B3KR"GY!QHD4:1&TL'>ZJ/(;)AAN26L(ED"XYM?LX%>=N1QS>R?1Z;L*& M,@TN:HS7:OT*6,V+6U6SJFX;:"6<`[#!6,/6#(-H\VRC:P/QM1AOQAJCL%M+ M<7HJIA$<&'!M6$SFF&D(`L(!`5A;^(=:7_Z%G%$N9VK26JUDWM7LR=9J;JM^ MY\GYY&A\L7N8YVR8Z[RAHBIM$HHM([EMAIL PROTECTED],4-W/[EMAIL PROTECTED] MQ_LF:$GB^R2KS$>@[EMAIL PROTECTED]'V]"U5&2>PV&'/N(:^CTT MW):F!_$_VIISCA6(NJ0,!AW=)UM\P]UO>(MDJ;^2'-Q9U1\(\9##(DR!*,'% M5FJO;QE2,%V&HXTY;<WA_7I#;+D<&FP^#86@)[EMAIL PROTECTED] MN]7+))'`QZ-('(^VA=$$!E\E(I>P3?;?59K_(G*O?BFIGM;^VW6.O;@=P'P# MNZ?]^DB\UWK9_P`#=E%V.K#V>K4,,4>T:[EMAIL PROTECTED]@C& [EMAIL PROTECTED](\2B<)@'\8Z6Q6RTA_6T(7IV=/?](8H.>2)`XGYQ$8LLT6N20Q6T>;)D` MHW32&#&S-3\D9XXJ]GKVAKP[RL5$>N6*Q"[EMAIL PROTECTED])S<(R/9(72-$?#'\H$88 [EMAIL PROTECTED]>0E'7C;_X2$:N"],LO22]%/T]9_'D0HN^-J.:Z@>:26:TM%>^C#-;FX MD^XVLA;_;C$M5'*U("\)E2RK(G,$UO#;3VNWT788GGIK#0T9\;)B+Z-`]Y16 ME'3J>P6PF0D'0^I^8[L;Z\QG/8J:(4>+<6CR-$'X\"X+.BZ]::.6!AOT-V%8 M"@U*]O!'O"VA,B&"!*\HWEZ>[EMAIL PROTECTED]<AV]USLJ0@@!^(/XGCB:BCU6!H8TZ< MU%3C,8&>NQWX#G\WPW2.HWT`;F<G>_P>)-FP,W31V=V0SMYA_8<+4T,_2*W4 M[!(G+O(>"47ZF5RJRTO:6X/+2V+Q\C*86%YQ9!I:AG[K;,>A=5L[_\>A1*;+ M3YC\>_#!_-_CT6.7_SUY-AH]&0/^!,\.^;\OT;;S?P\%Y7-$E<UQ$C<+%0^% M8+TXHNP).=F9CA5E!]Y2DBM6DM)VY4*6(D.'@?_AW%<AIZGBE`9.C_!IAC)) M"C[&?".41%A#OX&%4PN\)U$>)[EMAIL PROTECTED]&:':!<42U5* M'-XD<#"<6N4IW(T1E#3$H#M)Z2V$13$Y>9^_$:$:[EMAIL PROTECTED];,)RHB8<(88RDW",S M2Z<[=G5AO]]_";BLY("L2&2:O+=)*^PA2\Q+6XG=0HZ(I2-F`9,R'H--X5KU M(%FC;,+II^SU6<1RHU26]%B6VE`.TE1)R8(CK(MJ*;.>$_M0O`R6XDK3"E"N MD2)MFVG:3U>O15!9*$I(E91PE7',&:E"S56ALIGB!.927M/:^8SJ$+Q'XOR" M_L.Y/NP_=XS:>3-=)[=\LBM3=[P)(E+!_NKTA6*80B8I#Z0<")\\AQW4F5U5 M4]41+Z7!\!`:0N<[EMAIL PROTECTED],Z-AT<;Q>)\10RTYRT(AG84;-%DL;@MA_U MQ'ECRC9/GNP]I))*/.>$'8]N]#4)44XT;`V4P:ZSN.NLE"NP]D9SG*0KJUQ> [EMAIL PROTECTED]:4;&(<N:^JDGJ+.CT-K7[7KT5?&2+ZW3N6DYE)G"!L%^`,U&V+ M"2+2YD]I8)O.6HX3B&/)?)#=%3*#_>YD^;[EMAIL PROTECTED];AU4B2$6,Q2M` M"6W2#1KL+.L[V`/]&?;!US92"_166Y"[EMAIL PROTECTED]("]$02-@(32M%:"GV]. MTH6U8WZI;]E^$(!J)@(*^WGY!8M+766\;!N%I7L5NH-1-Q5.)C8N15P-I[SD MGTD6\W/[EMAIL PROTECTED]"Z>`]:,;@[EMAIL PROTECTED])\MK%^F2XU"61_$>7\PUK'%UU;W2"IP M[62XTAN:G!7:&+<C*!O[7D-<34MZJ9%,:23KQ)YZ8]#L*;9+.T`C4[#+;W MILO]$AL]9<#A)J\RX%$)VSX;?=F:+NKIQG--KH?GALV>[EMAIL PROTECTED];B=T2 MR^UD2G</FC>D'N]:;%AP%9[:>C/RUSETB3.O"J;&XK!>G!XHT9O!^.Q5FMG! MJ&!`VEML!HDO#G9YVQZL([EMAIL PROTECTED])BNC[!T04:>D2;#]W"8FP>X#/"]A MND5,5UD:,H.3@"YCXZ>C35)&M<;[EMAIL PROTECTED]'53J(.D![V9+9N<C-E MQ6M!@>Z<<K+%+?;H]GY46P,K'.V.Q([EMAIL PROTECTED]<D!>#J;W%J5W8V'+,/,BF1* MVJ7%7!91&X+5((-T%CIEX[&\S1:J::,$.:.[&=[EL=JZ*!#YM-:4Y;"Y!R)4 MUD>W:0J"UE\O^>KL6EB%GK$]^*V,Y&-O;"7=DTC&^9_?GPV#>Q"^XKO;&<(M MN3ESPD3HI.[U-*CM,=A<=/&-F"J"SB(`Q.>JJA*:5Z[9`&H4`,B+BG2#0Q!K M+=9V6:[L*2DR;%M4P6&B0TQNR`3^<J$U/=#'"4[O<LV>?]G(C3#!77(!;Y'R M"M2WU^EZ"[#+)&]KN88(YE6ZBV5K*GSKV^02'K"0EL6&4/H4XZD5!YW6HNP* M<#Q`(9XB=Y5:\>`DG2+DA7"NG(UQU%PL^[5,?,XA[$]MJ$!WC%-$D^L62D)' M.]$D$F_UQ!HGW.6_H*P3P.C^Y@:G+0V2P89*F<)8/%ZULHSRY62'3^?3OVLR M:R,?VD/<'2N8`;$(F>W5JG`E!_5TO>Y,[EMAIL PROTECTED]".9*4HH`*\>12B=RL1 M'[EMAIL PROTECTED]>G'-9+HYPT*#[G,2'16VM@(U"?5-$2A4M57>^L`_2%)=8 MJ.R*"*1`SZJO0.A?I3B.TK%FQT"*91.<:$2(8`I1,8>W+Y9R!MO%MCS@@@52 MJOJ.BCP%0D:C=I%!LQQ!]4&NO=7>[US;;NYG%4`M,T6%!+',09%I:+]7?GO9 MSOL:'[SLL:2YT9-.3_4JNH_,)1:5STE46Y+;HQ&P?<.!)9MX7H?P$87OBP1' M%]CE>B?+"?ED3?X%GLC5-NQ<)`I84CJ/OL>^",2RK+C&)*(]PN]^I+RV:&,? MGC>OSUC.4VDH2'?K!J-+L6U3K45MHK1WV=([EMAIL PROTECTED])\3,.`MX<[EMAIL PROTECTED] M?!;?MVP7/<[EMAIL PROTECTED]"[EMAIL PROTECTED]"MH$4;EX%\:.M5YK290$;5E(I=ZM*8&38^ M[%D^&^G&#TN5IN%`?'5*B";U8WB8ZY!0,WP&SS>CP[3O)CK"L<LBVKM+#W,J [EMAIL PROTECTED]"9YMI`%7[>ZG"8+:=Q(`-=`_@:M,UOK0G0#35<!;MIVPK>! ML'U12<TG=([EMAIL PROTECTED]"\$>.$&>!!UA!.)H\?CQX^?#7S%1+T4JJ!,^MY*B>8:12Z" MZ%XKU##\%P/R+Q>'GNY$TIP0FX>;<).<I[]J&%J([EMAIL PROTECTED]@[EMAIL PROTECTED])=M- MX7;6WZ+;NMYLR*F#JCWQCF(#:/X.:7^4I$]''N]I$'Q6<7?ET-&+/>)KS3BP MQHW8L;Z"]ME;]K:G[)'"P*;N\)2#/%?<X2$+K4L^T)VV1-Y$9`<050`B)L+N M8`O!YR>G'?P;0]Q]59+--083CB'K5,O\-P/M[;&AV":\/^?6;V%(Z8K<S\G7 M^\Y3T(S']Y/M4#"D(^Y\=/%+*6FP""Q$2;^_O[\URY[46G\C_^,=PKN/[OT8 [EMAIL PROTECTED]/\9C8]'QZ[^>_P4VR7=_XS'A_KO+]*V M[W\X?7%Y.:^P,Y%_</[EMAIL PROTECTED](4X;TDV\*1#O/:QK6F6]PQR9<ITJ6S`^ MIQ0/G\U^RI)5HYH6(\.2*E,YEYCPM9$KX)2&W3N?9&Q1'&][EZ[BKM0Y0GJW M_]')AT:38^,<@!G26`"TX3C"<&7F=*"C.W)$>OAE(SH+'-6X-F'+.E%I7#^/ MA*.BB6P0M7#;77H7M?#!?M;$-&FL8[&M,K`.UX/&[?_>,42LE^9F(%/5ZF+: M'64))#*J1>K+)#B>:(!M!'(3#V<+,!WB1Y7AL)/,UR'72`SLT82'(2A*&I%X M0A43X]YAI_E-6]O_8_T^K>OG1O[_:<?O-]__>O+LB??_QR<C>O_K"?:[EMAIL PROTECTED] M$HT3!&R[7!4V:9RPN9;W:#RY(.,_#]Y1VI>JHRXFPA7^MJM]G0NBA[V>BT\M MZD!2415'FC+H=-4]W8[Z,?NJVJTTR.1"L6HYO437I5KE(9W"&\XI(=:PN%\! MS;LRJ)TD/>R>P`+Z'33Y\.=[*OQKSN+JYP;_/SQ7V_[K[>-+UO\<GSP;;^I_ MGIYP_<_X4/_S1=JOB/\0N_6HM(]#&)=X"Q[!>-4C*ZQ',=TO!P,_P,>$]$J@ MCT\FB`93N5;%YKTY!"E\XYO9*B+*=W-RV;]P9O"<+P7YG3RJ5Y)IQ7>?+_U% M&*5IVS<Q]3TVO6]H[UKIZK,NI[%&#V0R<^^RT:G97B([EMAIL PROTECTED]"MS[ MB$.7;:HYVI=J,NH&P9VK0[_QR1Q^2="[EMAIL PROTECTED](Z'DSZKAX\IB MW<Y=6M]=OS?"^`?B3Z=VY*0Q:_VZ@:/(I1$:KSFL9BHOQ1LLR$M>B41G.U]V MN/$,G%]T<J4R,4J\I)E?%(4NPCX%Y)[EMAIL PROTECTED])-27\K-;BA\YS!FT+)=/9> M%7H[G4;2X)'GHXLN&[EMAIL PROTECTED](%I!M$(2GG/J=^M<)=1]K[/E,WSALMX\75L MY.[YL%WRB&89=Y7%+H=68W5C-EBWLM3#><+KXP%H)\.&[*)NGVSSN-WT;9[: MM=H-*^:ZA+*H%+V_F?&;0LN\7-MZA#ME;W?<C6$R[Z#9\).8UKB=-D/&XL\9 MPPXFLAALZP1+=3M:"P7745!BEJY'NN!O`3"5L:U0BMV;D7>ZN&X#-N6].0GY MAVT=!>=USX=$:\^>J:79JL7'ZD(;LT?!L`./GK!WD'N=Y!QG9V9WC-/7]JVO MR9Z7P2Q,:JZV]#AR/[EMAIL PROTECTED]/[EMAIL PROTECTED]/MA+1_N_+%+)GRALV>G;67)3S M<22.(_'XHJ7]9V<[EMAIL PROTECTED]<=*N6,N\7O3`])7D0,?-)_R[&=GY%:W9_I0 M?X?43T.C?6I1=834785S1,Z;FH0+(K+SQ.:"[?N#'CP<N)JQEIKO'ND5!HYE MVIU=#NIW":[EMAIL PROTECTED]:[EMAIL PROTECTED](3#@;W`/O'.X%W=G<0K`"R:G#C M1_=:_':ANDA`\\K.N`-?E[`.ZH\:NV]"=T$K/@,C'S/%/1/=Q]:NF3W0KY^Y M@>DSSLQJNG_F-J9Z9G=)[EMAIL PROTECTED]:V.^53HS.PA<<:[EMAIL PROTECTED]/@ MM\2X'X$2AWCMU[LHQN&WD-2-WR3HT28>XAC*SE`CM(9[R7TF+!=5=DV;R*P; M"W%/N!72X9\&A=QA`R">ZSDC!QLN9OLVB?F##]39CT1CMHYDO*NL=V8N`<$\ M)<<[EMAIL PROTECTED]>Z+_:QVX$=3Z^H+UF7,^19(9"^\1FB64=1]63Q9JC\\2' MJCQNU8RX&MA)3U@/!PX]A2]I(QBQ3#2\I!=V*I?36$[LCKP"&KLT'H^KZ:;( MK5$"Z8L&N1+:Q0`$9C^EX6,M^CB'0R-M]2+',/7QI$;[EMAIL PROTECTED] [EMAIL PROTECTED](MO1TB>=2U'9I.I%*1+JO4<3W2:I)I<D_CLI=7Q%H"]J6R+H"HX7, [EMAIL PROTECTED]>KJ].I2.JX<L\-]^0N.G%P,3>`U]U9?M2W1<@?$^W2JN7.UZLMV MM>:6Z39W?E('`:O6>FVZ6NMUK53.:\'53UPMAX"[J4_U1UV(??L.PO*>M?*S M=M7:),LDE04=K'?K;6N>[EMAIL PROTECTED]/);BZG^2<"UQ*V=[6I"V7-$L^#LE#66O%V9; M\+M4?H=0&X[?O5!)_O:T9M2+FQ[?:[EMAIL PROTECTED]@Y=WS7%MJ%32"[EMAIL PROTECTED])>)W] MH/SKT([EMAIL PROTECTED]@\.P3,R,S!+[#T>/_-C^[6[N[=&U\<1N"6J5JQD5T&]>;/K4 M;[EMAIL PROTECTED]"L)G4-3H+6QP MK]_O_;X*;.[[_M__?*(Y/E#_\?3D<?W]Q_%H?$S?_SM^?,C_?Y'VX>__>8W? M_27`S:?]#M]6^^V_K79*WW?X'7U8[?#MM,.WTYHC[OEV6C`,#M],VS7L\,VT MPS?3#M],.WPS[?#-M,,WTUP[?#/M_F^F=;]Z=OC2V:$=VJ$=VJ$=VJ$=VJ$= >VJ$=VJ$=VJ$=VJ$=VJ$=VJ']P=K_`ESA(UP`>``` ` end