pax_global_header00006660000000000000000000000064124012767050014516gustar00rootroot0000000000000052 comment=21dd720cb7b83e0a6504124c3d54b56f760f162c scoop-0.7.1/000077500000000000000000000000001240127670500126465ustar00rootroot00000000000000scoop-0.7.1/CHANGELOG.txt000066400000000000000000000073121240127670500147010ustar00rootroot000000000000000.7.1 (2014/03/17) ------------------ + Added: Support for SLURM (Thanks EBo!) + Added: Fallback to standard map when application launched without -m scoop + Fixed: Distribution ordering issue that translated in loss of performance when multiple maps were used + Fixed: Multiple SIGINT would cancel remote process cleanup and result in zombie workers + Fixed: Support for csh and its variants + Improved: SCOOP's logger + Improved: Queues (Broker and Worker) are now all FIFOs + Improved: Debugging information on launching process + Improved: Documentation 0.7.0 (2014/01/30) ------------------ + Improved: Garbage collection of Futures. 0.7.0-RC2 (2013/11/13) ---------------------- + Fixed: Bug sharing lambda function (referenced elements could not unpickle) + Fixed: Bug sharing method (elements would falsely be treated as methods) 0.7.0-RC1 (2013/08/02) ---------------------- + Fixed: Local launches could generate zombie processes + Added: map_as_completed() in the futures API interface + Added: Benchmark utilities + Added: Support for multi-brokers + Improved: Documentation + Improved: mapReduce() now won't communicate intermediary results + Improved: Future results are now sent back directly to its destination + Improved: Reduction has now a tree communication scheme (logarithmic reduce) + Improved: Various socket optimizations 0.6.2 (2013/07/02) ------------------ + Fixed: Process cleanup on csh/tsh shell variants + Fixed: Using partials could crash - switched Future callable identification from __name__ to its hash() 0.6.1 (2013/03/04) ------------------ + Fixed: Support for Python 2.6 + Fixed: Logging configuration contamination + Added: Preliminary support for Object-Oriented paradigm (Parallel method executions) + Added: Support for lambda execution + Added: Pool discovery features + Improved: A Future can now return None 0.6.0 (2013/01/21) ------------------ + Improved: Launching submodule + Improved: Network handling + Improved: Debugging output + Improved: Documentation and examples + Changed: Default verbosity is now level 1 + Added: --quiet flag + Added: Nicing levels for local workers Special thanks to: + Stijn De Weirdt gmail.com> 0.6.0-RC2 (2012/12/28) ---------------------- + Added: Shared constants and functions + Added: Examples for newly available features + Added: Support for interactive sessions 0.6.0-RC1 (2012/11/22) ---------------------- + Fixed: Backported modules conflicting with Python 3 installation + Fixed: Memory leak in the Futures queues + Improved: Time-based future buffer (instead of count-based) + Improved: Cleaning remote workers after termination + Added: Remote broker launching + Added: mapReduce and mapScan functions 0.5.3 (2012/08/24) ------------------ + Improved worker ouput to standard output + Fixed: Launching distribution shown in verbose mode + Fixed: Python 2.6 launching issue + Fixed: Remote worker cleanup upon abnormal program termination 0.5.2 (2012/08/14) ------------------ + Improved networking + Improved bootstrapping + Added unit tests + Fixed: Memory leak on exception handling + Fixed: Memory leak on Futures handling 0.5.1 (2012/07/29) ------------------ + Substantially improved documentation + Improved grid scheduler support (SGE and Torque) + Improved hostfile support + Added: Python 2.6 support (see documentation) + Added: Default number of workers is now the number of local CPUs + Fixed: Bug on some Future return value + Fixed: Memory leak on a specific Future usage 0.5.0 (Alpha) (2012/05/11) -------------------------- + Greenlet based task handling + ZeroMQ based networking + Master-Slave paradigm implemented named Broker <-> Worker + Tasks caching features implemented (Thresholds named high and low water marks) scoop-0.7.1/LICENSE.txt000066400000000000000000000167251240127670500145040ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. scoop-0.7.1/MANIFEST.in000066400000000000000000000002071240127670500144030ustar00rootroot00000000000000include *.txt recursive-include examples *.py recursive-include doc * recursive-include scoop/backports *.py prune doc/_build prune .hgscoop-0.7.1/PKG-INFO000066400000000000000000000021661240127670500137500ustar00rootroot00000000000000Metadata-Version: 1.1 Name: scoop Version: 0.7.1.release Summary: Scalable COncurrent Operations in Python Home-page: http://scoop.googlecode.com Author: SCOOP Development Team Author-email: scoop-users@googlegroups.com License: LGPL Download-URL: http://code.google.com/p/scoop/downloads/list Description: SCOOP (Scalable COncurrent Operations in Python) is a distributed task module allowing concurrent parallel programming on various environments, from heterogeneous grids to supercomputers. See https://scoop.googlecode.com/ for documentation, informations, bug reporting and more. Keywords: distributed algorithms,parallel programming,Concurrency,Cluster programming,greenlet,zmq Platform: any Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development scoop-0.7.1/README.txt000066400000000000000000000004311240127670500143420ustar00rootroot00000000000000SCOOP (Scalable COncurrent Operations in Python) is a distributed task module allowing concurrent parallel programming on various environments, from heterogeneous grids to supercomputers. See https://scoop.googlecode.com/ for documentation, informations, bug reporting and more.scoop-0.7.1/doc/000077500000000000000000000000001240127670500134135ustar00rootroot00000000000000scoop-0.7.1/doc/Makefile000066400000000000000000000107521240127670500150600ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SCOOP.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SCOOP.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/SCOOP" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SCOOP" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." scoop-0.7.1/doc/_static/000077500000000000000000000000001240127670500150415ustar00rootroot00000000000000scoop-0.7.1/doc/_static/copybutton.js000066400000000000000000000046771240127670500176230ustar00rootroot00000000000000$(document).ready(function() { /* Add a [>>>] button on the top-right corner of code samples to hide * the >>> and ... prompts and the output and thus make the code * copyable. */ var div = $('.highlight-python .highlight,' + '.highlight-python3 .highlight') var pre = div.find('pre'); // get the styles from the current theme pre.parent().parent().css('position', 'relative'); var hide_text = 'Hide the prompts and output'; var show_text = 'Show the prompts and output'; var border_width = pre.css('border-top-width'); var border_style = pre.css('border-top-style'); var border_color = pre.css('border-top-color'); var button_styles = { 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', 'border-color': border_color, 'border-style': border_style, 'border-width': border_width, 'color': border_color, 'text-size': '75%', 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', 'border-radius': '0 3px 0 0' } // create and add the button to all the code blocks that contain >>> div.each(function(index) { var jthis = $(this); if (jthis.find('.gp').length > 0) { var button = $('>>>'); button.css(button_styles) button.attr('title', hide_text); jthis.prepend(button); } // tracebacks (.gt) contain bare text elements that need to be // wrapped in a span to work with .nextUntil() (see later) jthis.find('pre:has(.gt)').contents().filter(function() { return ((this.nodeType == 3) && (this.data.trim().length > 0)); }).wrap(''); }); // define the behavior of the button when it's clicked $('.copybutton').toggle( function() { var button = $(this); button.parent().find('.go, .gp, .gt').hide(); button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); button.css('text-decoration', 'line-through'); button.attr('title', show_text); }, function() { var button = $(this); button.parent().find('.go, .gp, .gt').show(); button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); button.css('text-decoration', 'none'); button.attr('title', hide_text); }); }); scoop-0.7.1/doc/_static/logo.png000066400000000000000000000554061240127670500165210ustar00rootroot00000000000000PNG  IHDRg@tEXtSoftwareAdobe ImageReadyqe<ZIDATx]\U^>}齐 P#Uz&WQD,`EWQQE!@ 餐e罻cfvf~Ivy{{Hw,, yA:@ La24&~&mL1y,]U=ؾr ޸ǂR ˢO*@@+|Ip7d&W1Y &ϋk`8&;\b0y/D rfH"&ѕ'A;1yRt gn;OE rpKL]+ Y@8}e rv\T¶a2ĬYfA޿~2m)P JؾɉxWѬFk1dmhOLJBPn^,/#\n@֘ }:#YRJ=(WL0ica T {|,PB42=|IEgr0I0zth?i1Ye΃T<.`O9meЪ8:z6tFi|6'`z 4.(7M`&)$Y{Ϥɹ^QOs{n&^):eq+lړ m7|38Y)RjT.<̤E19 xW,(-&m';#NnEXi™Y$9ͷ\A;Rne2]7Q#GqA /Ač>nE3"QM9r=͏- y"nJ ]EmYgueFNᵗ $+eS&wq"{nHHY\Ň~Ыw29QjbLS=s9 4zJFm:03/Yi ݘZ6ufa-&ehccvG!$m4Hk.f=-'YϬNn1r ztRȭ.K:D>i8L|>CLeʔC9 wB7v{va䕁@#E!4!k5H<ҌQH{u5FTImFՓ_9㙹.7h@֡Y9c"lԶ[RdBTD1c ͠C ΪBG ađcs' ؗܣs[* Q&}Ln+J>- h]ʖ.[EN^PE19Dmɭ6>[ l 5Lmyn;t}^#8hS}W#a 'H˲U0%0Ca\my JٹU)(C{b\Q,K z#X,"Q GFN5'Э;>yk4kˏ#XǢshR?{MyL.A r(msm; ssplݷ)\Qncf0&x q};p ۡv 18bDIQ]iIQ:3/AFJAe^>ױ][63F la]͘!gz꿋r+M>rhP|{Up gAhˎb?9v]d#b51v|-(EÌܜ%mP_ݰ2{3P&*ʬ!ٚmb.5@U k?9xتТ2]Ph҃E@p:kf?]PBxJdE?v^JyM1s{RA3DWFL2 3@;$e*[0jN,N?ehRh1PNJ<Wz!i'ȹPFלgA"a*P)wakprұQVz[ L ?ǚۨL|җWIH rI+PRٮd׶5䎉}ip'!D7HYi[d(1s, pnMKS\;T#JBDJZL;\K7$*j4K8%%sB}vLԪL{N@lb(/EلrFĄؐ~+2u[v]R({s.4r k(jL\)~äT*OpD'3-l,f`.#-973g_YrhcŐ S#qؓHuՒJ-d[SM)--Ț1RXϦe6*S2f>SpTE2JQHIJ2e`MjPBvɮft%ڳ!fNb4+>Q)A$j/?u8=)G3[X"sJR s#iIxu];L~V  szSFւ?Yn&[IJ]IMZdWT П,R U{":駼'_83zKu&#車lIsɄh!3'liucX,>-e ԠڻXAλY-HHK _pjI|NNz,6~Nf͗sq_oLVrR1N@ǃܔT3w)ؽZ^*;=>_&_"k0 to-˦a)h]? [b71J+Ac6|jM\I&`rTȠ 7:<|R8Yε=lTGL>kLjY[;އ.ۢE*>'h*]E8С-(槗<wGMJF)/^]LBbkAҜlZڄ83A WxŘ/]U)Mf~j! f9$ҹ -ّ< ' rU>p\Κ=h|簺b.9m,+5ڒ21I=d,ȹR "/G=WO}8.xQY&Uf[*|!1B)Y[%-Oѵ6l_&Ps B\ w# rqd@Q;ӔIBv r:|+Y84尾R :$@i$ThEk2Ɍþ K–eӱ>tc$ٞJ[dT@ەű`W$ͮEX(>EQQDx`֘e ua7Lȼl!f0yVN\3AG"+I P4DY4|>xnR[Ⓞ\e?X ds2G$za]m؄4\F -9Sa0B%-;YJmЖusk>M=#&5d"L*C׽"l>A-ci7õ=% 1en~w}=x^ 2JjoQ52j^ d`ܾa269{<T[,yH4 h-H԰ߤ^e2*v PXsSoeZu6XijSqZ =ׄUuZwNiDcZF4cဃCCDc5HH{kG8 t (j9|7(2VF-sJJkG|9 l}p& EKgM'55k& cZP܂Zv)#+Ti}d22l%~4l6&kuZsLC֨>4rɄ!)1EQ4ݞ(%O5̟ U1a<ڥƏ!¨XLad{F*g"ٞ` ӔUAʂ\6N&aoAel)V5$ \7`_~Wo_bQ3M]1~Hzw0qttu"Lo%8t5Hi2iڋIn{(tWGH41b8nݎ;V {@d$wȥ&-as Y+`ri6keqÝӒ4 @IqԑGbb 0vu0LsKA Vxӡu\ΖK-NM#G3SO<0vlDT:Z.Z{JM 8Urx"eOZ<≘6s:[e2v lɕR}r1ip Mxk;k+ Q[߇rNHYAKJgJ6='x},5h߱i9vUʞB)9H?R/vvD&ZF3W1a/2Ǥ-v_l:6F*(2e#iGy`,i>-oX_SxsjbC!9 MAbO_l~.*5`ַK78aK8T*&?ĝ d Mu\2ed NEхCA\/<,MsG:Msp x@~T zPm~3qAůxN gjbQttg2 rP}>qdHdRPDd8廲s\'LƪeK𭫿W:Aa, m?_Yo9-!kޣhC 4+7&8夨rɋ_dƨoF\ojk5֖e46| JNY&‰ja2hwTq mK0iOP*EOewBT28Ý1ǏŸy_n}I ዮCG.u]=vʹ80t;yͱ3FmʡceH#F?~lof-%;#wSx_2yoCoXT`39N2 瘼l!$x5q1F)wubpWƸISmҖ1sJW>b6 wYQ[#O?o1u(smB7nW Tdb^j|WJ/3X2Eq%BD OJ_,9P =;Zߞ_y"MZ~'bmNGmg>N;]HxEW49*Mo7 .["f{#r@5c0<]P&j),FδJ'`V0rzu4oy ۏdCNSN'?s CG_W[XrCh]}EwOZ]Q ֡8r|`J ʊxU)2)qp/9!F(;c3_E'Op%Ȕ` Ld<Тe;]{g7~뜰x܍'Gőc' Nߞ~@΋uҪW+l}knNX 3GpB?Avoh^LC#-cF6cpwC DOŸӰfK?#b>,Gq|"d!JguKd<*5]DGqA V^BSC=deRC& Ҙ{+M[cnr4c {*G!d!~f|{n5J'kՄsRXayr{ͦHjcưH÷%r/loNʮd"׶TYɕǺMG(b`?AnS %Fm_7Nhv&l߼ `xD!K="?"Lrla!3ƒ.4KhE,jG!8&*[aWkQ=>=::<\K^#ͮҾ#r.*?ƈkt}3F7LEdL0caZ{IJʒ!/Ә$x.~R> ei "vr=ãM6ur(ڏ־7DHRe_]șlKUddt28g~FRQөޔ-023-Fu2Q6#giHݜ4ݑC~D)٥؞[.}F=y7>oРLf6 FV?nz,Z:jcQ_3CPۤB:PkPdJv Bb0*@S UwsU &ώ[וwK,OX߁:#Sb$a$-ۖH⦈2X~ks!mK&|ᵹ`XObS\,6`{Hf%_]܁wUw!o(a!@̄bLY!L>MF~wL6m=n1?t6fGG:}F/}63GT)eZj,߆e67ț"(_҆C]4ƋY{ܜ1)89] CەcJY|e[;*o-wSߔ˄"ɂc L̈́Ĵ)'8,-L1vӦ$̟qt˘993~NZ2@(M&DƔ.S$MVW}!ꩫ1TƱH<(F\߈T0* AZH/myϳ0_)ռGOML>P6XX_X+4}C;)(8کL qvwVD[epo?{v |1ޏǵxZF.OKe RNw0ZD"-1ﺺd15}58$ | 5j z#Yz#96bqN҂эK3ѨXLٔlȉEǃc,ɶG)]Q)˼ '/I$51A\TK1ENt7Q2>؃6k9Ak\6J!Twp€B {wDI~.'B:o'Nҁ:_z2s"r*2Su>L9ɆAҧj"SmN٠Nf> ق<1"rՙ[kkئcj Ϻ *kP2^֮}ȝV"0%c!GsB~6ASmD'E܌M2ڞnTԧ /liR5XrdztB}c4JJ)joLse bskfRO\'9TL^*JEH&݁BP2U!)d֣wOA?[\Ü#Sp73"KA ]g -3j'),.fWΒ-%njy$k3)Lk5,i:Q,~=|G"oq3W8#9u8E*tL [믹G՛gJ]%V0x)O{_ҠssC)1|42 -1k)s/c|ENG0͙fyx";l;?K9A9ˋ42Dԇw~/WH'P)`JvqA;OyN_1?I-X,A>*KP/|rN522HwO_EeA-(ݐ TJ?xdJ8F9E`|/-GB.ʘ~MۊH=k|qWN'{y|TD!39ރvHc7w9Y1G91?H?`~,j(0nN Js&a͖c0d/SϒQ%hiz܏L>vz^<2ӜB 3<4iG x0^i7q,/6y]߁èl)D̦n!۫cƙL;t9zsB"4\c8N_F!cR(E2v k@n;G䠆P1m9?tz;SKP¦g]q 3ŚmOw4 > U5bQGYPɲ5+vr35gxduAMɶTed6ܕ#GᔥM5v?6A`%s}b7ZQC1ȄіE6n`q1|UYhb&qy|&͠!-977>xTv33M@ZU;Et z*e&aۖf ?9"uBׂEcb]6:j}|B.EjUH:n4sZ&>U/oݓqx/d@9y?eX"e deT{i2fWU?2Uwӭc]R3F]~s]Nl߆:~Rw{P?eV;NX0sgɡeިӃi-\rG58J sN|Y#ӖaŚ1v $W>v{qI4Nl9Uɘwm粝?D TVPP.J_(_LLSOa_]ƱorIp2>z>n|pȢ:߆hx:'p_Xl|}>59S*S[O3B_ܠL\ĬhƐͅR<|4b. C+I8PLyQlɑhx[J84J᱔2&1)! E],N& zA ;ސ~T439e0ך <>SΤX؂{R~],Pl5 5?fkG\߅ǖ"srtRXv ǦUZY*}NِgyT߮ޱ1mB`O0nnT)yp ΁yqڌdR}}VޟK7Foh0VϞtIez|"zh`:il)+RC yxKry[Knn5E_k }.7Ni/I&XdZt+4%!聬] L'2=<>eɸ9kRW7#F~tjIʾ>jp2-W؅>އ~8^ U kn4i)ϝRpث/tvynT)Ii`x ET8L'1Lnc/^n6& 9tdd6 `3Y|I7Zt>I|OmZL^#1eJ(c/W.|bHܐs>8kOcIH{ 'K}eϭmuU~ ;&:7d=Yr,(xO*o<>CɶZ53/-95^WeD2¥.;{`d"[9l6} ]AT7>Uа|šBM~lOzt ŠI)*I/(r<>L3%Ac, <;R\eBɢfmlc9˽fi}+3k^;Sדf&|}^O@nElX41_<>.Z?sMkAh䬽jpo!LX KUt&ԔP. wWBdRJiK4(A\S0MsB*J3*X,bG_j-+,P=u$5"ӭ٦<6GéU24sypw^ $:tq:7]Йp6V_'ЭD_9sQVv>;;R)ܼ:pr)/y EKs}H,$3J yPT+QA}mXAϳ N'2IE !u~ەN>]3y ~6?d[KnaW"C*[oF;p .8K'*"lm D>3P\QU:7MHw( bB5.6'E^Q[?+-1%P%5p~fp zQSAW$h#XEK35meo1/r1S>ӮA֗6L}RؐySO?ޤ̀S|_u{sSH3 :>\.C7zC{@VsszL&qrvhnnWںOD"TFB\ A;ǖd1h $FL1 1?7 cU'4%{0$K<\o>=R==+ҝF^M\{6yO \SxgWջ;kz\=sţ\T5PڢmV|Jܫ!1=nZ':لyhSaY/!=[G=/by/JH!>nln",e>a g'M#g<+hl.aV~6= x*Ygck7;[RQ6 "X|L 4{e^ڮC)< `}ج-eڹ]l*/Nj@[[zF\gdz՟p//$' \#˙R'CR|Ujf~E(nf t$߈n5:#J"(BLt_|NVLך>Erg`F4V5bFX1[/`W}J rhNcrnF<[]d#UyG T$4U]B5JDF.kj ӐuA{^%;Z#n:7#*0v*hmhkSi"J@EjAʱ[nW>2ZmDiIhJ#ie'^*HI"kiPzTy0cl\Xvj ߻egd>ǐ 99cb*ǁ*Eow|>_uMY uoaYpIǡ/>{ŋ!o \zk3ru'sfm?UU-MMW ,/9Wj܊ *hI4ʺ|3Ӡk39ц.&|JFsc=zf1K! ZtJ9u(v?2O9"/ [{}N "ʡrkLNgr бmGLxF^m+51-,Xxn.v|h@2yIk9V % "9"i:X|=td̤f*D/@b $$T"L>FYi'oc'NFg;Cw6sl߼~?F|؁6]be4p2_d6ה󨫒_P&g3lMtp$'ẼN)4%jJQHGNRF %z/A/`pY磷.Ȋ') LDmoCh=]wnڂe7h$} N HSy?9W_H\s`P ]5WJN&sˋCy8PUT`qFR_=*(:ާFpQF6CgW3W~k8m&y1㞘^ݠ 9v<>rO^@(DM,e;\Hi,p$1.(E@c@SFrN$/Mq̅yQ9n -|b&!ߧGZ?eޏ$=ڥkw'P9)'ӗ۷~c _Hiy|8(G1% ?Sb[ĜN? ǞpT/^_KA r.L&#O^a|_.ڶmG&d"ST)Bis){;P[߈N9&lJd2YB!1,N:4uȂqI'cI%xLEHEyJlu8En0D0b̓[J,PLR\aLK:@{ 6gt]CW[vi[>NnIFףfǢ~ vmÆw, r4 F5Κ baH%v~"qM6JN7É*+k)i5 ~Uk# !C "M"hpS\Re~(\@i'+f-:MJ6XN3{bdU0Q :1YLd32B2qfZcʌhG6h.H('WƺpC\v \".[![7=<0_2@V$'*|x0Rbd,6N,h)zִ)~jHfݐDɧ!c=&]րPUDclؿ-i>KĤӰoVL3f5F2b?v4?ε/Ec5HtADLI(pĭ+Qi7Ӗ&\g2ՃWC$(-Lc2/z-h2X'2LF+3u?{aF>ۃtz@CGlF3 TUG(6[$[;c'm'U!a[⌠9[Q%C'Ŵ(6Qu=-C-lW=Tt $$tEg0jtϝiL;3a"8}!_?! fAxe&OًL]M(dG>=u~/%wsҐо6`,o6]Q}Y&iHF1HvD|S3^E'_/؂<8ݑÄGaf^wIP.dUapPb,!ܬv`##)39Fd ۢiLA&d4*~FDzNļi`)0i@9/⌌Gy8Bp)ዻجtm&J="J6c=Bi4;/%v?[ iAC;h9l;ٽe,}e +YTID,w 洜D/R$LڎTѥvڏ8)ݦ  ϋ pS另tv.L)x1Sޏ -P4d5luaYwhl~;fґgQ8}yA1pJGE}ihEa0R6m Y.Q }txK2+ V!R8Xݸ͑ |S 7Mg7R׊>]lbq|&u\ҀX9M4mmdxLv9sZ&ys\d >S9bݺ}oyI:Zߊ}?};v.>[!,9IKhڮӃ;r[1| __rQN٬bA5/ʉ |gf NL &@\s?G FI6l4'LWVЍ\!YsÚk$!"OX} o󇨬ѻ (-rQpʨ^坬$ Zl>` M,r"G& -ezC;bwrd|a{e0maj0,st8Ϳ؃6^hd&m7iL)@L J|` C-h>Ѷ8:m$"i *0 qbsr*:M13{Gy7[<7DM8M\󧅫Xq1A$e+M")P ]1s%0A$HwHw +W|F@q%i%{7Ĝf#~DM61*)lhoG-t!7MЃ{'ה_#g.fMLr~OmFOն'"@-&/)b^8+lBey0v i]]CF>^zҰs|M;`iY5uCdR4r޽Ƽi kpW gV$1Sm㓝&|nc3&s=8)o&s\>lv\(װ~\[8܄y`DMY!ִ &QӶs=1FF2R;|L9"-:&,\.1 S 罚2\A*yeeELT\roDmf.q"tS\h17L|p4Wx{l#Tuۦ Yϟ%a3 ̲vrvj4#eC[c1v ʨ&+`xcPI2-c 'k7L F1Elݞl,B?bє!.GĜ,bI_LqɟۘwJXx/Vs_ U)iwک|[T_60T%ʺZW$ȹLcb+[r3q8)/]Ln 51 ?85 /Cۻ:YEnz׋"ȹ5W1ӹ<[eN'cy ۣI+aBAä}b,5ɧ>M۠O J֟+~OjAѱ2ZPv].PPAr&\)$eLyK2/B@a8$?LjyGZ\;އk˯/+,6k"ok+PcG͕dgw~AKԿO3A.m4j]yABN^ ӧ@ڔ+wD ]95Y*U㵊5?K/>SG7{ .R?J˚M{u]6isDW7D|QK<,ܗ|[9:ƔaDIb-.3)9!kiX|il}^M-19׊7 Y0CI@zXLtW/˳1'A#me34Dj/'ό7\/+>g*v5+=(v T}*4zQяcv7Ie-ڲ*GBA[n^߁%EJ&ZTu?2sx6.cK9I4VOodѱ+0%tYEP[tb 4ۻ-HY2{'3uens_Ef[# z'VJM&^1LԪ-Hxxv3YZ›zJa( Tˆ"Ȧm*~b'1Χcv]3nq$6dEȮBb]296۰A@Ja]ʬ4o/H*i;Wi&צ Mqj 5L-A?DzBZtbb]n1|rJ6yO3?=Ŷm[6I S\Di2?%Ib!9Y3U!43ehVٿ lg6 H;DmbnĜR %8N)+AS=l2`c$mK;8ڋhA(1l˾}LũSwUFa-\ сLR5g>ff"kCL754J>`I!RH)6[scBl n}/݋zSPg<4.SL@ r=T_4iAq]el*zeqm6Vi ;VcĜ4Lx%")F~1YnY;'{?7 7[5+*n#{HӍ2 I_ cBF)hFb@{|][|ڔW.c;ARO'ڝLa( ؒ~%F*!g@ORϭ4(Yه(ZY:\T*'vAP0ʧ*W+4Ë_ékb fAރ)Əٵp 6-5^jAWXbq߯qW45ۦJ$a2)Dy8Y<#A&"d?[LWxrfcHQ` e;AЧ0B¬!P:p&*QJmȨYKx- mI[6I &?8͵:O`:Fհ*6w3kICg°r}JA|&i1m9 JIGbQZ8oжf rXk<ɵ7NrOz~"+ g_lnˑJ_UL a`۾Ǎxlfڣ7>džJi^!*C XxQ%jg<ʠ1UH:/EiۗwyOq߮Wzݠ ' QA?b4X% gRij+1wEQJ{&{x1BOބU"*r;A -NoY&mms />zz)u vɣ}?RA*YvNzoKIENDB`scoop-0.7.1/doc/_static/sidebar.js000066400000000000000000000120601240127670500170070ustar00rootroot00000000000000/* * sidebar.js * ~~~~~~~~~~ * * This script makes the Sphinx sidebar collapsible. * * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds in * .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton used to * collapse and expand the sidebar. * * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden and the * width of the sidebar and the margin-left of the document are decreased. * When the sidebar is expanded the opposite happens. This script saves a * per-browser/per-session cookie used to remember the position of the sidebar * among the pages. Once the browser is closed the cookie is deleted and the * position reset to the default (expanded). * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ $(function() { // global elements used by the functions. // the 'sidebarbutton' element is defined as global after its // creation, in the add_sidebar_button function var bodywrapper = $('.bodywrapper'); var sidebar = $('.sphinxsidebar'); var sidebarwrapper = $('.sphinxsidebarwrapper'); // original margin-left of the bodywrapper and width of the sidebar // with the sidebar expanded var bw_margin_expanded = bodywrapper.css('margin-left'); var ssb_width_expanded = sidebar.width(); // margin-left of the bodywrapper and width of the sidebar // with the sidebar collapsed var bw_margin_collapsed = '.8em'; var ssb_width_collapsed = '.8em'; // colors used by the current theme var dark_color = '#AAAAAA'; var light_color = '#CCCCCC'; function sidebar_is_collapsed() { return sidebarwrapper.is(':not(:visible)'); } function toggle_sidebar() { if (sidebar_is_collapsed()) expand_sidebar(); else collapse_sidebar(); } function collapse_sidebar() { sidebarwrapper.hide(); sidebar.css('width', ssb_width_collapsed); bodywrapper.css('margin-left', bw_margin_collapsed); sidebarbutton.css({ 'margin-left': '0', //'height': bodywrapper.height(), 'height': sidebar.height(), 'border-radius': '5px' }); sidebarbutton.find('span').text('»'); sidebarbutton.attr('title', _('Expand sidebar')); document.cookie = 'sidebar=collapsed'; } function expand_sidebar() { bodywrapper.css('margin-left', bw_margin_expanded); sidebar.css('width', ssb_width_expanded); sidebarwrapper.show(); sidebarbutton.css({ 'margin-left': ssb_width_expanded-12, //'height': bodywrapper.height(), 'height': sidebar.height(), 'border-radius': '0 5px 5px 0' }); sidebarbutton.find('span').text('«'); sidebarbutton.attr('title', _('Collapse sidebar')); //sidebarwrapper.css({'padding-top': // Math.max(window.pageYOffset - sidebarwrapper.offset().top, 10)}); document.cookie = 'sidebar=expanded'; } function add_sidebar_button() { sidebarwrapper.css({ 'float': 'left', 'margin-right': '0', 'width': ssb_width_expanded - 28 }); // create the button sidebar.append( '
«
' ); var sidebarbutton = $('#sidebarbutton'); // find the height of the viewport to center the '<<' in the page var viewport_height; if (window.innerHeight) viewport_height = window.innerHeight; else viewport_height = $(window).height(); var sidebar_offset = sidebar.offset().top; var sidebar_height = sidebar.height(); //var sidebar_height = Math.max(bodywrapper.height(), sidebar.height()); sidebarbutton.find('span').css({ 'display': 'block', 'margin-top': sidebar_height/2 - 10 //'margin-top': (viewport_height - sidebar.position().top - 20) / 2 //'position': 'fixed', //'top': Math.min(viewport_height/2, sidebar_height/2 + sidebar_offset) - 10 }); sidebarbutton.click(toggle_sidebar); sidebarbutton.attr('title', _('Collapse sidebar')); sidebarbutton.css({ 'border-radius': '0 5px 5px 0', 'color': '#444444', 'background-color': '#CCCCCC', 'font-size': '1.2em', 'cursor': 'pointer', 'height': sidebar_height, 'padding-top': '1px', 'padding-left': '1px', 'margin-left': ssb_width_expanded - 12 }); sidebarbutton.hover( function () { $(this).css('background-color', dark_color); }, function () { $(this).css('background-color', light_color); } ); } function set_position_from_cookie() { if (!document.cookie) return; var items = document.cookie.split(';'); for(var k=0; k{{ _('Manual') }} {{ toctree() }} Back to Welcome scoop-0.7.1/doc/_template/indexcontent.html000066400000000000000000000001011240127670500207450ustar00rootroot00000000000000{% extends "defindex.html" %} {% block tables %} {% endblock %} scoop-0.7.1/doc/_template/indexsidebar.html000066400000000000000000000016121240127670500207140ustar00rootroot00000000000000

Manual

Other versions

Useful links

scoop-0.7.1/doc/_template/page.html000066400000000000000000000044101240127670500171660ustar00rootroot00000000000000{% extends "!page.html" %} {% block extrahead %} {{ super() }} {% if not embedded %}{% endif %} {% endblock %} {% block rootrellink %}
  • Project Homepage{{ reldelim1 }}
  • {{ shorttitle }}{{ reldelim1 }}
  • {% endblock %} {% block footer %} {% endblock %}scoop-0.7.1/doc/_themes/000077500000000000000000000000001240127670500150375ustar00rootroot00000000000000scoop-0.7.1/doc/_themes/pydoctheme/000077500000000000000000000000001240127670500172005ustar00rootroot00000000000000scoop-0.7.1/doc/_themes/pydoctheme/static/000077500000000000000000000000001240127670500204675ustar00rootroot00000000000000scoop-0.7.1/doc/_themes/pydoctheme/static/pydoctheme.css000066400000000000000000000051251240127670500233450ustar00rootroot00000000000000@import url("default.css"); body { background-color: white; margin-left: 1em; margin-right: 1em; } div.related { margin-bottom: 1.2em; padding: 0.5em 0; border-top: 1px solid #ccc; margin-top: 0.5em; } div.related a:hover { color: #0095C4; } div.related:first-child { border-top: 0; padding-top: 0; border-bottom: 1px solid #ccc; } div.sphinxsidebar { background-color: #eeeeee; border-radius: 5px; line-height: 130%; font-size: smaller; } div.sphinxsidebar h3, div.sphinxsidebar h4 { margin-top: 1.5em; } div.sphinxsidebarwrapper > h3:first-child { margin-top: 0.2em; } div.sphinxsidebarwrapper > ul > li > ul > li { margin-bottom: 0.4em; } div.sphinxsidebar a:hover { color: #0095C4; } div.sphinxsidebar input { font-family: 'Lucida Grande','Lucida Sans','DejaVu Sans',Arial,sans-serif; border: 1px solid #999999; font-size: smaller; border-radius: 3px; } div.sphinxsidebar input[type=text] { max-width: 150px; } div.body { padding: 0 0 0 1.2em; } div.body p { line-height: 140%; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { margin: 0; border: 0; padding: 0.3em 0; } div.body hr { border: 0; background-color: #ccc; height: 1px; } div.body pre { border-radius: 3px; border: 1px solid #ac9; } div.body div.admonition, div.body div.impl-detail { border-radius: 3px; } div.body div.impl-detail > p { margin: 0; } div.body div.seealso { border: 1px solid #dddd66; } div.body a { color: #00608f; } div.body a:visited { color: #30306f; } div.body a:hover { color: #00B0E4; } tt, pre { font-family: monospace, sans-serif; font-size: 96.5%; } div.body tt { border-radius: 3px; } div.body tt.descname { font-size: 120%; } div.body tt.xref, div.body a tt { font-weight: normal; } p.deprecated { border-radius: 3px; } table.docutils { border: 1px solid #ddd; min-width: 20%; border-radius: 3px; margin-top: 10px; margin-bottom: 10px; } table.docutils td, table.docutils th { border: 1px solid #ddd !important; border-radius: 3px; } table p, table li { text-align: left !important; } table.docutils th { background-color: #eee; padding: 0.3em 0.5em; } table.docutils td { background-color: white; padding: 0.3em 0.5em; } table.footnote, table.footnote td { border: 0 !important; } div.footer { line-height: 150%; margin-top: -2em; text-align: right; width: auto; margin-right: 10px; } div.footer a:hover { color: #0095C4; } scoop-0.7.1/doc/_themes/pydoctheme/theme.conf000066400000000000000000000011171240127670500211510ustar00rootroot00000000000000[theme] inherit = default stylesheet = pydoctheme.css pygments_style = sphinx [options] bodyfont = 'Lucida Grande', 'Lucida Sans', 'DejaVu Sans', Arial, sans-serif headfont = 'Lucida Grande', 'Lucida Sans', 'DejaVu Sans', Arial, sans-serif footerbgcolor = white footertextcolor = #555555 relbarbgcolor = white relbartextcolor = #666666 relbarlinkcolor = #444444 sidebarbgcolor = white sidebartextcolor = #444444 sidebarlinkcolor = #444444 bgcolor = white textcolor = #222222 linkcolor = #0090c0 visitedlinkcolor = #00608f headtextcolor = #1a1a1a headbgcolor = white headlinkcolor = #aaaaaa scoop-0.7.1/doc/api.rst000066400000000000000000000045251240127670500147240ustar00rootroot00000000000000API Reference ============= .. note: Please note that the timeout support of the current version of SCOOP is limited. Its full support has been scheduled in a future version. Futures module -------------- The following methods are part of the futures module. They can be accessed like so:: from scoop import futures results = futures.map(func, data) futureObject = futures.submit(func, arg) ... More informations are available in the :doc:`usage` document. .. automodule:: scoop.futures :members: Future class ------------ The :meth:`~scoop.futures.submit` function returns a :class:`~scoop._types.Future` object. This instance possesses the following methods. .. autoclass:: scoop._types.Future :members: .. _api-shared-module: Shared module ------------- This module provides the :meth:`~scoop.shared.setConst` and :meth:`~scoop.shared.getConst` functions allowing arbitrary object sharing between futures. These objects can only be defined once and cannot be modified once shared, hence the name constant. .. automodule:: scoop.shared :members: SCOOP Constants and objects --------------------------- The following objects are available to a program that was launched using SCOOP. .. note:: Please note that using these is considered as advanced usage. You should not rely on these for other purposes than debugging. ==================== ==================================================================== Constants Description ==================== ==================================================================== scoop.IS_ORIGIN Boolean value. True if current instance is the root worker. scoop.BROKER broker.broker.BrokerInfo namedtuple. Address, ports and hostname of the broker. scoop.DEBUG Boolean value. True if debug mode is enabled, false otherwise. scoop.IS_RUNNING Boolean value. True if SCOOP is currently running, false otherwise. scoop.worker 2-tuple. Unique identifier of the current instance in the pool. scoop.logger Logger object. Provides log formatting and redirection facilities. See the `official documentation `_ for more information on its usage. ==================== ==================================================================== scoop-0.7.1/doc/blu.diff000066400000000000000000000014421240127670500150300ustar00rootroot00000000000000# HG changeset patch # User Yannick Hold # Date 1391717846 18000 # jeu fév 06 15:17:26 2014 -0500 # Node ID b3804596c7e1e029afc7a22e4a6c3beb8384f38f # Parent fddcebab50d36f3db24300f18acaf99088da439e + Added 0.7 link in documentation diff -r fddcebab50d3 -r b3804596c7e1 doc/_template/indexsidebar.html --- a/doc/_template/indexsidebar.html mer fév 05 17:06:06 2014 -0500 +++ b/doc/_template/indexsidebar.html jeu fév 06 15:17:26 2014 -0500 @@ -9,6 +9,7 @@

    Other versions

    scoop-0.7.1/doc/conf.py000066400000000000000000000204031240127670500147110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # SCOOP documentation build configuration file, created by # sphinx-quickstart on Wed May 09 16:47:15 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # Mock system class Mock(object): def __init__(self, *args, **kwargs): pass def __call__(self, *args, **kwargs): return Mock() @classmethod def __getattr__(self, name): if name in ('__file__', '__path__'): return '/dev/null' elif name[0] == name[0].upper(): return type(name, (), {}) else: return Mock() MOCK_MODULES = ['greenlet', 'zmq'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.join(os.path.abspath('.'), "..")) import scoop # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_template'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'SCOOP' copyright = u'2013, Marc Parizeau, Olivier Gagnon, Marc-André Gardner, Yannick Hold-Geoffroy' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = scoop.__version__ # The full version, including alpha/beta/rc tags. release = scoop.__revision__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True autodoc_docstring_signature = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme = 'pydoctheme' html_theme_options = {'collapsiblesidebar': True} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "{project} {version} {release} documentation".format(**locals()) # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': 'indexsidebar.html', '**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = { # 'index': 'indexcontent.html', #} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'SCOOP-doc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'SCOOP.tex', u'SCOOP Documentation', u'Marc Parizeau, Olivier Gagnon, Marc-André Gardner, Yannick Hold-Geoffroy', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'scoop', u'SCOOP Documentation', [u'Marc Parizeau, Olivier Gagnon, Marc-André Gardner, Yannick Hold-Geoffroy'], 1) ] scoop-0.7.1/doc/contributing.rst000066400000000000000000000062541240127670500166630ustar00rootroot00000000000000Contributing ============ Reporting a bug --------------- You can report a bug on the `issue tracker `_ on google code or on the `mailing list `_. Retrieving the latest code -------------------------- You can check the latest sources with the command:: hg clone https://code.google.com/p/scoop/ Bear in mind that this development code may be partially broken or unfinished. To get a stable version of the code, update to a release tag using `hg update `. Coding guidelines ----------------- Most of those conventions are base on Python `PEP8 `_. *A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is most important.* Code layout +++++++++++ Same as PEP8. Imports +++++++ Standard library imports must be first, followed by SCOOP imports and finally custom modules. Each section should be separated by an empty line as such:: import system from scoop import futures import myModule Whitespace in Expressions and Statements ++++++++++++++++++++++++++++++++++++++++ Same as PEP8. Comments ++++++++ Same as PEP8 Documentation Strings +++++++++++++++++++++ Same as PEP8 Naming Conventions ++++++++++++++++++ - **Module**: lowercase convention. - **Class**: CapWords (upper camel case) convention (ie. AnExample). - **Function** / Procedure: mixedCase (lower camel case) convention. First word should be an action verb. - **Variable**: lower_case_with_underscores convention. Should be as short possible as. If a name already exists in the standard library, an underscore is appended to it. (ie. a custom `range` function could be called `range_`. A custom `type` function could be called `type_`.) Architecture ------------ Communication protocols +++++++++++++++++++++++ Here are the message types from the point of view of a broker. Message coming from workers are always from their Task socket. ============ ====== ================== ==================== Message name Socket Arguments Description INIT Task Handshake from a worker: allows a broker to recognize a new worker and propagate the currently shared variables. CONNECT Task Addresses Notify a broker of the existence of other brokers. REQUEST Task Worker requesting task(s). TASK Task Task A task (future) to be executed. REPLY Task* Task, Destination The result of a task to be sent to its parent. Communicated directly between workers if possible. SHUTDOWN Info Request a shutdown of the entire worker pool. VARIABLE Info Key, Value, Source A worker requested the share of a variable. The broker propagates it to its fellow workers. TASKEND Info askResult, groupID A collaborative task (scan, reduce, etc.) have ended, memory can be freed on workers. BROKER_INFO Info Propagate information about other brokers to workers. ============ ====== ================== ==================== scoop-0.7.1/doc/examples.rst000066400000000000000000000144341240127670500157710ustar00rootroot00000000000000Examples ======== You can find the examples detailed on this page and more in the |exampleDirectory|_ directory of SCOOP. .. |exampleDirectory| replace:: :file:`examples/` .. _exampleDirectory: https://code.google.com/p/scoop/source/browse/examples/ Please check the :doc:`api` for any implentation detail of the proposed functions. Introduction to the :meth:`~scoop.futures.map` function ------------------------------------------------------- A core concept of task-based parallelism as presented in SCOOP is the map. An introductory example to map working is presented in |map_doc_file|_. .. |map_doc_file| replace:: :file:`examples/map_doc.py` .. _map_doc_file: https://code.google.com/p/scoop/source/browse/examples/map_doc.py .. literalinclude:: ../examples/map_doc.py :lines: 21- :linenos: Line `1` allows Python 2 users to have a print function compatible with Python 3. On line `2`, SCOOP is imported. On line `4-5`, the function that will be mapped is declared. The condition on line `7` is a safety barrier that prevents the main program to be executed on every workers. It ensures that the map is issued only by one worker, the root. The :meth:`~scoop.futures.map` function is located on line `8`. It launches the `helloWorld` function 16 times, each time with a different argument value selected from the `range(16)` argument. This method is compatible with the standard Python |map()|_ function and thus can be seamlessly interchanged without modifying its arguments. .. |map()| replace:: *map()* .. _map(): http://docs.python.org/library/functions.html#map The example then prints the return values of every calls on line `9`. You can launch this program using :program:`python -m scoop`. The output should look like this:: ~/scoop/examples$ python -m scoop -n 8 map_doc.py Hello World from Future #0 Hello World from Future #1 Hello World from Future #2 [...] .. note:: Results of a map are always ordered even if their computation was made asynchronously on multiple computers. .. note:: You can toy around with the previous example by changing the second parameter of the :meth:`~scoop.futures.map` function. Is it working with string arrays, pure strings or other variable types? Computation of :math:`\pi` -------------------------- A `Monte-Carlo method `_ to calculate :math:`\pi` using SCOOP to parallelize its computation is found in |piCalcFile|_. You should familiarize yourself with `Monte-Carlo methods `_ before going forth with this example. .. figure:: images/monteCarloPiExample.gif :align: right :height: 300px :width: 300px :figwidth: 300px :alt: Monte Carlo computation of Pi. Image from `Wikipedia `_ made by `CaitlinJo `_ that shows the Monte Carlo computation of :math:`\pi`. .. |piCalcFile| replace:: :file:`examples/pi_calc.py` .. _piCalcFile: https://code.google.com/p/scoop/source/browse/examples/pi_calc.py First, we need to import the needed functions as such: .. literalinclude:: ../examples/pi_calc_doc.py :lines: 22-24 :linenos: The `Monte-Carlo method `_ is then defined. It spawns two pseudo-random numbers that are fed to the `hypot `_ function which calculates the hypotenuse of its parameters. This step computes the `Pythagorean equation `_ (:math:`\sqrt{x^2+y^2}`) of the given parameters to find the distance from the origin (0,0) to the randomly placed point (which X and Y values were generated from the two pseudo-random values). Then, the result is compared to one to evaluate if this point is inside or outside the `unit disk `_. If it is inside (have a distance from the origin lesser than one), a value of one is produced (red dots in the figure), otherwise the value is zero (blue dots in the figure). The experiment is repeated ``tries`` number of times with new random values. The function returns the number times a pseudo-randomly generated point fell inside the `unit disk `_ for a given number of tries. .. TODO: don't restart line numbering .. literalinclude:: ../examples/pi_calc_doc.py :lines: 26-27 :linenos: One way to obtain a more precise result with a `Monte-Carlo method `_ is to perform the method multiple times. The following function executes repeatedly the previous function to gain more precision. These calls are handled by SCOOP using it's :meth:`~scoop.futures.map` function. The results, that is the number of times a random distribution over a 1x1 square hits the `unit disk `_ over a given number of tries, are then summed and divided by the total of tries. Since we only covered the upper right quadrant of the `unit disk `_ because both parameters are positive in a cartesian map, the result must be multiplied by 4 to get the relation between area and circumference, namely :math:`\pi`. .. literalinclude:: ../examples/pi_calc_doc.py :lines: 29-31 :linenos: As :ref:`previously stated `, you `must` wrap your code with a test for the __main__ name. .. literalinclude:: ../examples/pi_calc_doc.py :lines: 33-34 :linenos: You can now run your code using the command :program:`python -m scoop`. Sharing Constant ---------------- One usage of shared constants is to halt a computation when a worker has found a solution such as a brute forcing example. .. literalinclude:: ../examples/shared_example_doc.py :lines: 22- :linenos: Overall example --------------- The |fullTreeFile|_ example holds a wrap-up of available SCOOP functionnalities. It notably shows that SCOOP is capable of handling twisted and complex hierarchical requirements. .. |fullTreeFile| replace:: :file:`examples/fullTree.py` .. _fullTreeFile: https://code.google.com/p/scoop/source/browse/examples/fullTree.py Getting acquainted with the previous examples is fairly enough to use SCOOP, no need to dive into this complicated example. scoop-0.7.1/doc/images/000077500000000000000000000000001240127670500146605ustar00rootroot00000000000000scoop-0.7.1/doc/images/architecture.png000066400000000000000000000272531240127670500200610ustar00rootroot00000000000000PNG  IHDR+`StEXtSoftwareAdobe ImageReadyqe<.MIDATxMIv߃3zgZ ,,92@k=6t@7 'fuـbq/f=Xlax ^ lBQꮊ(4Q/)0WH>.o?|BN__u]=&m0v;@/xX2ɵ1|L.]_ SWXT*#8j<(C*`:)+ѻ""e[#8d`uNE+ V 8÷Nv~]uco}S@xWDDy:Y556\NJ]<+)T@Ν8BpBjBhJN/* u9gRP|̿ 1 ߓA.4N;mb3 g'{oE0b]um}쎜sŌ;*\S;BXP+X }nNK;s3{O.q `F9 xG2ܐc6}ơxKbIp@CҜmT{"Bz}]غIб¨}WX9vCqCy\ڮ/mB(ˆK %L?s9sѓ7.yxwFLKE$}޿bOhL6~s,v}t:y.=1ѫV#y^Ya%Oal;jDsVP :u˙.d;NE+JFa\Qz_pdRA9?m&lIn;r"q6v~c眣?PP$+TǺφ|L Z4b.sTS(!-vƎ"2tC6tfPPɳ2>2-DZeIsԦVSu: |=lϝ`13ICy3eJmy;*uF\Zg*Cu^ʮJ] a;iHWbey%^܌XZ")"LnH$3Uϻ 9>q5gȸgT&lyt8FhN؈-qAZ4k$6b#džZqF;dzܕcg4gawl4.WͦwqkcL @f5g|IT婪/4 _SCV,\_M]sC+u:zKt!wU=ލDlbakXo/oV,TX0U6q7Owo* %wlbϾ:zD 0T k 1Je~qO q*!PW%S~onQHX'xV`_Q} Qk]) »XnxY?T?Lg"- Ve ey&3)ooNOX9$.*ƫRwϿH {OXDJH EҼ.߾PڥTWY [ |`/orS'2+)CzELy\hjkGuB@0jGNMAԑx{>JBG+k@+9F`֍1^@װ[d)27~׈ @'g>fIugIX8Xo sU زhǫbA@,Ny]"{KPwcD`P@,ޕ."^RUa+d]+3*X@V%L| 9]B6FO2h"uIhHE Fpyp1Q%Ty}+DL5N&XGn4UJ)D@0`˧Gz\<-n˿]0"xO󌸈c7{9>aDǵ9B\=>F"c&-:, q;SxUcYCLdjwͭPӊ0X(27Ċ"pzsn3Pa2< Vf(,tZ'V7"}Qr޼U}$6"IxEg #Kek}h, AɔPp*V$fcdƄH6Vh$xVrz10BU !Ұ=>[1\7vU @zCYUe|;%z꥿3r4s̜iqzW+5wŬ&Z|,{LDLy%G](AkXb^bR 0ڲ@_w+Kl7J|_Ts^NH1+}^9ʜE(N7ǜ?o營ft!cnKdyEU'_!V Y#ޕ-xUFL}z 8dWX@-lX ߡ +UIh2>y^ VZjcYuP;+zUح+!w%%!|Dׁ#++V@ȾA+#dr`] ,|{:xU+!V`.UU6s,)?w%e98+9 ȹzUX@jxN" /mX9 +"7WD>FZSVgf0 X@1swdhGᒛ/;XV}R@#%r]J\c2hɴh9HՆ^@4!xWwƻ"Aۄ K;%B bwEQ'^CIw}Ur.0tFyyBem y[5lЊ1]: | B%H{)5v@2 $[.&wqD+Xf*Xm]; T<4 x^Q}tD-VA'`B_6*~ٷf,2ƳӓO& rg*ߟ V\& ڏ[;H>hޙp3ύw!K)|\]x=39Bv<y@A'ng9 18Nzo)IzWdg[q2Td)I:טWnV>+C]n}+s-\:+m);OЋI#ڨúxtqQhMe5;+a2њ'"e$Bedfyv XghODϸ-qXz^r8Ѝm *XN2oJ 1xۄ]}7]ߩ~FXJ=qۚ/HL';Yq'ZEd"w^Z T~ϻ.h+goo|reB,y޹\}w eN* +H&օoBR2@w?kO҆שFd^@@]Ȫ k+!4*)KeKY6ݫr3NH5#Q˲7V q=`RwFg*i({{{O75<~-d}Kƹ3 xF['_F{U²Q1{ cief]K']u1 =.ܭt[٥CiNz~ oyEoÙ<ɲ_JhMaM;Hy7=is!be o결B]M[{N&&k-\ضGS{ʄpNKڷX;@ƕ:"{(|sB79~7[;pܑsGrM&iYS)zj|ŭ˿1mQtYFJ?w|]&c伭_޽mƞ"Fr7CKV<z&{6 EEHE#眉}*F̼r3:}+2cPyW X9v9פm,rїZ(~CZ4}<+&u=Xnl^葼,hg=Wuɽ+J0vo{6 .P }Ȕ/'ӗ[_P`6_~_SbEڃʝ8Wg>nڔ*R$# u:(˿K_cÇDV8#ydIi'99>AN2-%ψ2yFT3!O/|_TJqK%essOʅ`{j3mep[?# ͆xOCN^F FpN;]j6v[B2?vڞʶS=?2 M*8 DҒ:iI^!T6M+JB0\8Ǟ wSW񭑑wK%n[RϾzb[ЇYtp|u>y%`v73oԩ |(bGŠ;ld3/)B 6u)q+HiSrI:αe"ᵒ8DXN9󌾈szKH"8$Vo^HFUqA sDkOV[&CO(YXV 7-s;}"H9;OR[Wxb&e!+Bݼy*#f|MQ[™8QKLJRM#rQyR(KNz ~3BI CAN@ğ~q~Ӊ}cŠAI0,ŹB:WJ(iSMΩS%__YdJiۑxy(QRh:ht]ɮ`ѡՅĉ3:eJNV8ɐȹ_\3U79\X>{rqKN ڳ"0()4;%)+Wɘy!S(J,B螕ʥ2ux5 {w"yƼX/0iXkjW9˲\sn"Sk.D"^9\ d'"E&Ne1H-Rd ܯ/.N~a!iW G@sxI}LhHZ{_=^/yG aTRGv+qD G,q})yD}l:'<J׉OU9;mɛgϐ4.SCrƋoy=AXķ)˿rv>夢+;:w=%(O!Hit.\W>,d,4plQQZ}*(4t Qo ?}^a7u-w_fII˵Xy7\4 xݥ- |qG+\miLZ';_O<ĹD t[^wZރuIӼ+ (iXO ul !ޕ T5۠V>C]}G㚆+}ܡJ(6CjXi`9RObixU"O׳ʾ`Q{R&*XBQ sU+P?PrL -Od*PYA"Viji8lM* +P'2` PѦ=}gfHy8Ro/B,.#6|׼Hf39jb VuBe4p RGP-UPw%r`16ڨv-AsfZ8c*+($"+e&+\?a+~Noy%rʻeߖfޛQ C?JK@z05z֫mP|gWEɄlDFip./yoĪnVԴwߔfjm*T]׈Mt+cF^}m UIP"\u_ Ƌ2y]*{:Z39oΆ>fm?k5^kjs썔 ȧ[dqHȅj\޴>^Pbr6{C@;1Q0_ك2ѸW tqp^ylv$mBzYr,wϗ^c..^'kg(@tLzLB}*ކ5UCaW [&MR鶢F Vo,Vב]yYbd|qȞBٙDX+ISA&vM CpC Q~lb&nHZ6bͬ~1Z)TZ+ю:W ː6. D V{fW4!p.Y[V`Hކy75v)o VGz1 LIfICmw- Vڇo*&˩`y I[ƛb}^oK8,AY1`a_7([v5%8-,8<+R늶 5+E"l5[. *nH:~^ I|l*&"R:[)+arGʵ.fw6b9g2eD- iL"`z%SGCJC tT~c&`kRBfQ8d2Hy&\& FxU|7?K@ S2?h-2P~?SFpXP}uP8@@Gfq@C tP78@@ˑeyyFB7b#cBh)ң,r6DL Vw_*8@@ːW(#+-0E#+C39|Hb īlYl ߹+C+X_2#&+P?,[V  @ _  A+ +lbHǫ2UWXh =FB ! VxU"KߝTNC@*[UFBC@4!=/ǫMXhu,CC@?1^+[wJW(,8 `! b 0'k`Y0 k\M#Z U!`!@ b P X8q}6CC@@h58 @ͬ0##X6Z@`G,bCVb{KyUfbAC@Ԁo/ë]X2J!l؂+3 l @,Ǟ3WR ,+8X$[9~ V6n, b`3x:#NX1!mp$`!z p'! V*o.%BC@3UUX ! VhTW`8 ǥS =!.-(9#LKwW^Q_2LuCZrW}c䫟(}eXV+3sQ/r,v7շ_|C}o]tzD Ċ(>R?_S/z~x!9 Vq"%U;{_-߭,An,,X>4_?/~#2ShZ\։yӲT"«Xɶ=DϾ~ݏ26`+P1;8_RP_=e'X:_Nԏ'O|S2Z`5C[)b\BO_d:C?7)T fж-+P1⡷gR'o~q3uhKX `r b\\ x}5^ åڒG!\hL}t 0 d5>%x W@Tt@R:-7y#&bIPZ __Z/- (IƽfQ_#+Xi0{{{7S7Zչ6c)jf[pZ_ ~I0t ب!m5zO6ɂ;߿ }E[Gb @sK+6]U9 =O|ƎE6EҁXȰqn2]Hn8HX٭s}m9f0zε~C:E?V=C 9^p-7~%1ދnP'6:)+b'%{~E}7R߲]-HW.16#ccsSɣW('0 t\ɥ%rlPar "U82Pu-|̿SZ A-ȤKb+Gvf*Vhڋ$TvJX+چ;Uge 7LtVL-x)޷[B +:9Ac sQg!wsb 2;Y !bkcεG0;+ s]Fr21m962^z} YYc.N/D ĥkϛWCQgMQCHкH쳵.Ҡ::qvl7BϊB?IcT*(W"s."uK[<D/O& Xl@DUU{dIɾ/b'! HAEf~vHǝb $(\}tE EdڪaQi8i,V7qin:w2{~$<Ƌ1qhNWޒiM4(^}kI_"L[V~;jU0 J,&Q0JZBbǎml%l;-(-xSI&Ċ!-\]ʐK!}zSdyHFDs gwv[! b v4˶;xv&y$zeq?LY_esJ} Yi7RCV,U_ 1õX qM0ϾXX_Xbj5wv]6mI}u[X Wfx6|OX#xV`]=6cENRA__Su2Gk l׶#+YVpFWөHe2/bel,F/l^L}>XC=:RϏO3U_M`~E/%1p(!)ٸ|{o!nSll@ '$H8 UJEK1$Tj y|@jC;|^QCf@Mut䗥Ls|@>QbD*$ tuۨք8@/! |ҝ~⭢x=TfG"Doxglc{<$βrEQpu0]=jU:KlDնcSD/s±ny#r]hʉ>C**zd]h:eFUD(8۝[}¼ gEB `@'蝞aDՍ@D\7x%wu\wL776f' 0D (%.Lh+,񽢐 1{1 ~5bg nJj%8$#FI`?"̣Cryʉ(8@73SBU+(U]cDL?(8h(8 oJp7xak+T|Hi\@vnwFiJUλr~@sqmcQc>̷>DIyjS"}ϥ=(8+'{lJ{X$Րڥ( 61zPf@D Q݆xq ]᥊ ma~2E7D]U>s_)`revwIxU4SZ [đ`i'PtMKHG`j v2_s;}3:goT3wyQ}!YNAb|@/;=d S{RhD /EFCrAM0~ <xۘ[CfQx}-f-Uw>řan %! v䪜sҨh}_oVƸY(' >b܀"PJo!Uqo%Xv~w52\f\ݰIQ8AD>5- JW! q7XwlRek;ÞML|RajOlw>[1;QpD4R{֑#!a>XΝQ)`5Tn! 7,a7hY7*+DZNoeFB"'Q`HǥtHGp}(ʵ 7oxo5DNoFq]?QrDnD(`ن-j}^r*(8 .+EbzxF(eh_ !0B w CCn>WQN_=C(! /V94 oU76]fv6f' 7@;SB=o7qM](|} xV*YBi%~wƭLQpO C )!#w`x/n()t(8" Ûhap*Mkku|DWTb%*;YW#!a+܆Ɩ(8 ěޑۅw'h&O'l9@oڌpȶ,' ҷ, Q(*1+=}FO ]l%xG>/6}T=nB;^?1{ MlX~f̌^f ۦ;cj>v o%YvZ /t+lReW%Qpr'ު`7#t03va/^Xx!; j4CPFE!D#V}OI-~;ˉ_1+| Ga:!T\UD߆xJv}Flq3ic <(.{=o Hj'xˉෳo2["NZ 2\4j `r{sKCorzx#,KyKQp57}Έҭ>AоN CG"/|[m7VBZ&Qwؼ1#|@/L  p}l [/zW܅7Uo*!! zxCTV4;N^2qx[<0>a/ ~ Lt@p lg=g^aH(\Nߗ2~=~棝.er7@UoC q8NrGGssN<6ͻJć|DoO;#JE!.DŽ*6(Α#ܩ^zi /UHo(8 xKN@lw.?lvҧf٩'N<ђ( +)gr~ڐp7#bMot7XS_fLMAL|p_Γ톀ؘ(8 PxV=6R|貶+ƥz.2n8QpΙD3rn`_*Vfrkݽc!ًV""J7=!.k>l~nbGI;͗sgQ>W½yT Wus7M"EOfy[{oڥ* )D_ΣK^#`w ( Pj"8tl56^B|~+^ӧpΧFHrʣUV))ۥF]( .w~DҶ+͢f<71knUQp5FB3(?7 w"ݥP]>ƛ'J@. ƀ!;xCݎx+Ԁ:J͞[^[1lq=.0n"'&][N{|*9I[[r\ƛЋÛXUr^ݴ-oK\Ϲ;2(\KٽȸZUm:D Jp F!F⽥\VLeVomc r#\}Mj[Il:D;Emx* qE!rR˭߯Lb Xˏm/( NjĻ5Zޅ|@eWUь7xrEK'|;- q[_m9c\nn؉T~k?fgFU_(eiE!"Hߎ|@?,, @҈Qx#'V)m.{Cs̞{yzٗ07( /; ~UK5j#tH]UJ*%|+1?=! /*L7Y.Ӑfp[i?8/+ ^̅l5PfTL .)VXaV-ݏxCL)V^%j8Z׏ۃQNjPUj<)si&B*=j'[hvokI4űow,&I ^ގv943sPE͞[ TRŸ[&`21;&~v$kj:Mu" *nn` 7)pK[5>{Dh\ )F*#d WgD==mu+V>ջV |@;=IDRwM"upK\B!"*:=.8o~_OLM%SW^VDMBE"PxXx׈T8dM[鬥X;T<_]׵ԋ#3MT1f@-6Kx> wԼM-C7 wIU'j`,rN&|naG^g3 \曫ʺu{OٓJ(7xAJ 8vR/jڒKB/q8jW c53-| BÕ?'gG6y鹅R/A7KamJߔHSKDmSs53)C}aoNX2Я9`1ιAn1N6'gN沖< ׸ʼn75yV͉d}k)͌-ouW<Ωlv*]He[@HA]#l\DBep"-jRBBU:&3'xk;b^}/Er6 `KuO,gˍ/kXyQ"ěԕLdOLIYq>HE珙=~Cu|:ߗiXyͦF=վ*M^p)\.;:6#槽X.45~w>ixؙ(x2h8kc lFN9iLԕH*VbaΫGzEWo6!$\V!۾D|PԊzyuc A3|^!ׯec3_ Av o;T 7G!g)gy[#>^jހ#vV=[]AM~dyỦ#{G_{n\G "ު|*2>*r~o:ĀQQA\|(H6&j騗-55Wխi^d;QbfGw.Ґm&|5yߊ dKϻ=צ ])Q@mZP~dK; ދa7}'ڜhhDj5|/;ba>XU6RmVo1l6[tUۍ|<^Z5U]t|ݹz}͕uuFq&`#~o\x'j /o#/x*{y=Vŗ|Cu;c0 9pSQ|xK!wXS_klzsTM#eX `B-El_>Ȏʢ U|y^c, jx7giMnFK[d칑ɟY)݃˧:Έc7G!#/J`P&vĖBgE+֚=>NQ/LFSDyMMh $DQ('JQ{9^ r \.k$/'ǀ\Ԟmܫh/MOZX3GK!AJ喃>h/@lvW|M͉_;\0)TbȋYmQhyhf@߿zV7߹ nE". \]?Ԯkr* gZX ȷ>wQ剼x?M!|畚Bcssqټj`F|PwTߨE7#ޛx7[ڮ:M'rcD2ed^5y0Bezwx3%]S_Vm=wdL~arY] Tpo.˯G`U*z#Pxo2)*2=:^+?#ߜyZ"ݴ5gV|uך_fo,b[o /WhQ*}Mt.7?sŗu RwUBwFNfϿ {n^Di+ܔ4tϋiT,!ɬerW=>$6֮Q8oRTijV웚zAuA1{NP7/7T%鏿ځ]k~KbN)'G Np[&X&(R;>:MJ+92;kMMT Vh/7R^?_᢫}Ui37T-ޗ_~E1Vi(;VI>\ɳb>08[ \m|u bS鵿p_sTnrMɉWZRR|0*rɃGBȪPF: r5ِWw+XZ0{}([jͧ(aY}ft]Wȶ^!ne]*]i}bY_ohˇuh]#[PƖ}&#ފ^V:F|#K]$%GLiDr92`BAENTFgk ;0oUT6;/W(llϕX/r#"_7c_ƀ^'ތ7wFQl.o5iͼG5\*֧Kl띐rjަ5UӪr=wT[RW|S}ͷϯ[a!bVNOD]oD߼^+Q|ACW-R^E|M%'Nr-^oDJ_)TmUe^/̞K&כG+* »\pik@m%xuBlbޡK\vxK[9\rfS|V+.Ϋy묖KRZ45ZdvNBydvfDI{V}m,w$*7@{bQ;-G4J2Z7ŗ|o}Yg=^ScZhyd%?0=}^J4Rg7[ Һ(3#S|P| _om_kyET屹~SyeԟlmuZIj"񶓧n!N],KʷIˢR_,KoX&S'ă(7L­ӏ{/i=W @ m`5`9˦.r*ogcZw/?UQ9qװB tZ޲eOUrzal[|~B%D D[U[>ei4rK^EF,֯f* @\*□-U^!gOA)1[Uҕ薱΋)qLMt/7р| Ԍ˭܎|7 q%;\:qgqN Px_x,ܾqs_G/!b{-n{ /w 11zv]N~ŗ3<-z A  Pxgu7xhEL'.|VNxu*o x}N[<:r(#ShiFohR~.d"E̪Ӎ(ŗ4]< _ =$#ǻ\❶eOB#.Yvt_/ŗr|wKnR@E#H.Y GFp/!:JmP0~A}A7A6([ȓB7@/7UK @lK!czOL -<  n֩#¿B"dA( B[/!< Gl;|B7@|{X]Em3Foa_R| !|3mC  1n?p JἣQW<=BK1L  ugfP.A4>6ϩ% o0wBˠ兎Zlk?nAPJ#)B!oP&aS__B0oBrL-[Wz*Q50B.._BӁI# À|D\A.BQ| ;),u)l%@oE|F:G"6c^x(I[<7 #Z"aUk%Ml%@-t4g8Rږ9z [Q| 7V#oږA{0idA(Lc+ ୼'HdE1(j[F%8yN*(tZ  'N[̲'mH_:鮠,m @7;)5 ?tCW7r-?9OSG|sbj~V==]h3c}N;&__B5m[tfB?_~gr43m,;ŗPj@ À|/0ץn[KmCs̞剣D!O^LWFl.#_B5á0 \%^Hw m+3OMA!ŘaoMe"KML]W -v 9 qցl<ͷQy#fPGȗqm!.erk5I9=/%5*V6؁7 ޕu~ ^.㭊x# "/W _&/ڒ:j:QǟzOwq$;TcxHtEE%9~oTMON~ʷߟ_4Ø[>gE-d8gr^6۷ 3ҭto<ѲbMjfϽ8)1;iHץ?Ȏg,đyݣB8: ^}z*+F#rz j|Ou =V xo=kx+^{9>=r}.fϼ!ڞ*Rk?4=;&ug t%ߕ?"⮋*Tyk~܄! A\MUQqvoOfcӧ=-6/* ]'c<:60?81 X.+[xe#?Wbߡ̋wHԤU$"ΙV_uCE]S}XvW嗥x_SϾ2T3l[^f$aF&޷{"ޅAtunmh^lZhy~{A; |K9nS?9𛇎4k6OM-! ^VY1~(T+Q\vXnep_RQ(uK+Tvr |A cΉw낲ts#cG9q7A o)*mO;Mo*Dqyn9wspDosS&9Ѱ칱/GD׏Ml.7鯍DzeR|-ރ(I;"ޅAYMm{vJ;o)7Tlva❬mJ&jHV^8G'Iٓؖ3mVmn\مW油,MK$ |XUI>緯Ҷws.}JoB6COcn~uM+^p([[Ģ([HFGr3cb~lt&6n wxv~4Ԝ n'x(Hʠ)'s3ǏM1? S.LV];a۝HMgfkRugD.ŗ ~m÷p&)Ct\DCkm"ekW| Ne1v}(V|v*\z&١qPG97EF7t4֛pҌBWNxĉӦdoPJM9]~xQ\.t񥙀g:lvc%M٘-̜MV"k~xvf=43Ksglpr17S|-ޛ<l&}^JӨB6;Wy\_%φt+V?l.`zK$R&UC(k{sZ|yъx ,B˶d9FVAS]@ s|n׊d~e]jKwoxgua]ሷlTM튦6Ӽ#c+MpU1o;l7uQf|MB .юx;:w\|#jxnflm{#|ZK|3#m¡K=OՖGf仝z9}9/i;:z -DzGO'ZʥrUBKGrUkv*9>Bk8w%n̿GG `upwx_BU{MmBg|Z4('^]euc2/sz'$<剖)̞;=y؅n;pdnL6kZRR.׍VA+ƍ~-> .GU?_Vxyz|hiY~N*4Yr߉#hNwYoyRnjZI:ICeWmt=|o׻t$ͣ3)1;G2 ʌԹhU7DѲs{l~KӇ **e]rӔSMp1RN֖%YG]8:xj20cڶ,_|Yȗȷ_E]Scq,;~إ%@_g=WL6MsZݢ |iՈ|{r' kkiFR .\D - #UM^Et*5쨜EJ=q:ϕ.9 'tŗ5/=^l=[?<Ƕ (ƛ߯2.~2{˕~}P71ojaEV=?u`oFֵ,7VQO/K:]e3I*ל0o/8LؓnhO4-(|W#]nfQ}jR+T u67stnE Y\*o$Uw,P#Z -g'RMȏ2U_$ 4]dyE;+gIk/ymvمW m|/ð7ɡr;9K ~>ly"XSW*HMxBD.Q/NQi(l` B7&64Xbm[آxiUV>G] -9E~"FsIM%Ƕ_jN5lWUտEz(o -mTmP"mX}mw: .^x򃘋kEB":7S"_yM~N?Vصkצ ?v;Z2 ׊Vȶ{W D^/v~۷ 17:'ܩ#%ݕ"V; p Zɼ_븸|[Lj/ ~[Q%7= [юEMCt[n%]G -ԉ[W@\P8f ϑopC?CrP]4%Jn.;oY\wzf- ͺuTSwRNjY6NIŕFZ%\o"xEcc_|=ug.XQkS3-xLl_{USv:8cx](7dlؖ-V8kHBK,ln<*D?U>{> WŻȆƵ΍7ҽ~|arz<b>݊s'#.]^^?֛)Esa+hg,\;A~ ShY^ֻye8~B0x${P|yQuo)V?ZӜjL9rd>s{gqqttr8xԝ>w6Q]+6{mw:8^*eeOz^0| $F(ه%XCWShsB*-"8n/-78)?/747"<(|Q-sǓ;DޝFH;s~mO>7v -=1ROTvYH Tpu~E%<oߝy2Z-O rRǎ'|s)w uJC?]NR}G-wD:wN!* Av=*E;+Dw=[R|&kj(ߋɂH%RXU[RGݹ+\Ф !BerBK*ޢ.%R\x#TQ-Sɤi d[]2_WzARφ~:Ŷp|Av -z["+Mhz;v' Z >wWmr`E2'[2XgrnaӋ{]¼[N׈B,?9 a'S7|M۝wEy-+tٓ2-VIk)D>v'7 EzVh}ljQS5^w<9b܌xJj#!9}'ĸP?=_ Cto Yu7LfKH60{|*736+g@bN]lFȜpYJ4&QӆxQxO /¥M]NO(ywmW'Q਀(tq ;*\W֯Xկ-_[Nα|Bq{s[R&rDܴiڊ! ]^H ߛ7j*rZcO/կ)I-A!}.Vt^ pJBJilb[=nV:8,#rGC1m lws X͝G'J9Q )ݻqDo]#)઀ts%/_bI9DO8*yVb&)b -Ĭ鱤Q"ݍikɫ(@!~"=DԌ R3>/*KUH}칃MEڃ'7@+V7_܌zb{Xv0+&MNLQUh[<# Qmlh"$0trw v`L7bAUB9k;LYQ-u.w}бguBˮ8iQh[nMhAuR7ѕϖZچf0t]IK{;y+V-Yu^oxo!ުQ';5g"q2|TN. 񘲪ʹղTTR EVtE_@u?[鹳# 7T(ޛb]⢮"'hGOp%* ~FfݨǧK>)'>Q_Y]l*߯. ]ǓRyu*RLWwN"R o@#B[i!+êMJ/9kguCJ<7DPU~-R[XE6zĒ 08RO-\LF#rݐoCh}ANNhh,}yDW[hɖ@!Dx8l 2|Z[(\(Jp-DxY>=R(#ǻ9 bxZiQJp'rzȆxsao}#+Y7"beo/ -Kp"ޢcCjaH 2-'des` wWPXFILq̡1◈/>^q?q#Қǫ)𼊄 PV.#B<]BxO"dTqI:qwCZܿl2䵔xw?ndDs7.UOvI*Gh(8 ވN3L*bVM{Lчw\Ż ?ּÙ(Z~*M_p!ޙ"oE;S> ]K柞w9qiZ.sǷ `نxʓ;ւ[x\ ȁ&b)vy|W7mzRV}}[ԫd!NE }JZnZSNN_Q;Bo{loE+.Qo] a|h\n*Ų[p-|jXrt\/ ߈= x+5\3H\ ʾJV%K%gZZy%^.>/-|Щ[?7sn'{e C tcoù^,$TzxxBRQj[թCD o/4j"$aN'ƾtcHy|wj{'GVlc{^WJwݭ;_݈"Z&*ḦCIa?+y#+d<o{G,#=RyR |=x]z[]I;Ļ2 :aud!7v0U##o+,t7~aVtjD|n|["ø@/Kά ws5kFղmJH뎱J -m.E7mݍ m?⻸o(] E7]8Rhvpk Qp64yr|kH_ -w>φxl!'"˽Xh| :eDMa[1H7Dﰊw:n]BﭓGA!`0|z0x2h7PD(ptGTc)F"ߺY7G% ޖǮ5Ԇ AOhu'#)t?u֦x9S MWj,>tlM~_3|\j6D?oB߽s|Qpʷg%WJ96S}OBDhǪl0ą;>hG.{:Aº5|m$<;/QpɷGv0;L!W;(>&)8gFX$'sb%-Cw$ Pw;Q-D!|QQ}[Ļ2:$^\7Xpq) vmvw(k[ {Du%N< >`ViS D:f-D"Vp?)O-mԦ5O92ϫp%rKQt ȶP))*ci'RxQ5gvFMKi y[ZFH mDw]K|@ |U.a(DKqѤxgD!n xiJFG^ R1(*Zݭ!6~V H9yE|fB"g)fıبE =|L*+ږTO>C\|Ahn\a6[:W+Q\[E@uA?{ϪK/^{e^ǧ_;rKa Х:sT.K'x76u^mMJx!3xܥz_.y#ϼgCcKbE]8Y4j*߁J9Y,7 wp7; B[ 5E*rq.GΛLes]kLǭ"h%wŻG }q8#OG.3^vpzYQnێ7i'c :i<;no503Vo[x*YS]8%f[ z?pI x-exHΟ[m| [](Jx+-o3{񧧲 z$qvva^F|˦{ _B@$=>xuڧ R=r϶Ợt#&ӑ ީd]Ss7WoQI6`UQHɤi>uډv,D߃zA cSNlTN?L[+J;%ޅ>B4v83GEnƻ?-IG 0w`?h]tǏ<3>oQFE,"\XO{ou̩sjk 3~6ҏ< 蘋y:D"@wMxO^:^=>p׭uI6S cUm, Ψ*EDow;,3znMPt@`>r9$ Q͹\4@]_\.B5(S-3mM峮eKJkJB\5\Ή/ 1j SOG%W}ѫKkD6)J/x-sT4WdzPQq߲U^\pU|PPWPwI]yT*#4E644_RSsFo :x@lu{['-;MENg%OʡW\v?pUo4nXwVsi'M FU"H%RJjU*TJFI Uc$Rbeٞoww.̜ݝ˹/~*@>&"fNi5˴9i'wΧ'v)' P`/2`]53Me(Mi&ǙU[{EIPto8bĻUkJiKl14nZ%Ӝv9}6z]Jfم7vzyk. ɶt.rxSw<_n3on٢כB sA.KSk閭q4$w AWu}xg[6 Yɻӣ5.eS(Rotףk,o05vIإTm^^jʷWnǩ|e6W) 鹨AOY\wz>O(MbeS\ m.xwt|kVS4 ތ)4@FG|/lTAיݳXT,3 tDu{ %xK+lN%8"k5вvsR*i=1{CݪNCoN']67N#64 hm,Mevvkk=bSNz(7\~=N|hfJ `j._6o vaJkT6JO)H0ƭ:! *]axJt8AZ-AZ;Si^TJfzy4Y mRN*(eQmf\>S;VD@ۜgs\ W \o@K;־R!J(8|VY~#*5b;MԻ /Dy|ğf6)'Y7ネ~nƓȷ"[(YGK[|EKn\݄hiEp!w(&&\5MO+(^d)݉B,'mrTUbFqQ`F>YP9rȗ m>ަ7 s'ga=t $ $'fm7.\n hF3>W.iPeբfz%ӑa֔Y1c ɾ )Đ7(.3ݮY& T1YMzfU9FEGyII Ź P8R i7?)jVEBGNfV˧MZ62LV_g\A@ä;'͞{dT(~-∺&G2@7"t>^̋|{op~ 8n/}$Ɣ^JHkhl?G7;yq$>z#\$Ρfp xC}9Dl]불ϧ|cۚTmNsLuZγ\#d:j\j7&% ""M7&; @Wob).ӡry.ozZ |\ܴ^N7~7Fo%1Sg$DUMOr[(q;S\P5Wק%~a.݇qN~N# FtC xftA\98|'5GG}x\_IEj 8r տϙuB7bp6M"&@5Ԟ|"'$to$;8=%4 ES4x xUm<0 1oS) t؞8<ӈasFxv"o(_$(pC6<'M7ohs NJTQQ! M/ьq).񛪫KS}Ua$u,'4*GQ.optYDwLpnJ+9AR;HtK8;\pc@! 3Ox/ql0%3Q|n K%[6bb%(1d7 D8=UԾzP)ܨ=Qߛ w( œ5, 2N}$F~.+$TMZ~s͸)QL@LJĵBAAŸdદЬG`^AA1-$$@M'4JQpvhNzYFw˘S-4U e$!K83C'TQ! a yPi&3 &4EKo22 %"NS.QD>97 PQіCx}eŕVX$(MSb@7367@Hxq !⇗vˋw xCʿ}z߇?6 X.e%MC:i<;7s*; C ::A)vX+v9 fG p7@=C}Ʋ_fn?pH8 Qkme_i-dd>A!ox7ӻe3CcB @Mh~nQ.8mgm_VE!7 FҖ+\=7C/Ფ;, |a@m 5 O%AK8 M5mXmQ,Tx$<,Bu)@:7ޡuH%uBnzt 2oD^ev DQ:$\m;Dv"Kǭ, @śX0iMqȊ*2 o@pq" oJ9‚lj,ލCv3Vq7rzgL\s3o J"" PKH8yM¾wWr r.B#/~go$կCjJcFwwKCc';]8ǀK9!5].G] hV2lMZ֢gu(-HXFKݫ @#f]] b /̰OQh,?0#257@iګM9S^8Ͱk_}ox2](xk-2u9nХDWIENDB`scoop-0.7.1/doc/images/logo.png000066400000000000000000000554061240127670500163400ustar00rootroot00000000000000PNG  IHDRg@tEXtSoftwareAdobe ImageReadyqe<ZIDATx]\U^>}齐 P#Uz&WQD,`EWQQE!@ 餐e罻cfvf~Ivy{{Hw,, yA:@ La24&~&mL1y,]U=ؾr ޸ǂR ˢO*@@+|Ip7d&W1Y &ϋk`8&;\b0y/D rfH"&ѕ'A;1yRt gn;OE rpKL]+ Y@8}e rv\T¶a2ĬYfA޿~2m)P JؾɉxWѬFk1dmhOLJBPn^,/#\n@֘ }:#YRJ=(WL0ica T {|,PB42=|IEgr0I0zth?i1Ye΃T<.`O9meЪ8:z6tFi|6'`z 4.(7M`&)$Y{Ϥɹ^QOs{n&^):eq+lړ m7|38Y)RjT.<̤E19 xW,(-&m';#NnEXi™Y$9ͷ\A;Rne2]7Q#GqA /Ač>nE3"QM9r=͏- y"nJ ]EmYgueFNᵗ $+eS&wq"{nHHY\Ň~Ыw29QjbLS=s9 4zJFm:03/Yi ݘZ6ufa-&ehccvG!$m4Hk.f=-'YϬNn1r ztRȭ.K:D>i8L|>CLeʔC9 wB7v{va䕁@#E!4!k5H<ҌQH{u5FTImFՓ_9㙹.7h@֡Y9c"lԶ[RdBTD1c ͠C ΪBG ađcs' ؗܣs[* Q&}Ln+J>- h]ʖ.[EN^PE19Dmɭ6>[ l 5Lmyn;t}^#8hS}W#a 'H˲U0%0Ca\my JٹU)(C{b\Q,K z#X,"Q GFN5'Э;>yk4kˏ#XǢshR?{MyL.A r(msm; ssplݷ)\Qncf0&x q};p ۡv 18bDIQ]iIQ:3/AFJAe^>ױ][63F la]͘!gz꿋r+M>rhP|{Up gAhˎb?9v]d#b51v|-(EÌܜ%mP_ݰ2{3P&*ʬ!ٚmb.5@U k?9xتТ2]Ph҃E@p:kf?]PBxJdE?v^JyM1s{RA3DWFL2 3@;$e*[0jN,N?ehRh1PNJ<Wz!i'ȹPFלgA"a*P)wakprұQVz[ L ?ǚۨL|җWIH rI+PRٮd׶5䎉}ip'!D7HYi[d(1s, pnMKS\;T#JBDJZL;\K7$*j4K8%%sB}vLԪL{N@lb(/EلrFĄؐ~+2u[v]R({s.4r k(jL\)~äT*OpD'3-l,f`.#-973g_YrhcŐ S#qؓHuՒJ-d[SM)--Ț1RXϦe6*S2f>SpTE2JQHIJ2e`MjPBvɮft%ڳ!fNb4+>Q)A$j/?u8=)G3[X"sJR s#iIxu];L~V  szSFւ?Yn&[IJ]IMZdWT П,R U{":駼'_83zKu&#車lIsɄh!3'liucX,>-e ԠڻXAλY-HHK _pjI|NNz,6~Nf͗sq_oLVrR1N@ǃܔT3w)ؽZ^*;=>_&_"k0 to-˦a)h]? [b71J+Ac6|jM\I&`rTȠ 7:<|R8Yε=lTGL>kLjY[;އ.ۢE*>'h*]E8С-(槗<wGMJF)/^]LBbkAҜlZڄ83A WxŘ/]U)Mf~j! f9$ҹ -ّ< ' rU>p\Κ=h|簺b.9m,+5ڒ21I=d,ȹR "/G=WO}8.xQY&Uf[*|!1B)Y[%-Oѵ6l_&Ps B\ w# rqd@Q;ӔIBv r:|+Y84尾R :$@i$ThEk2Ɍþ K–eӱ>tc$ٞJ[dT@ەű`W$ͮEX(>EQQDx`֘e ua7Lȼl!f0yVN\3AG"+I P4DY4|>xnR[Ⓞ\e?X ds2G$za]m؄4\F -9Sa0B%-;YJmЖusk>M=#&5d"L*C׽"l>A-ci7õ=% 1en~w}=x^ 2JjoQ52j^ d`ܾa269{<T[,yH4 h-H԰ߤ^e2*v PXsSoeZu6XijSqZ =ׄUuZwNiDcZF4cဃCCDc5HH{kG8 t (j9|7(2VF-sJJkG|9 l}p& EKgM'55k& cZP܂Zv)#+Ti}d22l%~4l6&kuZsLC֨>4rɄ!)1EQ4ݞ(%O5̟ U1a<ڥƏ!¨XLad{F*g"ٞ` ӔUAʂ\6N&aoAel)V5$ \7`_~Wo_bQ3M]1~Hzw0qttu"Lo%8t5Hi2iڋIn{(tWGH41b8nݎ;V {@d$wȥ&-as Y+`ri6keqÝӒ4 @IqԑGbb 0vu0LsKA Vxӡu\ΖK-NM#G3SO<0vlDT:Z.Z{JM 8Urx"eOZ<≘6s:[e2v lɕR}r1ip Mxk;k+ Q[߇rNHYAKJgJ6='x},5h߱i9vUʞB)9H?R/vvD&ZF3W1a/2Ǥ-v_l:6F*(2e#iGy`,i>-oX_SxsjbC!9 MAbO_l~.*5`ַK78aK8T*&?ĝ d Mu\2ed NEхCA\/<,MsG:Msp x@~T zPm~3qAůxN gjbQttg2 rP}>qdHdRPDd8廲s\'LƪeK𭫿W:Aa, m?_Yo9-!kޣhC 4+7&8夨rɋ_dƨoF\ojk5֖e46| JNY&‰ja2hwTq mK0iOP*EOewBT28Ý1ǏŸy_n}I ዮCG.u]=vʹ80t;yͱ3FmʡceH#F?~lof-%;#wSx_2yoCoXT`39N2 瘼l!$x5q1F)wubpWƸISmҖ1sJW>b6 wYQ[#O?o1u(smB7nW Tdb^j|WJ/3X2Eq%BD OJ_,9P =;Zߞ_y"MZ~'bmNGmg>N;]HxEW49*Mo7 .["f{#r@5c0<]P&j),FδJ'`V0rzu4oy ۏdCNSN'?s CG_W[XrCh]}EwOZ]Q ֡8r|`J ʊxU)2)qp/9!F(;c3_E'Op%Ȕ` Ld<Тe;]{g7~뜰x܍'Gőc' Nߞ~@΋uҪW+l}knNX 3GpB?Avoh^LC#-cF6cpwC DOŸӰfK?#b>,Gq|"d!JguKd<*5]DGqA V^BSC=deRC& Ҙ{+M[cnr4c {*G!d!~f|{n5J'kՄsRXayr{ͦHjcưH÷%r/loNʮd"׶TYɕǺMG(b`?AnS %Fm_7Nhv&l߼ `xD!K="?"Lrla!3ƒ.4KhE,jG!8&*[aWkQ=>=::<\K^#ͮҾ#r.*?ƈkt}3F7LEdL0caZ{IJʒ!/Ә$x.~R> ei "vr=ãM6ur(ڏ־7DHRe_]șlKUddt28g~FRQөޔ-023-Fu2Q6#giHݜ4ݑC~D)٥؞[.}F=y7>oРLf6 FV?nz,Z:jcQ_3CPۤB:PkPdJv Bb0*@S UwsU &ώ[וwK,OX߁:#Sb$a$-ۖH⦈2X~ks!mK&|ᵹ`XObS\,6`{Hf%_]܁wUw!o(a!@̄bLY!L>MF~wL6m=n1?t6fGG:}F/}63GT)eZj,߆e67ț"(_҆C]4ƋY{ܜ1)89] CەcJY|e[;*o-wSߔ˄"ɂc L̈́Ĵ)'8,-L1vӦ$̟qt˘993~NZ2@(M&DƔ.S$MVW}!ꩫ1TƱH<(F\߈T0* AZH/myϳ0_)ռGOML>P6XX_X+4}C;)(8کL qvwVD[epo?{v |1ޏǵxZF.OKe RNw0ZD"-1ﺺd15}58$ | 5j z#Yz#96bqN҂эK3ѨXLٔlȉEǃc,ɶG)]Q)˼ '/I$51A\TK1ENt7Q2>؃6k9Ak\6J!Twp€B {wDI~.'B:o'Nҁ:_z2s"r*2Su>L9ɆAҧj"SmN٠Nf> ق<1"rՙ[kkئcj Ϻ *kP2^֮}ȝV"0%c!GsB~6ASmD'E܌M2ڞnTԧ /liR5XrdztB}c4JJ)joLse bskfRO\'9TL^*JEH&݁BP2U!)d֣wOA?[\Ü#Sp73"KA ]g -3j'),.fWΒ-%njy$k3)Lk5,i:Q,~=|G"oq3W8#9u8E*tL [믹G՛gJ]%V0x)O{_ҠssC)1|42 -1k)s/c|ENG0͙fyx";l;?K9A9ˋ42Dԇw~/WH'P)`JvqA;OyN_1?I-X,A>*KP/|rN522HwO_EeA-(ݐ TJ?xdJ8F9E`|/-GB.ʘ~MۊH=k|qWN'{y|TD!39ރvHc7w9Y1G91?H?`~,j(0nN Js&a͖c0d/SϒQ%hiz܏L>vz^<2ӜB 3<4iG x0^i7q,/6y]߁èl)D̦n!۫cƙL;t9zsB"4\c8N_F!cR(E2v k@n;G䠆P1m9?tz;SKP¦g]q 3ŚmOw4 > U5bQGYPɲ5+vr35gxduAMɶTed6ܕ#GᔥM5v?6A`%s}b7ZQC1ȄіE6n`q1|UYhb&qy|&͠!-977>xTv33M@ZU;Et z*e&aۖf ?9"uBׂEcb]6:j}|B.EjUH:n4sZ&>U/oݓqx/d@9y?eX"e deT{i2fWU?2Uwӭc]R3F]~s]Nl߆:~Rw{P?eV;NX0sgɡeިӃi-\rG58J sN|Y#ӖaŚ1v $W>v{qI4Nl9Uɘwm粝?D TVPP.J_(_LLSOa_]ƱorIp2>z>n|pȢ:߆hx:'p_Xl|}>59S*S[O3B_ܠL\ĬhƐͅR<|4b. C+I8PLyQlɑhx[J84J᱔2&1)! E],N& zA ;ސ~T439e0ך <>SΤX؂{R~],Pl5 5?fkG\߅ǖ"srtRXv ǦUZY*}NِgyT߮ޱ1mB`O0nnT)yp ΁yqڌdR}}VޟK7Foh0VϞtIez|"zh`:il)+RC yxKry[Knn5E_k }.7Ni/I&XdZt+4%!聬] L'2=<>eɸ9kRW7#F~tjIʾ>jp2-W؅>އ~8^ U kn4i)ϝRpث/tvynT)Ii`x ET8L'1Lnc/^n6& 9tdd6 `3Y|I7Zt>I|OmZL^#1eJ(c/W.|bHܐs>8kOcIH{ 'K}eϭmuU~ ;&:7d=Yr,(xO*o<>CɶZ53/-95^WeD2¥.;{`d"[9l6} ]AT7>Uа|šBM~lOzt ŠI)*I/(r<>L3%Ac, <;R\eBɢfmlc9˽fi}+3k^;Sדf&|}^O@nElX41_<>.Z?sMkAh䬽jpo!LX KUt&ԔP. wWBdRJiK4(A\S0MsB*J3*X,bG_j-+,P=u$5"ӭ٦<6GéU24sypw^ $:tq:7]Йp6V_'ЭD_9sQVv>;;R)ܼ:pr)/y EKs}H,$3J yPT+QA}mXAϳ N'2IE !u~ەN>]3y ~6?d[KnaW"C*[oF;p .8K'*"lm D>3P\QU:7MHw( bB5.6'E^Q[?+-1%P%5p~fp zQSAW$h#XEK35meo1/r1S>ӮA֗6L}RؐySO?ޤ̀S|_u{sSH3 :>\.C7zC{@VsszL&qrvhnnWںOD"TFB\ A;ǖd1h $FL1 1?7 cU'4%{0$K<\o>=R==+ҝF^M\{6yO \SxgWջ;kz\=sţ\T5PڢmV|Jܫ!1=nZ':لyhSaY/!=[G=/by/JH!>nln",e>a g'M#g<+hl.aV~6= x*Ygck7;[RQ6 "X|L 4{e^ڮC)< `}ج-eڹ]l*/Nj@[[zF\gdz՟p//$' \#˙R'CR|Ujf~E(nf t$߈n5:#J"(BLt_|NVLך>Erg`F4V5bFX1[/`W}J rhNcrnF<[]d#UyG T$4U]B5JDF.kj ӐuA{^%;Z#n:7#*0v*hmhkSi"J@EjAʱ[nW>2ZmDiIhJ#ie'^*HI"kiPzTy0cl\Xvj ߻egd>ǐ 99cb*ǁ*Eow|>_uMY uoaYpIǡ/>{ŋ!o \zk3ru'sfm?UU-MMW ,/9Wj܊ *hI4ʺ|3Ӡk39ц.&|JFsc=zf1K! ZtJ9u(v?2O9"/ [{}N "ʡrkLNgr бmGLxF^m+51-,Xxn.v|h@2yIk9V % "9"i:X|=td̤f*D/@b $$T"L>FYi'oc'NFg;Cw6sl߼~?F|؁6]be4p2_d6ה󨫒_P&g3lMtp$'ẼN)4%jJQHGNRF %z/A/`pY磷.Ȋ') LDmoCh=]wnڂe7h$} N HSy?9W_H\s`P ]5WJN&sˋCy8PUT`qFR_=*(:ާFpQF6CgW3W~k8m&y1㞘^ݠ 9v<>rO^@(DM,e;\Hi,p$1.(E@c@SFrN$/Mq̅yQ9n -|b&!ߧGZ?eޏ$=ڥkw'P9)'ӗ۷~c _Hiy|8(G1% ?Sb[ĜN? ǞpT/^_KA r.L&#O^a|_.ڶmG&d"ST)Bis){;P[߈N9&lJd2YB!1,N:4uȂqI'cI%xLEHEyJlu8En0D0b̓[J,PLR\aLK:@{ 6gt]CW[vi[>NnIFףfǢ~ vmÆw, r4 F5Κ baH%v~"qM6JN7É*+k)i5 ~Uk# !C "M"hpS\Re~(\@i'+f-:MJ6XN3{bdU0Q :1YLd32B2qfZcʌhG6h.H('WƺpC\v \".[![7=<0_2@V$'*|x0Rbd,6N,h)zִ)~jHfݐDɧ!c=&]րPUDclؿ-i>KĤӰoVL3f5F2b?v4?ε/Ec5HtADLI(pĭ+Qi7Ӗ&\g2ՃWC$(-Lc2/z-h2X'2LF+3u?{aF>ۃtz@CGlF3 TUG(6[$[;c'm'U!a[⌠9[Q%C'Ŵ(6Qu=-C-lW=Tt $$tEg0jtϝiL;3a"8}!_?! fAxe&OًL]M(dG>=u~/%wsҐо6`,o6]Q}Y&iHF1HvD|S3^E'_/؂<8ݑÄGaf^wIP.dUapPb,!ܬv`##)39Fd ۢiLA&d4*~FDzNļi`)0i@9/⌌Gy8Bp)ዻجtm&J="J6c=Bi4;/%v?[ iAC;h9l;ٽe,}e +YTID,w 洜D/R$LڎTѥvڏ8)ݦ  ϋ pS另tv.L)x1Sޏ -P4d5luaYwhl~;fґgQ8}yA1pJGE}ihEa0R6m Y.Q }txK2+ V!R8Xݸ͑ |S 7Mg7R׊>]lbq|&u\ҀX9M4mmdxLv9sZ&ys\d >S9bݺ}oyI:Zߊ}?};v.>[!,9IKhڮӃ;r[1| __rQN٬bA5/ʉ |gf NL &@\s?G FI6l4'LWVЍ\!YsÚk$!"OX} o󇨬ѻ (-rQpʨ^坬$ Zl>` M,r"G& -ezC;bwrd|a{e0maj0,st8Ϳ؃6^hd&m7iL)@L J|` C-h>Ѷ8:m$"i *0 qbsr*:M13{Gy7[<7DM8M\󧅫Xq1A$e+M")P ]1s%0A$HwHw +W|F@q%i%{7Ĝf#~DM61*)lhoG-t!7MЃ{'ה_#g.fMLr~OmFOն'"@-&/)b^8+lBey0v i]]CF>^zҰs|M;`iY5uCdR4r޽Ƽi kpW gV$1Sm㓝&|nc3&s=8)o&s\>lv\(װ~\[8܄y`DMY!ִ &QӶs=1FF2R;|L9"-:&,\.1 S 罚2\A*yeeELT\roDmf.q"tS\h17L|p4Wx{l#Tuۦ Yϟ%a3 ̲vrvj4#eC[c1v ʨ&+`xcPI2-c 'k7L F1Elݞl,B?bє!.GĜ,bI_LqɟۘwJXx/Vs_ U)iwک|[T_60T%ʺZW$ȹLcb+[r3q8)/]Ln 51 ?85 /Cۻ:YEnz׋"ȹ5W1ӹ<[eN'cy ۣI+aBAä}b,5ɧ>M۠O J֟+~OjAѱ2ZPv].PPAr&\)$eLyK2/B@a8$?LjyGZ\;އk˯/+,6k"ok+PcG͕dgw~AKԿO3A.m4j]yABN^ ӧ@ڔ+wD ]95Y*U㵊5?K/>SG7{ .R?J˚M{u]6isDW7D|QK<,ܗ|[9:ƔaDIb-.3)9!kiX|il}^M-19׊7 Y0CI@zXLtW/˳1'A#me34Dj/'ό7\/+>g*v5+=(v T}*4zQяcv7Ie-ڲ*GBA[n^߁%EJ&ZTu?2sx6.cK9I4VOodѱ+0%tYEP[tb 4ۻ-HY2{'3uens_Ef[# z'VJM&^1LԪ-Hxxv3YZ›zJa( Tˆ"Ȧm*~b'1Χcv]3nq$6dEȮBb]296۰A@Ja]ʬ4o/H*i;Wi&צ Mqj 5L-A?DzBZtbb]n1|rJ6yO3?=Ŷm[6I S\Di2?%Ib!9Y3U!43ehVٿ lg6 H;DmbnĜR %8N)+AS=l2`c$mK;8ڋhA(1l˾}LũSwUFa-\ сLR5g>ff"kCL754J>`I!RH)6[scBl n}/݋zSPg<4.SL@ r=T_4iAq]el*zeqm6Vi ;VcĜ4Lx%")F~1YnY;'{?7 7[5+*n#{HӍ2 I_ cBF)hFb@{|][|ڔW.c;ARO'ڝLa( ؒ~%F*!g@ORϭ4(Yه(ZY:\T*'vAP0ʧ*W+4Ë_ékb fAރ)Əٵp 6-5^jAWXbq߯qW45ۦJ$a2)Dy8Y<#A&"d?[LWxrfcHQ` e;AЧ0B¬!P:p&*QJmȨYKx- mI[6I &?8͵:O`:Fհ*6w3kICg°r}JA|&i1m9 JIGbQZ8oжf rXk<ɵ7NrOz~"+ g_lnˑJ_UL a`۾Ǎxlfڣ7>džJi^!*C XxQ%jg<ʠ1UH:/EiۗwyOq߮Wzݠ ' QA?b4X% gRij+1wEQJ{&{x1BOބU"*r;A -NoY&mms />zz)u vɣ}?RA*YvNzoKIENDB`scoop-0.7.1/doc/images/monteCarloPiExample.gif000066400000000000000000013517211240127670500212710ustar00rootroot00000000000000GIF89aw! NETSCAPE2.0!"Created by Wolfram Mathematica 7.0!d, %\JtX)AO\iygFC\AR[pAppOffOyS^g(FPN\fpf[#BaQSˋ.aґAԘAלLȓNܰp֤hy..jTOpEfVuN^E+b1AJpfykY֒޷ڷϵÅӗԜ̑ܨɬďč‘đŒŒœƔƔƕƕǖǗȘȘȚɛʜʛɟ̞˞͕̂͡ԢΣΣΤϦϥϦЧШЫ¨ѩѩѫҫҬӪүԯծԱձֲִ״׵ضطظٷٻۼۻڽܿ޹ڱljޅկȪp H*\ȰÇ#JH4%z Fq 9Sl9j4aAlS&͘et1 JѣH*]ʴPi^HK@첊U+׬~*@)eԩ+ 0W.ݸ6 `E76 LÈ+nN$A+C([Ƭ?L0aё0 խ_$@7qPv |؇p NJmVhS$_W}b0eP`ۈFI>տ?fyҀ.aq& 6(P P ɅjtKC$0 xb & ( 蠁D$ 4|C`$N.$|D@ \va/&4AюjKB !)`':xyg v€[ :ʼn '4ftdXI%)i[x4ꪬ:'%M&A! tuN9'1 $D HN| :HDBA$ij$ j[TӢN@l@/t#:5.ᘎעM$׮s =!{;o<μ|,G?TWoSwއ/ODEC:o1(A%mH@ :g Ai`'HE , !Ԡ/{Ԡ4asCX 1y0på!?T06@.tLC,"0DLX≇ ?t.d Nh/!/k$]%G0oRǃUaTu0H"eHF*ld+NQ L |Ѕ+ 8h #TVcEzGW&ŖLB pR`,hLfɌ2IEB3%Mv Mր"#!#,ut6ĝA9OX=O ?L(-P&t}&D&48Dp&P o؃-aW0HJM|&f]\ChS PCӝL3SH 0ra "E RS L%Ձt(W 08Ocf5kQ"jB8ǀZcG*Y'áEibY:4HN WRfw*WGN5D0cŵVem`ellCsS23])6 sk.iv{PRT]kfۢR4D F.!Pt \¶#>a^6.~P @Z.Ё(HXN#Ag}w]W/n1lF(*8a { 9&%p{(81f+HڢSm[_sp*[Y=mfg18! M>2\Esuǚfpw|᠖"kP $ gSNד+e5/·հ}aB38 +n.!kƟ`9z o}80x3+rS[k\%;^ [:/P {K~stF[~_R_Ao;/ NI>Kד tqٕr Bga5=cGLg)zl_w^=0e@J{y^_t=qCǼB>N9wm#V7o{R3B~WUwļrCgPHs8-Bg3Oe>o#ZXbt{zH~=ߗ vHdl.fqq ؁*mski@ iK^EUwr H~YyHqH0\p0\[/vKeKffO*G~4[6cæQN'v7`zTW 5X{Q[SxetYX|'Ex8;TaW_vOm6~gYYN0%4|^iY8j}yt,GY] |_x~jÁXopWwVI CkUe@'p{yY0 H Hs~w3XfRx8Xe yc`؋\x3(r։ڸ <*fDC}n ~!h `pqbX;5v-HVVd(gWV+:HP@|('jwYؒyV~̈E]%`ؐ츍6ȉ)ZHkЈJ=铱f}8jG)fc Nusb%uKU{o|sY t؏\hi&U(WQpLc^Dv樂Rw"bw]/)9*M@cjx}:ř yiyp d`h8)PÆ7̹aPyȔiÉ=Љu|]֝VMy͘Ɩo>AYguq68gpIgXS)m阉ƑHtpPٟ% ~UHɕ v!e'AjީpաFHP0ɐ}Y-Zi2| ڝT _0]PDjR.:6zz=Uٗe0iG )QIA^ AKй|6{ɂi=y[G:Y~xL 8Io ҞѥLJJWT ]w| 8~5a38 j ntTZ B[Z[7!ګS {pmFXʚZ?Hϊ* WWscIKz8ځ Y0w*JHyٿ:k?`DZ"$,7hYʪ\G WJylya,/A68BZBCL_gXcm à#(#MlLP׊ tem Le]-Ic8,ѵ`  P2Ar (-n+Ϝ 8 Z3` ó֋ʨ%-gC۟=`]H 3qu~PzXkg½v͚Lop]ʍs۽=- iټ,EäG}Ќ(֌ӥMЍ=ߑ 4H`j%i.4ݝLd%k-. ͗=[e-Σ`ߏ{v2m]n.1^G\)3 NԨ2[GtJ.49+>׎Ԯ\_~[~ힾ4BEkn.fBLo^,]F  k+&tr=M;]pInק^~\ x\A=\maH|p&<> ȼ.h8҃M-ŒLAʞ[^p!7 6HY6 ;/]9yPVL7ʪ<d鍡՜eoooSݯ)|ƹ+oA  DVQP,1" -^hbF7vRŏ#MVY&shIsj֭&i-f׵uֹW窧E&\Ҵo[Ti[ɲ3;O[wS,/Z3݇w|׎{=omSzPbjRj~ 쩹R=䋏=4=O z$ 1*oBED3qD諐$1j+9wQ5Z4o: ٪()CpIB<r!J" 2Ҝ\+'$S11ʻJ cM7k dB;4& Jg:LԺF]OEGdj!j9 h>ȸ6"48|KӵITTHDG[K*bSFP9U_amT՞)^l@Vv-mT^*W,MX-37T?Cie/CeGնUxw+.Q]eZ{;pJ؃_ KԺf5a`7ߑ,VJMz9Rd?&RYƠN3n0C]G坙(qv VjZbb2es`@wl Ws5K+!)ksntM{Nf5řd^{@Ol] , 1:H-k̇ijִʃٽeB}?5ok=Y6o% S*uâ* Dy+%/z4m>ΕS'x-+凔x,7H:Jw=S p=bCc,7D/Y#akWs NX6ͯxCb 6*pnR_@îIxcw6Ka&(xie9r#1A8&"T cz^MT7]flZQEP_j %V)cR*J,ZYFGBI v5Y^)@oZ `|RbhrWuE*DwX-(PD]5\Jvڵ&,quryKl~4MBD0ܦpQݻ+ziA̾C:)'Z87c3^Z9e'ZPZ]O˰=vfkOֵ~:y:4BwŜq'4A'u&Ap2JsPwg T|[ 촍%CR5BrF"]3>_~n:)4j^i_^aB}Lsyƻ7{Hov ,P?7wC9S93@q=@Zj5ɾ. ]ZP uz:B>9C:xs502'@ze? 4"̳VCA?)KJ$La:ȖgSCkB[/4x@;r'ۻ3s8}B0\0W;o"82;C\A35|?ϋ܈,87AZ8:3cDk_h@&D3@6+a5(:Z@kj¹ T U 7;lE3k#*RD!TF0.\EEԉZ+5# P)dMD 5MQ/dSLDЭ;C<̏[7F$HCdNPY! O8E+.,?DMne^7EX ЅWƈ435Xu،PT@ ҄it9Y_%֒tLΚ+{zs dWy @̐hH(>cqM`Fa~QE}]].2PȄ[x޳Y'5~Xuײ%xچ9rXbKM ]e'GBӴj84SFrp4YnX%_u+Ok[|ۜ;[]].C=hc }e8YNXtFYcՉ\Y|hMa]p'elyB(`~fF{8E .K?iGl,+=0]+iֻZ$s3qޝ&%ĜX1Yi'E_҅ؐU)5ӥvg4Ni~d[V9HdER5ZKhޫ/Кafhj\fQnVh T[$ *nӭܽ+Y6N]dJ$vҥm=P⇼"ЖkecmqKp{<؋\Yfܴny-fkDіn48X[^lAo(^? Vs_oej@VmhuP#n+LncQV{ޘLpҺܢ kpnnUo&pCuvf`\XBRM[ g*k؋~ޥ~Q>&<8ojFBh;irVKS8%w'Y& W.@j.X os)k*aȁqe.d46TpW8T@HfV=ml&g z-sUʷmmp\uvgc854];V7%B{69U_voWWLOoV attdwN5ޕAIǍs}'A;mfLŕtgW4_gVXCT>>uS΍T}XTN}Smu]F' um^t@Om.s8[y__vsSCtxeu"l Ew?pWL/\sz^tۚX+^EsOi۞tȹם 5rG{NiNLq%!>QKyx]d|{co}'M${R*㶗|PViv r" 5n|}k\J'D)vz)~}L؋Ço|wwsԇ'L?z$Fc{w ,h B&lXpÈ'*dHbZ#Ȑ"G$ "Ɔ( B\9eB)gQ&͜9q:'КAoڜs(RG ֢# %R@ҘE%RG%{DYײm-P**޼WkTۖW_1[tia /1;)Xe}gM,T2贜~i3hK]-4ec-_ִZNY/{bƏ_nf-ۮ~gŝ#+;ǃoS_?ęQ-t_|i`XYY1iZeHK,K\ ]L5hf(.AcBu tX)\9=J4eT8!tCĢ 4d$̠#VpmxsUC2I}Zz҅e@j@vb>(@ |9ÏO"s D4#$hʽ*yd$hxX0 aHpy\v;N cTLQBm,؆'PI!$-i!6x/zB60c1I(o:Lm1W;1MZLX0xzC,iJv$<#YOfB:J7b }*3Q E_OU:4PB+:+-gV֝dYxJlha N< :R feAP#MaLyŝy>jQBEJӨΫ:]ruf"*Y霓z$)s Z :M)@jώD+kjᶘTSE\7Ůj4OYbe@04bd QJ&tj1HeL0[TʊvpR2͚" ac8m|S|[ς Rn+lIU( [Yѣ֖tyJj+4F&^70}XNx;]J[:g\kmY5%}\G3A.US`/Mb 'p5MPlU z:օr4 pl|SXU1G $ Ѥnx_ۨv϶? qzZ.K9Yg4GX MsSLbl턲]fS?9[*mfF>My~o,`o2td˱Ҟ*pb Idq5krWy\Ou^ &pf~|=>/ 2mQUdҒݶQT-1d Ms3ԡ 32`|?KhQ^]n;Cnu2S 紏pOV-QK׼1%.۱O+M㿱+٭^#uއ5h4nlO߆T-EZ=]R7OڕE:\#{ʡΏ~^)Wihvo7q8rv~/n۝'6zy- 6=uQu<םUM{eTb?̽aPH!Qc!.$09e ’F/`{EX8£9QZ^ 'a߭$CnP!U &4@$d, Xƛ.VeR]f9G-acz[ ZcRvdR:jV#rdCjU:&^`ǨKL%T%~NjerBmgEdUZ"׹dJ tvLqL*Cyѐݽ:ӄoev:D-\r&{'aE['sXqA8܂P(g8DhBZhFbh(BpshWA (Oh:eČD*(~ōZBʨYEx(x(-Idd))œPDpJiΕi!)w0)j))MOLiJTbi4iC(&xi#)**&.*6>*FN*V^Jl*v~**퐪**B*ƪ*jj^N:0Cb:LGH*HDñ"U(:kI4E:@IG | }'4@.pG@G/+:P+yD 5+(p ڒr4 ! %PG@k8 H @%HC i:xB'@30u&l&C,'83LCGp.,:H:0,l0k,:dmGtvl6Tl&6pr+5T@!A '#lQG,mخm:nڪmْ&RGRJC8$z@r+T:C6/ ȫ0x.!uDR@ܒkrn\''(4;d;@%lz, ȮC6&*/v(j0D6/J-=P7./+:$4p@ƚ+@o'@@k $ Op~B.(TGn44GC:pn4xGL- r6CH 1'/17D@!d,(\JfAO\jyfFC\AT[pAppOfiJyQYe"GWN\fpfY!BcPSˋ.^ɗFґAӘAךJǑWܰpצhy..jSOpEfVuN^E+b1ALpfykY֒ݷѶÅҘӖ՛ːޮȬďč‘đŒŒœƔƔƕƕǖǗȘȘȚɛʜʛɟ̞˞͔̂͡ӢΣΣΤϦϥϦШЧЫ¨ѩѩѫҫҬӪүԯծԱձֲִ״׵ضطظٷٻۼۻڽܿݹڱljޑթȰp H*\ȰÇ#JH1 eESș?rHPSkA:]7bFu.ժudGş@ JѣH*]:H2˾]XaA.Tbk&(=epKZE`&)L`  LÈ.9Q2esS^"26 D]͙F=^IH`4  . %llI^`[ǮVB‚TG#ʂ@Xd#8nH !E7' "< 6 %09`ᆘ(R9C  t(")"9,`3PC;#7H0a3@,BP4#`oٗ}$EM#xa,l#4@(diD#MnFn(t%D0c gt 1Wh 50tZt5d $m d}c ͐/!Y@[tӗ5)무*\s8C#t &9{{|j "P]:|(6 7 1 #B~@A`lHڪ;T"C$##昐Db9Pl1cA#D` @ 0)|@Qٌ@20WkH'] cP ܃]͵ t :-_@c7ߘ# &L" @߁VcmmҐG.A 0R 1m9Isa:54GV噛N$0箻BO+o;@G/=OgwIsO槯_Q:^7A$JP 3@mr?Xe$ HH@ ZpzdMO Ơ5 rUb0kP"1Qrp_?,S!.Ĉ AAh`h` A6AX%x."@9a _B&8!_P U1 _(>Ƈ:LSD "C< YE:9GF2D,448!ʃO,fQRL*WU  1t mB將:uab&3IZ2)d#Hfҙ4f0jZtuMi&DgL4`,sh`8r<%MeOSOK9B7ʨ\&шL!DiU/m*%]*K!Бg5 J R$nƝ8JTծ9Oӡ2z O)TUX{XC.!Ƹ!ZIְrT[-i[!YZgf&1oO6=sx]*FA,f c,R,k$;*Q%sA{zhJ;Yv|koK۞z*]kO}f,xW{Q?67bgO{v!w BE/^wӻU´k_zSY܃9 D])*I+H.(+^^ư]W w) KbM^Q%]6j "̈́cXy1{u:֪Yps88qN_<9WʳЪmŸchDR+ mVX^rx1=Mlz7];ӷ9}(6(+cJr"'eR2I/Gĝpl3YT%"a6ܐ r cowmiO6yMYfzJ Y03fts-3fwE#LN<mkQ"'DcV_N4ƃny\r1zikF`IrIC8JyK`դUٗ0٩nzJ5 gw'yڨ'w` SGGJ+ŚH:b稥(j!y|gȥɡ?J@зF ICOeɈʨG|ꑎ9絥iA \pIQe )1hLٙ~ɐX' kZhy֨(֙zt OW0F:٫ yٲ,!;h[nzga)ۡkyo j{0抗T(IkX|yhJ93; 0}ުeFZ,`y:teB{ˌxۊ{˵^Yto{qbnۋfNڠ1k~ ^ Zјfݙ(*;˙OW)Iч;wevn_k6;D ԩfNE{*&l˸Ds ǻ{'yۻrwbY4Uv{/k+{`Eqv{t<K};=[wع*T |Oc`hz+jsn)Hlg)~e|YJʪUOU Ê b|&;nɟ<˷E ,·Eږf 6%i וI*]JL!Wk0|Z;9|ylk?eYڰf&jHH9,WY~ r i ˇP iˎ[GX\ ;kvѩl"ʬЗN@(U`&@|l{,ǡ6ɵy^ ~E x`j;qϻL|,gzȣlw%Δ[;rٌS)9`` lf5Zȉ̋L?xdK ˚EؑТ^n <}|_]C{ƉJpꊽX+\Źk"m`0K;}^,Όf p7[z ,̡)rw-z>WiE ~+%֤\ze+$|f9ƬTeqiꋟ|ammid Y0(<\+ɍ4 Fp-ܝ݄w+6.pN]MK:|[y8y' Vfbz.;X e4Yk;i"W ݖ)jiHУ^J쪗Pع;J&. ;Q락y(lJj=riB<\}z|N8ޕk; hg$w^[H w_>JF)hL]9=+R*D-M=a9=1mjY+}s> ȭ,tͣ:X ^ ?n~I)'dЗlmrVu?_Ԫmv jZuW| rA_&QN{/?~a>BɿN{,tr[ 2n*k@RY~^_~ 7  sC'ʅ,ߒm߾CK]u짊ڡ1a,Y 4(BY 8D )"P=N2d"M]Ѭ^'24ةݶ]Da:KY ThL 5Q#J툥]QR~kmkqkMMiR5V{*LK1x&K&}C,):Wـ\]01Ŕ"r`7;SgGD z:gv>'7b|^'_f9hg-QMtgGӄΦFYIb Z^ʢ|2fefB5Yuz[kYh6QͲEoh:GknyTzWewq+3TYlz=kWtRI$a1iiMx:r?WRgn}3ksVLl3ev'G&yaM+{?$MlB* 6(ogzM=5 i[R·?J- &NI5(ыQ&+\,#CQ @҉b*H&`RቂBO2z$&5M2¢/{rBl',Džm] Żh @ `+4I]Wt=,O&5 N;kXD ipY_1>JxU1k Ѕ9`7BZҒ3 ֐()D&Y/EwC#GT].>OSc=Ȯ`+E>,ƒSLу?tUF4%*@?|1ƱQ<Ճ9kclEèp$H;|S+dܠFF:h\3&@a3ʥ0@_"O&\ځýZ0,[$+"y%|Jr$K$@9|\sԼM#j1.*J?k$y5BQ;Jƣ: dL;663/\$EڶDˮCB̩*d 9܃ɐ܂B3:JH#(9 )[RD3 {k7\ɸۤl;c8KD<8䜾aCmQ3H=ƃ+p6|d5$>\9*C3b͚0%dlAo15?]3BҼI?CmQSԼa4YŸ,Iʤ<3LLg좾К;P/IH JsE؄Z)۾ƺ"., WX1Os HAQ֔m14lt({˓QnB. Rĩ,0#̙H+L44- DR1lDmI#KdQQ]+u{ U >UEhH-#M#YEhEӥZA"T)J(:TL[ҲI % {)u&-C3ƃ{U6h%V2Vn>J5S3a尚I9/+$Zb6u\#NͶ-f$[WJOpٶhK9gk1¥aտvS#T\ MfljiƱ4>#|F6x.N@WyG%Fx.`sIfХ;VZF\S+RǭU=$V>l: x45\v,lLdoRؽ+#=H0&9*kmŴ/ $=ZK%QS-qCV,Cuƨ2i3Aa:4le<ѵ/5#J:EѹXNnlbh ՛B8%A1hfkDL6hp+4Ң8TTGqQȾI<`Dt\mTB 3Rq0:fU.m-'ؠR(ri$qU+q;ci^/R:7>g]s }Z1pnѓcmP.ܒQ^>&d}?zR6oeC6iwV0 ,UDg&'Mп#z +G륫ٸl>Eu-Ow 8[saUjeU0x5.%F}co0Ue; qVLt|rd.%nsХ͢u2RF؃X 8MkԻWg܎FKf!88zf# . Jy9!ݬyb>\[XۨtfkLZk|K$[vm*j;sҙ^{\pz5cѲB4f2+aUtUϤح؝IknvRe6no ސ/˒W0$i@{THi,pܞs@{lzX+8an ŷɁkGݛG(n!9밦dwmmlP?Cs3]n (xVYe *lXÈ&P(q Ň/BxQaFCh#J$S*,$LdJܨq͎<3g?-j(ҤI U4ӨRrAYJD`Ɠrr`ɴeB\iV'ֈ#㮜Y/$_Y|8 7sP e:P#?vXr˚V݋!ƒ;7!Kqe:'kߒ4Ӧ?NiW{ Qɴ^UNz9Z 43uحs޽3YO3w.pu_XzI/4кٓGWt[mu|h `&!FQ EYv-FjXg-_|H"puuyڊ Z(7s7nT[)jN(YdM6tQ!Pnhxހuِ[%X%8GG-qi֘ n @4ȞGB(W`EYQ5Z铓M5%EzeY'lHאhuj:cX~n*[M&w7b%rz}^c,Tcs]((GM,H&*f)(\&yڗ u` ٵ[˥`B *(ʜ`uù5R-noꛬ𥔭j 8]тWn .^ j[Vl=֗U.ůG%B\AH |3w1Å ބ ѣ+1[ꋷ:G!(>{Vɭ$]9)N#aj͚|5mc vL=+Npqp75ZǵiM0=,@aMΑmaByFkK uNj.]Kew,Mx<0ӱ7}ct(!oGQԂ U~ZUjwl_/dٛ婏ǣp.U㫋/tޣhM8-O9@[r&ax}{S.+M SR Zȧ)/ Mh'ǹN#YG>BWıh,z?@a'pg:9v,6ҷᆆ64!\G+U/hL֒Y z#;9@@tB,#rA߹d=1"CGb"ɴ}$y?VŚE-:z&bw+3]S(0W_+kc:75Y\E)P ЊNj^l8"O~5Ccaz+}C\=eqG ښBl)WZK zRUT|_fuqs:BƓ~3*7i4K&J;֊P`.+jnT XXWb=c*TK,Npјw?Z1;Wpa3v|9\c}paߣ՛#ԣiJnU5443' pOX3˚z>M$.HEy_.a9EߚaG|>˷Ѣ`R J9pf>3m?;uJJh 'CPhUH׎KogA^=F-/S:9ũd\ R܈^QL8jDv،ٜ歐)ʼn)uS%ƹP ͖ GM@Q͘Py ر_Eٰ qq U$LI)BLIȁt`  YUMRlexaY٨ +@%\f^yVz݅*5 #MU"T,!1V `MKҴȄ`Ģe1۸!yB䁖)&w% \ǔ"i B%Wȏ4FNѐ+`Uwb0Z5bEq`fdqڍ##ԸZ"AeX!2*cFMoM6暉x_9"pn59+E#gwh`ISQ P]H_ BZDj8ݾ!`x'$(cR/~WdRt]tQ#3~S]፵وVJYVEA\%v!pAWR ;ҟ#ћ^ND9EUFeeE`Y"⮸  eQGNvbW 'PbiA䢩%hLU]r eo.ȭ!mx3IL 㨢^*! 6斡֤4I^z eBI%JbU6c":eh0JB:d6dʙެ] BZvRgX&⨝GRLE(\LB $"raUE+d!xg) Za|ERM.'/r&Gce !eV(8ڼZ(n`VLZaW%o>P6HRFn(kF|!w)hͨ<NV JezN7͢Rj&nܩRZ&y }n)I XnM*Xvc̊bPU𬑬r^n& RM }<.O k8afY,B:j;GtYFB%[rۖLebd]`?jhYu!(Xr\Ͷvk5y^hfsJgl%q(g^wf)Sg9բ [dba}%^dd>];,hDkY]Пʇ, $he{Y Hl|f)-s+,zFق]"-o/,V~NPJ[*6B=0yI-a<].B .e%V)RۀSvװ9%ݼ#6EhfF'}Rٞ"N*>J-@|jڨR GT)b.^잭U.Xj+_BmHղi깪q%[ЁLUU HakdJOb?$~ڋ# M[^ܬYY&˩nylX"/3|D2 RrĒ$1j |Ư2! (Įc)q$xӰJmW`.JDc! R6T }#p"eAg2{c.k]do"O%\m-6/Z^JjykJ|}1.GƎ0ۚ0^ J.-N` nJrܖgrIYFfXK'J` )X ĝo)BTdxdRߢņdcn+UF1'/֥LM3<舰TjbJ&NNt>?N' ,vרq2򯺲D}NKxe6so-S>pmc,|j$.I˞&.mV vxlnKt'NjZ*Β^P)@ K{q//#72lL![+åY~Y20toEEROpdÙpA-v{8-RD2>0K~`Uue'ah@&n: oI*5Yt"tM,|o/ⳕ@vqf%s34eB ^"?RYB~GI/Ϭdc '$ɵa414~$75W ߥ$*_nrpzn%~p#q}/ְ(c-s5ZkGè1ܝ1Jr@1?SZ8Ev*E-36{CxHWk 7x^8V'2CP"4t/.j6sD) n]x*w8}p?lujj%'L2Tm]GaۍnJYA:XQsupvXt^|we,.spFRx>q~b꽨cdm43㌛{G!WWopۛ0qv~lވ_VݞY_3H]I.h5{1ߤZdϚP;L %pN<J쇓;/|`Oc3XlVZk{f>~-Xz Q,rz,1bSZ!9fe"7*>W>ۊEpRZ,SD ,shf.[uglıL@8KAΚ0`ÂL8ѠB-:l8 Ŏ#RH1bH$="4dK'N4R̖9ҌifJM|u91bUjiҥL>:jUWHî~MVd٭.n֬Ɯ =܂ Mi@o)F[r؊t+צ[La}Ԧb=R%w˿/8;5e-Emw-a|m1_ {7JBHo+(@?Nֲ||/p㍈Z  [ h#P7+ `/3B{CBCr2Li$hF,i̪G&p⚣B#9-Bދ}IB5Zq:'NC409"l49DRǏFF014#ldh9dBՒѯnRQ4.E05 + n+QC-ΪR09,)qJ51Ss1Va1]s3FB6,X>t>u{S"ک 0RZ3QG*Q.˸&+S4vS1Yi_uwB>+M%FqڃKP2K辌O` XZմGu#0kTdU= ]U%^L$3ƛcjMZOo{M%w$BUucRG&^#wa i(X&CDz8hfNY+{e,h&U$Sn.6{^.CVN=,輗7>ӟHwwY.|!8[*w?^ZϘ%Ϸ̑}1L~Ԟfz'3{YgwNY2<%Ci;=ؒ5]dm[T%?'Dy:=QGW:Ojpě۳y ʓL:(J]jik͂_ZfMke N^ώP#a!܉N" &QwKQi JWV삞UZ8eQ1֫렴<:{Y'22D[3w+Do;BcAuڸCmW`)k\YY.d;=CC:K[EVSB 3zHkk|YuL~/n6;mk(MY*J>fMٞr1I~*\))uav!+ORۘ3;CW*nY8Ot{smVFB,yW:+Nmg;>.If!˂ٗ\f 0Q|̛rmJ6S:šnՍH1*zh9s|U|rۉKح 6=4*:Fb )f>ϒ3N{u+ DB*R}ۜ$- ϤR/לq霣dpљ CJ*so\-*"%XF޽j 92sKm| #/k3<ĝS5f,P5#u%5Is䪟9.~jRzmh adO?0hGHI>n̈66A|n^S" :[eOzc({6l`nxOքN 682(*BI<ˬ\lަ:f|p-Gn~aM'zd̐ E&eɨ}H}REeN⋅~~"H؄ ~Nǔ[H OPpnM:LD ~T4tgצow*-x(jR$Z-G"CN?xf: /nQd jN\3I=̋f$٤sqsJlpW+nlw* muQr C~xo I1 u 1K Ϯj6 M:QT/̫D S,W-4/b%}1uЌ䢷&0*>?$mRoސIZ)vF8 !!ilLE'G4yޥ&QՖ,'H72n k.hJԦ|$_EZVF.[.Nm+}Oʸhf(,K%r2 32i8q 1%}p-}IQ0_1 iKۭWڌL8G , =6_qQ#|q\/33Ic eMkhis9su[r,iLL3B7#2l&T  t'Bs,([j24S 섪+A苌jŰkن0NQ -8Nr7ھDK꽬q?prnS=./H- t"3rfNS$"9y3A,2p#^0m0:_%F3*H! I'/eu< xV25Tpر/5[ΞSeM%{3FmL3$6ʇzFdIc57e@WT#fQVSA:e-&fVU\:fS,5S_f4KJ mێKbW6e1Uo)ꂊLKJ 6:UnjS5 {Vn0MFs9 mepTӳov!$3*4J4puWknPN|geq_\(](lC) e NUt˂I''J)7n+)*gSpyqWA!~i<ݓ<brWQm,5K ÖqWU_~/fg Slגlx LJT8ςM ' %l*E XBrAv^GXK;V>,ShF6Y7X$"V>{"-W0D8G`hSWbyGE71#"Bv_+v)jK xk.q=vWmr9kQ]d@Њ,v7>r(vz]m(eVU7yxK]E,oIX}VoP;4~2~A2Y/o*dAaR0Ve'Pݮ=]bOJ[ ./3-A{ꌜ%'f'vOf'scB9#q}~ld$aAnNm\khA2IGl zhRN2 $h7; wИUgIcVn?DD s`}-}LarQR9ň9wumB> 129)i*yLpK{7Xms yA:jm(NsFujo`0HgAZU@sDbцa&lkzhblf{"PBaRt ]`̲3$lGyNJrG%){K8/`+Ӄ.0O֕/ eE:eX [jT\`Oh2(-Gjql ;6so4.^yZUWz;ejS6yqw,>`N5|<[ҍϨ3}u%7z[g_0%9SWm_o[޺`)k%-=&R(ڛIUwIRKCu%󈾮1#߼ ,OZw]ߵe`ƍ%c󳥖VMΗ_a !sxy1N; =d6n48t&ѦwSѯTK'&} !'d^Qޕ2pߎ{oeVKt2Qu~,E1D93ݘy;kLQK =f̊;ec;= @EioB Gj6cƃ;:{gYâZ~\uU{I}#1F %pyftz֕gd)Ea TIơUxXh `}uG\ fȜz(6X_2A E_:`p!7Tt&VՅ'Ҷ"NiHވh$ U4a8he\W<'jba%PPcdFZxVCxyifsgvgNFf]uvc'4ف%Tr&g щ$R&sAb>X{fkW꫟YYrš83lvBJ-g.2u`'e֥)vl=Tz)eKJڣ5%U^BJziPgt0go] ٫\Sh.1dVj$Z hZWkes h5 9c~F];s,`fu:4Z4:g4HrZ)G:K'ţ5i v Z˗JRQ 9! )5FG1Q`9bRnۖښjRxbhݝR@_Gn}1Qa 8Er@8n7޼|Uw^J5.Uyrԋͪy]a#Fu6Mo̞&bi=ܙ42(/[.ŹdF_ܴfA1Ts8a/7bMzr&GS" USN>&KCһeqbbVGU,W!OZDAKV6m+{Wlvxh; 3ڢ}!}kttF.j H)m zψ&!b PKGooSqPDHt}^bOMx 7ꓕdWP?.,2$ɻ>.PYwl*PHlkIz_ [q~ c %ň^P2W(םrsh7zi/]{0ӂՍ-M);}M9+' @ Bб"Y_X[kk((puWxM%kVAV^8]@.,h`3ewZ.!8d\=UE,9CMjx-s-醴ym}ΪZ^x_}h0+cp;`\&HHUI$/9x{2$b`M`rU+UG6"Zc9:X`d3(q48]:= 6ukLi8qr ތ&mps :P"=r(~4<Nmb;iIj`2sh|LE.P_z"RYEQSgz.sx [N`Dvs` \6dIA|o-B)4l_ }MT&(ߐ Xȡ qټ4,[결$UW d}:i(+fmW%پ+Yeorg&QI;v蹘ƒ2[X}P۸%vx9S1"~NJgr~Q/=v{\qי*C)`Bf8/It~u^=;/ xwX?c6QtȦn Qg\\T[קy5 ldPMʞ*]FJ7dGh-x*dis+<85RX{O8 l`Rp _+j9Hy ̊q: ?[st} y$Ӣ!ƒ_wQERDe<|'uQSSpcn2Vfz4M*cTci|+H4b$̷3pǗgpxD 6W'DwH'JqAgC'bMB"FQ(yRk܁T]7rf#l,R 7O9:ej=7zJGACtq{mI4:C3(@*7N?eN9̴~b`qFyր5p(hW-8(f|PQ7d$x'xO7dD$qp_9bw`fG70#lYuPCk|y&~hxxusHg#VvDsePEtB)Z(v\:HXz9%MQ3knLkBg{H+@'!(iqg' 7sr@hqiV"H}{|W&}A؊B;!x$.fr?lē)bF\׋vuZy&Đ bGBV>DB20WG`KiS|PvCm GRqW%YKx(*}&%L 6,c[Yt}0S0:HnJwBzwcYB>-n8P*c 8N6B : -2gqxa7lwGaj &=9i1(@#oXN1'X9aFL2zxn/(dj~_AvE<3MF(bmŀjxY1N9D]m{pHԠ{pY O釐uF?>asJ$JIS Ch@23thJ0Tqip5rg-h=jsr5mMUiSq۵&xk N"T)Xß"]x:xw2}Vz:O7SX~; KEjyjr ]Y7pka=(ؔvO)orju6㞿Dm*]znYtcF`xn=')r3{"c2: e` _*tc:nytjI٘uϗ o%%߈FTBF`-1hIw'#W-d~VcZِY5LQ jܘQiS:LJz*%5JdHgGnCakgɦY`!<䊪gi㢸H)+#IP':}bMX6q/ƱԨ{>nf*ڧj; P0S9RZ"_oK-.-T>[h44&ˇ:|~kDpKz&$cWȳ~Y3&kFƊ$q7c6:+}yy#ҺYZCXѹzz™zF7ʂ÷)8JE| Kӕ5Իg{9A2Cz~1ٳ:3#eiDi؟Yv <{j&*QLHKA$R4"JeLGR{'dgk˯+>¢͹; RmxQYEƥ CpWlpާi;x$7:nU1[&<@"v&Ý'w{ J"liv>kW|` f|PLL3F{XiKBRTS[h+kR tysA4e,$f+MW,}M .[» d* 5ȩӶT,Ef=Zm &.A٩,Ĉ*PɊ]q'4_4gNa*rN;ʉ;;Of/CfkwPoQɴ\:YC%J\1wy cB"k1 `eu0rw 󷂷Ƒx*{\<ӢlIF[bBAuw䊚 ~INzOXL$A&{A~̙ɼPl{^<5׺zG 8Z+ Jf&,N}"=?wLUOW]UX7#gW+{,%>pbr3gY[k>MԽ9CJ ߩ~ie#7Fr`.obF2Z)>]?y d~sǘm49(pOTgiOIJ"W_留îHd-*]{],뮙~{]$2[#}'ڝu&zBڭwJ[ 7utSq^bۀ*2/ަ&{;}5 Fރ|%Tk@oCՆF_( Ok+kb虶%BVI}p[;N %߻ԃgZWϽ%JA?Yf D-b@)R:ϯXNt Ε6 f %p.Ä NѢ@NЈcDY!=XѤŏO\xRĒYnbI5B)r'Ǟ9mrlYQ rbSQNjN4}.hR(Ɋ]ZPX?u QbѢ8kJ!#%JP޾_.P%^xзc3XV^GlRWm_j.{pO?}zJ˦3c19#=8n5֛2]oY|lj_.~hUV/v%ӰQ kv9ڑqlˈϰŸkm39*mꢋN.:B 5g 6bqAPŭ&F۴Md :ǟCn0"sbRIB'ȃxFD0x#r: ǿ< )ג.3Ũ$S5,l?=O/D@@t8!)%̻⢘0`HtI9EP QWrS6U/LVFRE;@ښ{29RWt2ż`Ma:ӐԚU A#Jm~qYI l[Cad]<+yeS1LR:E,DzKGьPAAkkE8` Ҿ$}lA e@]7U]g29[ю DSsehI-UL( Zayo-,Hc*J )(O^`=jȲ,`=tRn`y7f--Vʢ ޘE5z 6אtƳ34Ix%߿E\S\FˉS.|WoTXr-#k튼7N6O?{d4^F֣{[k\GkK"$efS=~e Xy 餔 5f^Glqp4'yt~,L|R,Y.W6tX(Tl*yc}( C]^WiO P1my>?afv]L%1safS\XGlz@Q%i􉏴x5*XƇvSؐA_ZDˉzA3"S,EJӖX}dKg2IT֩fVJNTꀠ6!XEJ}NY#4xf/@.xALaYٰ i+`ZD*pI0 J1e^jXPWn9+^ŭ vlg_EZ Tѭ^Szj@YFm,np 5CPb wuIXM+U{ؽI7vZ'rDʽv7pO&V9i;${ x4mkYNDc`8_A)JLO[q"d>dHD"o,* )%V#OS SɌk<61{}m:եrj<$`y5e*?m7 WP=scD9cO蕺˦QQ,҄gTƭ5Un[&-IP+ D= []Z$O]_+=]f|-k.-Z^KGӮ#%V:[fwnx[w}o|[wo\'x}x&\ gxpG\G-~qg\x q\#'yMLHTб ˈ1|49xX.aIS>th>h6@g *`-^`lT&TQ:ӝ`8ڧ ܠNi{yt?%ȁ9,A$@N@S1xO/ A<D46/xv 0N F lﳏJ04 sNAS>*4( ;sF 2$ `p{>W&BS63k3Lj؀;iDsL<=?{ $>{ ܿk# <ߋss Xٓe@ cPm> d^k?ߓ>3?)lo(@<`Ȇ"n@Gȓa`IaPKXl8h t|ܼ-/4`pۅBaP.BBʄkmuI <:sHP;i`@ EЄhޣDDpPNsJ9Il-`c<{r(I:o(hdqHfleGr,Gs~4 pL/܃F( t8Bw<)X%.n' ܣ 1Hk AP`$`@dO*uYI])@#Pdia1&3E)g!bL=}D -\r*T&)i@RI~@0PLjh2>@C&X>CӮ4@ߘ"&%lh vƒ`=uO;@18-1 >; &AQ$.32tj[T$E Cl> @@'XKLq_"= $R2G OꊟrGwKP,,3:<'Dm A3B!]1A3d5Z_=T< ; )ʬ+( '*C:tۍwn_> 11HG.f~Q>l%4O磂j)(lP>=-30/=!O57G*Wo'D}w/~觯>'~oCL@%ro а t7gpL*@ l>0-x a4C9aTJ"r(ĂLGdLD#h=l"Bx*2чVt3:0{K@ QPH$F(BC@6 ah5ᅷ3эs$YH"HN"HH6d$% MVҒ cEɇH$88$j0qY̥.wyF\Px!6q g! !Hj.U%St@3V7bWr攞~%_GB "N5nQokkV1*UU没[լUzux#6Ԁ-RN!q8[Ɠ% ^gؙ)I/SdyЪ~7t ZxOhG3g$Fj%ݭoW:_rMD*ch8V]{Xߗ4nr[]^8QMu"f;3ba^zy/n#\ ؾ1{F81>屐_IapgL]r5reNx4A Q0U [g0TMwۣ0WOLjJ3\e} ڪ~J[ZuWzF;b640|v\>&؛XԪsr7t3zEeէVP06B)JX05Mm 'T_p}%׺vMlz5UQ~] @CL69w:1^fkm-^3fuCMgUq"ܝ7 QwɁm ^r7XBǬ#⷗;Yz=P,˭Ao6Or}FWW<5/ 5Ln|ɀv`]#?pqQh0 9kiJ3Ϳ^m?7~, {E y>Ǻ&GAs8PViEWϓ\7ɧygswpkmsR}Tv6uYn{YwaQygo]vLlk}ӗU` tyy 8g'0s!u*'v}ٕx xq'~vaxrP6P0 x^ 7yWcqւZXp8'_rmsU`7ȅ:5D%tȡvg䀈F{`XEo'gnU`Kio`|)Ihzҷwjx:Nh]>yXuGugb{'ZHzC؃agrKO=d&w3FR6Sugy4-7~&aݧs׆GhfdXv]% }^QG_6f(W*m/kwqhЈeY}xw p{`yCeXwesesDQ)IrP觏~ i;Jc҈t- )!:XɨdXn)e )GȧXLJ*ge 0wwF*n*|::zj]E:N gh{͸U t0r 6JwARNXh}L3ꌳq+ZRƃ+*Nꉳ$d@|閻*jt *pɓ7fX vyJ~FJ]WE(MkK x0trY >劋8@)W$}ꌛ٩)U呹``{簺XAZ+1g=mj9ɜ A M8z!:?(崋7YWYqS9 SY1w s;FHٟQ`u0x[Ku*Ux jИXAkp'h jqAʷP:~"BYj~M8KɤLx҅K_zj;mpq+VH}ֺgXxx2괢;;I\G֫ d{F7hKWD8K۹ixTV j]KɁQu gʻp۾E:x|JTę|:z֗tHڷƴjrzK N6GXg@d >똾[ċ_c[Yqm˷`yZy{PKv8xă| ^Pi+Fۣ祖k+wWF:eJ(گ6Zk%|Թ~0H:OʬL]0XP⺓d qn9ՋlӋ%ւw(gk X˼ɜh^[ {ӗ;K@8D6~\Q̷{V꺬ٔ'zkOE[۴Q PΘm4@=&,rL+%Lo{HhQ }|=XjžʼnHt7}&vIẐ*ڼ"l=jJ_ФWʟJܴ CM.7lb%,ͪ\,y׏&%ZKڳtй4¼XͿ6^S[сzr7%0ek=l?M06W*Nx4g[ڪ ~|]Y׃̭(7;.]/ۭ;[v WǻYYrvtNv-H i~{{̲-7W2v5 mc 1'yMwz~($WY| YxMa sKiNlolס<{RMښWLб I FsH- E%\M;=ً-͞A,Է러Ur }r~yrJ&-~W Ⱥ{0[=mDZ3L#Jy{;  :!A >"C-8 F`;P/DRJ*mhS̗3Sִ%N=}sfPEe0HpƐ)r̘`ө1.MȐ!ՐTvźҋN]VبQ xQnۮRn LT6N8KF ,Rᛄ&^xgbɍ+_F*Ǽ_fkH[CC*[Z~,g{]sz証aIÌ<%}Pin~"o߬}U8G`%}r%l[wǫ*߀ 0Q <&L;%*< 0 аj"T6`{<cӭ=s-"bO8u M3Żx4!j4pF-ʏs%N!uʎ,+sv;RbړﭯT>O73 $h,+3r0ʪQ. OCL-tPSNǴ ):OFOj7:Սj6RBIr\C I5ΨlJI;>miǰaKS4\-;U?|^#\3>;rABrI07u62S[M-=Y1I&c%>$FTWQ[i_\)nUP69(tۜ*\LLgl6%`+W?޲Dp.tVTEM=]>\@قy$8jA{z۽.ZfLuE1Hs}w#O3_UƱКQHVSQU8 νnchovl,qWR]BdOW^oGWp3Zb+($A D;41AoxS&}kq˛\NOrN97/[7=}e\$-ɇp3 Hl^ý5Н RpU#N=o#<эR ] ݊|8,yMHTK<'kd㏔R'RMD<ܭYIØl#1݂ gKTeѾ&ՃQk:DNKeN]/$Ʌ5~7<^bw(SzcM ABrDصQv 鶖HՖLY-YVX65`uRS2B:ImEi0g^bɼwm;c!fiV ,)L@l+d / Z{gB6RBؖ͝ZSh58hpDURft.TE+zG&W[+<[pծlw7KfS<2HJP!%^JܳixR9F4ѐDphm[5荥߈ j*(G3G\ATN $-I(-=qP1:DqKY)ߚ^ gOH=l ,$[;-o 8q$@ : ;X5s))SFRwmLi{*]X<3|MǶ'hbxV Q^8|_a͘IJe*y{_-|mdsvz\8hhl&HM.WH@U:)7x3Y(}h)JDRج-DZs6cNfjUy>x~\B(>9wr"=( (CH+/zѸ/1is{tM"Bܬ{Tu .o>xiB|Ek?#pC>,D#ܫ2x&]&:_׿MLzZhI/>*;1,Yj74;$è("x*g`2kxCK$8nũ 1 Hg+],1{ q *9Hq13ī!8z=QBCܸ!ESCQ !5!Ay 2 Ô E<-Zr\WcC_2 ڋC%0Mә*I<ݣ=/C-C!L/krlP暓B¯.B#X0tLG5K$otvY$?4L;v+;*2C9&K4=s}ԭ@AEZs#ĕ:bфȘnQpE*JbA哵!)'mJ8*.|*%d,Ü(M)Q3Tb =P8:ˣ'0bMT1Dґ2DBeDZ.B4͠ 'd9Z ;OlMBs {Ad,,d"pJloL8W8'WP -V4FQ7%n ND:$I:|€G3$j,hIȢ{Z+3[Rv ¡Kx%=S4(#͂*yM++ RkW51Sa :{WUuB0G WI0Ż)D{Ui=BπI?ׂY@A>s"EkOЏ922zV9|TEC͑DXGWmXŕu-HHJO3X;6;쭋N2͜VkP ib3j@=5FQۀ II s[֪EVӼ[Wy<CuD\u8i+&[0D=.%[(I -Vc@!;C!5b|^22#^"a;531$X-4"E8Z-$ ME_6o4LΥԶJUR3}]^D$"DC?uO 0x)^7Ty $6_=\KS}كZ;_b!1ݙ\QIԈЪ"4hWw.q5LJOSm`1\|HuUʔ?՚MU]0J^y ܢ0[HGauQ<N}^jБ^%yʐm57E94N,Z8z&0ҭQͺ= ] L3S)aKw)N;E…b{$0os`4$ƳL%bvgU6:dSDٴcnMj@ue~Ɋ&dִ~o[blcz# fڔ#)Mjmu,dĊ>H6`ͳ6rfc^iPT]&ᒞ-f%_U3ܴ1l:4Ah~rdtSf+Z%5j.R80?s<]/IVE7vkHytts_ĭ+$1Kz>&R:߹,B0/l(w%(Ѧ n]r;a+,٦nGV6l #Wch P)fr棱Eʨ"S^gBF!Z EVo&Iz !T_|4@+;ȊTܵJe݌ӻ]Ub7}j&ovkKThW0 ݉f /'[Zhݙra3c iX 6s ?lڭ9c9\O6q1!pÅv BZmE՝6Er:V<Ǥȁ䚃չÎtޣHo_AOС}wl)7xD*`_Tw/]D_lOFЖyvbvjzjSat~Wd rx^I5|G0Yf3XSOjIF`ϙWr4:T]EߞW>t՞_F=*_8gGc 3Ik~И2iub~38đMWԖ!MV\aIe\8ilJҵȤ͞ XL!-9JqY UQ K1m| !R]l ǠLS<Mr !_ؓs V[1!ie\ aXR[m]YKוG¬WP ie48Iر^|FIPPKiّ"߀$ BqJqԄ ag%[n p0F8E0!(l9T[H1qYeUe\A509`dm1A$!52Rx i dZ)FH=ceQWd&8`B0C@_0!ˤ mЖMAcPGu$5N !3taԎEed;ҐLt;U>S)<ȀMH ! I0]Hž) Ab[ 4F]PGayoS y5&m\YHJ" YfE=\ 7\Vٕ:%!F#j]~m閫&aD%"]HzEYD+`Vi#KF/p=@* &M3"9勀CҐlSUgyN)JTpޓYjTW~UV?Rhf,1CA@,2t X7 ȉ ']ɖ}ܳink&Y$Zܜ'LA #dퟌY$lv~@wC@:ChBdL.ZYS-̝՜EЏb%e(.&U)OTH.Bjzq-gAg J]3B3l@'tʈ(^$DSIi{f!x Ԋ\ ʦwj "9(Ġ.I Y%;l@ t}q&Ka駰I~_a. (v;T 9adt9hJF(<=L;9yþ>7-)y_!a:=":%+^[ VZ!WkZ#YqF@(a)*Ђ~ݘpN*Lg!&qǩ!֊ɀʡf`엺 ֞beeIPCXݘ@iymd'u4Bk^NH!#r*&}l.0$xA+gݥlqm b\%":ŝ!FfNcrkjm=[(VП)Ё??É=hQF(}mV`-- nڝ MIAh֤ú*zgalNTR *foSZt`M؀%^Z[IOVgx.8QHcӸ;)0Ze٥$QwX EnI0lβ !%˵^n"FB #=+RJ,}SedpiMK_R!-n `@d].0AUkd#N=LpICN hPz-*l^Mmp11`AK nbb1IId{n{hYi(TVٴґhʕln~⧧z*)l2^V qt T*/S)f[iZ9[UcжjJq!Gm& ,SEh)?ݯ Ӻsxg}Q*'7+ɲOqbY w-D93W`3Z3Υ\U~2 #XjJ&S۾P. feS^N,tDUr(mm;>gI''qp[zj-!S;A2N4p"KskkwiQ՝^5z11oe)mae1kGӖtÜIēÀ5fs1ZU^l؄\bbư2[Š/o:-M!B;ZN+T]S4GSZrjuZ5yNYJۦͳW30HI"yov=u0i*4Q: IozXoz@Z--"7/@"(*gMs _&<3bKk..Qb(, ^@.Jti R_L_j#٩9ۥ^˷N=S+Ykh=d njy䆋xJcxEw4`jonjNU]wQ\%>t1BPD-ȥ Nȵ@'~9S`0ypF8y(vc$9yјw3Uy  K2Wt=M¬3bd1e5*檆9y 8>7SKcǃIݟkT7!>A!]OBS-v=y ZڤGU:X'r1gVrl$J8ŝa4,`(4^dU$V,BsWz繲4vMmB/"imo^罞-YbsG#{eUDAs_GS57߱7B'䙝 o=-K7MVcߴ@ 3ar;"\w߄ӦZ7Z;2gQPYz!8_ZgǏ!YN:J[ݪ&p̐uy4Iu297o-)$>˹عS90<'kO2@xφ &$hPaC!F/`0^ԸQE/f92$0 L@I.;Ƅc˕1=ĩ$02{3F,9EjgNCSlO(r͙QybHcYVkұJ= 4Xoކ|;patZdQ{l2ʫmrg4=Jf:WO%GֳNX9̬6gϢcWIs(–k;nM)7(hnoaQ0Á|';Xo OXeO{Ü |^a|vfd:ohcg,,{j7~@l-*-@hF0/NdPڌK30ϴQ+ʔ5S6d72[ |nS,55sz(IBSE[Qկ{uKՈs߹~JaW;Y+08HR8u35Ov;8 HܖP=7&7Y%0 |2Y{v1Il[OBG MI1Qn`QfWҝ %rHi^>5c/?`@"Mq~ClHBR!-u+\{ٹ, Ӗq2>+.?8A&S?UP0kÃq?)>:\a_r!7FäZzwj/zHksnS樎J?3,Ω5nl]GK4p$W`K5?>GW30W tJc\8vVw/y!0VUR/.CU-IzZ+~>HZ=ݒH=ՇS9WtQ,:ո `Ɲ UT it192t1,Tz 膍Z".:-j [CIIpddܲjpYA&8Vh:&$LZ8!r_L&lS\&cM+gUk[_f0nu|iB#Ѹ]M=xg$bJ\I#9ez#. eÂplR1riGBEB^Inp"+lY^'ѩ G,KhijXR6 ңb 5SR0 Fjl1G+c(bo: Dk(P92lyhLXmAoldʖ=b&M+nIGP%X04̗R) IUΘճ (})SG㺵:odV.ʢ%Lb ),͗* "![|䐚.P9IodTAQ~[i5J}L,5|,Xʳoi&$`Wr7-J,du;6w'jf d[k # n*e8ne0b87YE76Z榳-:68Q25D"(dKp/iŲYpAacߙn^pG|WB:b:2OJY}b+E Wpcn7LɕӆWhMcޑd+SfזA{&LsMzށG|kh9jkȨ$J0"3}#,QuF9e(EY=G$pvTaR姓w+VkR-Z% w3j |h6$gdw5HZRvk[MnF6f4)gCtO@,wyPW>.bDfݮ ݾkuf3Nni?dc|a5*NGգg[Ȧ5j&Zr7c8Դ n]֔4Cvm L(s|b!ܖ90wB]nMSorG {[ 7=qZHQ-Y֠gGv~4dSHk$GlWùq w=b_&B:۱ז1ws$KvغhBjT}+촄h]N H6z$ _p+B Q_f,%.Hh.=:<')KJI˼0,кn~ ֮ Ơk0Q '.,W^kSo"z!59zfg,tX求<Y3zlL0F|"|l5q "mr/^mϨb21DK&x',O"|E $sx5BwM nt9idSM˦D iW_(f]N#jZp $OOb:&Z4~+~\zDv+ emkr$JDow~,ӭ@O|7Ob5t̀Qıy'_Qf7@ht)͇}VVLЄ52xh7! /r&;Fa[e# } F 9w /H+"8.݂ēkҖ!ń-"QH5O@!!ġ4AAE |cq\݈]=2O1wT>SrKRr_ P*| !VnjAWT|]֑fi|s67+,d!T4tnVUm9Ѹբ!hFE4<8ar` L` 4[PE_8O t4-xwRah.mr6^5E+JZlK0! v@ С4AyY)/:L4.ً5[7×EXb NyxgLv8n&P>0q+Hɪ[ vK9FWd &Hș1marxk%hCl.c>PmڣqS *P\1)qI-` ql/?AFa@@ l)GJ\C@DZm@z֠5#.TeBV;ꑣͪ֒Y)dpKYRA64w ( `ۮgYӇv S tscU.;?BF GUXwק-F,}t;!7"h!#IՍxZ,93;\~Y洌g4΁zJعrNJFiԯmTOL9`MuK3]yPDXb\vxsIkƱxH[ F- ᓹ4ƫr-5E2dԹ6?lZn%p,/TUM'BzaWW.ɛԁR, ҅Lw~mTa?;gIթ4љ(\Qղ İ+\^˩S;i8`(~1.I4tкQ 4 PI6N)F"3=5D!SuOUXkZv(oؑ}W׻FDDd݅)MjC\K͏Põ .i]NяÒлQќɖc.dTNHw+Ѐ[յL_K3@6m{Ydp%Te4H7O=y^cewڲ'ogyʌE䙕MtYMA*%lfQh%ؕ!1g 'x5þi05DX qOZi nБus]8u'V'c=6{ğ=sMe b yHG}%$73^SBZq^T"/`(p„ l_ \xp_!R0ƌeD zT@azTQǍ:ҔeM64yr%PUt(҄6w9+МXlʚ={ֆ lۺ} -Q5VU:H y5dCR{AL|xףYꅸPv.V5A56yèO.x⍜kmvS{-[q?^RϲMӼԝ:)53Kƚsþu[2/\r^ENbWGo7)¿ZnZT{}tGZ r&]ci'e45Ri agU-[tv9֜hu4ۏEm홨͸Us8pJiV"G' Gny~ǟh tFHn>"f. YQj%yr Rf؝>=~_(ymh(U}y/eqjQyegeI^؈x!9)RYclJdکҥ]Y&y ٫MlwL'\irh:(Zt!vmf&fdxgҹӥzI릐~hFu Mf.-u:f!Xi[Z \߶'Z$7ٷ"w綹AV{c"cA*jx*g>K,Pꁆ) p]փ NL[Lݞ/u(*,.g ~кKk:kxc^muVhէ-%$[]ZiE>#-!E Q.],1%MЃVw钬ev6cKLC `%axW7wpB'b;}+weuQBU彠'?]e3/^q2YzbJF7υ)h**ǫ kwp'krU;.މlo2ZItT]h%>(b^?%V0{ɏEX׸+B]n}Gܙ̇<HU_{YaT UK oJ S,Y:hƭ)/Y i߾[t$-o yYtDESM)UbnCj8!bb&$71ռ$19;O iQ!b vc];Ę1ՈFy b ڧ1|eXIOE6Ua@ۏL>IE+)-&%,j/>}94%qbtܩw0ל'.Χ]h,]~2*bc!4F]"ځЂmѹHwE6Rʑns dIHn:4VFdDڹUmӚ-uVi-NMa R\J_ [Ⱜ)׀6'j=mB+g."}ΊtM'!yN=nʄ h9.2Y33 fQBlтFD*Tf m[yQ&KQ̡f{qk.\CnM(L[X _hkMlmo9 ]LBg6/ⷀڭUp.[/aD3z{=ܛk(Ɛ$hX`fEq'B} V H|/,(WlӴ2Uӝlѓb)Pnk 7j8h=Luݩ3y%#T TUh6]f05trjr{2ҜgD"뜊I_J躼jA!CרZ:k,7?Fv43!:".BjkxْMS$k/ɥIhͣdʀ'WPAίOI: @<̫aZU]~Ͷ'ɱqx0}vE"3+ZsWޣw 2y0f1t̘ #^MuI;83]-"onBUm%CsRc.7zfJng4juW~6W4'ZL3mSjG[ "g-w]-U(w \U[ vyؕeg|tvWi|v8#vxxB$bu9.!ZaT")S?"RC!ywnsZt@`Xus0rfW"v'QCUo>u1@!W}%hx&v'jD8bqf6i2*70E8O8v]D$pp-'&D_bVt}:F;:R|! hOP"m3vs?o#gGKr`*0Kt(*V%5~bL^(R䅦Ft8?w-5 ^0-(GMcSc|W-q1bi%vM12cN%95G.҈Oֈ6U e'\q1cNCd h)4HCE=WLDE u:< }2vPRɀ^7LG|ӄtyqDu'D6,m489G}}p3C*Tzu/8.()fGc7j6>;yS'%XJIUuuuo^v. YYkBB5_s:ctSDE,g>Hp.9Wm53wRіPguM}}?VdȅFH7}Ń~ @.GɅnHKrgXK[4 lqk6-y6mS@x:XlDȘYYY-ym2g=__dnz"ohE䣗S>0U2RHQ~8+]3_g\&\zij6$hIh|T&bF.\KXHZ|YFWIbK}c9j{#l{D/ҳN6d$ef`8Ei&8*&YeB5BGwWx4\㴡i'F'v|HbY{yɧEx2i| prȋE0ZvVȲb\kœCm ä 2e&1c('7W_& !~(d_gzs7|5'$gy!J@^Y)9GQ\tU ֐hzGw4 }U59)iGNFqI.#CJQҧ{EzJj{:.En(wegxZXraq$PQfb/x&r8UH T&#YȬ"+^iy.xsHՊJD K!%&-${Rr>ꯐ/VVVKgJX< P(vWT;Wr'kjƳzSȈG'cYZ]z^w 7,+4tQ{˒;p˞BR3H0;gjzk X˻੆bU{{j+8>Z:8dV WZMT|&l1}m뻊x2aIdۋ!aDb;6{MXh(SkTrkټw9f0u X$'bl;g<gH̛|d6V1r빬?Vp( $ӊŪ(oZ1ccgN ƝlktܺXt,WS"E̲s46}ɰ(u01ցDX4{Mf۳hH*ʩ 0ShɫS|SʸpЈn^o`CvxqM'vǂR zI|Ȱ_dS33i]YzCl;"<}-$5zsOY#"' - Bb$XfYq [!⻖%<))z?]Ƨԥ,XyzN}4}S[ -H}$l-@ԳDȜ0KjƈQFVMakqm|9vhJ{7;=wA8fNyD$Ǻ/ 4Y ղ#ȆtN0i=>OEIu_ m9y] j@(6Y=WX/R9cXX`uax ]|{M6}n빔 わCMyl~8ܣ*{nZ"_wvi~aZfdeμv>tI^a<{7嵗&HP ĭ./rhT i[κVx]9Sz̭lyѝۆʂk-8FDɚ?̛g zax{ e+rbu' [ oR ̆DFv٧)>rYxDU nĥdά)F傟i8xy¤VîubEiJKy'}\A6&Ud:.ܧM+v몘#a3E}t]^(5= եÈB!Zt3jw)"CGuT~oZ0RS󋯺HD|1^a#6lɺpDdu`C]&d@D=u,1Eflȷʑ2(/ܚhWXYKќ]Y᜞E*NW-ʽ;YwwT.jeG; fDo4:t6a+G`Z ZyX>mWLbKq|o-s](˄lZM3t%|Fqػn[Rמ,wL?5ڨQrXvNʮ]{=ݙ{D `&hW_*FBB #NlHA!_#:\LG 9~Hȉ),Rbɑ=nfŋgh%͖ AZiI"4#Π.4q'KSQV-yRa/܊5Y Neqm؈[׮ ?]QI{'V3BhTʕ5"WʏrETʇA e\ul\TsgJ71f퉊!vάv˽w :)$O]yD^Uݛ{8 K\L/hN~zS{VL 9!j*FZ:K=Тjϲ#-"+9T[θP)zKBN{A4m4D 3.ZkcIb:؄H gs%*MCTM3mj'a8+"/13Bc ®~4.2˴0/K,6PE>`32," 97[.HDҨDпP:SCF7'[J-̴T>0HTVJs>@rF%pljM  ™$3´xOo\ $RLlòx],VUU]\Õ?R;40ꊭATgHFZC3S*6pW1QFi:w\"mG%e넊DW*e&i?ߴ/)v.g4O4ay`6fM^31+T>k7λ'M$ۙ.tn[%s?y M7vC~ɶ6S%YzsVpi}VtM_rfўJpB 3WٮiI+ُQ\=W3r9:lRT So-մͧ>z)sm[c q+Pa~aD["Δ!om ]!f.[ƽK'&[]6Ȫ uR[[<" io['җ78e^o  `P#f^-' TR߉6TkJOM 4` Ϥ&H򓜖aH7.c"#mdL(oyCڻ֑oA_61gZW&iD](YV+|WnC8m43vBNc(?+;CSe6 5ia"VK(Sl2/&Jy2еٽ?#"Ҁ¼S[>ܸ\L0 VJ|-Bv ~tng&tW6\ӎ{@Vz G*v!C/'n z_`%$&4>ONpqV@)6.2iݖi5b_dsZC=)^9Cy pbW0Ӡ#<,_-_bmĝ/S! qt VbTwq˵d+L{^l뽀ɪfv**lD$!ԯ-i5TP앟y=U"*tJ&5Wg@nKTB)O,8/799y& l-͒0r3i>< c&PQ?Z,lj3Qܩ:E&]N`VGn6Q8U3Gl8x1Fy.Ievմc7u UCȤ!pYb? (ɫ5Bc^j:dKUMEczS*cg5B.//c ,8b&bӗSCĺZ6SS&SՑ[ E e}F=&=mbs[:NfADHnqbvLd4Gk 4d`UtKcͶFظt#Ŧ =""QNH^H8Lj&-Ah4|l&0Qz=RhNjtPbZFA7ede`ý V1EL;4A?;K]:Ն// tZl)|˔*A&ڔlrf:-udFߖ_ZFkaKm;1D+*fZS fm-zec!"|Vt[݄?EcC[c3pI[i5"m գh<;ʻleV`{a1RV[svWy0*epNpK"iFtc&C4f."}Hԝmz`M6nQl(Ee:ql>c^vlk@$ Φ1+bBoNAO$a oE\ϴVOiW}[ʱT ~IIy%*&f:#N*^.$Q^ʝ}qPXs{b(?ഔ#ogT>l\LU];wZIpSPZ- [V^u\iZpIXXtLUNfτ+sqΉj')#YNҦuFmHܭ֛mg4,%fN j$\dG9C0Esoή=0ş: [}SI@Ky$S[zmQU1&{CYf߿FDC/o#/&kjq*j0WR!b]_pRFjbBHBVf5@-췣$(vf#JEGiɲP8^Ƹb/D{|s>Tnvٔ14eSoaYgZtkOMt,^*jbߑ*T10"G%g)]Lm':u45Fi2&g,^uMh~|\06i4>qXMzXa.0*ױK_͏QI1 8Qç>~:_Gsk`$x Å5dA~aHp`B#>"H .TXRʌ[JO Gd̉79ҝ*+.4cO,z!SU.:4Β ] Fڽn+w.?MQMKҥ__ X 2n1J,P*Zrb}>pd=WsҖ33qEZ3fs#?TOo6Lqʭ>^4s?6MgNN\})O*2ʡu L~]r7jԙeuIV-$0Aa]E7\LFxTovq]P!VfzY_썸2؋#8 }W `dBvS"9&MSbxWyT]wI eVt:YzHQq^8cMm՝1XYggLdpujގ9D-/Wb y=9'f6_s:mZڠjOjlb'j cHYU +Sp>ZjZ=My K޳碄J`{Af(RҝI"zM~ڒdcn./L.(MQÊ._Z,fZ׫J:k؊aỬV{{BWFW(.')sx>ZȎ)!,~ŚXRǘN(/1`^RM6XM59R?FGdj.* 6T7ߊZ6YgsE[_Y97&EZ8^V*;>gM 90nޡYvE¦7&Wչ͐#|`n ԳWs=oNl<Ǟ{jU&㱪<]m0댶vw^_eAh]S= nie>jU6UxWQ_պA ǚݜ( uݹp9rUW[v?gV15-K3'm9ҞHE!9IYX?tuO2ThDAmok1)q4dlAC2D3_ޜ0:Lb! /',757K+h#txH刉ݹjhI&(&3=$%+(9Mo&g|fR.4pr fA c&,Ā K%Bar)d$J 6 ~4?2ڪVьJ'I͟{!.X>L+&?rW$!q{WTЈƗNC\_'.Kx" OQeT  )MDҁQjz!IQoshJwR\bM0*ɋr;VNi+zQ7(T3hX?wBcxj8x~CY 685.j %&}`UȤ^5*biY3?l>AAKLof:eP ^]oP@&TBĸ[k(+ Eqm#evlʨ֓yVJInIB^> \D\Vwor[ݾV}h2 .zخ+Ez!8smlԆ%G"Z2l˗ԗ`=-u־KǏ-;3aj2,/^08} Env*H~io[,`gHO]y[׿Wg˞̗e^'˞[-D$#1@&-;ٹxbNBM= *P<@{2N  7rglɜ'DCx8H>('d%:T@=8*869^\=D5_Q9DZ[᛾C 14 \C)@6 ʜ&ݩB2C * D 3L 3x<`<,=C]ab b!`@!d,!\IrU(AO\iy]AAMH\AT _pAppOfiJyT`i-FJN\fpf[#A.DoRWOQ`ɖEґAԘAלLȑWܰoئgϓ6..jyTOpEfVuN^E+b1AAIppgyiY֓޸ںշĄҘҕ՛ːܩǭďč‘đŒŒœƔƔƕƕǖǗȘșɛʜʛɟ̞˞͓̂͡עΣΣΤϦϥϦЧШЫ¨ѩѩѫҫҬӪҮԯԯհձձֲִ׳״׵ضطظٷٻۼۺڽܿ޹ڳljކԩȰp H*\ȰÇ#JH`>mݑ$m 8* r$HO]1Gd ~%o 4%lй4ie5 JѣH*]ʴiU :AGǸz+1 xnÄ` BJlh[~[È+^̸%1PPR d2`9Ң v(R=T8Ly`1X0]֯c-Ԁ+_μsB E`G͇L = ?hq "Ȼ;T ( 0<0ߴ,D v", 1^y8Ѐ֓gu   :CbI'ЁZ%W c=P1 :mVn"7B"ix Q!FXї V&*Y?.q@6#P*KF3JS Θ /dsɜtʪ@7z+. @I3⬳c@x,h[r ߑSճRAL 2Hɺ@'X)a@NpV,Я& 5Ƙ€BSc| EB C0RCr*s )hĩR2'03J0}\@O|D ]OF#h=7,d 8 8s0uC3y׳7އneAX@|-%А APm1n-I@lGN@I٬\;(4\w{.{=@2ި#>ldT{BDCQ? 2ܿ{=__2ژ_6>o#EHjOL(,2 s( ZDƴBA ztP.OAb0(a )(Utwp|@S3kPB~ l>0-v `0#9aTIPH0 gS%Ɔhغǃ1!dc=(G9@FB()F:PD!>Ё h(PDv sx-~ ipIG6ұ 9#U"ĖAeaZR!o.(;Ř y.Ydd,^X6*/~mv 8Ǚ tQx!6$ Ai4c>ʈHF_Fs RMQݧ.T (1)bѕzhBQz4C Іz0/o8g7SnVpD!i-\!tC?i[f)ӤSπ"3lE ժnUeEPg撙t(=JVբKH =T3 7ǹMp>ݩ8 +]$RЅ4=5J_BT1p mUQRU2 d.յ}kjF B=ۋJsUHM|Co` /gMeS3na}z؜F-,DBСU(I .6UW঵`m[Y;۷^}mۯ#C+[׷[ue} .ެtI6`wZaԺٵ+S0yާW>`MpiHOu kY}\7 l8iJ67Qn+\.g"gD `fA }3GP8똫7m<c4?hVΝ/dX*sYƲ{ WfluL]b5R|c|t,_Si*}US.@.P leIWxX2b3]e"2_BH:z<6շJ볪sjT1-bGp/,֦>a+SҗmpU05.o{62Cqf|Xq\݊V p) |VV6weI]t9CCDmv,Unwa\k/Tw9ZcKXNvcwv\ 3-@43 ]xm !oә잟X+y뭮Q[w'4\&j y@U gٛ6˳Y9Kcu+{Lzu_;} qkJʥfh*搜SwؔY`$Xj#jG[if(ɸQ+. Yi:H+طGfJKw w zRg7{Z}[NiԷ|\GIsiw곺 mjyz; <'ۍ}sb 2nSYK,˹8FQ*[;Xi|?:xX뮠);)& rP|@aU{Y [I> )" w^J ދjzÉdZڸKqeiWZh&̾TR̟ mZYDgC~xW Zp{rHw( fpm MQ`c?ȦEz9Oh5\Xr7P;Q&]zS[ŴqJ]Jq{ zX )xKΕllp`a7uyK{;z>v9gXoZ`HÍOgVM1z ',j⵺\ڸbˡ;T)vb]]o/z+]0>ڸLl~<ݍQAzmQP…:dbD)^XϡE 3~"G=F,ңȔO4 RL~S/=+ʉShOAozP_Mu hѤM)VYo2 )UZ6gWN.ڸOu-&ͽ0]lƗ| |aBX_S6ͫ;} ۲4le[UQC_e:ij[l[7>.CZ$8DĈ+_ƞY̔2!OGx}o/rVEs:k}_8**䪮֐c35;/ǬL;OsoEiFZ\/c\P|K) +D@$'^@]<×zOO-#ʹȴ\ =+eQ][ 5E QE0@x]^W3SV/J.w^p+DvB6ۢrgeӠM1 ^C\dޖ_&[3I.-%SJ!}v,qpkܺy㹧פSe=(?3=f 4-4x!W%Bu9eN' a[9M,HG_U "Us)_4('+92ScHwURT4̦ /xOZ 6P$PDqT ^S"5N S$& -Z%.*ı)f?&i;Vޒ1K $gXTtcTj(gJ5okHv5Ĥ ?V  Vx9 Ѥ Ҕv~ Գ$Ymi~\iSbu] SeQ? muIZ2l"Qc<^&+Ze:1~#o62m}0SZF8x,j$,#j|xBTD7v)γE!l,8- -;åY*xHdN(HjR2q #&;еA`]2#GS4>:yT zXP) B;;},:P΃I-󛸋.t0ԛ 4DYK-w*,E;Er2=jA zx4Ȇ*+25/`I-3"¹F \.Bt8A?q;/z+ tqVĥƸ9LlTxdHnSMmڛ bGtZ#;C.qf2lR@G#ӓ"F PQi&9iڢٹ lG2C963\Bzs" BC{$PMb8"Bьк q>nD& %P-ߛ72:ܗH3N*+9kP~"z&2H kqB!>{=jךAM72ZƂAu9(21,3u1t|;I^T~In(|F+*GKdOO9`P,?.{+R&U0㩣 âb524Qb:#TMSʻO8 %m,mV;RS"W'Mrt,-P`cN\Ȍ 1UIʪPWŸ32U'"1ҼDJ ]uVfHTNWDFkhdINa%ϲݹ@|bԁn+o6>f;HJV8pM8+Ud1 g`ٰ:.ܳ^2iulUa/H=^ __ZpY\PZ2^U@Ɏu(0 Oަ۬"EU4ൔP_f,_VBΨTϒMѰ_V,НlSFkGKbh(dO»V^>ʹa>0=n'ULߛs$N t<@+8kSRDUaq0Tod?je?l:VbbdP[>^ K# 쬇9IRagEq_00xڔ!ɣpc99&ye%j@?=2wW[8&w8ų]rz .$Tl h!JS]vCIjbfuU xWFj yvbAk?cFTT҂8{V!{!"6GDr.ɸA}LbuWBlf[˥ǀi78^{t.0u%Xl5JBzYQ/GAoM1'_| Pa ‚2`È &t BQZB#H6@~IdG*QlydK0StqfȚ7u@E%ˆƢ4xiШJ&EֆMf*Q*B zZUC)FZ5ث ق5ըu˶e(^p5p؆*ɳI9mrO<-L3@{B~buZ+X'+[ZO^}-Z]J.߼۶{oį+B-Ԁ2WT睜_>v,+ϼ=mk-`ׅRd=}(UP%B1T(XReV&`)XWi^QH)Uj!ڵx'|.^x7f-G]jph ]baixlݖCP5WI\L^%ux I [YGMGQV(YŹy0G{y#K9VkxeVd&X *XDE™X_.(\`~$Q&_ew|Y{x٩c5PA]u9Zz)J$Z9K [tinXDHNkd^QZgl Ypԩ>\4FT,TldFp877SEIo&/]o82IhGpͰ K.MҍЌũCk#@[P|vî{y 'QEVDt%D70t,O!;J*(?DUtqKD!rp]X'SYqv~M  wIM4g8Mei|m^4ZtX30Oc ^!3>wocr>Ic>OQ%Ku 9 tU#_ ^ caK| u* \=~Ս*''j6>-d4Jd)xGnP5zyr<%UD(03_ ۭk`ױ3a|VH]l6WWaиv往WD$TKMUTH;_L"쮮m#:Zel:],tc+[*@6 +sq؛2y M+rz񊄠ѮY&5.je(yUyȗ6M ʬs-5+UA7Ү2Y4Kˌkj1`pg;gJ/!O~k]M=tw\vZmEUEҴFa ڡ8{8)3MlCf-,bng|Q:7r li$[ 6 HءrY"Y_I %bk :pWBY|O%^(o묕NڜI!qP'uP@YUo!89BQ Z׮=߃%z'OUf͝FV%1e!}6*b! &V)-m-tAxJZ! ʑKD]OlYсlѱi@E!"TA"mL_`!>j%+qݻf aG] ua^rΓdfd ToW1Y !EMפ]@O5DIfE-)3r]y^I=JId.q S2^l n|o]O^Pp]H߀$+mb9sғYt%z1ܦQ!PUŢ IFT. qؓBښiCOǔYcLUXR\^lr < "1'c#* e `t6$m\p#~e!Y̹YԿuLdq{w] EB-YP] tQOuH^C=[+A^V1JɄpZm[>}hѝ,ReVu (śt:$SnJ° )9xFN,uy`DbYUiAH 4e_#Z'N85yWi8R& >P޽(gL )*YRpi 4D_h6gTme8UQ6Y xMиUaF אN4 ^ :A4 H f"8ZdjE^γzVmZҚ=FBMazdn]F"Ի'bcfk$Prif{ر ιpZe>i'!#ټa%MA妥iZYFʨXUYYuk!hvtrTvӀЫĉkXN5_ouY?z%Hnj֍Fbrarv+mfK".#*fs% jc|y=J[ 1 vv(1mkVFSw#qT% Zg$)jYcXt jbfuDl)}1"mʆi`! Y ᣂC=,t'<"6s$FS)KqG2Sۆc:2ʅlWR3ADm=1Rc'(3O tՒ4h4fpPQ:dB'sq5G""ͰrxJNE#%iÔiu|{Vo&?/-&(ȾkO`C 4t3 C8p@'0.I3@%gqֳͧCJlv4xdE~QY&2 `v97Д3π< &0A C'.d"9>+$uhZ~U63Ϯ5ԗB)"dV!W\`MTMZC*AG^4I6HV3%88l >)g':ojM\_fpkm6 ihϴiT?naV0^x|DȷY<ɶOV2nfG6!nX]:XiL)v۔y+l!uQYKυlKȭѨ4ߵ Y 6.߾$[ecRhiMdybF3ѥ`xz2FFw9'j$0\AL(Cj{kfʰFz^ݱx>YY츸ofohrHh8/$!G'/oA,E{.fVE;u Ҵ2:t3+/P,~_%=DVE2u|% ]Y{5e륷=.:AyX;!W&φwYpbVR޳ *VuC\7^WV :IHF]`ҢfWRՂS:js(mB>28%&h6}"T+BYsqx3M#U#ܲ}3X gbOr%p@,$֛ J'-@5P`_*2P  6dXp/N0F=yFCh2ÆU8L ?iM;On9$LeiV OFmjczN^}5kW_b l٨92$A[2d'P ޝX0ˈ3j+T۔}YW1_ƈٓ_)Nܓ.CXgˣ=^[4[DUXXmOz{wnc|oR<+g6 zv92 r6UCt0tL{iAh-A (<3гrZn2 lk49,JtC"KŖ2I@آ'r?- E ir),) C o3rT>LDoFO'﵌4¼QL;7LQ5>ӀN@,NS޾- )/);D-b60llUH1f 3kSѝVLMr,=BxU"WKHZCƒP k²lJs$5[lX}sN5ՓΣ3#"/߭P& CBYPC7үqTZ mUqxw+ngmc33@nDA)QT Z$UMjsC W8A+:zSL?HQYS@4+xųX)l1B<EB @ĖDˉ麚5"ڭ};Lj u9Z&<䫂h_e0Td'&M%O_Bk 5BN,sǷ0* މ:俟`|ަ$L3$*qיpY44&y } V_6]V-~Jh<;fȥ,6bЄ%IB/`NƜbid $ǀ:"N 9eI+<)dAk- a$ ?82"):s0=V>CqLPnTutfg~%Δm# WI (;{Tڶo>_@(W13#=Ɏz_7DqSj4DU e]6)1T9 rS0e+gkSC^+6_48喷vtUGgkAsV eqf= 1(fnlJx*^.Fvj{ /'(tcуu\"~(NƼ $l]^ b}6=UAetv%\8@-fb׫9ZmfQf^q}vU!&'|˳V\k%/¤PRsX&Z;-(xb&O5X2Lj̫ oRN fE\,ZTUdک6` f fNpY4oc}h sg,4:X-#-22)Wѝj4dzQSQa lINH)+l`n'{{Qyɶ)Jҍ&@ 4l*U{IV)sA啑jܟ-cS?i|29e[aV5 Ըn1t`sUR1?k[g?L~Æ}sBQIրsQ3j>r56lppkz@~%yw Q 9Cf?afF^qEW r9z=rp5Iqp0(6vks-۵rn-ciWVCO2^PYxXvRn%eNsg]#*8U)SibvYbOsufƽG0ڨ$88q~-yj#pve҆lppvA8fbnA&zLHj0wo~Ÿ.ت.X&kU䌾vD QIi(iubM$mˎ"Zoum ~$k,F*ծ+rύ q0fB<&.ohQc|| v O1jhlO`FL$M~ H.֬Q61б -I2QI"Ь*Ạ̑@ I'"Џ#`02h *ǦQ [k#0Pa% mlr%d[v($8|W")e-.2)v-~z0B:)|/pC1Mbk'1,NkfnfE]H0"J)qf!B 2ch!{Iijvkj=SG*QQ/xqC5 >dh g0 I0 sBrJஞGuoh&2c3P(פQ2Q0nG,W J A * >Tq.'P(ej%];nĎ1 dĮS0*44¯$) nFKly$%4Q+RqB(E@O~Jf&'Ct+6"v~N;^qan2m198pP\1<S$¾LJ1HtWbi+MC&F$hR,r.f_2pM-WC%HåZ5]5P)iQD3jJ(RL"iܼ0S'DѺ^1l d-)c>.2#IB4׾0ѱ~dJKk.Tѫ&pg۞vRKmȲy닧c4C9p>sI:w#s׌?j?9ᢸ:HPpMF➷bQh zQVe+VJ3)K13I[IОAmZ캠Īl&BيFcOnZψX]W;2dU61[[WNŦEswղ 2S%%YfwY }.˕._jO!&5U!qF4o qUipj O|l,l֖ 5=x3\-n_to&[$Sy=W~8*ƐGvK6ݔ?Z%9 9RO?MZL0p9 < YqPsixELFGڧ./*0hΑs2AU~vA)mN'rs64S4̈́.X߷4uxsP/\LWMZ+m,Lu'˪r̓,s:C7-jMfҠ+6ii|4 E֡-Px$ZFd~{+`I=11gNIdKМKaܢ4WVLpP LQ I0852ֽZR0a;^JoӷGQ\DMrJ1Qi4̎Z|2.|aӓ rDug025݈av[y+ v 2\=WWӟ3PĢxAYT𸜫=; މOSwGKW}0i:rT謍=zRi&ȥ39 Cؖj2RɆMύ_}G٫HYTWS6L龯L_IY`;[V{GmT%sg];+~b<2l#M~LJEy/e &LXB… Z|eȍKJ ǒ^ٰQ/̙4kyΝ<{ "”Gx"(JGRԨGVtI^6hq+TJAe iW!:vb֬yKѬZf5JذƧrI$q3z75o Z'sfʐ j`9rT(Of}P-l\5znܻ;qȕ$K8uE>iԡBe^tԪK%&3L=~@٥#sX35y[IXZUfxTE%֔oѷUW*ysnq_yU5x_dfcϵ`Qۇ d2zؓ|(jKJVވJV`sV)b\irta-VՐ6tW&dGjyba}Et%pbN$٠<hbUWV`6fd,4叝|h_f*ևYj28qVܫ2^ˆǡ>'y:2,) v&݆$Zԑv d9n[YRƥ!gP1,NK_*Yܥ")d~8,:=".ؗwGg.V(p{fcl'fz/YkI֧&ԑ6[6o8]<O5YL5XwV-?v2|hH5<)\5'K~Im*-c5Gdz멸y"n0!|dqYw})!93cA&3Nൗ9~1+"tKo-4&;9(xs(`ˈx*UATA眃[D=0h;X'gpaٲ79~kN{眥y ESíMNIj3I0ttJ_Nc9 |`<䡮5xijbD fHTuG(3R7rj&& 22 x*_@uYh7baԇ(KALl@nV"K< `xLf 9 a` .q_J\.Q^P$ʣh_%"DA;.fGp@)a=H`Ix_&:X@Ѯl?DxJTТW4MijF%g~98 lɊ/`iZe0˄J KьUb6QgKK ""I AíѪ4~p\ADv^UVpikd:͙!E@s[D8!<`;*dT~$F)RQݤ \F~AcTcSڕYƑ+sZruܭJupyLIz^U!Aՠ-*Uwf'U%|#4(MՑU9HR/66(StA1r*n47rrG۩L \@vSc hFt*P!3NڎJp\R&VTwk8&_e}[Zɽ:Fr^lk 5Sk \w?{|rk3$"ӴJ*''a,W`,1 Vm W4Pdk̼%n^Y:hne"g|god~zK,vEsZRYD0M",t'rp=S+Q53SDr.[M_>zg0HM]cgh"Ҡ~u{;:;|eb_[7&A[2)!",^su2m7r)3nFm'aHFO+g[TW.cmMny15(gd0RD/\3b)("2?$]OHd7bx)g&m9AG0Ň^vрiHSu>*d:}#PGFL,#Ff;"T=t2G)6jnSfD,NQդXhGVWo,1l5(д*3'6*{Hw+6'$6E(Ez550ї&@&c)uRN'5|hj;f wA_.%K`AVvw\׋uwrM2dO3$8% +;'նO]RgA.W\HAH;.[H~^M??W];bfiv_R(k*N5IF?(\`֎'g_77GBAcؑ&6%H,I8cd RP("fQnPr.&Oߴ5vy/f reeT#:tY4qC8d(rh/aR*r.D1шCk cFutl"~W&g~*\wmhLnL'xƕn}(EogcAS7SX@(W-yh&KRt+@7vjuYjvybD!(gG:cWi|-XD{? ȷ P׉a~MV?EIwfOy@9xpJěYSgHgaor0Jw 99ҏ`>xn_eyZ394^VD)i؞#jEbE%WoJ?=JW犠 נT5g#9\iTs7d?IPTWV9@76$MxCe;i,ImrysIhq eP;4WiF5XW')Hh5haz+.i6aui,yآ#/Iewz4Z/:IBD /6"5$^,WOHjcsuuJ/dZ\;Y@*yRpD|!G&b݃hePXK3VY$<iAuy6EWNAuI]YgG]ׯx [9sn0UgE@\כMFزjTw'd&&|Ija- WydD9!pgu642YXInb8kWE$<8k`yƦ@ >*ts;φM{T<LEOBBah}]T!e fʗ7j&V6$3}BJhb |G[taki_V|ۄuڌc*Y|{=$չM6[Xqk_xz`̰~0#bC$v!Ddǎux3sZ?؎kUR>Ht*/B &1Q7GS!gt-[ 2'_ZDgzfN}5WInt\Il-Ă uj&tbvl@"y7crs Xk} fi4Zl3[q DHדz;*JcxQA7ubŲ#9d'h`#HejI/m$KgmĶHmz g;2NMv\kDuѳ|ҢQͥڇtïȓَlWyKe^~Ke|X 淩A9`]pa)ȝ(Wt+ڝ]W5-1!֜O3J鉆kyt3!}5Wjd%PLK'훒~4q^*zJ%f4 ^)òRI?Q r15ΩE;BmБIJp >Zŏ#\u彀C0$3xEx퀦tnZ F4}Zw 8uWכ=9+s蹋1\Ұ̑Kwn,[ߓpYYMw2PV[u骣N]<6I ar{xWJ4SŷL<TfM{/<* 07R 3 O=^z039 ̧ /<418iLE2ɰHZrŌlqb4o9hЫ 2, S0dQ2x/<G[Ms꺟C {Ԫӽ6/@[3)ZjG=do=j Μ4\. A@rT t4]-70 RN%m")-EDD 3:>Vkj/dsMET3ʾԷ\4˘3 9E>rTt*5ǪdˣV/>%J]"vÄ@_E^bi;;s9C9tR -*6D57t(>SQ[}TU1jl1HR D×K#6_Wgiu0gFVմ6)GC$HU6 ֖@Ue!t9(EE{:XY>"3ѳW9Y8*HDaɜ7 >̠2?/6-BXyut[Q7ѺvN&-ݹn jy\SY{wx'dLzbVInM(̰>j9 =:aCI̢uG vjH:+Q x!IND@Lth)duA0~ o{vA1%\C{4<G>&R)jt"}^=2PyгRv.,`=|rU=~H3Qezv HT㔍p\엸P.L29E?" ! wH1Bud4$/~6;(#6l;Y.3Y>rm~0DltP #6aA$15&ĥ'<ӬpP0yRV 3zS;ʹˋn$&ˋSLGr.nW{IR-%5/+>4`APฌ4h+T ~G꓿t(sL>S-VTYL¿?=iMRЙalSg)HUa+D CRҙl  $3uZ ƏFj,*X~_")t. Yh h0*?ū) =jC@]-RbS\zXd*i2Sq[R R@4USHLZ:J App4UIeL'ͣQ1 Vugk)NJ&*+7vvQL(V[Ug#z#Dk;"pp)dkF:'9.tZ2#r.'pS'B(|uy-t@54ZRQT bu;fY)/14`gîN31 KE&`ߪ/ žףY'(Õ+,XT.\y4.x8j5ٺJTT2˚}іX2T|'Wd{Nyf.Hb̬SHU`,-mf7eM;SoN\ ew6eF\ d# zw';8Զ)#:(}Nv#TH^꓉LOA2_k71#lhQ 9ҁ8[8>Pbs*l=/@ӰL6/5=MKlA׆lKW[r3 L[(K#;28&8p)&b>}!8*6Ղ=#y^{VJ5M$݈[ 34nlSv&?[KV,vg٧HٹD܅AI0ylZ{\۳F5J` Ǜ5nc3Ȧ8m֩*5xuqoه+`i=W:NID<6LOe)y29 [EkٔCHhڊ#o• *=T!=3]|>5s@@{D>9ۡ {;/+ H@cz-b=pCX8 $4J˱$WQ5{5ڶ5'Jbrm#^J+#|cz 2gI*&ZB+ D@I`'|z/a7ɏK<+=R$!Aa0r:LtD>qU6LT #,pc[D, Nɩ8,Z9{@[28k5 jØۮ:6)5Ʊ<+'2".`=ߺa^RA#a:-a1ۊP&R9:AڳW9fC/r/_2-bārk.#8;5 :k{Pu]35FU,+w?=Aȣ[?.X3:iytAANz0a6`6S"s xL_)Y1>>!A9Q; 0H#ĐB&K?*296AŚ=I[0WP=L z (z8@( )(s7l)z\5j'X,<˝8y-DU[JH)L+ 8؅+z`INhH HsRKBݴ,NG= >̜EGDI)hL[TP rP\3<-aa%Ma8r*bH eBUqn"3 iۜkC*?vPIz؛h $-)tHPd =ǚ%tӆ\"l;F HU}2E|ǢˍZ˫fC/hY3%<#*K5!J}%mEֵK.Y/K,VR*{::̇O Mr2s2N-и,ČL=>A*I(.C4}HY\4h5.¾Q̷RGɸW -.C2iL(cZ83>_^mn8K狽,$C,+(͆{S2XZJϫׄl3AR )łçKk3T,eȾٴj834W-f5LhTգ0\֥M3,Eca\1>QT#e!m"GJ:K٠q)I\`F7XﭽKN=V\#jh|>ݞU7c >C^qS"'K CEŮ;JɍuRw?{*JޣceE4i ح1^U=MAai<)M㉓N^'DO#eɃct*LtĽ$m0<.( LA>ZSsUdړY^~K9]jRodJ6`%Bu=2%r>i1ްB_]V'̫JUdke"0T|gŷe)$V\&M>|Lj, 09Dck7LM6UЌq6 Qq;,1 \j'^c ӮDh*KRclmUa0֢U_JI߅_l%i{ժgY("2LeMJ34lWAެjiP%)n EP-asl;|jԝDd]Z)ָeUcFs We ' :*::D6T֛u2CH, KRʷP܃[ T' _w/[dT$ʹG.P`G  0Hq=F'> 6\ʎ$dDiUܛٺlv<0+Nɕ,%.]$C%f+4]{2@{)ZW B2g·ykms.ˠc;w*-Rm۹c9'A9H`oO7h#[ג5.Lab;[B(Vw24ۍxV2_\i3vnqps#5>8aYvpi-YUٙC-}h>r擰'EOF8ͩ s-R f3x5۾BD5$tXzlYn'yyPv <U Ngs2zIo#m[?%dF|;syKxyY2iLo*U# KŽ:Pz(Sߦ*'Еm7S]w#u(h4@:#sBN{Y'5㷞F-ܰ272^јe]:eUNB석R`֋ӸL ԉ?a;? ZaSk8X*3!pikQEP4j5ڹf)2E݄HL]%Ɣ`2O!}H d52D0 %<Ʋ5ѵf^cB‹RLAZ]w4hOE+=+r5t%JhA Q:׋L8(n4/^JlK_V(r=dd2+q{{NXfe S[|@0 t*#_ H?*UOF$"ӷLFUk%і.^3IAvv{TK(4M`^ (@d?yPLյorNs}C1$ʥ\%g6ݙʔQja=a1q2_ &YrMH8dBTeuu?MApCysۼJ3 eG)X'/5,~5+tLA9 ,ܫ=Y[5=\e-7LW$-MZÆR Ve߿eea9ʲ=zsL 6ը|Z˦}X1z0liIة~1ErPk wx zL4a`ةV -2 A4Q$TrUGPw!] _~DT ŝaňiԙI!:!  lm/`%TW)! սb%~- ͠IR1-a~aKYm%M`iNn8-Z I1IW@q uL/NX 5uUщ YMɣ(qmV$diߏ-aPe  vB"{NNE=M29yM0긟JT(U9SVeܞUR(U@p m $ d J:B p5@,.dcݔ*f!ؠw%"z_S]#1^%J/XlcۼOiEY!~ )blWNdMF-H]u$ZEA݇`&'g^$mc`\?QGs- Jc%a'%zWL\ޠJn.JI m"eL6-YJ$"J^EU0Y - ndT]99$F8!X0\ TIM>(P`4Y(JEa-)*c ۗwU`_=!bSF^]҄C!D 5kju!a gͨA&DQ)Ue圙V| Lχ ArQ@|ԇNV]hb^[p*)6dZr%x h5R!]B5(Y"Z-z1鉕En^JYu[XA7dt$qXKJΚgv3&%EOhqK~ ͂5䊽ƒI"eYAa7aIg6ibTՇ]ڳig\- ܣގC㋹}жERrWj'xJRtZ]D G& XԵIEDì2_7!H ڣ0h4/Qgz:BhbB_0Adn/(F5юF_/-7_Ouճ7 }zg (zܖ@y'/$:)S?_wHE賓Vo{Q{D(68!rC 3|@<kEDGQD:25Ǥ{#o'Ļ{nWX|+ƛFz X|(|H#}+3};C}KS}[c}ks}{׃}؋P٣}ګڳ}ۻý͘}}}~ ~ 8XE܃: ~6<H}K8XDH s*'<xDCs= ; LDO4xaB 6t 8 @aQPUd\ddHaP"E#!`f̂:p7"G{ĐS} 6ut&*

    l@쌰Z@̦6x(>BLf2:brG% H-ZnQ h(f/zҕtfJYp9ad%JcnQK 3mVSdZۮFI"jCQ׾50Gj1V.s#LCǍiqQ\J"Pڅ4~*_ͥSwįh{_2gkYdŻ-}Y9M/S\2U+r3XV&tW JC,kͺ׎qV1Cūc:B>[}1`z7=}?'ov T^yŇipCmK+n8`v{NgzWNU0A~'G\gt7iGtip،emHפ DfL~zO؋s,J08v&sÂ3~e7tQ7pywenIqhHم]?n`[5l$f >Ǖl8es`|FtcggfM·x\1yHٙ4 0Rpv&gUwL(!Z'@XFseY_b ɓ4e`vItقיm/ٸxWWkjOc7Oz2YgYhh'sYp! |{ijqم|u)gb茤(IRPzv긅}5)wWVqZGgAeW I`?ix>Kl8V|C: s o0$GKP&^yiVg -*TcFAejȶ۹ t7?irqXV ]`W˲mhZzb{ l6: {5tkKKGۤ~U1 u`ˋSwUJj([ڻ X&`{{J:xnx{PmKʗgˣ T _嘸lścUl{zHVai[x)pp(U f *̓LZRR9.K翩5ȎQ Z[ AD5lڝvy춴AfG||4WP!%b)\Zˈ˸:_h)p\zh:\פ\ezcO_%;bꦻō [ 7?; nۂتɝR&JڼzڝL:i|~&L`ˏMdڨ[pLX Ҩqrz̸٪L2,> uCg<`vhiEa٬xhrY5h< ά=J$aкpb˟7wo]Ifꛖ){lolaˆȌJ]s;F9KsςdZ fN,V%X`+FXkvV3ڹņKAĦK|֜tu' jOdz'ީՕ=iW ?許wh=M} v"۸Y Zˡ˵m|x#čʊZ ~M*יzgG=p-Mu͓38SL:}˳+ֻ K)䚐2t(uuVˊKc9^^ם\%> Ֆ}̡pZQj۸TiM0L4lO[ ENh_l>,Z*g !Ne(@jivZճ nf6zqp{F.:ƫ擼ׇ,㋁ɧkz^(iZp=͚ ȌqnڔCݥ䌛ټ<}N6N̶}<ԋ:O=;1ܰB:,Mgi o1d#WU宩l}ɊĬYz$˜,r>䨫橹l~ _ͭz.n/m5ImP_쪝w_ }L~ ې;Wt\a/z2(^ *TpCZpA%"EbGnh->Wը1O&̘2męsfy0q(͝:o)ԧͥLk6Mj4)ϨKJ=JUjSXur굫ӱ^͞ #ȵ' i@q=VRĸ}玬-I7ɒx:#>.x S*"6կDZ6ӫ:3멳K+Ңb"m[Y+2rA+=b5.6y.6~!Eޡmn1wGV)2_fm6#͵ԬO),J5T#-(0kh1,L #Qҫr+0l1kŎμ4pJ)& 6ܭ#cjB%o3+ td6DIdhƁ L/H3?dFK0d3!Cp\Ex#"uI$˪r-\?+%5-J*0g4ESH3CZ܈;?uG';̳\QP=CU^'#HAϷMՒ&uIH,RjӶKQe*غ2<ǤS@c}W;d/lMNܮc#1 ^04dr7W.vGA@O{#cܮ@J%y\DJ]W-*kuW:sa_7͕=j{]1ֹuUjgC I/d]2k?dLBvRImf{]ڭ Ven5{عasg>W~utc1j&q2ZJlQ(.}AvdL{Z93w1Uhż}7cu~|Yߝ]qPo)4kη-$߷PhVj_tzVzZP™$ ]D4 R:loH%+#e\=a"jrUZH=`Gy=☜'"$J.Bou*_NPX뼨v9r4hgM#Ɍi` #9 Dn!(fEje;IX@Y2I*_b !+`6:|X5 Jq{r-2w G&pUhSi?g a)N[$5 M[nYrգ X^B;G;b-<e t7wgZb_8oX J2t7mV-u>=N?pqU MaeN1ñmWբ*Mc(MS lR+$3D8fc ,iˌ'WeUYFpn3Ot;alKZ[6fcMJZU0=r0rEO0d!G˃h'?^016rIi#*C,dKӃ;GI;؛=1?Vr[(R* Ӻ:{?aAX !>W8_@1-s{ԁ/کqZ8lZ'<3!37$j"8rRC3 wb8tZ%[H1"=s4#KsQ; ,da:r Zb`>:;,BDC61"}"0iAC6+S8 lB2SBĩӹ{!KaQQJ3"94ʥC+b5)2CeL2 S5Ł*;J)sTDd&*Ê5K"O>Szd3.J̪rP?D3GTi>[/hH=BLt)}C "b DШ/AEj g .Z@J<{ɘF8<( ]F_y82yF≮̵ٓB|éKc̑񁻝c/^t$& ={O yh/=/5%dQ']ĊZK&!,zѺ$GԨ_*Tbŀh'O+E@Ջ -&/ \Sxc@DS< 1<ʼn,0u%܁Iol$2<\cT)`*m$'"4+ Ej@@JӍQ0TJ`˹RDk:Ұ+!:0s"Dvs7ӶL|SQ׼O7 [М\Kuh?2LX'IVGeHIl`I6|ڼdqF&=zMt;kz>Ӹ%j5`Y -cgEJJ8PY:T#l 튏0=Q Z#uİQD3ȩ|9>N)uq&,jإMkZ/bKIzSP57t[6z;Vn85"?&??11dEj uV} Ѕ;G$uB:SŒV0פyUP"5<3]+>3y1L$(}zX5G ؿ}b5 ±B0$aQ[+H ԗOKj= hU_pa+[ۼ1 [J(Rj;:?1C|O<Փ`7⋕ >J5=6LU=OC ,ֹiλ3eI)Fڛ"]X&2bMSMe#T#F.e&XxFA:`9@;:LL1$%4Բu%& i7dӅ:>Tᷖ-k/#nfԥd^$A8=S@* ΩU 6Ye’êAM=1e]LRmG]L7 ;*[˓P|}ZYU5ԽSz>bf)lԞ;⃓Ef=ʵZӮ-UWU&;a%80]\[c1&Xִ9@H12T!r6 8zO,D[ef?;[6DֱyB;4cj!ihE4?%|@qÌh[>-y,t2 V7jDB#gV :D=@SX=_ ug?Zc%KDI,.?؆SA=9bDCuS~Q-0T2; z VdJ(MęV4y&TtgǩΟˑv/g*i"52[gćXMbaUo)o*㹇*5P|[REw9$:no*ҽ,/q&I8YG6HpS-|p۽g:?3^,%"LxB!*8ѡ-TLQ`ąbDXI!+My2gΫQM9qIM?$:ԦQ3}UT)ҧKj*֫$rYʯ\ X+ȃ=B<1,ܮM[oRn_zI.Ic2 15z,1!3G/]sʴiQS-MqҖ=lָv `He)L.r 9"w;ܮWU^BBXdVW5"}mıt2V &݊qx4Z~",Ɩqr37..WY JI Ԫkw]Vشؕen>ZƁ\!ޣ*wc^ܦŞ!b+Wxr[qnPbk&r$:JNZKkBi[>ƹňlG4jMDz YF-(yO8Jv;JT%'_jqLC(Ļ{h<4fp[e6%muZ>*bA !) IPn3tBxDATQڮ`e<i9IF8$licqIKb G{ڞ2I_]RjPY5! *!I>  4 l`" c>mA1zBvyGjY Q2.+d%_xXl7[-^Lڢ4V[v2Dd(GhPBQ0ft:NR${4Qb9]vY76b^Wh0'|z'QlMzӱs=;LNKf] HcbMG28,d [)KDBhQ06az/ӎ>:B6Y. hE5y4HNN}~zOOPa`ecFzRvZi-4+Ӫl9("%1Sr&bT&c&q;h#L<Xov[vshe? m]RٔuVZ`5UJ/U_ X̱zv bǣ:H=, /$ZJf[#hDJוӺ)Sl#9SȞڸ2QHjX/UQ1y3b-dJƒ.89wo Mv'I7ѾՀt._6^s!/lx$Y(%NHxt>Kx4( [x^ؠVc'-VU-ƪiAku{6WZW ?|TDJ,(V߻ O:+$.1g3hm+-A=v"q? YOF^dN -1|}/^yL0 %@9Q?nru3G)Tm<K-XZZ0SeYu^J؆Ve 1[ UԏTORL]%םpM BUHYIZӛAB jݼi!PZ -YK%Wc]!VhWrZ< AB(ڂ1Hٓ2f׆]#yc`NJ4Iq  UR״z,$-$ѣ} rNA8M4~A%*mXLl}bsdƢu OED9 (96U F<޲t&vIV"M_|4ȑ%#jW 9OTu=\1$HW)<=!*T .Wn?O) Q. |®d a#։@%l5Z<]M^m%k@MaqfRRΈԴ\Y4m5cvn!;YT/2Vbޚ}` H?5y E&b!t_f%#jn9I4&[] RALч/!gEfElPVդcymdn\'VI]R]iP9NrOm٩!T'[CX/Zy*E ξ-iUcA4O?id]?bwQ%bq]VQ6)%g9LTYh״XDE9 9^Zu#8G #N)w[RXwfWjV0i$!NJ'J֤)"Y]QM'}\!`M!&i$>-hWiT [*e)xp &KI-dy$۱%Gbf( \A&@fN^%.rVaܖ܊5*",lb*o=&QuN,Rڠ"Ў.\~'N10c0N%#e֬Ѭ]7-ٖK+QlGCB,2b'}s%FbUt~ڲn m]UEP[arnי`nr7 ST]*4S.aw@ H-ǂJf&k+k:g5fz/FW4e30sadeJ]Gz3|q0D8_2n$үK}^q-pET$wo  S݇E}@Mg$@,sًBS9HmzN0Qqx587$C9od1[@!yW+vrM/l RR?#骉v5ӶI-R[FGyL5ܸL< A tL 3%gjj7u,2 FQ:`yfy6TzE77v`eBHW;K i!T\CL C a%aH"bYsv 'g7# <$1DCKihi%\}aK|ߗj?[;LqoWqAA1@Ԩ1O< L8aÆ .$aE)NbLT0E9f,˗ z|%̘7a &O:k9);->tQ*2T=B6,n.G e1=C20 ճ0/#]TSXVZU:yTkD 9o;!a D(35;r+P+@S"46cUv=H7^HܩmU7+ V?0'[` a7*PN9g-Z CNd\Ti[[TY-Ug[Z6Z@XY3\o8GhǢ\}5_ƕlb9xq}ɱ 9Fv1R IUŌ FErREsTk/ڛub2Wז)]t#kT)tASǚ4ՋY/jvfgNkGiʽ̙G9vOOs{rͦrks72? V]PDs5#}/9!<  SeD={QBE8&hc'ip ռ"j1[r"ns69ALWր> OсοbGd6#ϊfCQe[8 qm!;z.w)e*ΐA9oz⍑CHW1OaqagD#5V#<c"GOܲ`& \ԥ,‘J_tSœ@ 0C $X_J!V-Z06oYE@rjgQp9-vY0Ov068Ӟˮmʋ4;%E;#6;+эJ|$W+/%K],Zq~<8͈(AhfT`JQ0` *9%YV7Si܌B('dIk۴ĨKb}mLSY_NQ~^9JAVdINR{#j {B{ba'|Tu>5G>EU8Ϩ|#蛿ӠYb#%8ƻ-c$NED)wS2LTwoI98b˚Xilz5 [\*+ 4)?%ċ:3䂦8'Mq\*QӨY@c -:"DƿjkYm ~suqiզm_,{aݲJd*A8uU\"+S5A KRLv2X=o9m}.I`ȑP͐ӲCG |2b+ /dNZ~~hٖzt,w_ZXﻨ_ 犔Ή#ygRgn&mg֎bN{@pɦVnG"Dmb-mGNO('ന gnELy>y^Κţ\M:'#4ij`\NͼlbLJ Vr]̺Fo,t"ѴnOFŷx4fCA ư * 5JIc K JwրИϿF˃Zn20޲Y_0d09Ɂ'"ȳ4&+W/hΒm Q켩̈@n/Ǡ &,V-Τ󤉩8=.?~R opmpb`& W)\0geҲ䥜EDj^PzTĠz $yNmO߲Oy N*r~įҀ(@GF" ! GOD[QY, LRTOl e1,*XExpɒz|Lo#o#1ɏ< Or2֢0hl"ڑ. Hn8)fB/. X$rQx1%2OqTh^ -=:-S n,fMTx{'5Q=qTK ,S Eo0lqj [oqHNʩ\?ADHA $+`" [+7eʦ>L)v;񄺤RJ*Mpp QDS êm G֏v`Yml譨i6@ۄO>gjr"ɩ_jh#-(I.m1 T3S6mqA &7M1ǬHD4r4IH&j꣜0&`ȤEF7M81`SFPPHe V(]\QM!e]Fo+R4R-1ʎ{FqnxYrxB!kZt0>01m$:9R(Vj6p"pX.Hn|`p2wi R\IsGPʄ)ϨJURՎ3P]'T*7<12T5Hq3PQ'mԦ5 ^B34?tNj Jd aeu'(H\DA6y'@>;UbtK>[y6zk,1 f52R39Rrx\<3ly`vcG*i1w=ҀAZ5lMn5\s 3O5Ym 2Tg-KkFIpGܦ4`;d pvZiv-Bl9gxP$S rYvG +U PtwLoz; l*%lUj8O)[CE+z! )h F?ꬎURm5;˱FS1lG<;FmK7ְ`_ԺǘlK۳xnw  8v^ד|T6wgw7JbHE fD^sݢ rPPFȎP,{зa7U20d`ܑtK^ȨgLjByږ8e]RQO;4}P~qYf9`bO0tn5„,{JM) - B7h0ްNX/fI+zQ-GFtHQ]ZZ;OQvkExCXU7֒wUyȒ .g9WAOl9-*xM%W 8T{t@+OND4(ߊo$2ٯu SVIZ%֋PYox] 'y8-ҢEf]юsC‚VۼS5U \첥xlQLqy&q ЎRΈũbC+KUcscPfϙu:.쯔Xxŕ [12gxh|ƴX7Ss]$vM̂]p+Us<ÕZB\^Q\mZ{r ~:|B*/^ |ZCLbH] x/s7x;{Efn5K؟yv, Vy e}ʻP 4.}[,@p˯hNm{GR5T& ogئp'ӊ {7gLoI;d`p6~#cN>Kp؎/这:6׆Lýv[sB=E]΋.2@b˞tPHݾEΩٶ՟K9ro\ǃ^[觉9_na1u;G "ڤ/ X!^ƺ"nĉkԠ1y7z2DZp$J_'O6\K/KrM. PʘDO\ihЄ2}MztL EJҝ=4,N[l:hIblytֱ^sƅl°?R%8aSt >U>"+^l#ۋ-ޡn /@ZB †G[`RlYSiYm~9S='%W7иfw8s"zme7|CI%%bƎ;x{S Ӗ{JkM^SxYT^\ktuvN1HX^FtR8U%߆EuHWԓy` EUŵw4*hZXT䡑}g=}Ĝhj(`ar sʥ`r&!guinvfpAabr\kfsja֜AWUng!U5ۊVIdHd|NJq8j#Yoؠ}y8rD;ǫtwp z`jnVE9*B*#>7 RG}鑝也@ 2j%= iz&'R:F-X {wy FwgA)"ߝb9Fk&f!W mHGH 1=Cڍ##;b7o~[b5FEu*@o"ͬ9"lov"98ߝIT>rƶsh k6:xyiqݴfl]q,BNޢGU]|:am7o0呍k:xY<\%zTuRټז,qiKjԥ ,.˼(""iL73$]/RqӲY?v8Q\V-LVK̄pU'-rXW9k;ɈOj. +I$e|3_uXϪTV X +MNJ\Z>@F8DE/AF\Kw&lB(v`YF~nA;-D`iUb$R[uN&r:D\x&"aOlI%3+z~u=. @80M' #/"Vphߕ>nJYbJ]cIDm'wf7RS5^#,'oڅ|9-H*Cl$UTRs]KWVn0I P&P5rgH-vDh((s}#az,886Zv]BrָvIfdž\6}V83Sm;Eq62:s`5e%yЉ*P  B0ۓ<@04(Z;X&)9@\s7t6foC|-ticD<ƂD W Gx!˰   @rRv3gpxg#2Hʕu7 fl~@߱-Z3(闵ȓTC@ZbUg7mnWx=^[X7j 01` 0wY r 1 d}9J8DiL]#pȌqԁU'mM^@8WEÛ0cBFQ3&dn.Yu FWJX:WmVXb`HCvXNd(L8kw%iWFyaX|W/%z8K)yP"*85zTe>Zsf}(.A^ abE1eiH)WTkbFXd<:>bWƵӍFYB]ِ'o3`w`̔15[@!a@`XeE엤jr(Aˆp(GDzӁwR=+ct~-#PZu7hXHAYńu{:nc*ʤHfḣ!Vs#xʢn#7uU|JEG%ɭ`p[ъʘ29X+fWbgJby3:`B[)CiD~=zʕǨ-7Jp_ C?XcøE|T6&Y V*XJe G\Hʪ)ak3GylKFGY*YOd27]aB J[j:zj0r>4|Y4;鄬.b}3zY&‡RJ&Ehfz0}e;iBrP&a,Zuj:|E+ KcbMcqirhuCY**J@T pf,Lz&xjڳV`{,iB7vuK8:f&җʁzoΚ¤|^6(R c@]Ƿ^;t i^u;me:ux ==ڶÙ)Huʢc/!*SţF4DU8 WK,jR*L$S`{&CtfH=QŃXH;5ڦS2`1G֫P\kh!QU-Үŵ'IYV'4l礌Q9DOJNL߹cXZ~Ӗ#{7vrlEbP)}ү5;\⯰gf i_iiVwg5etk+.a<st fq5U׷)2O67H(YEXFej\liG9J ʋ+ۚ Wy>;T 92J闼gEMU-T Wy:s8t]1\+X͋j?Gk(x bĹ:3%F#[뫻v׹B b/6U8P^Nl'S;|^m"yؾ Տj^JF|Zf{{+#Uڸ'=]Sa7gX;kI~ȇEp4Z_&CCMa鳺Ix [jxLR)0 ?}'! Zȟ2E5kxI UlvDs%OYUnu&gY1vE1F6-Ԧ/3ͫQcԪT^Zu+WaŎ A 7D˟bdk-ݍnI"ۖD۾,p{)¶x} y1+1,ҢOE&֋7sa@#]dhGѦF~:Xq`/ξ<7i16I%E۸ϝ,;>mV鶺.O;7w"<: cϾ^@+2[h'9 3p**s.ĉFL;V3/+Dn:BnTB)6`jMGGx$18z=SQA˳ J#*#J 7а]9D$7[6+_LnN WYqlRѼEkZRm5>;tф*mHctWo*b5O<ղ?/3^݌=)Na5`Tpn/XŮ_NMT}L-L @V+Xx.TuLs`AE(oK-IU)kFAKbaM`;=*I{9^vc5xb^veʻ^Ѧ@ԙl-duD>yTZEx꭮ZX \CW~Dޑ 6)E<$OqO94pchCM}uw{uZ= /++z<9 :+mlZr-I{EkˮLcnbT8 ~Ԗ;KԪl1]ϊx* h#k<ו!|;K[d peo!*=A`u*ucJ21cO~F(v77ی wU~G&ek OSB0eE5*"OŘlKm3ЀZA%n&06ѠU=j<;J!' W5N[ T6 ZRA 8H?gug4KtFwr"kb!j&FR@%\'&шR5_x'Ѱ9WPJ9a, et _#,y_I^fI"@͐yA<)dV-Ck1MjD'q7%ΐ(&3(Z.X6Gxخh{h\:Or̨|y+l*;mqS /dS+X%=IIEV?rC^T^AԪA H}!|fe)@K]cD橨@ \8V<4 !x,vzp@)b 1ߏtJ:iUf 'ȚP]( تyp#2m3=CdNO4v[ݰʍqe7i-a )n|4,_d͍NT)j40n5b `YIEc%$Mn@da;| oQ&־p2WpqfyY&box34|Әu2Y= @^RS7RPs[x.J.T[-g82V@PPZnL wjS E  HE^({_kf$H h 9N Pe?ZEm")<*.iwCk}vaⳡB@ӊ%{'J2 6fM))239l-pś<0?ȿiG;;C-n5J7RI(JK;WKBi?̊?+7HI4- «"NH*ۀ=5E!ryq3e4Rk$xRs' 5Ķ'd*42:%5F$FQ%EۡD +㙘SC)<\65YFB* &@W AE8)R/2B+ӌ*$Dd;JOZ(! ⯍0hd4ۓƀDk?št\[nhq)Nٝ,S_ M*{<Zi2|̼۵s :I4kL&l3ϛ 5HTaFb=ʲBw̰29)>Gّ̦K},ǵk oDDP\#C`|q۟ha?\1C1B>Ҽ\(9i\GI L#7@R$ۯ|=H2dJuBHDKpo;+mHO|L+ϸ<:=~,O:HܖqcGK0?P)"(,P2"M;̗0&άO=LdP*$|IzŎ:eJ/QW+}/d۬fc$츃4̌}I l&I$82Y:*2B8Ԗh[/]u]/0 tUa\:rc)00bLA۷"C;f!8$Fʄ Ԍ}|? y*ŠIKƥpm< j!u,YQž: M j˹z?b.XU/JuتciRܡ\52?FAE\ӧe03]uU$ 7*UNbQd7t|#WlR0gh[-]֍sYUf\Z b^l}%+U.&*EX3,j1^. dDpdUNA`:U1cU$!CJsUbvnWSԋ.۫(eLSZl/Zj|PwUWY>Ujk쒜X⧲i6A4h@f Z!*Ѥ)ףF$[ c d/WDZcv3URb{.ЀҬ>s/e6GB9=4i&e֨'|uOUF,'-}%y/$ -D\@ F-T0f~YsNaeL̼qJ󩮣yG,&\Պ@_+S>%D3:?bb95j nHVe q٪N@2:-]c$}7man0Iz]1Ū\"v63 /[(r@C 1(8uP\%o?mP vqpUq:9nM'?YOS0лa[ " \-eށ_nYsc xhspVԕ]Zht-VEZ( gRfqHi;LAʬG+/b?,AcniWj # ϓ_ͻwqjzX>[Rc71]IW:fTeyDsnƚҜ!Nĸ'^q.ETʀHJ_sX{NGYu' &;5ry`#mLhǛŒ!z[ Q~x8}vhC}#J F)s@P8|mqwN}Wy'if ǧ峳M5{&9Xakn'1E\AYU.}jw:lG X6o XH諗2dq„(JXC;R"^})lʑ Qrdrė"a<%I)1֬RfÒңҞ GcΕMF5RKcZS,H2'ZыhJSΜl7Ҙm6F #NlaB*8˰~W6&*43f^oZ˳ȝLӞPeH:sʩWs&0iK&m'Nq`{Cye˫YW  ]['$x=Č:.=g)SbwlG\މTyoqӁ^MZlUP_IYZ{e!unfwLM5ؑz؞0ڋ'$T4tqukvNKNItCJXepF e\WbvJTpaZZ_k5e9h'[69Yx^z1q̍`(AI#oFM7!V[QU%SH DaiV9\j4ReN1H% e8wlyifryheژ)ꨏ hL}juM!駸%EG.Vcqbnz''Ďe)ٿi%}u'E '$^)]K5eæ6F&y ג_$Y]liTk۵ܖ\ ! G4Ui=$,|g@cZ'Xu!L3L qhV# L͵}bV'Zm^^ çyٍ͔,W \bN\6MUvusuu4"йwR@F2oIN%K)ӿ<)6plѶ"M=aRk^GvqBN3=N͖k"?86B6a >Si<($֖+kMvQ8)mՖU<DcN3'b.r:iBMہ=n} MRyJ?rEZUV[X5Y uaE2ex:f)W4>FùfG&*eY5$&Zb5~e %ko.\rZ'e%XhJ)^X@rRA}^>e Mĥwr[ޖ"[}`]H1& ESSM]~[zKZ+P+%r™>QO㤠]Z*ejqSFLA$kb)ۦ&cyHIQ34FY2dJfQ`l$o*f^Yhlt_וМi]˥XYr1# ɥvY xRMMn[UVyaTQV nq^pJ&Y.ܤh$Vi-lҧ cΘg%#sSVcY$jL٫^,dfsYC9wfH\8ؤ*tOmiQ]*_^O]ӗ yDQLR㲉հ $+e֕vA.a1᝘U A2F-Bj+ Wg kaM&#vOW2AҦ{vB$Zng9P\jK\Za&Z׈ݓi#($Yd6"Ɋҧ8c^M8[]qVّXHA uznsjE=͔bʩ!bF-Ja`ʌ-n9**5onOVbe atQ >qI2mNXs>\MHh-O+_BW%'s"nV5TC ppaqiEϤ,WM`lHnq}ZabN{/'cQq9\cNP˓ld!V­mTuL8(VC b[p?%"5ٮ&.q~W ؕ*Z KZ8#R13oB/qtY" :-՝sײ~$OEa-W  kP>~}f!ueEQt*uYkòQhru6?2+ 0C`K`-d񲰲Yuu+,0ReA<-ub 8& 7^M-J\-qIд1isl䄗V-h*W9hmd*Rv*OHcj\p]֤q{&.s BN_)6ys.$.$~ G֠ +Ԍ 썫/v0RU+ܞ/2dtpy#( Q^(q 6aJ[K#sG#O麕jj::xh/$jpeש %sBv™'"wv&Z۰t/d0W( B~_ _Iʹa^. ΛDbm^Z_;tBiz')vrt]mV8HRkmD1i&Aju)]$NbU6C\,+Ңը;U9kaOa5O]+^:OBW9{cWCL6e:ϸ-sf΃T.7HbvfZRm|ܕ(L?w;jSw.u;5שFt :Im콝 UM$?=VjP=3p%EnT#K{\cd"}E`neyԙP8t&XvIC²9nCyzl<֫cƇzo|' FSmȻD/_,uP`/6\( &X7`Ŋ?TH0Ŕ/T1NjALI˒134ٱFQO(ma #*)jLJ:)7lXcɖ5 -QZ4F y8тИIܸ|r$aVT,.˾ M:%Grպuu mqۦ̚SXueh3ow=oe*5+M+=TI.KSʝzG ~;G ̌:si+.Ntߥ3(<ܣD[-C 1  +䂭?66#: 3/{8l"c4CE=NDgc*YiCŠR.5LF7Ա9l#cFjdg*S8, $.:?J5 ;,/ӵsk΋ i9*tʡs75&C1?Kl1R"T4!ձ.$-Ġ,- 2Kpm2#u3QRT2?lmO挽<l" 7)KX-Dcִa咕Zezd.K$JHl_Zwᤳlӣ<)H q_ʰr6j5eskϋՓ2Nv4TE= ?2=$9ŁOճ@MQ:|WuŸqZ儩mũ'ư/uW$Xx+D]06Q\e{j^wm-(y3k)${6,R]f o9T@Pͣ݉ѼR.?bp1Gf*{i=NQ_s|trq=w2xm=nMj!>lYuY֓A}_I\#ܘѨMT]M1AMyp9$QRNY_0,fqj>׼t\ g!p2:]iCI >x}O|R@ >_DئEa1}nDX 7ϴ4@)-`SFh.ۉ(-ĮG[ζe+"IK;?аKزefI lCzuLH.Qlg,x!307AΥ+,0/JTltsRhE~PžL^ P<(4l"/ c) `©pd41WElgq'pbLrR  gidR[ԟ8pAlZޕ?}Ց")$,rlǝ˻|Wy{4FUБUԘNDZۉDH3\Qkpz ѢVd[blujt\.I5f, 3jOl}NeRfls[Vݤfnǥ{fv쪡;ٲ`17lUnWUmgwԽʖtTǵ<iT=XsSmV LUN7#jƭtfQlMBZUP/E҈74Q!@ӆ԰FB_s']&qk%&yg ԦBjfu Ux1l`/6x֬˸E$ţd*ΩbRQ]Keot^`K>pG YR/P@S4; F3i8tXr2l_R;cZzEɂ/B^Ye؆Wbjr&cՋuMy 8#ֳ"`+xā xttװKXUZH1bcG-_yT8!Lw[SP<γcςLW`WK<_ԬWGuZv{lF<7kB7n8XU[Zz=23 G?yƤ>/L5縜Fw^鯖ᯌ"3>H'NpsrAN%JF  L\׳ge mge7NX#g 2^+>25]Oؙ4('D繈W~_8P '+/3PN7?CPGKUb=,VfpLT,!upqxN `bN-,c h@`,T! >$Ģ " a p "n ox@,P ,~`2 z`PP@4!,A@ 1,R 114,1qT^@,!v ǂ<p,,ځG̡* *IA V`Рo1wq7>@ j,@‚u+@ ÂN A VA z@XqV1HA11 B"Ai1#: U`j!0@C qQAsG$q$5X`kq#B($C2!0q fXq!5!:D `Q  0 U AQ**O!Nف `Q$a**F c-&AQ 0  0 p.L!`3 S*An@M``51m~s,aS,A-ł!$s,>,s8S;;;<1!d,$I\fAO\jybAAKI\AT[pAppOfiJyTZc"GWN\fpfVA.DoRWOQ̌.]ɖEґAԘA֜MǑWܰoצiy..Ei fOpEfVuN^E+a0AAKppfyfY֒ݷշĄҘӖ՛ˏޮȭďč‘đŒŒœƔƔƕƕǖǗȘșɛʜʚɟ̞˞̂͒͡ԢΣΣΤϦϥϧШЦЫ¨ѩѩѫҫҬӪҮԯծԱձֲִ״׵ضطظٷٻۼۺڽݿݹڱljޞթǴp H*\ȰÇ#JHjEdlඍ~ȒGQ Gq$ jݼO8QK5c$#DoOTᷘKNuBԈB`G[z7Ÿw'Xp_>ߕ" H c#H,tD x,2 b )(Pץ,x B%ʍ8G;Obq,"ΑI0VIPLF"Lc5GDB6&`tU0KP:%J048B.*蠄Pid=F@i 6!`'%d*4Z%)SN)2@ CAj5' Ms APi:^`5F+NW?(CW N~@j2{. vJsO̰AJB1@VA$/%@A'̒@lgGAa1,`ďADlĭLms @p; 8s;N (RC0G h&Ipz  @MP;LO) (3)'-q@z~7)C((@ 6A3x⍿8_TA7wN<蠳 (No H@Crz&@N2C_(rC6Ș*4}d;|6،QwVӛd>bԤMvtyJȘեd'C1w )0RE.ra\^ehCWh”`9&M!iI>ҮT5SIykOTJI-ISԧ4-)Cnzؑu%QIR*u!MC8@4z 8jVV xeV[PԶͅ-V9 Z0Cf4YV:+dNհw-jvR""ݮ&5KYsft} y[*OSAQ4\WЫjնZ/v`'9 RL7'-*|9{^70fRbv1 M"1uoNv%kWpEU~V푓ծWVJe]j]8?\Ysҳ/-ou^9Hszc:$so|bޝs< >Еh~op SNmMيR@ TC.f-2.g=,IX}Cvc[aV@N4a;i&HvtmUSbZ 4l@ TcĦ﫣[ʌ=<rnyf8|]{*te_ uұe.lCV xpPύS]uG k\+|=p<;s͍C?繊Ӎ%VNf_KK߶m{Y rA Y.SB'cis5sx^m{Mny/)ߤ7ogPOjT LC ׼~]wW?\x׺>z4MgK,FmDy~x~)?N>GN/*PE£̩{D5wdEr"6Ίkr3ث%SߖU|tHUu_dǀZ7ev6Wv ٗ+{w~gbvj:Ele~s2zoW~&Y^5HfhuLi@؀h`UhdՀ lq|PqQY4(k;pi~{t} EW+Lz7z/}$Džg~WwFуFuGVxxvNbpdhhGopz n~wpnfh;W}~ng%7%]CWwjdj5pxx}[烎t}xG8xY_Ʒ_Pia~ jg'aX,8k'pvwtWogxYlwhmx8mJVUvgJRl)'s"WHoF5g~&c l@'dY{ w`lO{ [xuK6ŒV' PZzp ‰7k FhWg.fbG){-hc7z~#yRHxh7mwHexwqe:j׆@wYW-k؉MYԵ}L9XE6Epq!iaI&DŽ(u?F_pp$Vhk~yxlqXawQyِPofGh qhgwxvf i|G(Z z]V-giUWI0Ƃ"ek;Neh[ІQ7$IR7GJmͧIי٩ cPׂu,um'*(phDuh7sJc擹i8xHHc)蜄mDǧ3x % P}6xc-ʅɛVn{y~U^ yk~}bDvt[))mBhdmLu'飀f4pUo M\Xr ԨHH]b:9tXzZ耍iׇF`w6zR6huzb% pTsc jwbv(k]ȍOџ Uh}ʜ ̷ɭp`噾uep '}7h]&WbYJiM?[׸nGNaUyȀm p) 'uJ[VxJi9 `J]igAyVI&+(WkBywPdsx[9@|4DXV p$`i8\"z7ٍ*^(WqYexZi::˗(|zPMhqe鰲(d xPi T&k/w6kz$ZrXOiVae6 ɋ6ʭ 냿xm+x˪JFtL *]Y w:f6^۞.(%dzX_:z7mRYl:uZpWrr皱9һV*ot&o{Z:w*k`|;:G} Zu`z sPo Kn{:ykhz*S1~c3ȟ{Il y ;w{F̪LtڕxVb˚kwX VyLY~ꪇ: lJ_3Өj`{R:熰o\5 a\}ۦQ|,L|غ* yKdl)#ZGlpl{8pd=v|MH뭄xQIy;V9}k$Mg qKڢ}ɏ}sݼj;f=htI[xh' a"9ȅdU]PA*jJ1i&G:ݼ ф ĭ+ݿ{ 'ȠZIKNnhJWݲ$gzNo\#jF=Mݛl \2y`9SLZ͒(Z&1iw i큣,dljm-míWLAٽMmGҢPm us`|kT{;\JL|Q حKWy x}B[yzJ pԚf(Nܖ~w9onl-ޑ2ாTװ: zQ-.^Sxz՘lʽ>VOP$h惩c5:>X)  #ɋDߣOU&]8|xUztc@,̽\rsj%'MѱL'[[+x~Cmn y_Z;m,j_큩|>;chMfX*I ".Gղ̎&OPJxP _? *hCJ(:p(ҏBO5gPx-XBBLZ=\3 l˼ q4F#ϳ/ɓ+tӨ,8>k6U0MB[4.+Muŏ+a=2:PL;%:!;2\T2κzDs;˷ۏϦ²=L.5)F+cYM}ѐ#W 3nX$uAՔJ'2.90wj?(w]^]i* sYesG?s,HE!ubks4mHj_drYz)_lUBPB9 htu݀[%vpu09au;uH⿲͐=n.i3Z~ Ϝ{P-W95MBVrOR6;]U,onWn5\P-&dnŸ뮵[1!5;D'Q>]֔Ԇf4M-{--`i{y'yoL ox ƞSzdW^{lڽuu>UL[vaIZdO\_BV~ &;TxcNM@MIdƴ]H%V4GԼb bĕ,n[񑟲JGh!K}L ?GqaDl )pwC 9N7!8g:Z ujikk>* DHuT'=T-ܭ:QUJE i̎Y-Ն̕挥2alb.D5c xA*Nʊش+&'*5No}%  (xC4q5Ris$xS2T>wB[TT)Quwbu/䕶8txڛx׫Qt//4Mk^kmk ?vUvF_"{H镒#Q mQ"Vc B\Y6E9O[L<\!aV\Ip!8{:Ӫz}qq\49DA!X6̤u)PRH3ڭGY#kƶfw,gQN2V%()Tͦ%>ziqt^*o:D(;>ol\ cG°R%ZGz!@SYSxM4o&^19yû&u0F,qxb"Xq~6?\ iJM9B X;!2ѝ+# )P*?;!IZ(BiC|B3- ,O .bxHʞa &Yk+"ѻ.rbӰ;ϲ0/SrHQs9o[("l |#E+3I[$72l+5~CHt w*ȲBkLԽg-YI;Vcwt!26r] (6e*>A"w37&R& bOKS݈B#)™\D[=Ly/L85*JĈ3d/7\L>< Rj÷DAj*13̶ʡCl#BadcYO+G\8r4F N:7l D"ATNip<3SJdk'$[.hs:Z#Kj%3Tɟ؆h`e-L$?s?=4Nm{1\|3=͍ˬ點6yH M@KǷBL=3dS7"%( ȃw(}"RQa7\@ AQy(^Kʦ:̳Qy F9hBq?` P0fT҃48e#,FΨ x (Rpx(/[UN<<Ź*VSUtE$;vRH<(BzO"MIэ+Ə45?D шKxȁ؆ P1|@1tœ:A>Cˤ U (,Er*$&n_ 96r?`+LMQQwX_x$p(_'[CƤ>SOTLNZ% rL9 ӻ< (H<j,Vҡ̀3xI+MZ*{w}e\Pj1D1"bR]ҢLݰ7c7iXØڝs^ɰCg(e}JReԺ4I}5͔LH{WUKu-m[tc[S_@4}N*5TJؾ3Iab\K):)9lQ{ѭ؂pQѫdFBQ[Caĵ6.S[N<](AԦ5B%& ٣\(~ [gMhyy/-v^3y;d} D[wkRәKPu=9@İPzVyvD(5iF-%U«]_a(fч_Ti L\.k2 [1(ӖCĽQXJdJs7Q5s[,?Mץ범XZڭH.h[{N \E { ƛk)DӼ#SɅ zXK&+6PxJV|eQ66p@<33L$*[s])4m6! Ⱦ3v1 \6=~$j&e#l/beh zEA_"pz5֕#T3]̱"MBE :{Q Gס ̚Kn,lK?`wa~gj~O_/5UE/ :_]w?u3N7jq|?ވ}lEe0?';k@"i:ɓC:m!hLNj5.Og(r-&Lq/wgi5tߦl]8>̅!-]T>N9"+PvEB8dME5CTEH?*똖@ؑo'B|q6>l͊ajZL.~&f|Nxtp:ʖU|)g4H9,h2]3'JXjeԑr2Fkvw$1|7HPlVw[`o ݹdvd;)&ΕiY#l4V}ӹ]L%W/|nuL$1m7y J: 7bSJmyg[!5sKL*H,׻jo#qۍ_&W>;C#9-GaXĴ)Y ,^EA7:ͫrU 9xt˘}4g3 *dSaDvoeoS U;ޅ?N8UXӠVB4"N)o_nĺJqWKSdnH?ʚF5'[R. '~ hhCu|H `%!MD͗AFgm̋QLh $&/#^͍$tq&啑K9 pC =j݆ؿfH#"g0B# BMlbU[bXUird x`8o%-MquIS @SknuiV+ NMS1(.ra״3&PnVt z7.lbA&?1 ݓXSDY=E _-$HrgIl2wXHc4*/1%x'2S/)g*&4Ĭ@s\S_26]7!t5^i z">I!\ڭ4qf$YW#ǐtKeN A^KRk*rLΣZT@iʀ\Ta-%V;MI2?m"mx9L.H,@{xH%=s|}XT2N񩖣Nġ"45Xa/_su3X XRqNVo_h)O]fogT^8w'G^{Ro9Q>}ŴVRْiUkjLqc˷ʻ˓w͇X]ؠa5kS}5~S2yf4,/~ۥ;%Mny[*bf7Z~- 1 %1U|f1Wަl$cUr1isC6 ,ny\~_Np6E!w2[x%mJ6Gqb}Sh1ܶ)rLC{=-,ÝQ5-~VZQ_%UT mZe3qTVmׂT[EV{ KnY[iH Ɵ4\ %ωe`R@ jZ8 &]))䙑=85ۧŒvX}M }Rr(;RNO`N굟&^2 ,@R)%ND%AM5iO#}Қ*z)wۚb&i]5*^y% HY^}"(Z- ݂QMlT͋UP}WZ܇Y#iUM`A[ÅGYuO ‹\ZY"W@`q\=]:}UavUSn9])^ѼS|kq۱|FVxܗ-Q[]A G!-"84b15NJR8[s`] H%(ݘRh Er袔 %;$cUQ!`AH$BxC0QD2EPb5Q ]M&In "xtIa %ѪOud(ѕ0pKkѡ?L3]T} E[BeC(NZG`eg#a)}Aqe# b$d^1NX%ieM)grMy ts `9 *O\"f|ŕX"O8Mrg`ϡu]TnjFoD g$^Xb(6:ڠm b*%N$'\Uwu M9ub]*U9Ҟf=Z{gFgc~`MY ru͔m!˛miǩ&q Uy0ΘZ#]uiV"K$>MvUaѵY&!aq"V)!1 *E{})mLJ ~Ԧ4W”M`+*Gek\u*M-A _jJ*fra P*Wb*_z^$+H&߱MENc&Q[Ripݰyά*R,tA"ܙԻQ|`l"Fb=>\.bȡvzY'v,X^Zre:΢b`NmFtNZ#$~D+[rU"gs*ZBI=f*@MEk!rŶ[Q(. LLH>HI.[)Z c&URɁä9v$bE#TS+7Klpj,ʢx#֠-kp}n*Yl2t)\LmRܚou J+#&ݹ,eٺœ*t%.N_en&iWتA.adoWљViaY&#fFΪ ( -hE& &T:HLPj Fm L݊#HN\ۉ:!m]f=ѱA!@{֬ 1Ԫ.j;0U^#QDIU2^נXgmZ0bX,X#βF7ڠ}J˜`:E k@e 1c f/|\R7om'o9&3ntdqdIBزio#]F^'/bX rCQT^  4^Cm`\6]b,)% $Z@o\=>llPn2r ՚,hbEd*@&.cq$Mu")jF_/lh05_9ґ{15Si *QF\Aid.UsDr 2#5*` K:[S5sET6Z>n)mq.}.{b\hE[q/{'g.@+;c3Ucݑ >juhr"q-p>:;f9Km4g.f]m5.16fcRY{h!2\)yΘg&;Jkl8["Z@!Bπ0aWg]r'{Q5uD(oQTYfηIm1{K)9ji+!M".LNb}RA4b -Coou#-x2k%2}vy%2M17ePImퟍSڵYtآ;qWv̺87UuJ/O}vhu.}鲖o]m͊W ԄVmw*E+ZڹXѽIP-I Y6JsmYqo5vvhp$Ն&Y\.龝l6e<2eXvU).[9;W52R Խ;3^)G3ٌo{inwsjëgKۛ.gF!Z{ό k,bA7>kZwn8֥b]Z&w8 NeXW6s{-$#* }/LtF0B 6X& Kq3o(!hTYAG}pjhf%e))Yҹ5\&oXoO) u`[bm=)j\tj9ޞR~ڵt*pUeV+c3U 'wvgWjSVgǙ1*$`kJ㽋] G bv ^Q`;G G}UEݴ6΀ ۷1@E ] 5 B2$(pBPC/vXQdƆ)NDС‘))$F %yJ3+rRh3e<2H% aԩTNZkV^juWe˞ m[oᖥ`$΍5LaK1'ͫQOv Η+E*YӰ({ܹХ&rbA na|YkNS0\13[5L1S#9[c}NFj]{\o|딥W֫R9s/{dϴWإbu> {,Xӭ@Tؔ6=b(~z,2|8>";kѹEx*<8B: ĖrOl%ȞڭL.DC?V-6<|.O> KP>¤#T6FԉH2M{KGѵ뮺tB붡;=28ܨ&=2o[;T/2C;-? yRpJ[ͼH'9uB@ %TOZDPVjдXn^4Q4.#p%K=/SȾRS+SCM)Mn_iu*{]4,Hꗨ~{)z=}ek4Fnrt[qGs&ehv܅4pW ~AZ&X5ɦ ,Bj<"]eOWBNu5c{edl1̑Ӗ{Ֆ"Gz.5g1XRs%݇ի8DuQQL8پ|#ۺI$4V\yw`ze=TnXU@j&70eVUQer"COW)O\^ML`xB@iʸ9(XkhV>QkDc&JeEVnK(=&\<5;"/- wedPt#rޜA]rK;V2dհ.$?'>@0AA&rdK3=4 p3j2Jݮti1a,H5L9CUgN3]TTA8KIQ#Y:[!{}hs%{4C9sy}='Z[XlW9!QľnZ1Ocˏ};.V8cImOo|%١EC#H#֙%Pc6Z'1~ߤz(qEs=Sflz0 βM!wFsCP+Jr'$-]O)V* `5v]`}2D$;ouJfwJܞJJWsx߬ygz9^8rgZ:ExǵxKIJ^Dvwy{,0}ko:c<~̧ OgrӬPooUX FVDL(tn\.pMH雘%d"vN p.+BOL+x>hRTɲ` SČK>aVc3$ olʃZp0Kh)XL)L. u~x%~$ E` 4CE B#iSHd \ȒΝHaeT+sH)L߂TA\0anprkO*JG~CwăP G-flF.+ q=x OrbkS  oKh:IƦ1 \MCl6bp<ODȚINp1:&F/(>43/JfKb/Ֆ%Z-Il0I8t0E[`9 L .&jIpĸ }Lmdv~72 hlWLDR({\Q՞EEg'˚jujo (F}.ɼ2ϱ׺ żMRrmt8N ВzG q %B|0ڠR^<ݔĕ,eKrZPjPo8h~V6Lޞ*j,]Kh5 ns4D F6z t8+(xS@3G&iJs:3"N$F8 n^/Lj0,/'S/#Pll)檮/4ACDd& rH߄*)`4W +*3q%/wOݠ@Oj==VF6ё0AgT nێ(EN+FH2!p4X1DQ;^FǢ+?H N*-"QR$@%>ks{4U...t"M%dQ؄B E44.KDTH:91M˒ [OC|z {ЅX&PВs.>Qm2A̒PU]V#@aP]"N>ӐPZ0.eeE R(qOS:_Ni2Dͺ$lzEr4h, ;1}rFU33`CN)ԉ(mf&Pti-I,F}f'VKO7 KrdkBrh 0#r M:35!`rSJSXo9c "x LS LO&oJWKfYldO\u S:')m+/0R)>ZI.Ǔ4t8|Scp *p%vhp(֖cS_g+TMRc@|/5CfLAn%WI7)dub^ӎ dL:fUY.DtK!Ux.[p~m_8נ0Ts}G3m#SnAgmľRlwrvWկ&7< 9o޼\%! -P3QzhwGyD,ץ ,P(Jwخf7sNpIp,:0̪\DŴZ-S/B+6!k'M tBO⤱cQ4Lhc^V0xN? ;&" "6-weX!Nx3"8˟-lB~hEc/gˮY&9lI)~U1xƨM@u˘LY O?&K$m̏q|xP?-"MHJ^0Ȕ{A6BeӞR!4_Ռ1dJ(CY 'YuYI lDF y-Ln6MyhNWRi_ꂄ-{z IЍެRr=9ˊ'=9U/As<Ms/⸚xs?dyz38WO@-wUYO;)KSoYyG){ @=ZӸS6hXh͐8oFʜmQzYָ:VU(Tд= (mir ?MIԥR9seϓJvZABJ6+ءlòXi. ڰ@6pĊ:~<@\82梔YlgVxV;\93YI<U.-ZR]+?94rv7 \Z7Z7rIsbŀ!nq_ܼu֘-YQhUZ%}Dx @gH%eur-SMP1ՠqQXs _\#tq啉څZ!(7[f8V5[CWxWbG{5f|Ai_PqEc%g\z=ZaSmJMlK*fhjqM֦rbr`l6RDVCٖlg~RyXVZecZNUbi֡Yh=wj.敲juyRFb[It}g'n[m秃V&^IJ}JmHXZqnש8Jf{f^"r~lhR)V4up5֛{T[[/0juqg1}*Q"dJ6֎ǐ %2ej fT+i) v֊UN++ݲylo}w3doXZW߀noQt}T:氆.O݅~͏6 zyz&mV 7RAluk![Ѧ>9 /+y؂Gm.4I0$iX%%VuT;K, A}f;Xp0Myf¾$N䂍鸆00SbNYt*jOZ>S)돱#Ipq1%S2rA#jDː1D!kduApcrDd7z$˒w o?Zٷǣh\&8Mm}"A (n1ر D;f9805 FOa[g4zC/&'(K q63/R%MIozV\aasy*%ͨK5HSuc6K?|$ H /7]2=ؖi)NZ(=aU|2[ڿ JEӟ"/I\Y4p2WGݪFк> %'WY;O.>?Ubj 5Ȟ YڛS(k*=+ŭoLRŕNH"j6 ьYMH&L] ln}B|74tm+Lg-:}|ڲ}؏ ӖJѫ"HQڔN%<B(PQ-)a Xmq,"TϺD4mVϺVhc!nw?^~W3k-*^(*"6K]8sȢ H"aASuݣ 4vu.ŖC)"NF(3K <;6KՊQ b49lRiW\~ζ+kI\R: ̎-ڭd}x]ƙ䙥[GM.؛柿%:E"Pb?/g9gP[&lwDnբ_(NrN_'o:GAo-DH8'N{5v#K[U0'\H"W>%WpTY(rA/^rH#!g@φmWQ:3ycA5)Usy[s"R9 ŵ?$3@',5vh%eՊy8~iE{hDWb}CyUp$H+\Y]1q0ג2hlQffX0rBd#uy[%<jWBwT>Qr"Aw}5''(p[k5+G7k5 vٗNJ7`|%hv険,x,kZɶ#z l.),w@7χěL5jfu)n*u&d\ -Of XV$I 0%`.[`UpU;<3ssޱYznB8[EdY=_8f${XUZV6AD &3`yAt(nsp9ř0 @@0 (yȿZuZ5̿h[43]LlĚS3J,KڋB?9hi#A`WA5?%vpfKGyq  C` sKt?.?ܺ0cwԬJXufniZtXXE[7O?wNV=t)}JN6ڻѝ[yZL0ĎZҭef+[NɍEzO\ l㯩[WmDU'(&[O&'(:-MEY= 9!2?+ͫry&;V4dj.;$V^l -knc՗g򹳯LSjC^˥#Z}UH{c,if7iÁ/BG}-.Vѝ;NrCiW \I%HEGB%9Nylv=[.G~: _ 8PMãY8(OZƝ~ 2#5@o5zU(=OjeǦ6뢋}sr^toJ.+oPRLϨS"b7,W~l_(ڻQ?JVmٺRVylwu=qAYixe$X ]@AÅ s5\ТÉJa.R"I M(JU4q#KiE9sC*шK!*4 KF0$M%֤ׯ!?IRϫ:orWX T(E< a]'fر@Z?=sjJR3ݮ"Nf)dAIqrS9[N)*뷒5ޜrm9\u}/vE}՝oAo݊;n&~:7~S}a \qdO KHTK* **b$"ʩ߸ͬ2j,VJ/AnL -,3iAdP:4p14(3%I(9XH4"2^Ӳ8 7xSM&|,3(ݺrGk, P6:I&.!Q-[(Mttѐ;O9S:to14Țdbu+adJF 7!uki\ͪ/3m;.dP*t c-A t on[0@&̊<[-!YX'& T&O5uJqۤ9PմkAE.82L<_ܰ<}UWI Ad.7UDߋv-뤝]k`?cTYuHDz+T|m,2-CX\eKkLXgBlWiɊ N uX6DYͥu=4f2N@Fs;>O1~U>0&RWSa=,+αmcLgkVd@\@x5}*F_7;+u^ K y@aH-1% qڌvnعzTy&U oCo̘NtI2xhz?s!<%/qX_w6K2T`4+ Ҋ6)ts޴ơ.B>T)*tybxFvzҸZiC9ԾFM@tO7/wkS5D%uRZqMIj`X1?"RD"y}{Q:>tc*o 3t(pH4MgxV8RղmS}T`B,PEl7TͪR䪍49tFce[,ip=^*&I/jPi~pgrW#cBr~U\OEsǬnª|3mR6 l":3~>: 'ĸ'l<).3|tզ(Svg`tVzΊb, ջ J0>߮_^RC.oB< dR͹WaR'ܕɞOv˳l*V#Olje8 ]lKFg+\l Y0SkAZ̒#lػ(tp(55 W^\"R, !TJr&7uӊ'ǑNHȔ ^ [:}v"++8J :Ti{7Յ;R sR"$%4 JMKj3Ž.[HD__#=2IDV&su><4KqSќ*9zYkVXUz@uesp1$/C9;ɫ'=sad괚sƱ51M"R8Bje m>ii )c<٤/cTvuQ F:VNYt}ƥ:(cLIf dM4nZ7;"I +R)5#!cTu[mls WK"4jz1߃g7#3.O۪+9QN]C59 k?]Ap+i(1*q>۪ \(]>}L^%BPɬ r46ɬA&{1DqB3t2?[ )H⡞9#3*ExD{;BtJK&RA 4Z)5T.gA)3p""Xւ/2-Oķ@n??$|Ҿ؛-Sđ˵/F{q;_dIj1"ŒJF0DDJ8J|HDy2.+"y";vˎ Y,iF,.4.2YIE(z£0M%T20kqL{ó6QD!* ;o11!Y?2j?pTk5K03;{z8ùr@4$u`>s )T*LI.;93͔*Fl9Y<1)jU5taQ+#xɀ874{|^oY#t<(R183dz*;-P$FqH;ґ/SΏCCQJ%tKYPC52 /Y7C.+S3L@t bVrbTljӲ?ME# 07>WK# :#P#9pK+420W:Z`-aV$ȁ9Hr1 ?BͬH9 IyAByWVPaNaT[<ťqI052'ɔM34UST>cL8>4d"nZ$&T3좶љESn뱖MV"%XE0>̯]@`̟ֈ,@"3+lk ԫAڣ] I =W *.٬=?2 :Z F_%##Xcs.[pLI?O@<-SO`*Slû29$*7>wQU|л+=|ZY(74 ڱhB.[KO!i$H8Rٵ?쓷?Mܖ[`\)>q| :Ij?mҔU0tRb̠̀>NZ:15-ʛavl,ϛA&"ߍSKf*SSV%J3xLD9/Z_IKCw,rr,Iϵ9-R8YrrƵhΛЬJ[AU $[g\$DcMF rBݵ+XFd HmOvq%>5u+4zEdC;uN`݄2³^A[kddhDe%/ I Sy/&CK$ lf6nC|:֠Rf WIWifnCUޒld5ݓ HK-LQ8Cu5}V&hDD @gI٭ܙӛ.FJ1~nH-+RX^Oڨmјv3ή&rT>e^}ki5M&r%]e3n*Q fN$ 35 pn3ai"^~Qg_cND e1! Ӓ/ƎY=12UQ]7oI޴k.ɋ2&enft<]#Nyf R5WikvEZ -n$C9`dƛaU 2 ]g @.$˺<4e rn34U|52 /@uƕ(b;wB=P/&[%rKOm{BqVѭ&b?oc +哴 KrEN}azf4y!4|Qm|\>NQ]NvuK6௼<$OūbPlh1qK⣾̳m?O:֞&҅O3ښƷq.>H[p3Am}ՆSВCO?$:-vIǒ"=!ZFO2RmE f/ŮvZ@beEka-nG֦u8o e Wzu 2);?@hJ9yi&ܯy9M秲d%XlCHB0\WF{A6rvM4#E!=qkԕkث=D"C4\6UA%&a7DDϱf.iM.> ||t,+*kƒ*TPa.:0"E6dȎ;z망d}apu ZBHȚI(9z~My&lzΫz!r worٛfXiyJ.кuڹu.cwKj纭]&[8V, 2?GAoX^R_Hѩ]3amǙiiMҘ:' /WW)\ }]ڽtT,KtW2yjX12f_\/(UƔ9R*UBqj2te(ѱ'LO&2JMSxܑqܵa}c`#*%X5Ȓw8JT Sa+%S0{>Q`jo)]I"hIs-=Zm5& fb.0C>7P[e9HGuRb׫2jgelDNl\(y .`[k\KE9 `5+ 2I~e[Bi\:Nه蜖w6N6'f}NWɲZUq~}m,ԕ-?(*-(rlQKHWְxAdDE`i^ٯ)stA^i*T#h١?뢚qu U?,f쩟Ljh 9V#\g"+VQÓOO݉sR饱gS.^pZ5OH` V3ޝh!ꀉءꈅ1ӧ_ۢ@4Q#<:E+u-дzqDw sB (:rq?}( %Կf{%( f$uK4I3Oo2&Bs񉾷TKJCh%`qtzo t{= /dE쭛9i6Q5F65x+B,rAr(.ZefxW3A J*I:&m4kpի,[Mqi9gYopµ#eyͲ ;ӌihOh>ؖ_ɮH.qt >x X@04]UxP|0yy" M\ޭ˧;,WcS 5ڪBF8MZzE *W$-tZxrxt /pzW© #ØqvLV&w7ͬpoenS5~  kGXw8"v~}\_%UDht cr x:%w$a`$k_<[_@a*ie˒EsW^dOYG,4ƬU_Oƚery~j;?aC!]G @gAgW"\V4it+Խ־5a[e'}R ZـdՔ| VPW1NNH^ IaW.$B 2;EVdJ>Q6jZ-L@d ݕxSܛZ @=\֐&,^:TMCЛPAX&pƅ[|]zd1I1׵ƕSiNNYDW IUseLFx)ظ|aH_.|3`C%`yR1KAȊ1Ȳ_U%!VҀxȊaY1a\؀ոg9 yeXH.XVJۄ&V$ Lݶ 2`+LN6”QKRʖXeF SFhIИQ,VN@?uI.)jT .Q#TLOvhUm\ZdKenvQ%4HZOdROC-  ^ƶgEhij Q $&%l%Zh-*YC!i˝b*֍צ4Ui"OMmٱ\@%^ӾԷo⤜O@}Y>J"MkX#2)b&m!یp醢nj"UiB߾Bn+%,Ϭp"dcZdNfV Y\0쎪OZ7"b m VrRgN,Q8]h[e0D@خ'iz( %my(cI)$fhs' u.VfO] ɐ2pY%5Uv"ha݆Y|hiԓE*pv  M.qYdl%ϧ%Yg"eYe@%pPʴllI' dԀZ2-K+/f2 i*~\5n_'}LUne|p'^PQ'uoΙl1[A [$XHVu_af,\inrrmemVUhߖr܎湽EFɖ`J5),:$I 췎 *W-Kʥ2+rqAt*ŊUYuLF$q2_٤_b_]?s /QeJ%6ZՙӚ! !) ~@Q `(AvoD]\ P$z]*}( HݑP0PPb xVb+SjZQ]"=hY"&&Z 4sNjhê.7vd%sި}oF/^*83ZCaae(JZ98A+ ֥?Ɠ"3z ]iT6sjS;UZ$Hc߶,c]rƌ3N,5X%SتdcieZ/}SjIAB XE_2ڲpBI.m]vgNCtut3!w4)kwΎYGoC-}v_unǨ]U*sbL9uncB"3ٮzk1y/ gn/жN1d5 ww8g 1bf]FT\3Wm돖U" ҆ʙVv.vNK>/KpQCcvL p"^LO2)씽IHAc)vue!+"ZL.Z5[3NqԤdS%x" #mYL&l/Nd]uk=CKIxG!$nӞ#)emq,rz3_%h/rFlPF&~:RvvV ^PSF~IPַB(y,?gmY!'+fX=WU~ 2l#Yv\| ؝!#jhaj4*2[L6H5ǟ^^J${ bV]霂[ny7awA95 w-T|ZFLD O&n!CB'q'&l8ꖑ47_;_^rN:R|MU9-Gu/3#z# (ߧԆxZtxM\eRsf8?bC|`HS$:!s+ǡnx̻eN㱹-=ٵY2k!ұصݏ_V}"_9#!*v~\GMoId}LibpSHuN~7e!J+4㭣i4Lj8gp80ûh8ԌÏy;.qo^#+Ӿ+p' VD.]X0@] 6EPaÅN4xQA pcFRhG9n\Xeǎ*-3ɎcFS$Ɋ0tYR#Ζ(ǜ-ftXQՄ !}hH"*Dx֗bE} ʤI= L]_p` 0bұcwTG+.n'Sw\2e6cznl4XgY+5Vxf]%ϏA%nw;< RV\"tOw;Hϡ:Q4 9V}‡jy"撚 Ii]k4H*Zj@4z)Aj %8 J,%pC뉹,?A U@N•Z8Ԓ =݂3- =-.Ⱥc 5n쵦,0"?[MM ;"D.}";;;6ڴj&b2"6LӻqtqD7ZlSN\d[(1cDMA:|1t0[8ޢʯLI?2+=*QMBGULlSq%+th s۪hK3]"EENj|6F#vI"|mK ,pBIU6*Ri{I +D T OP\Vzה^: vu]Ͱ8VJk3c$-fpfd4?rc2W[.eOC>8mAcЦ68#[Y>Te[frzE),ۻ:ju>N0mׂo'/ &W[/݄{#W-[x(&׵甙L(&ꐊ*CxZ341Cͣlpv,.%'+ͤtѭT 9s]UGk1M!d1Ιd/'}F#^vO% ވ^W:M\򃎞Dv-Yb=ד.g+)w٘Z(ѦcN^/ bI\&I(U4^ Ǣ(0j ǀNÉҡ40/eDWԲ*+-KF=,ʂ~3M'trZi yXف,Ӱ g+@7ًqߏ mn3Fى4{Ëcn!jG쥰x ;&AMxaX0WCVI`RƌGME_]^DZ4Ʒ ~YI&=肔BB\J6EM~}$TO2o9X7Of@f@\x0ȍ$;d (ӤޟLc-j 1rE]f@S=(ʙRT.xV3]rY$S, `M@ tjT\I44B[^4}ST/XLӳ)-uuPƎKqed VnRꔴQaSI;kjU)d78|?`ɶsT%*Di+zNّM17N}ĖONm3Y!9iWxLja TI9AZ٘+aRh^ײ8 ~ĪƔ~SIuU mfQ0~{ǪILpϿ oU鈦^W("C*q1ܩjIg|eMrzɌ3钨?r9ގ*+X9U+dXlEJP#n4z.>nk I_]}S><ǃ+*N9SJV'+X)W`B8X{,mϻM;ZBL.h2N$.կ/^V7,R8lVUw&T}y#SYXk (gz Vc"MQ1tK$ZePsDST/\.7}*R RlN{O\T(\?q䎮hcB]xFLy>oמ~8QA+z'f$0츲_#̬\+tZL"STm&r S$N(a`]]s/9%ahvYw̉pSIqFͤ(g&՞8#kK[=ס97%"/E=w뚎KVߺ 6`K'M{vr6HYe.].=ꬻʜlՅ[d>noq&7f3(PԼF}OVLt*qc|T8\si]X$Fd B)X*Ϻ&D eT*tsGkDѤ}ZF*VFnYmrd<2.,v/ dMKKhdl & PGǞ̑$bC8/r"a"k >*iNr*unm ajV^|y_MY*R_tov8itAS"-pP^N\#îkO/KiRO5Kl2sG. XڌlY&NIj6.ONĤ"PuMfWxc.H0ɋB bcBƪcݲ uFZl-f3ġ͏[&R ľXF+O(xMX2 fm(L?cn,0gW"W`qN%dN%|N2Γ!7fb U&^c('M%')H K|0OpĪRzÖhx 8L f DR(n"ёJb%`r#ׂpz-}fj.,BަMNH'%pzfn֨AD62#_T҆Jafb(b R$Φ(o|YJs);sƏϼRX 7 Z\krI1C金&(_p'hϐ߸mއ2c&5EQK H:֋0}h7Pw~ Z.Ւ_h5GȘ, cGP6kJt4QL,1O { PFZD:i4(MU/$_3ނ)e@l<,ibH$5Qj <+kZQ%ʀ^4CثW-sdZ/1,I W3}u1J+ 4]Mb}1j%/z6;KPBlj~ʉ[/KNYq%L+O+od{=ҧp ;qp=EgƋ=Ѻvr8XD6wg/.0/\.TJwNQ/^sF1JVR2~(X,~,M,3vlv +mp ˠ#XqqokWd4SPժ.pnٖUjNxW FJ⁰Ζ{F'wQ|)L0A?'OimGQWj}|mjSpV|@3Ȗ8~(1oY9@GB Mtָn)e%8XuR5NӗŇzϪɦȦwO34,(2%pANw1H:ET.=.iji{?u3|m<[D>.(6mNZ,hCSTxxʓ OTH)`[ts9YQaAXJxƪ#W0SozQ;7J.nmݹ&bhnsXpbsdcs1hoBkU=)F;XӊiZIBSSnr OV1:vL]`ۈC7,vr 4T-Y5bxsULi(3oLb_~?eTNAE,NF,X RtcX%ܣ-0*LN c5gsY [n‘UwIy͛uu7m+r-)f#h:&~S:h:y}2ZώGrKeP惵\xeSnVH8CCRe +wya.6֩C+lQM"`#E[}^})A4~+.]qHk+6%ޱ?eH!v_H^/7 {xQ!'23;iCDZd\ w1?*:f{" OL@_=/` g7u:fq,Ƃl?L H*\ȰÇ#JHŋ3jȱDŽBIdI&S\ɲ˗0cʜI͛8sɳϟ@s JѣH*]ʴӧ#BJիXjR*מۢV+l䩴jo- n\szksԃ"2ƅDgB %5!LHa#@@CLLDtcuHЁMr[ .ǻ,DfYj&eEn a pP8wH'.m{6n!M1F5,v?gOw#rrɋ*8THG/B,yX`j`Iݗ_HdCjlR;!~gJ,Y) (R*N9>C<`;Tp7%1X;NPGxe([$)}  $4FŽm/ %(1;u6]ffbR 瑉H&H]D< 9NаtY&6'dBΑTuci)5G&:M2)GV)#@ yb! 6WhDDm=`,CiJ/N / e1{2m \:C;Sͫ$E0N4vuɎ%Ul ,$l(!d,%H\sU(AO\jyZAAKI\AQ_pAppOfiJyQYe"GW% N\fpfVA.DoRWOy'̎.^ɖEґAӘAלLȓ\ȆAܰo٧gy..Diy fOpEfVuN^E+a1AAJopgyhY֒ݷշĄҘҕ՛ˏޮȭďč‘đŒŒœƔƔƕƕǖǗȘșɛʜʚɟ̞˞̂͒͡ԢΣΤϦϥϧШЦЫ¨ѩѩѫҫҬӪҮԯծԱձֲִ״׵ضطظٷٻۺڽݿݹڴljޞԩǴp H*\ȰÇ#JHq`Om * r$HCaFd ^Ō/JE6RTJѣH*]ʴӧ]X@&v85r& za V,ٰID؇ u+-IjUTĀ"@]̸ǐ#K\Љp@/Q+FѥOAPSܔ%r? @7˘5]bv* h*'p8[УKN:dd#r|"fj5U.KFz߿&KQP(0Ah6,B dz"ǎxCs 4 )0f֕h(X c'h$$ sn' $;D9d/DC09@A,1 Aؠ M\ 9mSfN D`3$XB*矀 TÄ$04q` s>2h‘QdbWs (A7pJBw`4*w<%uBW%&6lt@} 4'`'G^y }0.;{i*P?LqCe ;S\A&IЙ' pU\(c3狏4WlDQq/Su, DE1zĎdJ,@J p┤t#D"}؃ڐ3Q ,cPFXbPc#yIb>č2L?F!4&BFIQ|f5iMK ʔ"NXlf"<>L32OwN$CF(‚ ѿb֥IkRؒtdY8ŮH3;ԑ}| ]xt(51^y r\W [Z*WV`!-d v %3Ψxw0Kߥ֨:-1|;\yg vsugjWSs4}Y*] j6XmoLְT]rolWYCep809ZdpX;^/]kXt浲0!+$}mKԻJSI~g'p:q jM؀h|r玎ZUh`}GIvfyxrؐrdm-(fǏ;v]wHxs|lGoox%yY ~[dcm~uQqf%~Y5 Z }؎d8]"?pwtg YxCR!gmoH؏OȈxu։Aɓ r:YrVˈyUW` UVj``xc 8XXhE!FȋVwu(Ntp؊IH~aUH Fehe[9)j:i9yzeTu?h'{8} GwNq|ΙIlOixdWVH聖W j م"Ub rem5g9cJNa &YAhlȒ96vFy~9m!W~g'綁hVKv Ykۙ/R IybA 9S7w|X10}8UVg[r[6tuד}  ~:%Ȅ:j֙ux⸝G7ssJ|yjKZ<]_IhN egHrtr#:SjȡV(uفdw:3|:gtיʙFsy6)v{zb!4rYi_ZazrRֈmT7ym ev%ZʙxIgȜ\io|{٣p q'Šm{y~*ɈaH:ViʃYv3Y誣pb6hIw*b=ȇ UJJIz]*v]uXy\z9; $Gٮ(Xwٴ#ypv~ ꆶʟ0;~zv[ȈTX}bdڡ 9xuoVPFfŝb{|P t*plx7x[>XRj j)Ǜ6k+ bgr IٯV6r {m~Ʋxcr(xwdڷF( d$i[JJ+_, N9}n&և9%k⺻w6抵Z6e9뛅I^Zek׷eHvegiIZYB.{+,}Go( /[n@:qRvuUS[TxH[?6jf[; vr po#|6Jvpؖtj{+bcxꇚ|j깺vr`HW,A}z橶 0Gi1ȫ8~K |?eOK)KY>{JDeOXǫ+ؼ-+ &ڣۚ܅&XjmE^B8u[]Iӡ1!>޼&w{\`j묒T-]΀"~}`YvPo .|꘱M^`̥ A}x^]ZLBV ۍͷGPeTt֭N1I ?j}Ȼx-0My̺·*"~Z͙lg^K }lܦR[n%ܠvq筰Nn=QQюmط5@̚qmgmFH=\sYϡΗP8.兌ۏĤ{dPbD 6\H1#B*X#Ǐ!AĔ[2-z4Ԩcݚ3ӡiuPQ#-e6|)oI3[V1%`3Uz RɎGvX䁑1k &Х3 5uܝxMZ4lYز[-[uLfZ/p}V'ܼr~{:d3LÉk-|͑i9G& 5Z-귺hkԖ"j6-cM7"Au d . Y boD=4k?>C̫F‹i1˔$IHœP>[J-栢7 6\kn-| LܴT28pb ,$QL嶄)^$>fO3[i<$F( k6% >T(K+m5X7UN *ײZ6o5Uj%-)4>(?}:2< Ք$Hcr40~QG@:\K:}(_74cB9YExBEn߮$|n;s|t#rr+Cy%HT1GT[qEȖ5-^90 I76{յ*POƫjשҨIX̓#ViF8ߓi;^ d~#vv+i:WlNBP:qE*+:e%N;0Yfh >9 3ZiK i+2D̡nU 52;"VÒeW/ۻ{t8Z:fVeDKCv3~" j` v4^j#+sO 1c#@.>,A[VhG ,tȴ V:afrs7iLOtK)C5: nųjP%>~w؜j&7lU^H`uivdCZg}^uUqF$ЂѰ|(A#kpx9ttYKˊӧ3l5Z\*>q z({rjGqUQO Ͳ3?}v\jAMJv;h 7Lnt%^:1Z|bذZy&C`Q^/k$yD6Sw0y)2lS'j2YZlb6+?m4Y1,}lu[vi$~9IĹ$B[Ћ½(!Ot#q,JA1jd)J=iu*6L̒a !>3d8JN13ܷA3E2$1kaI JKHx0UK6dA FC)M,--phȱLʴg+G@_7Xӛ۱əA!t:ƬC 4oBrLllȀɢ:X.I Xh5hB"~O 5*DdK#ԴK y+*NHέzœjZdǧ4,Dˀ edP$otX#s8.5<+sq  9Ll4L<)C;# hd~ͷ_kRQ:+,* vȀ!Rh`DŽљ=Ltmâ.t"UPQPt0 ɎQ/Z2M̵$3:т CTJb7gTHǣEgdv(l؀7BC SIéK3Ryxu0Q9%?T!Ҵ Uc.T=/B*RIy̾>#'{vh)#+4CYAS!(KPasM[-nK[ml=YB3GICt7]~s\ec`hh(ۗd!9JZ VDC{+rA[CuTÞL ;4|ǣ۝0`;a,@,Z  xbQKm1$.74/ q--l A-[aή)rZ]}G)V\\cr͚J PhXQJ!9\Lz%XTw}GӰ)LZCpm]ТEb̀BV!cd`s|bj_פYr%9ƘPɔ\e8U pL,rccxN3q*蘔3ͥaP}+XU/" UV:XTeVVq6w]=&qHə90)1^E ͷF?;ȺC4 sʙ2eS{(tH 'pĊLkZAkvP0KRCadDNZ5=hW!]՘o]̠kvtCPr-2\|y!E::(ޤs )dD(0s`n=I5Nm]n~2,7 ]D/WRS`/24MrHͤȡ@k;G&>VH6D#џK"MWIN;{L-lӸ[΢iw,K[KPo=mǿʙ<0 EZ &"O; @b&J\?ѣZ `sőm]sȓ ±gj:׵җc5-$~˵.X %=C&T4μ⍯(=sS$NήFe-"ZVA0JR 7nC-<:'@_rb'roRkn5l1_cB//4 ۧ_Y7׳}ٜMd6LUڳd\=AutYWG͙l=v[Ǔگ$DHYmy;h-4^iklÙ 6(5G+,V4z$mKئ|utqUE5&O2+OrrL#}6jtxn+ĩJ m goǵC"o#G=%OwyƉ #yӝVS 0RY]glO%#\&ӥsy|$}ubufůT:̓tEϾ[xjJN0\ fMwC4SŝXeH~R^9=|`( (;"FQ{M)10{[ZAbX[&|h!‰.dHQbÁ-dȑ-~LhrFbgrfN<ZgP:q.%jtgӞO& z*TIJ4*װbDzeĒE![O<۱#Ŷ ڕ!FsQN%_ <ck#5i,&[gϲt2w[ժV_e3ڵӢ^΍էW߯m׆Z\7Yd4 sA>Ikd 1mJ._K/e;uEƫAL{٭o_u1}_tVGrTJE(!sbQօXVme8MćVF)*[חHM|&FG\!&K32{*I#_C2K"֕%`a7 ؠJf\5(\cxu5Ye륇}J>Z)z':hh%jP^ ߞel[(]^Z}iA^0Wb.E&f&NxUqYc8*BLʟʢw}ifzF~ddiI֎٢iV%݊92iX&vj[ۮSi"~vy%ZJe,q1~,uv1^RjrynQ,uB*&ER^hk}h[XK\5U1 bA (nJ_OWdӊ곸ɤv)5HqT*{Tmt "V4P[tSloc5YlqM)^7~\[5%xWv/kgy{xY<(7ǷH~zA_Q9 c]t#0옞8׸gldgWϑCl2j~&zxo5Jh:CMeE:W|$-%^3"WwʠN&ip; mr!|oLcZj^ zo ~ak#[\>(ALS -6FS.f [INaH F&OiYsqrKʅoS^2e?n>e88űn$%Hg Nte:,>6rtD e"wF$q*< " udn՚dkfUI=E2c'IQ5IV6S1PYk)h.+L9V +LGk$IyvnSmCm iMIkDEh-m?cĶ-N[A,eW+E$™v'M\qK'6:ڟe3\tV 54ݴ?ߵ$RRYIuĠ{e\F @vNh+mls8OQ4%܃D 6~N67K3Pj'*dSشmud%˵Lo.B2xuO4tu΁֥HˢNY4]ݨ T*Y-hcꫵkxyHU"/ʅ^df-.tD 4#'DhP)KShb>7XZ.h~n}ID؊t,`U"DzMxx^WE*ZB o:':Rrs 5?ŧOy=\+:Z0DL#e[7RyRE2%KjZ͉XHmok>ѝ)5cUH3ُdvIؽx>a; *6q>Ts'j.#6Z}(}ib2&Vk1ǰw9\*KwGd#{㘽Tfq.uQS4:&(x:.&p Nlm\RˉL4&LW l;gܞHW~]rezWy۷w9׾ TbwPAQ 5W^ ib3L ~ DYY#Yɜ\m`KA Q!PUE`֍d$D״<!b "95QIUE2Ta3[ $^bHBN1OL0E7H Zw]YxN<`['Ya6 9tՅмE]Av~$-U~ )KDⱟiP)y\Wd9ЦA H&-uԕ@/Z1r8"-I NeZ> I}uRMfS9%פֿ&LR&VT?"($ Na}\=V1b4^IئͦIԋaU`<'P|D!O-"I}LU] htgVE04."|<* 3I̙%Ѩ_QT$)Uy(UAsI )c-Sp⩵U 4Ia #7<"nJH,EU^]`mgCJ "g`SFހ =EQmZա蛠| mQc='f`hZ"A7%JPn]W}PeNm=Q3yQM݇U9yb`q=YS-iMb5aKʊ3N$mgʦoRnN}+ZH#a:ګ*)VJJh\ `D!̠NAB:dLKQNm6'ڕ!TD ڔ*^M}d+YGvIŤ,߾6hϠWAZ@z2p|_zKPKn"adDeh%aHin)x*D=nƀV=ZRLވQLƆjq|f^VPP_OΊXv6Z#ϧEՍ>Hw Kׂ tcY@*M0Bv|1$(:)q!lOrGJmlE ee/0U%6G6NS1}HhAka @n(~r R' []%օ%F "$=޶qF."))Ej9zӪaDŽ#rkf[_r!>i >6DN*1ָ"#FqQ˲X"pF(s q4/*H =1`^gU#+Nєx7}ýa3`(Ru(ZdU%qn"*^nxeEB~&*m)&Ӯ}j=d%9sb bO)a6)ygiٟF3N\Y4%9]PgnxZXy`'=`P0cq˒¥s  -ı&\k>"f]'%TW|'\\0ez_1[*?V@kHP0?ݢ!f$~a:ޱ S "hэH,xeb.d㺥YB\%Gʊ _>ᢩ%+pm&<: R=RO+8aPΨ7l3wf͡T6 DAfNr*'O ܼ 7`q/YA<~vp`7DܮVƨҾÐvN uL1a`ZHUVVi2f3RakZGs']X Srxvd B5'^$(Z6KCjUYSi0wM"=ߴz7҅qٞ&IaHgijHNA_%&͌`xA{kXpJuyl wft0gue!&!֪/3ӏNI7B?֮3!b /An]&6_QV_XY50o j_7ԞfrdkyCTfr"}\6`5//n`pʙjs( .u3~[;Qc"pmUCl0bE^DaGA.\N *Md̖nDI&̛5gt 3N.tRL_\TjOU&=5&M\mi4+X&i>EfS9uӨܫM}.4UHUsL}Rr\Ŏ9J2dϏGδ2Ǧ<|z%5Pڃ&|*Utps$zԚ6QΟWM֋ݾ9ܸoQKt},N|-'#bb~њ1 *mG+,K.RɧJc++䖻+<+6l0ӍR*1Z*P*R r. I l23P@)'OΰJ$Ȃ<϶kI23N7KFj+6\:L ;\SPd3-7*;M5E+K嬣 R˃̲JL#J UV˦[ QD(nNK1;?NBz=?eZk+\60KMZ\" ApǨԊƨ֚eK+A5+P*tRk V1|-l>Y>u-O߇( iIK3hsjDd [O61M'cc6ej]pG{MuE;tBٳnZ48Pk6wW=yɑ,UJ;|UȪFmoWf4M\= 7+ʝX|*FٺSVO}YBς$<\$7{о $#&h/L|Vy䓸N+vR;rf'L[bq@]'F IMOTlr03Wa39EU:Br"#C \0Fā,?TUioNsQ]PV-HKt*13[{F@6ő&D1Ӹg`G z0R @#BJi Y0%N0;`EEĎFnF3p:8nb5Q˖ebPn,Z CiicI0`h2 1ypJ=Æ3d+^GRn&M)]JV@9F4km |%ZʒՒWP **I2_*+mT(ȑt"&;o\`8>9ghXK./,PbxɊFZc$CplFmweEJp,,,  Ĺ( !A  eLs\v>ϑ"Q6J++*7D ZO[Iftޤ#gv0Jl j@%gV?;9Vxҧ0JGu1>QFv,>+RKɘX&+,<31ea670m[S20Hёl@c+ita5..#E7"wBju[ׂ,Uڸ,vL2Jy,;=ܣh..fپhJ2F*["%bL=S7 JZZحW \:]7nfkK Ȝm@[Ug,Ԙ]B Y5!UU͹eTs53m%yMK6EIi^]Svv,,c KceqTMMi6u#[~cpG mXh==q "a Oz ]4/zR̄4V]V%S֛Mܛ{ƆZF!JMك0V)scHr3$lD :g`\5{d4Ucsv)INxA$Kqf`mOV|u_O! žHRֈbѳMcڪα%qv5]<'ۛ3Z7oVU0[Ob5_]C5c654 M#z]u傅s{uͳ{r)6Jcó1,*+ƨ S `<ˡ v$&$n )('ʼ B/4Rΰ @c$XXt*BND@p\$>KKg .hlHjh| `Gfď|F,S4td:iҭ&,,*n˃nl H FJdhTGyH knjK7.G O۠I@`6oKFȒNconʉtR+0*x#`pMH eBhLFHl%b |>,kӰd LJҊ3 cޫ_˧X*)d]Fh-m*qL2$ELϿFP6&OF1t̖+xͬ辀gcыpqDZ3D6Mhb1KNL@NE>" &'|,2$0&f]m =P$\MR~*X2ƪox*EO S.A)pJe`>%,!̎&0R4tLV'z `.v'ePиJ8IŢMgzĈϠ+B%ͨmN0OcM\Fd%`T8ʲ!î19KmP ;nݬJ+'pSwJIQoMhRNLIo*Htm0bh[tGἓ'`p40/skH둻VE,NRO1T krgmh'Ƕd3&7k&S0q P$f 8aʜF1_8G;.,2/-mv ,KRC5ȑڈ&XVDlJM;Lg8 P 9\""eYRC+) -DubP\5PO9qR$>UY(^WC SZ&.͏?)u9IJkAY3 0<;3Gp~>m[ ,YGu [Eت٪WWgfa 8AӦ)U_kmj^NWF%9&'%Pծ+7O޶fD(EU&kRЗҔ .6 U g3ϣFudodcO5-Q)k 琯hO'ZF0/˪}J/ٰj^:vIK9KMuj +)1okVl3PW=F(#h(n{cL4QɇrLytEQC9EPT Cd5Mx"ɀ:)m2W H 5<Ѷ4 .{~*. _-Gl=3 GғOi QF ]2wz=I,Y|6fGvKQ%+w{/DwH.guT|'Qٌcs($x.DSg8-|@JI:kOX[nLec,w7w_[>j(=; oPe]A- ڨ\Wr*w,13bI4\@F{[y֖E\g\ҞʘQudG#2nuuR!FIt- 2tDC6GC/eY;٨ՊH q%YC <+ujdMRP+n;ѳo3 ݂!]-!HksaƷ6oLUvX{ק&V]zKxWpvx;kEpӝs47O[鳕8&p8 Ң@9 pnK #8LwE .aItV-Q`08OVU8az pq{Yy\Mn#sMWv^'SDgF`;BmZNvY/Qa9t!_&"5]1DtueL[h7Z5s ܏pPW+δӄpMw׌nxګOV7ln l~s W#t9Q/$K |0%#zȟ2骥բ//ڠU崲{)eT1Hk o[sjX:->WZּ ~Ϣ uY S6.W xJ%{)iA:1IQ) ŌӚ8v -ƅQFcxTl2)IfKY aUMx@^X"O!aB31zK0~]w[ ;*JIy~Uf[;q!KQ3*CN%h%xFblK{L,w6Oxc6=\5N]p>3t{kBAX>Xl:.,<`B r Ao!T8ƅvX1dG>|"D$cÈ#Y6DiSƐWq"N=t3%Κ kyD4ԹP˟Y 6VM)"e4ں} r龝7oܹt7`p. (ԧxdņ#S-dfof옦do8W^IBKfm1sv նkӤ&/shΜ׽~/׺jGSf KnF~sX轋/7Ҩ3u{Ub o+m<ɷW>e^I0yJ]u}G E7'՗Us.$ue^ٵWxEx:eAalgAXŗjd`oBB&PZq`Y!b[ZJ5G\W@D*&Kh*GL1_BŁi&Y"jh]n&Tz&E^\X%Z( _Y 6jIzদ}ߪ"7+cӇj%k\URt\ZQ'"ViMv3rhvn @=gnLfIZ_e6j뛕[Չ2U6Fa~b ú~~i_Mՙ.[Gr.{]8KsY.PK$mKTՃƖU)F/} k$aUoB*^x"-g%ӥb9tJKH7 k3cތ ^w-,^agɧryq9']ƭeo_oK̝ W 0S6 w@"mEヘOeʛyv,n]ƨ;;LS\l)[{f&KZp6\hsyMHӭ˟ղB=9]ߛ^)pyRK3O5"@jڼŎC#LRD@9Q؞NX *#2+ bGbBPgvH57h7M<+U/`"~3\1 >Sm"!`S EGdvQ'G9,tAbfTўIm!Л%,W|K;ZbԱ?qOHXv(I] b04 Y/KV䖘6Q0 rG,:UQu%< ӉFŮeCe'sMuD]֤֠A.s:EfۜI $ΚtN**V*m[ʳSL{r>92ml|ÇI#O^kX%}lYbVg^̲f9dJH!+<}W(Gp4pSm#^d^*uw'VNbe1 WÄvzYA՗֚jYNꓡ ix;Uh‘vr4#Uii]ZazMT&z0 _({."'+IJeօ5fѼ~f KG f&Bά[kX_ ZbU$ăoA{pƂz! ]6r3غT,{mn}afjJ;h6 2HŜ:fh%b 9V[sJO`df1vtgX![7:㘂`NRaQ4#, =YVG>bGxnmck~MP4ù.e3{G";9X)X`(JJC͹*=d^J]tbmJnclOq,dKe{"ZT @SՑWKXC5.N[GsGgw1[FWLUN=T~҄up"JH`fJC)K7eqdveQ>SgeF"RGe*w~WjWK'ÅrSP? ̠{5gG]o}g>X^q1HwW?HgGhpP6]BbHzqjbNJG~kr"VtsVrA%autRxg|esJ|$**&ba~(a$6|TH,X8D@y'eUAo1|[3 ^ySc2[X(jTya(B~uu,559eih5Ji3&!Pdy`|qPi('!R9Zi!x҆#)ZE#5Boj 9(=DG0g7-LsR>Tv;X4u9%uD]LTmrhKF'`1?yO/'W.aRwVY=Y73+tdry%qbRq,c2nNQ$(P單|6V#etNh|s)U}Pps1DNH{D%T3s?8ĸuMĖ(AёvJLe suVחɅ9YNRRTwm*QFkt>D_wd^3ScxĎ9D*Z'mS9iurt-T `!БZĕxwͤz'TAXd׊TA4חTO>)U3(G8a9qgnVT:T ͩ{'c'@ Zpu?H3),uLqvGt8V"xiO:q#!q+? B%x։$W{6vg^ҩcMC*6k#X&^꡿z > }zKHVj\oĔ[{Ǩ)Ւpwdn 3ڕJJ7`i2j.g6ydheؘnk]%Tu)hq$g4嗒xZd6>mPIy ʫhe$ՅvhWgB̆unvu~(9c֕U(TDZVad:ij)=Ul 0EZ]iN|zU]x "LW-(6WaBb*K:.8C4x8I=n7bT'uGhaSQР7khl}[8q98g率xLqt~9D\E!Z>veTZMLm]IlZdVW֓j Z<]@}&tًjO)FՓNZ6^ڇbJ6F&FYy Uw)Bdj+^tM7r~mloZiM8nK1pK,jQmU{:Ʒw풢qqO'_. DXE'd~8r:RX5!%lkF[{>+6qwN>)d[jFH)3 UPNfu:?k;_m ΀E69x;xwsHrTLQyG^ET”Bæˏbv%?MF= eK~2}I`pIicb)z1_^xP@y&iH?aC4܂V+G0kH{ +% Jܠ5sD0o@ou˼@,I_SKKhJg;i"Tɔ['Uk׹k]$kUy`V}=ANIw76. P=R (pwvArwIƘ/Z KbJ}6X{τukchQ.!qd}V O-8 fInN&Bz{ vOh ze}J']\&=߷Ë;\ݒ'`}\SeCku6*rA_R46HpUg5ÍɞH_b,຅ˠ@<-JP". B#E.|HHjHI*Of0KKvRN%i$zgOI,-MeCUVjպWaņ@R'[C= qHFo}QOiߚΔ'"`mnlj_h ɕS^V̶qK3^x94.by5#^ 뗢o#Fr=vWUe|lY7YʘIYxVL7 ]tGT'껒+N7]hSko{Ӛs?X </k =3+$dϥ3MNA21[DR+9Zt:,N³,:IN'tn &+/$K*AM@DsX$tì7kSJ6KL!zpA$KX {3˼t\#Qͧ6T^p[bR[{[ ɣ7Jw-3XpǸEj 5Uؕz:(c5?6Jzm;/ǽ:m5:2`aԛ Ha-lfc,^[i֐G8NO *iG.9W?qͱk;xB{K7K2["\/l41kHw3 7ϹrkƝƷAOg=չD䞛q%-x3Rp |m]Ҝ-\xY;`r52Ja[XMh7AoANW0QscXGAcane6w5 0O^w٩@S:";Q|(0%;+•=֘z)Ŝ<ɴ+q፰HYb L]39A(MZ>gE}7_YMSR@d9 L-J!Łrh 5#It(6=Ƈ2课ars~ƭZkhvCi1_hw)ŗ+Br1hARڴ䋚FIM;ȿP7c0k" }$/lo;{rPLᩏEN]s#(:7>!ohe"Ivn^D$R9oKs71AO,yeƎzkBäÔKa o,P;Rn-The;[lEO);XVR~r_H^E]&ë>HYP$)}sƐ? ;f4ͰhcT)sUE4?eV eq=%lhjLND&;W(j۷`z\Y ף;$ȃ>LJC(rO:;!3!,63AvlT{&-)4zlk !s;*\h(L@Q115';ɵҨGE"& =ŕDdnB,s(J+Uѳ1y,xI h츲G)1:t"yF+ 9BTwL'`:[73|Hs8Cz=q7{N"C*,?=FI¬Bå,ኵ0K>k&&| *`A..Ϩ"b;[9+)#|RS2+!:IPK:T?tԤ5F'ϧʛc3D<:F|&3;巯Lys3ˬ$S:2#jm, H@U-LD"ʂ4!-SF1ҭY:%sD/3BZ;-@T63C`$jQ78;s P\UzJK#+ز*ET4$%9:tCBJbATѹȼ2ӵ=bS*/`/ |UUIsIN7 zMR>S9qN?}9cۿ*B3.$VcdrtB7*߳S:dS#(1 tUב:V+rcJkXRӈ:+M*U*P|l3JL%C,i XԹ"T9' ~yP1sEL";=m6xCJT D. Ei %dpcM9 ڕ\;x\ :4ә0$ I3GJ G;. a[;Յ'j# F[6M9 q_vSްELɜR۳.6(bSLŕ[2)!1,MO#lS1,REhJ24T :Ɉu̧.ʣD -θCF,ABjY1"B k±QD~;HREbYO4XG4HWW-䁳[d_ɹ @Q[ֲSer\9?QL)\lbV3Eh/eR c2)c5=h;5̳8ssw#q"fDŽWY7%]a/Ѯz $fwdR9]Q6?tY X O3}8mT=| Gl_9En0-iif(^bS:%fIn,z5&e.2.Y}cyr`ukmF8TKXAJ#auUk΍=:m_,8+]l1hiMZJ CEKKfU7}:߂eݏ*vv:!kk.m&)Nhd:kV`Q^Q1gz2VON]~X,IVP'9To!j0bje»nE4D{|9M8`1] 6iy^=#T!MF. 2^{X}e[ 瘟(jWo +n/?1X(T.]Ա m˂\7b)~2E Lȃ1@|0%j̎U Tdܣ R#ZKӻ>o,cLn |msfdq6nM?Ej ^/NP3ɹ)٪pհseuK -X 8!.BlPD ',0Fdh0"F /^XФʎWފ2cƐ /$8Rǜ d(rM1yS"Ȝ(u=iԓ:MZcB!)҇X{ )S@ƬRvt{7/߾~k@Q'Z}ykJcx@CkT(w\%3?>U۔Nk]3_ӶuaF~ذ}Ӱ͝XOhY݆Uop[#ܫ]ϣ `ѳN߆LU%˶ٲ^bkqCZw%~ f%oi~mCeSmbt#RbAH{!x[H6]G ƔN%^z?at!QT皅^ VTWĜgE٘XmUX jiv,ȥI&U"bqN>祙V"jޖuB*'{3%UaՈyanKh*e&e[B)`3^`{Wid~)fq.E^f?̩)>m@ݜp˺` V7衉/`3\ѹLz⋛XƫQsHԜ65&uVi.'1S_nnD-/ _7anw;P2l3 rťaK{PD0tSwּNYp9M~㡥[ZY@fPgh€7r'wζG-XTl`G6HSXƮHlrVʏdC /̠ b_kRHS~ z>4/ ׽oD"9%,5d\dբMX26J~QwV,iaCsQ'1NR Cqk'+r)yc]8GyS>Жq# ї)z/# ^I>ݳq;iK!saJ)uPwdg;ո~Bְ 1N*MJD]N@팆&K9.6hGRk`5CƟ(}8QĜZfȭF(D)Q|:!T΀*YՉV"95VEt  -uRz 4TotF`f:/PU[L9ʾ.+h@%  ]b9mf4$&S(db$؜t2ֆH\[U/ϬU:FhEQlA/I\ćytl2kNJI YF G]Ng_9/I Z6Dľ-XvH7 XC}!@wW*'M(ôlF0K,\rЈc yJm: QN a" ˳[qx8~k`֒ H $K9PXB#qCL{8Hf ?xtk.rvP;GOm}-)H4qBlN5ҷF*\'Z_7H ;20HAP !᷅[$fczii^}9qKsUiuY2"oy1 ʦ'rL4p1;]tr4Mt3BB:C:`G zv% 5w6cZL^ o Asz$ .FGSr$@{jm\i KɵJ5 qel<-c.ф@dzy7Cmmu?h: /*WٕN9T }.@#R4M/c%W7= ,tdo^܈nl߉֕Kv_mf5WYnPNwS$XV?ĕ 2Qϊ}"6bڲ ;.F:vL` {rV:w.HYhMpԒU4/бVUX3 ?9S ը\ӡrE8ăó֨1$eOi_`Y`P RSDaےUs(PVxD T P B DdAzC`j2A OI)aN IV`9 f 4}K| qp5YN=`"~Uy=9*9` yEA2hz]TEOLᇹMm$%vp0/XuF˙)WRT\ڀIia Gۄ R,]APR T##`9 EbLϺHO} %)YP~UTn~Ŕ ؁Keǻ!O#2: 81"c5ڥq!kx!bUROlmTg!ȥկ44_畍ˠ1\-$Pʘ=rHq :enQN-%4"鞒Q$[# %yT%k8F[)EW-"^fGz`Kl q \K'\3~GOft5ָptɬeSdYHm d`Z/5AnqfUbV˕qBEbIt!@& ̗A`%F8((>%1XzǐᏑIL֩! a!bAa!S15̦$eU%ZDcia(.Ntnre=MSW.RF!`mPa}$p`Z)MBmEf) (sn'\]z ]mB"QQ=h** akpknUu)EoS j*1('oyed\}#F#b)daJuD)VaPF#0nFTDRQZFye mTZ63a(qLri ?(BP],YM&~v1(=6ךh6PmNݦ)a?dt,=%yBRx\1$$,]F'bf`l)eKi {l^_^]`tE}PfdqP5D䨓 "N*3O_,6.4-(FGܥ)d 1&A.8L`%MW1"rPלͲ\,1rfZhɧЪlZ%cdvΗ)")nHؓ]]!տ] N*I\*uwDjRSbk_~Tc# `սlД.Y=֭|9a҄.eQ RY5Zmqpdr"ජ]?.,P"آ!,iZ8 t~2d O TzLrJ:1jڢ*%59$0 q+$%r!2&VWn]pa/@H-XII % ߚ"pq2Eu!U⇿ i XiG/(ACcCs:aGEjkUR+3"Z=,uxKgYxStb;!`}em@jfej/f۹t $PR\"tNqmA2vxQSCrz:W=PыvxOcn,MJr!P'&R(wPd3_krGVU|,W5XwWʨ MM8:-m"-c< ;'a1G+$o!Ff9oWYޢ+.l+`p9w&N1U]U7w_&TV>!,95!|)ͶP/#f1Z4߽d}|ѤœOp.l:dCŧ")-=wG^+2;vv#e{j)K_nSm{oy`)態GFfS͝Z :up@q!L1|G \%ާpx%CԒaAt- 9dj' ?"abxp-.:V6ܛ'{d&-I6yBps$غl[f^֞'} nQߠp[/3 Wco3[٩*Lf=d󫑰&S4C}u\5~xJhfjlga)q6{;r4Z^@ux+0YT-Om>z61o&53W/3~ ߐ${Jjc`d z ".(>1D8k`?C.,Xୄ DTPČ ZQaE)$H/V$yRȅUrpǒ R2Ò7qڜgG*E uذDG;Tgі n$dF왕!;kٶuZUY>IVwoI7СOnʗEi'UBR{ᄚr̮]WHͽ)E53䄨u ^U)wM׮e~ -2eB%3iWq:tĵ~-\Ŗtqk>*5t^tU7%+ȿì") +;?o<8,2KnBCF䐧0|<-:dnl[JAch:+nJ39H-/HCC)ZC =ς L0,K[R9ʢCG.P1p3.w͐6"ڠ3Ib:̃NԈqD*J̑0ФtDξKjTT4CUmE0]Nˡ|3!~zYxk{חJ!^̘|xx҂󦅥{psY|xHHLȭ𡦐1LQIGQjJ > 6G<Yr1V+B{g3-6LF o)mh0;͎Vמ^h6g.pf:t AͰ0Dd9!(Q䤴 :<2&.5XupG&xsjX,=',ڤWY~U c3)dW6}+iN e ,Y^L8ͥXʃjm.ctŠF0Q*E7R W&}Ȇk}337%U L:-EJ1fؕDٸT@[΍ú;0 3zU@w'̥n %LN$H Md|qrgLNICخƅ6qt1q豁e&].P4y Ffi 9LȆ&  6m I%EOzƪ0aXj{'T1}6 * ,,~>in^*2x奮_e#f/ጱ|F POcL03f,3 =P5acPغpIz܆ g# 2<)4-⎒{mܤJM܊b KܘI (gppyFpO -~̔h 񘳆?_&v&`r (ĵH-'Bi0Wcʄtse(`]Ix`{S_1Kـ2oU2i#'=qg:t-x4qICJ9Ee>gUfϥ]ֈ1{V3x(Wwy`+Mf5}-]7+nU6 4>po.!ˤJp]gBOmuQBK5|7nK̈́OD˹.g6<.K1f VaY58 N8hXpG4+(%yMTȋwQN =.tFɒlGKTJ e)# =E/,UAiWXʥ* Srqq|H&:<ˀ~wEsˏyQrְ%qnb+q[8 ɷKVU0) 0rƒɐƷuZi5[y'* ?jOPRA8udҫU{a2W6.,J2[*fRuvT` vt;\eGo.#>\wOMum(S̢rV)jb;=vk9 9砧ǚJ_"Ez܇<[apZ f.q4Gd{`i ŏ Nq!n~4v#͕N+g|`j\I1p5(TS|ZmD-]i!༭ 檬!4pAP!A\ÖyJY !LAO`n˻0E(o 4$f}ez}zIS+q[2KQ<ʸ/>a!$]$=)rә]U&}s^w{^臞^闞^1 ^뷞Þ Ş^>^-AZވ!ܢ!;-!d̮# lԢP, r`-Hv!>@ $B " a3xn-J "5.2`B Tva@4a-!@afT`B _bD! H*\P @b@]! 3ǏyĮD>˗ Ewt ϟ EaE4v0@'ЧPE4)ۆ@ 5ʏRKʎ#ٳ0C1ŮW,1 pDe7E; V&;Q;#` #T@) ׁ̙۾jiv:1V #9OѬЉ2F,Ce7ʁ>| NzljM(+Y 󼭧kwY.CݔKώ"yb" "Jfyq Gd Wꊜ@ JѣH*]8 5pn'9 @9\J0VPP!́<@F*Vvz&4LÈ+^ʑu0xu X&C͜=֊X2<+ |e+- N*P|, *–M\炆uܽow]4Y@(@a4lYa˦n|B@_Gd% 6ؠ  E &\!& ɐS'U XQ 3b@GXO8#<-@@@щ>6#ψ%Gr)d ,{L4<tݸ510h^%ЇtD0@140Iٓ El.'5'N_ ! )무֚ ,$ tʯ,* tW_$ l3L]77e &A&T[.@zEiq20~8фM$I`342`Tioq>dV2"'Tl:*>(0d9YL*sůŽ4"O PG 8@K=8UlVQ^>P@pX}vfdJȃeUpIӠ@XBϩ`@Ŋ8vx>XХԔWn$O6m9ՓL?1$O8Cs.p{DWzpWo'o+7;|Wo_;}/ߏo#V~~DdNo 5@/AHɣ,(S,'8"x c԰~PĆaAfâ"? "1 x#ЉL! )0 Q"b~XE-JR<D/ Č A#PAּjG/@ tD$шF$D6̐ t0^aF!R!JPxto_K]zMb4,K{Zs})L<.tЇD\jQ%JvHe-zԭիH5VJѫVi*VՇme[rT:+]9&7!A fLijfKk)"tPXu*ǓąJy8h_cg7kS9l6Y7Vu8{Ղ'fkVct\0ЁlEVz?u`Eض~mNx>8}ur(GAw悽gw{6t֎{8kn(;{EXHXIM@ }0C`h}y~yYXI`jhUivfh'؄e q.כ}W*Ww74Hreg>www璹V5b hDxqXu8dfxIXim[a~h8`f(i (lDWuWw8g/n{&Mmmy }Yy=F`آ~fٌGu3Zu)|yǠ3fxgكtgiQ]oS o0C>bVKiX[RŅ[iv&QWy)GAyyX6b,gEʠ)9f/wwv6HiՈ)vhx|%ʩ) ƪmAP8HR*HppYs'Y t8֖lxzN\>2B˜D )ZwŏTdnWٔ՘$fD7-ٙwOX*wgblDuשwhw8zT(W rxksh99c-FȒN*7^{' tJJsWI}By䆓hLi)Im&ĩ"$ WOYbWq8ib3ۙ?71șڨ 3Zǧ2(ΆaYLS)jQ$dO>m9j)Yo;ۯZaXz(6+MqY % Km\xOj+P'I}rK|YAosezm+[|֊oؒJ`+b@Gy4GKZi olڄO'ʸ͉Y{ qFz\ %Źw%^y@5Z'٧;Hۦ!Vfzdʛ:d[zf鍧Ƀ]s,cgJ'zf<{fE yw0KrtS;ui9 xxzK#wuV𚳱ڲošD ʎf0?zy9K_gSf1{ y@9ޣtH8yewtZVc;Q͵ʫ\I^ZNph)W,3Azؚk6ˈFFJ9M2-x济k<> X<ޫVwls/SL^l8`oKWޜ@mP}-w mw>+c.]MfyL:D[%Jy_èq4JE;lsh}8֛m-2nrܲgtv\k={fS|t@sP UKX_P>q/ߏ 4Xa… 6Q|h5~cF2Q#1Q^h$Hc$i%ƐY92dL53˓HmlZ3N%aҔ3%HdX#Cd)64K1mٰۆ-ćkF|.[u+\ƍ+W_=y!Ą\.lˠSV tGEYfSk\#iOߦMNy3llXiCGtsO7zwƐKV=x;v/Y@WYʔS7qLHI6Vj3<?&ϰJ AJ⏸j?i p{+%RLc:]n>0cƉ1ml8.T5r?#ͨFI0+mɚhSܪLI w+sL}rɟtH`svqnH1z91@Lo<Ͳ R`L;KH] &zS0DM:lXٜI8ڔ҂ӵO)^B;Q @%d YIË.ճI E[J Ŵ܈4e5L. (0؜d])_V|[c&7Ns09Z]Xٝ "qHeݸ,d/4И;㾘G0Am7ՎiBmLV֜3KzU uzҩ|py_Wr-=9z# &;kvRC^\rsG-^0ssN<8T |aKo s9 L=ar|ɱŘ~aǵ=h GY>R)5]nC*\jl3P8Q 8>)&<Nٛ "@tKnSL- ywiεv(mq ]R0TE+ND5ЀnSS)T]{b(>80B¶=vL#F8"2oFc-5fOְqiR O&Q)7F;@8}+q&;8]0`[+ ~H]2RckYuHDE<۶Kmd!)F\azIVVLUT%QgyU4yQkyeO*}1.)/Xt(A QbKmmdRbҖh"]r2H&*hNɧ}Y,a3.F)sT gґU6 ܩuc' "x ݠJT)R-dZ2O8KBgf 7ȽzEp{_"Դ_/DcVBJ©3 } X=TM_ ?Zܨ6[MUMtjWg!m C;Ul)IUĸR2yR ־(0\Pm{('EV4H4f0!N+t4I!I b:9n]MϸjH|eH8,5Ch.iÈ^:6\xtWU9 }n6ySn9΂!Sڄ9~ ^QȔH ^ T`,9|8L(H,WR.}d!*ٔ8yjTGB-QU 7a[7J,A(^rY(K~K$iE+0g ԭ$1+ő31m6fNK)aJllUeUZtyAu2C"!zY.ɑ^02sTǫ"~YCA2Zԭ*ιٺ٧/K@9~ $F%qk^/Ѫ21u1ovT}[b'K]Vp(yBtҡ8azΎ9{/|Vc mF a7W$|@L _ǷXu_y?VyTg:o9!_a K! :CE8 GB&R_94IӻGx=l3m "P³0IKt>Q[c3tˆQ<b?z2cc*0šN@L%ԩ-\zɷͥ$4P Xu5JQ89hJb;G[6lcNZS=GG)\UIEԼa,y+;̺QqMp8+y3h1E"*r]]H3.J4>CC㠫#77y'MC]DN PE=~ͫ:VC&ћGbQcЅ4bRB#E۷=|Pã>¿"b:;OYK6v9ֿMܙ.VQBdIF^X)(Mū@@jTH]W(m0@S,Dr*Trn,J1 A*+-BXktB$Z\D5t]ו'/@+)?KZ!MLv\ԆZM|(JYT D{ҽZ5c46>[e5tYt̡'م؀Vxepd@^l9Ij-e_ |:/Rc/uApia3:̛<-^`#*h김>Yθ-^:CsZ6Ss-=𓸖cop #Bllz^mFgNXmDA3ldƲ.<-U5U>͹T[:a\oF<; /27;DI%D9sY!1xb̼_D_&U PfD39y05ڬ66[ -ew"nzSWjrK*6F]RG\_2G{B3T&m=|T놽dxkKt_^ev~=cnFglt#K6t f19`~BRȄW[u R㋄~A G'@t>kp-0Ɋ3Sj?7*)O0V]ncԃ7?L_!\v;MY!=> ݹμ#G?Y0R}65;p+U6OsURUqf;֦*&B;scũŬlExb^E% VݿvD }tq/b r~1'+ O܍:3%aLJYVoVl>J16ȃT:~U޳!}ˢQTQzDGTW8RH3ճZ!Kį^4_1t:F &gTƋD(>`>%qFEs `\Ee,AY"+ %SjO;O,oeﲸdg;r8V?g-&_̻/:~2 u csDJ́iyrJ nOʫT+i! r3/}|6ѣЦ\6bxm[2q sigE&x]{4@T^z4nVS VI3F<I-#ZY>कu:A:cU| 4g7l|[KqNͻcKeMrxpwa?+KsagTkTboK/vΣrTSw(qftؑƧ`9 wg'x 3Fp`Ab !4(!A ,1D 1>cŃ"n8@?^Th#0@?~7&Ι7qI'PAI&R=w-ʴ&ӨDR]:5լUr EvhVʓ/۱KaBȑŻ  \$DrʴSD6ڰ!L,Ō!%p/˲-UqgfT+USy>Y{Qٵƾ]i~@T:$j~jɎh\v]{C?7q ;;-KGNy_#9H"|*t(GQ;oUP]quxTqf ^MhhZ(u`c^g+RVgqa|%Gcve$fݸ\Ey\5I`gm6dGd$rC؛ojr'$H"VX'N!Y9gX 7^' #bL>W%hpeMvD{tGe{W_|b".Dvt}wVȡ'rvY"qs+l hSHZz]B}ZrIeZ6fH*ZJ9XZFf*;Yyo`x+.k1DŽs*ᴯaxl ; g="WxF[T&@ߍZzWThd$>jz)c,wX',j\]f^h%amzJ,,Fp%5LqR_"GpQsY֬>:Wۮb4^HZψ%ɗB6\l\יqc;,ew=ܳ~PO.ty͚c)K.uΔ ̎0 D<̇oi߈V壑T T'*jsB%!Xӛ _)oo \4pn L`\ڊQ7RN YîVMdYUi"l-ͼmd;jgel˸u4(=hFut%FxSjʼnŨˊ9`;ɺ%,6e\\kЁ< {[TE:ҠxѣvhIV|ziBh>zB 1BܤUmv5dʹiOb;oa߉)~ }*g'ͺv=h? 'g!!;u .Z;9Ϩ!qmbH6ZpvO,v=wA}찶kF"ݮ1Rq W=I\VQCG fZ^M}i"g03 E\^+Zo=gzj͍$53fHӒlM<z~O=!ԕ`hX1imJ@٭ʁE]|XtX H:NLMYAHJ J=Wqm9R` T)d@-MIH\ɐ%̟uLW0-pUCeH(͹[x  !A_HA݀58W]Z_ZNQ DؽMŝW`l>ȕ5[iw_ QrR-8]Œ]xPܡ=ߊZY.IXh4 AI=*$Jb\R!Ǒ&jB%I%N5 8A0^ؑ`aÕa}*%gL> $Քáј$Ч ]a&W6 W T`[ .蜤TN+~dT-323&!0=QB`KY ]FY=c,")x.D7FaT'$mUōa %C$>zeR J̙9u7:9;"U^V.RJciM#q`ZȱEtVX -P2f߰[rjpgV̠Ob+Q(uabcZUGȅE~Vm}T`V\%]P(qMI9" " M% ʪ0jK)UÇ|Dg}iu$_=2&7SGm&DhPV\ \,_ ir]V&ӥy%lkhpSPm$%B԰WGQ9vXMHj[ W*IZ$GumyjYbݟuHz()ν\u]FX*'fEc4MFi}Ls H`%fV ^KH^ՇꅿK$٘ R).$`^jb kfE5 {a&k"֌|gд 1Gd\zNѼhEa~f=V]&r1Lt7&)'^c$UYiQ">>kuOt%NϸafjOd=%*AK=C&N*%0jEl.U2&Tu{!P]]0MZ^ag᜹8ꝡd-˸14Bhv+M ک؛[@VY!NY%_F)*cȊU 0MZ, Q)*)T^E= jF˚ ~1fX_Unl[Y#ޙ'Q Ѹ,W-8t-֚t0ZDRfV̯Efqb .Ѽ'K$LnIcWg YdL հ VU ^@TBbӠfeT& ;g~c@ٯ {_`L/%lx]&/MzJ$NJmTl.ҫ$MSpwQ -R0X rƓS"$+ Йbޙ-g\Y:+^2lRFC:Z/A$|yِv6͝gHXcm.WM,*NFr0p2a `Ke!^ ^'s(1|b.D(-^c9b/oa,:#A@{'7}%_)M#J_\W;mO"ban \ev^y1"~gtNlé$Smejz)ڢq)ɶK`l\P e$f8 3!v!T{G 90U?9)Z4YFj7=Mjse#:XR~Li4{@&!"EثzXcq,tv`Rdz$aah@ɮfz6Y/S.f~b; /utNq WiD#e+s#7TVi :jg^]-u};$=ny ^qSwiAbއ7 v^$!0:~ ! M,% j3_y[}.L)0^* Wίaqjcn6p?ĸ#접00>~2cƮV,MsonQ& &1^zF ?Zqr98Hڢ,}U"  3a0Tr-%Ȫs%S7+v~80`bqb'Zzkz  ahfM?b[V?"n:;cuk]񽇏v1=m&"I@7`?DXPA Fl).,PƄ=RdI#X /U$%L6eƜ X͚,cҼND{Ezs(K/.}:hNI(Ι2g: zЭ<~ϭKm5Ѻv~=-խs \fڵXB%إeaLlj/bȱ'fyRɏQL18˲.kV*zxww_͉0noC)3a (ۼ~|8r&,5nv7qQیB#)NpL O=*l㫵Ϸ ޣj k¼Fg8Pȃ'Dơ C+@ p2OK .dp[wsn> b30Vd.N-Vj*63..BBӦ ]5(mK >+1@/hzӿ0I̡U$2`/X( +!5EtDIM)@ŻxJNbOI`\kpu`ru6\clCR<Ys?KK3.ׇU[.BuSQN0֫Q j㍳CU w  ƋOL$2OCOmNNOGp-0Z$k*VzYtݰgt1#UR,rXsܒ<1o>ݘ- #-IoDVɜSG'0/#t3]Au=d^b]lYHe2K WwOvTroY-LǪqBTՙ6^3{L[奃ZWl4WU7W§]'S|B9T&3$Enݐ>1 n17̭ArF<1~HF3%YXDGC(^դgL^D|d>wNJj$^X0F8KU268jTZĬ3nc?h&yQJQ4ZP%XQJ]3p bVjF2۹93BiIg;E9!3*i]h66%sra`%I*zSIBVQ-k v[I$"UKwtM߹=ų/Ē'd궧X5$ȿAYv29La:p" % ِU4=sŊ8u0L\jE9uXO_o10SgR)Ni5yTigbPyn/OR?2sPÜ9$FHu11VptʾVY^.iƵl|4ڂ[.fv x<]݌PW:yٽzdBT-]]mE;U"rZE$ C7h[LT( yt^Q) Uoi-n*J}en0dEI^zi=e" 8+UFKD /(ΆXǚA45HB>6؈TʇڽnkXӍ`PYU)+B9|[%5Xz/|7reլT9u(PdPK:1SeU+|")@Z!4񠭚9e@ñlRjфxgy %iȺ6oTMl7m;,*s H[>i.%%aMDZk2u)a Y@@(`e~A_.{T6$DžChJG3Kqv1Ws(|"rxWT%Ns' K_5hEW&n6qħo&Rf Fʸ0,)ZEl3t chqzJ8_3[E2`Ier:o4:7hG $#!HFoׂ p7R*Q\TS]q䌈+^Zrŷݔ+-eץj=*7&rRQAntjr{t|p##jY@OLEN_u5\,V&KmV<–|ɹޔbbםBDaM{}k܋MktdHX$#Ɓ ԌNl"dƶq$-VL2H/…Ҭ, }|pJ{p E~D*gn t2sxtDP *$RiP0`M榋z*򲃞9$v'FO\d4H*͝(Xt Gyj(pA a~G / W eMH %lDplrh#(6H>0˻̅VdiGPhr|mq<( XEQkA|8 Ή3TrfhPQ}PЅb]OuS#q9l ei6YvEcy0 \d0,֜* Dq sv|Ф4Ko]l1]${~I'_(eάd蓌 dpP1!©PüBevnf>/S* e 3Fy^RpzJ`TH lr #[. Lά,([S]|f :R MP(/6&Cp qPRq/QFP$iIXj l*)!R+w*1 )s0S)=+D cJ~fĆΩL i-6R/ebh pxB,:b" 2q wєi:s̕8pp.綬բ2?tBs-#P1X|Gp L醪[Xh.H*$p&nA @`3+RJ2W&B38R3%k0GGhܬeGl脣81qSAK Aɺ-LƥM OCxc3#C3UW>sG/rF(Ɖ@slBGU*=LHQS?Mc>& x'Uᦷ@`A6br NKjz 39qClmRXsIxSRyj3s4LMۊ0ȓu7..L5)QLK*zAsKʇ4@*m#o(0(%ZPd)/MV'o nT0=7dH(&ut<%YU N>os)hSt>B}R;Q#0-gO8Gë*5y&а0 'YXϫ;5cUVc%&tHGjiTFAOC>C&(EQm-vis)&'D9dEYsl4+hIIc~l5j ( EǰS>Mv-Ol>!>4`ՔJ6 LAEI&~O.P0}.=QJQ 4Lx2vIGi@ȮFd r_J ί7^p ʧR0ӗdwA>u_4BE8F0fy3JS+Rs:7زLiFc1D׶&vsdwGd/_8iN0rvĠa3iKq팏ʖl!rdžKTPQM.]댁~u3lϳ^-D9I"23ai)3L> 8$%gtLO5IsӘrJ HVs9NEELkw4({Rmk`ާ۬晲`yf5m9~zLPsgʗ$>(?imHsQor?m@T\)eUnS31yEAVcHF'Uɂp ^ f+49tSUm(NF?NT~YvWTH[ATNJݎ*6ٞ4|ѦgʈTo݄pb=umʼnO mXD+Q3{cYoj))PD s@\Z6&1ޠm[jJecPCΉE8^_z,_&kɰͨYyGv}%&B9>TC)#sJ&U"9gu*4H]z]] ƾ wr{, q=}ꆊOԓsԣG9kjc͌4NAxcah!P=yY]a2p|Zp6!DZg",Qz0 SxzU/ɮ1p81o^*'AѤJ[0)V;> l$Hk[E.רZsnD+;>t7oâT芝eĀU"4VC\/Kr7,DX)F]̮qӬ&o[@sx[G:[qx= T*kgoH#ou?g[Y5zWݪT¯Qq]SߵLJ\MT́1{sĻ5*-d:V@-^h5c[5q{nc5E~՞tQֹ?t3F@' ԝpI[aOh1c ,H LИC%2(Ċ-b$a9*)ȉ M\HL"%"\2%Ň1UɰM-AV8EBu I4S#G܈Ңz=xr9#mڳk0֭\jת= .^m 80`/zCAjtuƋ`LS) &n.XW1B91f[)n豣dΑIv #UWC^0Ž&q4ј_OE 鈰y6p^uÏoo^ы ?>^­MnѮ}]dk[e' Abd^ASX Uru[mQǡj(U^r8}YO NS-Ohb&%Q552z%_^s-W\u!dPJ5~Vemi KUWTZN8`ijeN`tqe=^mU|E cnHݣD^yLG{ޒO*eTLW`jX Cgf@Zߍ2^$kgX]մe:#Hѵ´VBJՄi*v%O*Q6F]z;jYVs֙Hgn=ZMnqr90&Ȟ1mB} J16Yu)ġ!RD2OBZkp7v'KS3G#+) 5|TZ9"fQz~([I]]<puj#.LiRKخBOe^oy4?JM1>#4u VTwآFqO̚t@vDJ1b{#F@+d%PP͜ + K5f)f43sN\B')>׉VFCZ-nӂ+eEjX !g% F=oʔi12V!'WDYYQ1‰\=]óH4g'BC R\Z-͜*I5wy`:wK%ܥI!,FU 1UmnI?սFQV@4b/a>͈Gmra$3zAɮ`RaAQO XgA)o si !aJ { qJm2C7pS](g,NRYv z</'Ng*j;\-E1f@FŐ^dǪ*/z˧U*}qQuXJ ҂J2W?0tt?Z gl$HN&uK(g ,l>x_+/CH+!D_b}Yu_0 -'q(V9YKm\RzWlpM3pF5k='Q(H7`#yBEN|9;S4bY2s&IUҧ ȡ$:(R\If)K RLrZ AmXkj jդ%+JAV#+j*nZ~o=szɣÔbի(VX z֗bi,%eS8IZ:CnfBEBuYseڒe&',+XQM:J);g {կ|b$ӇNVv_PD v87 OLvhP|a7׷YvuT#Q;{Ƙv3z~Ỷuyt!]&k[:|b:qɘ]~g&G9Bxi7R|!^u۸Zg~ۄBnDx/׆\A(Y/95=ENfX!6ay?<KjbX7w7QVxb{ V#LTtzsTO^MOeGX݅jl\I2e58$b_ymFI5;]@%w.EFqe9%t$o\=o|tw|\%@'zRlImwaIFnSMt{IMIfEyE~4fuPpDTd=ZHNSQ*9|L$aB%7oʖahJ17Xt]܃;vrX;t(DG?1zuybbPC28V60&qTO*"Ef3G#eI#C'So^d.AavYd&HdױK_ZstgtaMZp|F.mWh'AuE,u3=s(XS\kUo9Y,/ FbX!e?C%ǁ<rׇcI #vBmUaEpu`XA2f!yelE;s !0">èG7Z`3mNOV#QYGQWgRwX*Nec]Y()$"_,BJ*76Bq.$vZ7=R+GFF%zxE8wAhkV&~S+Z3W+=D]kQ]#XfqftD)Vǐ5tR֑;q<v&WvqUq3Y/ywn?eQ6ه PK](p+[WAlOx,Ees4h?w[!c 7a.3gWfrh]SuM'"~g\ 0^'gIw8)rpd.E$VMY|pz#[tM( >w=+|hpQ(`Vxq$'S r9>ij/Uz9GJ:gC)|Qveh2V~zfR)WS2%oV΁'(16ef%Z|;Ee&4ieShA3:vPĈ `yd k j$6YI3(cZ5vRA(ao)]%NDf 8haԔl vM9“7xh<&t :{hg=,]8b7*g|=*6lO\8zgc?~]TheW[^'vvlbR):㲟Ȣvg&-#t.)BBIPɇV%㦹&q:qVm~Fquzijɉ3#j_%YayW&fKEW` EzUxejIWH[ŕ8|Fh`u(~c%u0`t9DwڂfVb bUE!(T4a2hpV#m##$S "z`ÖO2kV)¹2tw.!/PUf8lgURq?zd"!9'vNeKk'X~5Sm4ihcᲝ1x5pē"y!GBv*f5:Ԙ$ʜ)bFX Jkr2yLNzEHɛu(X2_ڨT hyKx} :wBlsz2t}Õ+Te:6^ʩ:6cٜolBipiʵT9x\kc;X糴~ة|oA2kxdo \x(+ /;A(c`F OK.CKD9y4x ;}uWEPy ](i\jqYGwaL> JӍI<[h?|*F[ Rte374it7ݗlcD7e#Liۛ8O)g8|*xcɮ BKe| `uft&HD-xoZpc#NPS1$)[Yȝ BP]U8`-S(olM"󌑩f m }4´1Z=>9jhKRQC]_Z;GauØ./sGu[l?^H"Mqd&Y(=ڞ(-fQ|ԯRoi_|]tÆiy()NgbN(| 6֕]T_ ߒXk\ڝyqa8{@@n%r{[bf]H\z\!neyfׅ՞ nϨȁFs駤&HgdƿR 9X3Zn 'Rt?HjK`ѝ5>Tߞl\Vt3ZhLB>WrNjo`iM-d/8Bڐ$IYjy;`ҋm,^K1mKK\'5|JUzH򌫝KqX;R{:]g@UW]^hI N0k+,nټh[}Z6Ku+&!@  6l"vӑ_|KY]#pys ?YЀ ƨIx ZȅTXTElEeJiJD̬F=wVS)}ddȆ-Q^}ln3KQjO") gGueC6yi0+-:D?)ҨH}\3ЏutΐԱj췒iאc׏ϵ3NnQs5/ D`\0V&DHB{!E4jؐ E-*$qH#9ņ 42$I3A)3$͌ )j4cO1XfʋD=9ɗA]ք(ԟN/lչs#K3S(SIDx6gOcT4ϏX0^L0c‘%G) !UhRB(-шN׎]yE_qvݓTm˶շthVEs$Ӫ^hVoGDw5Ctn5igI{]>Xi xn}hKdḵ{ @# |>@"hd9&4o':bob;ӊ-*@ /㞺(wZ*l˨\+JĶڸN*Z R}.$~)-'Ϝ@k@7 0AWyEN򑇅NȻt+ >OCmF;XsXK&5Ut/3S46ڑSH[LQdԎ|Il.8-KEQ%mIIK{/b327#p08 ɠ6A"&`I.NCIBL\l8]²\4kY-8,=&uet~KUMS̯j](JLQ+FQURQV1k08 MͼӶ,MR[ER9!>(SFŮdAEX{-*TQ&ǃX-50Kaݍ2Km{!:\Yοg14NQ.vѿ+LVQr<;=RXIuD>ݷg'Vp%T= VxXT;#͊vHB+)%Hc}.w =OM뱑'T[dlj3 8cYx,pѓ@J~Z°ޕ-oMw/]9`vKW0EOP0֡ Z՗!*L ͓CrhZWj>ƎxלdkN.s9-ЊAB*U`{(n}[x> lʲB@Qr3@0kIJ3G oy061 E[[XXzӐ u ]WEIYIu:l(Pd"9&82`ˋlB^'xRH7l혵I w]*s u:-ˁ1q (r+eɭyMcNEȹ.ܕ6umAқQc0GVIqZ%+v s=*f8,A* 1es]#N`$#VYdysY%SaCj:ҁ.`rkQO(CYͬ"Y%Bh_JxQJȰ >2'mڳF!}hD,:FB6J b.΅l掄NzҤ7#Le)Rуt$L,4 vORl5iVVGl;҆\X"m6Wy4ʷh\eIn#u+ [?q{j"ޢkφWuYC;]I2en@4\#yI[EЇ/GFΛJޮ4duB|Rc+:+Xd 4ar+˖=)s{a|]iZvu|@eU7Ը\q)9㟑b+ѓߚ ׈^+kfAͨd Ѿ%GysfĉH֙Y3Zk"x{݁ڞku=T6*732I!v fݛ5l鏜3 fȜi-Cp6eҮ Eid]tQk.1H 4zQ+jj*52-<Ӟa< 2;KGk ;{Q-r21(cji2撼h΋5Rɤ[x"-)?@$K:D\0 I"LK«&J( >Ʒ*IdEM@rMP@TzI}SQ3]Hc*vD\|'tIRHlMD2+YSKb9˺:b[""Id)\HQFIf'k,R#[7*]F]XQ>(FuL rlIrB&STȽSJF2&B|ˬ*& |;:K=FPWԷ6x?\v补,Ff01+5⭡{ʑJ!-줡\Ҝ)빷RN<@:۝V7UMC0g,T43ZL*lRڑzr9+M}'WaF-8ᡦ4OTGQ 8o;%cЂʼ9dR.}ҍx>KA>y#hU DQCo|8ui>qEZ̈́@D[$`33)@mu{i!D,#?y\A$RsYWA/FBʏ05,I;ڛW|ґU4SB:|  @)M{!9 H T (t7pU{t[p:Z;\֒"%+ݺʡY)AF_Qл%[KAXDmQRFPDID#Sʲ[ )̭ӪUM|80PHBlRGUX_իC*`*&&S:ZO:JX5%+-`"^}͞|9UH,)s-SY7z2]ZHzV;_TWǰOқ-ͅBλ4m4 .ia`y:(xᦃ FHE6тU*lEEY=:$L:J4B#d,(;V$a%.ΥYcR"+b[}eňR[2E;Ѱ,$lB?g~[P;R 8[R8ӵL3%aBέ=hDeѫlD=ޥ Rrd<;m_ɒSbbM~j*۳!.p ҊdnQ3QVB;`F,ǩ\tYDFb;5Qj{Ԍ&`6"}k)g/d`7dXeI-bP`{B1L"fsh |? @Z٘ɣ5WԹd=<,;ek9 FL5hKŽbH,E'JFw(ǬBd>!ԴS?X9YEE_C&Y^]!C$qT _)f ]4X]4b?ƃɫA9y̷k$[@8dk SP)d=.'33z!zL !iNwdBdĮY OBW[/zJQhlڪdm&Ѷ)JtζLQ;*kkC=+*BH &CJ<ݭ)WɻDgNDKXX,HgY#dF3$F\PsՔZ4Y~=:̦Ǜ7*\ɭT:vauO˟_755rE,55wԥ W1NFn^(Ϣjp/^!FZgLc0EhM2j]~9MfeW5%흳BѩdңP߽ R(+IC̉DxRLh+c%[C?2H-+/=%x@&1X )&cw{LH~J-n $y/RrPG'h5lFQ)kɟa>m@ݫ빳9X}];AÐ^ny{$Q)mV"сv v?' wzaMUfGA%! `059T~7 3<.G@q+\79 1,qѝ9ĹSz.KVwɸwj)2GY$x$+~]v3ʎq*_S1{1GxMnW >T( QV&A%i#ʓ2YGOAe,YԒN#J G`, Isa u$iia<쐭6|%L<ߙlv`eW?;#{CVYʓs6 Q )ia̼& FE[džF<(>$d֫mJXaU\09kQ[ɝ$A<\&<)Rt12LnEK v,MrЪWy? HK(GK yƳ"dI`4F˘ 1# !b)iju+g^BRaiuQ-DYT9 2΢(%  1p6)IaK-L}o_b727SAM+wj z )="ԡBsv1`{s*cw06$Yi؋#3dz7]#Hh*T-l1z֣g:2N7tsabf2MBfX!{mNӓcE(uj;Zg/f23x̡fʪ0 eT ZEl3~ꏚJ[Y4!J&J*aJOʮd,ɱ˝FF]JPRibZ!)Qխ Z*u/R,.Hn-Ii8͒d\lxQ(I)M؋ԛwT:8դ;K VPgP0WBL6]X>kLJ3lI<|3xh1 fteV ]ir^1Y718,݂)Hcb4mQ7Ø6oqUTQ39 J} R vb|Ad1)wL3ߜ&B2-.UվJ_kY`RkU 3q#l9uq,ȳv㋸- (]Ct:8VWֻJS%ymwwi g93G Y9U!P QTՎ r}{h-a|Q)*Sʒiޗ߭[Ѷ4\%EmxH:*&rb r\ىYV<V!Y_ešID ~֎Yi?nSɄ@]9 KyXGA7bn]ʼnZddImՕDP K=5PoZ6^ |Tzp{M ==YTloAױ$ n(`IoDՓ)% IK(dX ,6õO쟐[xO"K?U˯!]4Qm h]7-"d2u]$fQ ϐi`9hP)%́\% % 9M66_"˼$2_"jY?ԧYnbP`'"ENcɡنi#o uacE D/lJb&?f֍mU+a݋&WOoraN MHC_d}jFܙwFgKq٥Ih%M~!M,UaѹO$]ԏ h]S_JTQ(F5S2a'4W\3`ehh XQF9hY:,фmE|#Uq NmYzF9Qu7h`GF\6E_/LH2 e>%SC!- W%&!yO5l1oI%e٘bk_Ԍ*(CBvͪZ`@ W` ;$RsV=ɉZeCѰ*~([2Z͆yw~].^% `NRLx ͕;*֓l20x=Ehu'C WWT ewY赡J=⢞LȦ N~"6Ϝs2ՙ"IIPETqިxi -?vda@]aQ%@[TkZ02Y&,p.',V_*)|+*ޙH +0X<ǂa@bXkn ǴUF',I DL޹ᮥ",VvӚӂ ,jw.T]֔oY$Bֵ0㹵cJEfUulQ:F(*oLfG$l,4C>'$z9~JU ,eBK#qb`:m( )")Op].$S^#mxRĢK*;pn0CfEcz&@fE~ $_kE*k3V%uHI, !iXNqar(6&WP@ux0|6if$лܷk^PaMeZ-/hЁS*rh5mXIͷ)Pcȕxh 5rF9a¬:F٠J)YIpG}M;mjXv%lYjҺ?/pMwkkEzҲQN+=\e[dDneՠo QKv4ˑh>Ip'}mfN!b3"ZyqL .Aea$dL'+'[Vq#%%#_ʄ ٜFUo$'M"A5HZ|ݥ)]'? o n@Śe\ MF'rϒI2+ ϴM#fIm1 _i@ue|%rڠVӬ_e~>/Ϛ$#:P>m#'LPFeTz"NEzG@'tؑ]oGa堩'`/+5q)ym !xdF@`ƷMr=Rʚ-`]ꐽ:Q`a8P,f^LT͗Tx7QrH|V-Di`7%v 9ֽ$btU-&^.kj 02(iuaySաUiN6)y "wOTuqv ^r9~Cmtx J90GiDnb e,`^PpEBIsT85ݞ(S1d(o0Z;ޏ=L;s42ΙY2ZbX) ꦫes:.Bqf  6 OoC[;Rs[͞:^j_G7US4i*>)>;Fz_/6̕W)3:$Eٮ"_#1M7;sh+3VKfjKR~tBɮ %o~Dq捏y=;q6gkCvcZO&ubc분9 z t˲+;F뢓pkrBl@y|r/Y  (rH* Z5 .S`yooeW}== 5b#j=pR35G5 'N39`?PnAW3y4!ſL/nNOѴ }^$0g1.4f40aB  B#>hPcB/ň#Wz\qF)drB)iH&MFfXҢQ15Rt"OO%%ȥ̚(QsmZkٶuGG=*sʡw-Je(]!Eu:Tn%m.:+ކy71׫u SÐ*w2|g/ Fիe=wgBؘ3[z'm Zfțٷ۹c뎇yW*jXgW(FIeȞ&?y6ۤJrn)`CC5S,,&0#8> SoK/p OR[1^̍D <̫r2x#rԻ>&l2/G-1[A+34 ΣC&/s@܉L#-<-Ġ)# ""ͩEظFGz-3;4NM(JLA< O}вZc`Ws55!LM,*uU3uU9=P2U7ŚnQNۋQQ+:£L ⾌/T 0)es.,Bg OŚ\;+uQ'CQԊ]k8`W1)/ܫԪ<*Ta4S p"F xry/5,ċ>r[aRÞ+N|گѽƾ,)@;\䦻O 8ƪҟⳬWiOdӀWu+(w)ȑNzKRajwtpNc`[##$˕bMs>Įl2H<(*Н(d Da)OwVE0i5 = I^/ G߱J#v2RS55NXͧyB#mbu]3߹`Cs$w!NP5G =r[Fe 8ܑ#41 rjKwTɕwXy,>AeBs,(-?b ?}a۶v2ˍXG= pT23礯(s(ɪẺ$kNؽfV^zi=h/_$IB 62˱/Cc,]z|1y}zL$rc4³.&6h ;SY 3PYaJ78*@UܖU:Yڎp==c*>eݶQ^/T og{򰨜e TSnra]K(qlhW 1~lӮX26SgZFnXZ/D5Opd~0IM}\]";B#Q~ KHX@Pc7T&ްYɘ][Ljiҿ≨e VRLej:gԃMMY"|a0 t˟2L*:V=.K Ki{vmJ嘅>j29ZܸM5efLb #;Snb-⹋iwQJ.Ab4&\7]35ӱ?7h֬Hq?9ӧ37VV0WK^R(͎pb*tOFk9e#n;# ޱbpHD]k)Cw۪ꤷft94dl-Gn_(M4a :|[$̷_vKVWhoUtUc +.r{q(Ie9Z!-Y_j{\=ʛ4'#ܳ9|26+#QӚv.")R,HoHތ^*MaHr Z+Cˣjh96l{nKxD+roȩv J\^m_ lL/~}nJ>F&)rmN[6j_= m%P⌍TЫ }i@~jb^nPPƊcFM𾫆(&L͌rkӨךLϗ u /G ABxώʌ-*mtp>/1VS\1a&^.R?kȧk\J+VMm+ V+f)|dS&$}*rDʎyByL$$| &mAigf%"7Ϊ/놄}.K8bHlzhP>_/S^ƩLn58%$hQ삄JN2*!&O pg̷z*2iHg LGxPSЙ΄!5SK, 6 -wk,`_, \^ʔeJh^4Rsҩ TFNpHJ@Cuh$v]+Y_{jȍKQ"e'\OW'~Jv1J;LS7h !D(^mx6xm;c3֧fuqѡ{owi*nzEVjUieWe0ij.O|ʅe=L\uEZ CkhQWzQ0Mk{c|ss|BlpŔn' LTyFmH~ @ H` !dp1/Zh#ń0!G5N1—W6,Ő0m.IScH.(J iR&D(7H1Cb&gR%&X`ÊK,6۷ZI hCQk>mzmޕ Uh[ ލ{7]Diߠ.2ϾSz<$^>7K7Ԛ+WodUmeȉA/ j3'j&^eK~Ȗ"tm|MiOMI >ϗ=LG]3>uwx,o֯tPzigwKnwOWn ʆf}VieǠAŧ!g&ZrWbUIG%rt0GVRW 7mlT}hшav H VWc>PW>$bkYQoY[ z }d^ם_4fdZby%HRg%JTB݉ގ%`zuRogζ`FX_߀ QVeVaLe)}LM( r,Lŵ XNyzIeV_cp!v+֌m.&kkm囖`[UvI46L axvn;Xeyژh߼ jh-?cA*<[07`Z0pK ƜajG%ʧnYۮH盭QkFFAf;/Ȟ:U)lil>_סV7$=-*؊E,:L'MJv'm̧՚Zo kՔu:w[EMhq֮5n8o*ah x ZC+*,stjs0H?*8ZE3+fTb*'qככIMPխ\"=H~ޣxw>,/Q3.-IODi>R>)8x[HAu|caTϵes~>3]{MF#EiڛA!).&@kTSpA")q? ru{,)rL6YW:Z@#$JV>b6e?1~NO;<8ȡ_8d;-e#'yM6`p nvBB{DX8  ri_{,52,hW;ɾDCG q| 6(=XVQty> $C_1Ah8@2> 9:d`|ճ>e߼*@t)l?,%9@Y.< @<,ЉT @7~w~`}}`!`~_! z&}_@} {@'0< WG0y )0G} +Ђ 87(&vvЀW 5wG p P{xx 0||FJ  `y=` GIH&vpw0̦(|VsK Zp(8}Ҁt` Nހ}xxwpApp}7 6 2` xv)\|tРDȊbDc8XxȘ!d,#\IrY%AO\fpyXAALI\AY[HlpAppOfiJyU[aaA%OJ\fppfWy$AA.DoRWOyAˋ.^ʖEґAӘA֜Mȓ\ȆAl׮q٨i..FHky eOpDfVuN^E+`0AAMopgyfTғ޶Դ۹ĄӖ՜ˏܩƪč‘đŒŒœƔƔƕƕǖǘȘșɛʜʛɟ̞˞̂͢͢ΣΤϦϥϧШЦЫ¨ѩѩѫҫҬӪҮԮԯձձֲִ״׵ضطظٷټۺݿݽܹڲƏޞ՟ɼp H*\ȰÇ#JHq ;T w2xyɤMH&G, 8'S OשR!QBѣH*]ʴӧPJ-B@s:Xرeϒ5FUP`!́@ F ljS5kfTE#KL˘ ^QbN'T< Dd^uqj֮W`M|".x-w:I^EtfN+{ By=(6oEr`@>0l猀y.DP@<6=DAS0@\@H~+pi  %uDv(,f>p65#*7 @4 ;]H-@ `ೌ@D`3E'}H7ls'WPNOK t7CM+⠄jy283^H#IM 8 KZ1 d #_B= 3+nx pf°61А 4J#zby ^mVkR!Ч@7r A.z| n6~@V얊'ĂnWX/;.< oH" $`@`U bq?BH#p acАЎ1ČAcAƨƥ"oՈ8 Ď BؘG! tA(4q$ GFJ&o  rA#'iȤp)0n8f%QH%FT^bx#aBuT1zc.h1D>LBsD&IFR̜G25MFӒ&#LJrT&"ye*;Id^Ҝg5Hqvr"l 8+Ɖ3Ā0RPU hhC#Іb`D* !:C(xQ'%ө@ҴLrNyg”t'= O@̈́MԞɤ=r6 -( `UY-ִ2]k*o1T beC*`NfPҙSLl@YNSD=jQҤsӬJW ϕӯ-MKe*Z޴P:;|PV4%OBZѴJT=*݊ +/\b fh,NG{Յ04*MJYӳM ­Nw[ge^"Ok;ܾzSp2ӾyC4[l咟;2i}VyȮ̖zw-2]\/OطY>o>w)vf5,>9HW5Fld3^ǻx lmpo>uWjf9Ήz^o0xGң!ż҅ک/V1D[뢯s-nu;=q y^x,h˔ոc>j{YfWط{vhSC|Rg56n5vpI}Whc4fSU|U6T^y礀xh gvQVGj[ms6j<'{Gwe&.a;'aVs0FuPd1bV||VvWeٶxiIEnEhH(GlwuW^w}Ou]VWjvv,jl-W7\7wgB %5r/d懙%zJFu|EnVWdZu G yeFT^wx!g\'n5shf7falzȆ,vnJyPj ='w7l\|mWg^|ƧypΦuv@xdvutԈd6ogrfthWoogoxV{u\&c&QuPbc}yȏGŋ$ugcpxgvcui&טY(v$7izH*Gj'va389#鎥9awhfsU c78[RZ>h| b لX8|X 9pi7~J|La:hwn[nWv{'w}n 9(\p JƓEE腐H>gX^IX苕ה _g8q&zu/rwhg摩&6wX\h֒8~/j!UЗ&}ԶZgpLJWm 78ɀt qqgt(xg'~ɒz~q捬nmV FuGǹmM7(m99cNWiKL7haos0-頤HfIj@jsPfHu'ik[9)ut}1J/V)l~ǕgH*w踊/e[k{Po-Ji{y}I'j/Y9&UUٌ}@Z]AI.+wifnH9Uv({i 0%qGوWGt䇘Iu zWم(|H͙Ys֜娨t4xRQc4 v(XVhjJq:Zuڇ%{zno:sjdĺzjf}%hx| 9طtEǣ|+Pؕ6e(ȝpioLJwww4ePʏw,f ȁ*OGpXyy)`aXan z,Vgm(4w'ȊZ׶vNJڙJjitjyxm*C&|E}y dfZeȳ9gVزZw9H{OǕv cĩP brV9XʍUzHaa:-Ysٶ~ZV1{{Wjzַ* _Ph66`는ʪ|YYx+jgױp"W~Z*G;ں+먿Us{ w; 9λx؟ Rjz9쟊&6 ܏o*e{bhf!뻒:wh~zn(is;[a_ɳJ`SvEyY|x̌̆q˄pTiR}đl7} >KZ߸۝꯴7[K }ym%kq?ٵ!Y度d+ Jŵ\YKh²Ks>ťyᩬNY\%{%f)l\ĉ+k':ʌ{Mֿr,ەXg)lMnhgkf+kRʷ܎ kǃ{w عm]L\7+re[f&`9cL osHܙ-M +L)ĸ c 9I~X &) H;m~\iݾ|⫂Vq(v E yD[Ǡ*LB1ƿ {gtHeqI!Hrݫj -}9L͟ .Ky=]3Xqఔ lxve6,&3J-{)){xkvꬣN1}eɒ)vjQm ,s:4*ܢ|hLZ:H]j?ͬf8N"ӗשTLTt ׊J-ܙlh߮2K@j6~zoz'H J ݉vw.;E^_ TPdV h% ~kR9ȡh2.q+ ͪMJV,Ԍk͡ z-Ҽ @x`ǎ;U%[x.N~Mj`~"RL2szvŮh*ެ;n볡.HkĩJǚ%TݭsɌRHn}ܪ+ϋ=K 8ȷfN|tO^l8s~('l/L;{ zP0UZMκ;b>FpWl𒋲K(UȤl.A+wlim.A>,P~y΋ DŽi߇Dkj([}>ULتz8m +Y+ )C+̖rz~VctlnYkv rLԇ:fC:~T|ж+Nu+9 B.j`ڮދh8KтxH{KܸmVҶmՕSʥ(0 ?ը}b)Gl2pC{o/Xd҃gi!Tf-ƚҢ6Z9~ 'c0.,ŗz(>Ci/&Q1J 3>+"#qG"#, o$0@%dnĠB ފ3- |m *֚4E?_3$;Hд[>NJ0<+0H@K)Q;;$axym* 5,?[IO Y LtM y\WO:Tɰ*TFP˚ܮkR!4Iv\ {d"TEI+:IP97DSMdrp` D jCjO|fT3I睴]10T͛K SPrM!@ROw^\q8VԸ͌ ^5_ӔN )ZD&v94@Sw1H(L/4rĶv7[H |Rے s1E_OT᰾Jlb@FE2L%+67 X _ԼMъO}8+1gG"f3EVg"q!ZuGYUFRt\ƲUNl1\-W=JsBl5aVYNވCo~s`;ȨRKZ (;IBFFp`G-Oq#e2wB ph" +Ҵu'@_5f)ji<]YСtld;BcT<OH!ZNXĄ^TYL 5{0n7 |ܜh8vQaXԧҚB!Ӈ ?+4fԠ_R(mG䨕JER\PQKBhrw3uCݕR;:}K阠H NL7S)kl3.X6I~s< z$zvbk0JE%:oI?WS~}.t19aJQ-tBr^Pdg6]I[XMmFFAHg'y.ȃF8oc %`?W}SRsZ3@Ё40w4jv7Sf/jdyB+Lb uxU|JA:y\=FctJ*UtE:e#?ڐqxpP b"gx6Vpn!y-ËFx)!,!Q<"ytޥ]Q"DCr yҀEjtS̚"R@Vk6ZZK"02;o?Ŝ5YALqt&O*߬t)YJI/ .r =F P|&2 ]G![EA'ϰ[튱Ʈ)nԨHvWkۍD橾-3p:Ԝ_4+=VqhH"C;=j'`k.Pw~[~Ԯ]wƊdA{y +),r+2>c--YZqx4\t(o5-y8;Ň>~U:O;_1ZW&OE%exϴ*_H,&doer3YT?PA>ESb)i(S=I8N #5Rj@$a 9JɈ<@!+#EK0.^7ѯWJ>*ʬ:k=nS * t62#$1 :7΀t Q0A''B TR*-HC 23»."D,"f5`:Ô^<胾iBODj=Ÿh4 lB$; "ڪAUɭ=At^"@\a+㼘s*=@Vf[/'6|¾4EBH* č:1JD>42[ȓd[9 4d= "KzM1y3u.h:etPif"t/ gqZ"#cʠ|:C*ˡ:U?9? Q*H*C̢E ˸x,jjujdžGK cC524W,6ң "2\k;9D˞0@h:1\!Cڑ-g/g!'re:&20nI?QQ 5b;q7zA62JI{ R<)H#`[S%ԒҖ£\%c@L- <2^4B04;'XXTT%;!S|@ OC2Sㄸ9moAILœM+<@.˝JnL2kI; 4l!82;a EIDH Գl?@18\<$ʠh:B̸<S@syz:x<̧9VUϔNb-TԧN4fZ$QqS)3*'pSH3FXS:M$7K-1)iZ~TpGb2/*,LGgA, ۫g0qr =9m*qW&`MsTY5r$? T@<·?qMbb?JDĝQ6#$\ t܊.QUL\mfrQ.5` S|[*5.`EXql׍s5ԡjPgJWZ9e$)ASMr˦ev Hj4#RvySץ0-),ۚ%A;&5<ō\\U(rxw"(' h\N-pɷ[Ԣ@*,QwMUhdZRIs) bkI$?CG3K[TIgeTc `y%xVpc`P<΍44*C|1/eWm-%MU=Z|<2uO!"M~M <9ɫ;'DY1 < N6DVB-:Nmb+80bͦk:,3ƑmF.rTSwp~$KFS ^Z5 SoJ3X5*{aܵ;5G+<]ԍBFCgXhlPׄWZm`*YvA+L[茅䟬ɭ"ѣżA?4}aG#|rw?Y]7ݗ "fVc0bdʮmSrW̺^z5O H\S@$ŤԌm<@M<ۈ 7OjU]YpRP4!,rA5_=VNH"H%kھm5lY(j?\0Ɵd%.deY cʦsg\-?#ʄ5]<~mKfݦjKÔ.F#C]SK6aJ^4t =s=,^Gh}K(ң"Rt^ۂhRLZ}xE܌<659^᯽2jqrXHTZpaE%[EfI|c>a ʹJk颯6ȍKGrQJCjJ2ծ*LMJ ^7WWb$Gl}#"O[Mj.'`kӪ=%ϲQ>Zmv@m#-y`fs)ᮧCz t/6M\K)$Cg 'KOԲ4?9+D~'Oqמal~C!BH &h!C'Rha4G iɔ%Cj2%K";|)r%N)G@L1왳(NY]yP?EŒ:TaZk,qK32 ȓRJV CDZy-N[p޺t҅70y WcȎ'SƸjh՚GX˪ٱKkm:Nt2iMo{NظS;M|uاu9V8]kå{ w=,2 ]̸e/|b+Sk}ƒpv\OEGvWhfEU=UEeugLiT1/}Mb=X^yH$ftInZv$Vgu)Xv`VK9Zqe%gv&qn!ۉVumH҈g=ɣcDXǩ|I٣jPZev8b"9R75 hYz!GX~*es.j`VgM jdt zJXV^ަf]FFI)nbfglZaZw噄xWWz""XpJfz%uIt2Y  |0yj{K@{C~ ݩD/RuɲhU5%#uRQSDuq> <لTUSG18@) u ŠZ,PbE!!AX|&&G8< *W8bA6}4'=Fb#1 fiT:c [mjJ#¯p"6־&ĤZYu$X.nIO%﫼D-mwm۽/h|oF-t^"ub6^ 6 iv KVǵ8)S 7huD,` sniș20NK WA|űu\'O{f=΋ɪy+V1F& 1eld(EP Zw8F׮7E .þSN |UqM%PZԋkdG9{+%50ݰ0Lmj]ՅFĭ4I@ɵA W|/Bľr*\+B6"Oxυ}{,Y#_"d]lbDƒz0,3ק6qPZڝg$TTl:U&o_wJFn2-iOvE`ZR協'cDGճoT}Y<9o 2mTy(E{9j;fys JE_sJXNpR:fbw{tj kMuz(]\fDLzq*.ͭڙ$¨DA*,z8OJ&ҟ^P1olKԵvby/Ԗ>)Q&)_|ʢ4#[/VW%-]3yH))\QUU]QaqǠ tI[POǘ| ԕ1zY|vi8S`LH¢=9 SA>Y"fQrM"a RڤI枸dKܰ ʬb&! Ai! )N%Va!ree@ΩYp "W!J}]6ihi6ٖɡyY" ōmG,$#IEb5sQZ0$^!4jeVB'z#QǡVuߨYEjMX4ޜa  0֤B0"]2ҵ!%M*޵!s`]L#X1!`|0Tb zY85U-4!"ͣhN0PRS-B2^!CVcɠV. )d /_cihmL\HS BWdMe%KZt WՈ PVBD:\ fQ6^*#`&!WKXYSYu[q!)6[ epNVWT!"!ݙq)-,])POLcCjYH𜊑H *bՂbޤ(I'،1gΑJb|$V`cN}eI.S$NUh 9s Ӫ=x767uiALMt :|_'x~qu/KG0aFoU][֒X.5ա5vHfHfFɂPZ.YcdYChHfˉO{8iuƢ]j'>hVSG,`4(YSira*qalbeVݟ[h yzߦ^YNaԗ6~ݱؙRzI<#)`׫bMj=݋-Y5^5{v"F%S8m5E*҈BP%6ipӜ$hve!l&"$cyruf"( cp-=޻fn:NUavatzTWUcMYWrݝ]9I& Y̫yIaWK6%1JTt֊%ZSLLbB QYhav`S.HNHl&jXݭaQFڍh$!!Тixf[HRF,L(\b>.cУ=I^=ۀF ոHe^|zS-jWu\]uDXxL_Lte]ea>XjLe;Zm`L`f d_@Z%lAg|bOZT')M2gWیg1~'%' &"K4Xu.>܀Yͨ`'nҕ.x-ڥXX6(Y]R1hB Ǝ yP FoyԺ-}EYBN_n؂on!qNX ORRi~0Iv#(뀰J$-N =lXk(Ͳm^H,B;ԝ~"P>#ٖmq* O"w 2ž WQ l8n:B!yZoN"XARq pҴoB_5JT/J,2 ٭"m',*.ݦFQU)#"jJ-j곪$f\,۬#*t2Tf&[-)-x0~ׅf}؏hyJq!|t/Nc^YPl,4s Y :Eꮖ%\IKDy評-byykaK\wx㔊 d" ]+0u@z"D.Rmo^>9:1t9yϦM4j%b]k N% >v!S* ([ѯ>vKԂC)Dmau0*Pͪc2O׍O:HMrFMu;\+qR Agm:u fw2UK{YzcYxݬMHR6K~;(iQT˔lص'vZi^H#)5 ŴR%{R361Ն+_I(juFΊ S՘Or5ZHzsZOJi`y rr#fYdlh1by,d_h'YBMެy S27|HN8Xr۸ufr1g,ؕ~J#k#q4J X`i_ O=vKXKõyud eu2yO$X9BU^~fNN 5r8L6eG!p;6=ٹ3_TPlp )u( u.War7=/\%&ﰨNq2K CV׫K~v6#bgֱf+}z3h91ijF^ͶQ|o|NA7~lHOjFBHA.~'xvᅋo_+yztVHgsZziuڰ?.Ml-Dꠘbofpvo|IS3qn 97k: ƒ=L."Fo#kPGbBk'e#$" 04¬KFBAӄA[鈄*< M\jhk0¾qZc /먫TG)sK2OF3tF݄4T;9ܓ! ׊NpҴ2ǤUAmUM7U l|rCLmO/?EԷu+IZ/fwLLqHcpVd0KҰºU#kNH1Lk6}3a $?ʂ=A/$VbയJG9+=^1O8PRq.seDB Pk]B+NE&nKڴ2@xح׬5^6뉋},1XR3oCMb/|e#[6ŽR r=ߢ/K7nƌa?*v3L6׬ a`$WB"M"OҾNMsM6E7o?U܉VNw:J+z䶣vPUWX>ϕo˥' >UlO4?[75JvOϩ,& Bk㵭e d2Xv,d{OBH!Okصm(ໜ`H.u_שnlYx ĐJ,K BP 8p\hv,jeFS>bEl]E!8h3 w$vS|[p9.>U UvJ!:a[ ,'S-OjVetc"NCzU%b%si?a-_9IURwaz2duwYTɣ%jzBV*w/Rudj52Y;!gb@gPH͔qKnjW{$WkZ[Fa@BcQЄ* Ǭ(wn׻ט;LitG^(zfڳ:Y!Vs:˦cLfK@h,#RԠlp.ctLsԍMǎUْ}5q#  <ZlcPFD uA {$vHWZֲ,G*ԪNJp۴)H(u^,ʗ_m< (80$ͭ c %n ^OIfQꈓ$i~KNҵ׽\8u}5L]OD8Os&J|6N-{-I35ԹJ JkH9zZhV}/lMRIR5]Kc31j"^"ȜUrhS4g*:Mc c)U`DPIc3{f k!H 7Q%b'#NFUVwBܬIIGn+x#*ot ةWskCC]A@| hTi VfU%.EA,]yCcOv;:kXaF.}vt& 5^|Ethmvi:s"Ț3j 6gd h%4ŲRcҬJ^ύflژo곂R1|Г6F+i p]HaQW i(M$:{Px'^FJPDj'fMh >-RQ*tStDg&Ў%x TMΦ#:nlֆge^2hvʯN1zIb *ȔJ$gJbCn1Q\ ̰6 W P8pKD ҩ.(Џرr ̅///j6o< egޣԀtx-$"3Q$I,..iI&\~kXrU:GeM`L)Eπj6JRFJp`3+(ΜC+LS{ntdjx{G>Xm2|Q >'J)o IHGD/R/Qf8 +qXvk=DdOp{D-udQdJD^Eqh frڰʃjۢJHV*j~’+C/ƲҏDȓXi/߆OUN&,>7|x,]?mꡰpԜJT mA+tΌ\6ި]."A+0DyP8JCz+HKj ER^ȌDM,F)eg(/Зo{tNtg̾l`9CԅD> }6b#Oμ IK/*SԎ:2z+ 1|g;(Aۣ>^pҘF])l ^ФC* M #2Fǯ()0xu3%r[,v>41@ua g\L2/0BNn ZW8mԍ,J a&_&lYsBS=(3;rxrzsݘoJM`ގ25lHNFm XISi:Oq '/ *mrXp6ny10v[k Q<)5 Pypt 5 ++ `cW&/_,)N 7?蒬 ega4d*?iFD =+gR!ƦTbrZPEV^ Md X8V('m&Ksxӄ-[V 43Z8[ rz7w/ƳB&7&5-S,U!︠׿Dc^_XmeN4Lȉ&2GKy~>3khÓ4it?1&`sTO2u+R:K([Uv7Yx+zdu@ *`nk9u_LSؖl.ʊNnm]7ٻ e-޺M@ 0!KOhʒAIm.~ز_$ULT@sPVGD[l}ՔEWm1iљ`c4euUhd'%5=GʯUrg1TvIvyD|+WsCm Q c%[diru>XuuUOxR^Iw_١pZ~]w;Jڀ.b UiQF|4٭Dνu'ͯ$vj*_pܛlϞ*7IARl"Ĉ5K,FKwYΡzgF`SwU]{5l܊=T6|6k_ֵo3i_M#NXp~#;/4uǥ%t>7r}qԑ5Kdg5tC Udf&hZ\Rm(zeUXy'aV_9V[T9WTeXb/bc+!օD_~ QYW:&qVx֑a^j}|疾w^sq5 m9ᩔzGaXQݷbtJYHfbO=WP-ubqw>vs~i7wgRh bf`Z6I<"ș^\&EdAi`!,48nS>I`쉕^Iqj+oUYJxb-el3e&f f[H n;~J\ݹ:cun**yPx3&߶+ݹp.Lli5\r#>z/aHK4͂wJiFFJN]vUͱ;7Jleoi{0YKl%_q%ɥ+d b2V?i/eoQpB<ڦ3}uݫB+ޟfM[3qUw,,O[Mu.v)nmyfsRl3) 6itYC%{9٢[adX^qXK1a"9$V0sadxTI9 pAWty)]r-s{LP Z(Dγ.qq)fABIltڪ*zX*YzRܥvo/^X 5ejZ"$iU%L*tE4H4>U|pJ{32 $ձkMkp6MvTƠ;O3$yl$.<&R&QY!V{Ų֖̆v~4SG-Tc h9k+(aEͿm-ܺ)J"rNllйΙ^e@GLjt\־O( ="8t#4qѐxM^o*EGE" ?J Y֠X4L:{KaNIN#v6e$jPD(GRvaB.dJT;ݶ]Llbƞ"ɡ[ɞZ؟5E*%+5v ^tֳ82rNfxPʳjw2_!KжGmaUK{gA9 jE3nx bS;wO^mB'(bU,mȒ{Oi統#EK(pP$t4H:o,1/ F̺i*7eZJ3YVts?5/v*(8'a-ڠ\VW0usymU$KRE4!6*D3Et~Ө/|[CД2P,pp?tB әsOoSN)~ihEw˺,͚ L.b⶯]%/ gsp$47S)5 Or (VנT*Co+k/&1W*rsb:v+;(|H'fY]72=6'\TBjأG'+!@+:diY)Gh>h{nۀmmܻ*HTx5b{Lf׭5*`;QmÔLϭqZ|OeDdHPoqKl۷or'Pi'Vw=`E:8~mzr{7h7{F%#e|FUh HFMG$s^Eu.2 yvUYuq[ea5I?2:gPQD>8b3zbR'Tour'h$=8X|5ԣ! 55);8z6>avz f}R`ut,BGv],g$Bi wvz#igv:8R4$9eC3GCDf[bR|b$FP%%Z'"Y,5RLCZDSeyw~N8s2HXgb$_6xb23}#ӊsi<$|Y&r0z#6F4vX-c#hcQ:..Qz1 e 5EK(xƕ2s*Zz>tY87TBEj+w)$lCldXOSZ¸GS~)s5~r1z-3\@hoDhtc0;3;ycSi8J &f IGKEWxAJ*&Tx´BM AcGnswt]'3 7nLHQ(B&+ :imWDZWS|㒅UXi55?Ow"[/jG0C0J08:ZJJF)5iǍRxliLJ}?^(W64C!AwFz)]v7S5}w>U]# \ٙ~cTg]z9!EXI")1j*8.WsHFʲ:d#f|مUcswEzKժrn {jB{.D74RFeRd{jV$Cae{g heeJpu3rr:csw:u+XO0bePEh(iZ@2iRQÕY8%Se0Us9[F"8ջlShC˝5h8bE,?jk.|L0dd}'EFgѤkw&,C'Yw$dMMeA(B04bڽ=SJv?9fZD虐N.`gĢXz62ŽE+\Qv7zE5jAU1ֺtLP+fOP>.ʪ/w}}p2&);!DŽg3yx~YSƺL2Z{,y5u,RofhəWcYr6Ӽ̗7giq ČE{JFw38RVHk\ſƻB#fEb#Ex1稚[z:(#V/<58>6˯:Cu%_@$) FXlՑ1 =F岌_9)R,w)[b^K($E񚩇MmlVgJVLv^lqcފiǡ,7u.Xц5yHĊ itII(&>֗]Ĵg閿d\5,kP7[wTǛ?|?A 9K챓_ƧC*jhe:*"?m9, $9eiaۜR+X_jf4{Skmx` fW~zxX䜹Jûآɨ?>c^#-J㹿Fؘgپ?GKr2 79*Gsl8s c+w vKȊƄ=o*eoY#gH p|_;ƈ܍"eLIVs wSM>U$Qsy>Zlک|֚K+iŞRѰŬ8HJFkD\l1?{&jFu #uı0O@P̯ɼjܱDh1>hc28L!dPN jy8B g)?Ll6t62+0H+G+DID)C/#gRL"m@)Uz0ts*:T^;Uz{Ds3L$UTjU>tN(P3k\H Qp‚BC6aFSk,%12FJ:J|$-,8;X).}IZ P[plYn]B XN9"YW54,VYM 64Sn;8>o8m+CoD X&Xذ$XW( ^˄3G^r`;uMԹ̴ss}n"iENaÌyoawpT1`@Tڑȹ£:11D4MJ\jrwU-ceVH#=3ҧ^Fgf.;sϘ[IjKur#_g`D<'+35g=*+y ?bK6>uK#Dw*5%%#jW cx4$B<xaZǀɀlʞy#z#*k"b`lŽ;mL$ŧ11d51zue0J(> `t <\؄Ѓ.ZY5c8Ɔz1`k#, -VO)Ѡ=/DT%<َѴm\AHYׂu~${yQq2+9GȽKȶfl0e}7&cOɂ3%NyBNԤy{UE1sARn<,,0_ReAeCLOb((vHƌؽ~J)(w*(%1Z1aLk8ⵐDEXu4zzQ)3Rrڦq5Z a9n^`3ZZ_UYqjbb6-"Oۭn+ye l{0vg!{dUnY'FWZC4DVn=ݤcuFr0\(eʖɩؓհϹPOq|y1q!qT"Ӎ߁C8YoIPy)mٮszd?<~w!Jζ.eLw=^F\o2{ "nK/h݌Z&ݥf)=c[jV =2/_hlZ3e:>NQ1^:[NnwN]z i G+=w >4%0;*i0 AKHN<w*+%r:탸Bϊ"$ K$Ê* ayQ\/q ,6J( [ /8x1iy5q,0i$FRKLK[k~z%{v{R:g9742 0j"eqlc):S sJE0fAgӯQ/#+ں!PyN{鉣! 93Iە!κ"ky{ڰ^K -€f֚:gӼu< qa:)9GaD< $R%w@ Y 0$K؊3A!;3z[9&"B{>o"i=c[*-q7g|(ј6`#|8EYI1'd5F%{29'#5 l! +Fl>;ޫ "/zyJj77\H}#3  :L,&'>̖dyYBuMޑ9ztA%A*ʰx1DkSgL驝U$A㾰,M(KYG'Zą S|p?V%cÇB #G[̪R:=* ͚*\Е-룈 "#M ZN9C"P@%MAR;yWDQ9#Ҋ3ܔ ] 8*|K94*;UƞtMaœƳK| =]#&HZ)NbyH {UMԵE[@">Ğ A,B<8ELD]=Q_DO "rn:9u$*k7KI{/ ;OsPạ2@K.6bSz92{"UFO;ʸLj' G$c6O44D67؜#ҫ\;zТ0<3{QTYgDʹ)YU255\>]NO!4xnp,yLճCFM)+{N̝Y"ýq'YUWyGCZ<; b8>O.B2Müc2S0$ KI3_Œsj;L8C^%@dB ]8=},۫Xd#$5 aJdհͥLI+%Dm=' 9XN<UKդ+`e=#mD DSWDú-!\!|b=\a+0"KE5e:.B ίBUDS42Ƽ-:&߼آZS -z3 -grÇ<<;g<;IudQGRx?.< d>Ljٚ]h&UjS[RHU4]6N]5ieNmH<<3AO֬]zōcb%a䎡]"̜"+"fBvIl|fAħwU鬔->lj-1k.}@s5o&/l9F2$aHIdcgńR3>H˔9`e^GN6[i#XE[c߽`0EM+Ҕ>U׻_;cSn<$m]9Npg]VŔڈ`NM=PtTӨJL]7LS{lCsEtSG+2Ov69MԳii.[|Te*?Y80 WuF\qYUm^ia+3:C1Di918&| /Ӎ `Q11锽k˵/ hS{>Aic*?&.3IDj2%[g@Գ3Lm´f&jwD*f{lvHgV9m_ѴsP-Mv44KR.g5:yWͷ#{V'j4Vpp6 b* (%$qHxJd [0SQ-cw*_->jn?uJn/WUI΢bD v*g?fz16wcJ^4rӳ 54)#t\ gz|eG,,Ɛ~3 [ ( at(&IJ⒍V(aJe/K.C\ݝ7rEυ`C <%k;uݫ̭u6}|zuklG!n͎۪6 6ĉ{+5{6k5Nꭸ>C;rpAs }< +|΍0g{^7R޳lBJ3/yBm 0  Da A*D!ā hB"č+3Y#A 3^ G 2 *DȼF t)ӦN@0NRb(eL WY+ʭe5j91D9M#JdҭܾT+WxY~ML2[So4+WɗccxqGd965~>,[ǡ@m%޾۰V5_56q+#:r ;hsͲ*Enq${Ř[/*ȱ:~LxTɇkܽEUhSmœZݔm1o#V՚/9gSEGQfWWE!W*vc_gdZVaH#5cc8ĝgdva6so}a S&g-&ʨrZ&, +N 2(>Q.kր`hM:^!}t "JJͺ:}ZmܕZwtm_WN{w# _01<䗰ɫ⢆OYǨl eX?F%EJ=%xn[!6ܞe'6kk8(佧{r{vҥ&'ٕOw*הd찏6aMZ4.ji?R_٤ hBk.;9`cxG>D=HGJL5B5ʐv*?e݌w-6# 䪆^RκvS+sJ&ZbaL0'Z4G=׈ٕr, iDtNꭔkaA_"aPWh7:.f5#s(Yf=(P4l0e(D߶f47+i-*x=8}0TV7Z"Q&¬ qظnz/R'vLP1:2t]NʣKKEQg,Xݭ/R{\9WXNYr8饫NլM,g(ae9qg\C7:ޓ&J@GZM3fE]RoOkE]CqgS6#uMk+|M)™qӡ5yI-QL:lQ߬;[%3z C8_aQ̥:fe끚SP{xÂXI!Q_?ɽM?'&#c6t6Kcf7Z^VP%åMT )Rwn-gd3er8=Hq՚0=ZV/.UKۊ VBV D K&QMΑy$ "QG"*]D,AS]RVϰ%KI_ݛ)8IL0JyNwRGWąG=b!ћeSFOwCbi Qej.XaU]Q7 #܈ͷ8TR-ʡ>q" TV}\F-g&VZ fI勬FR}u䂩E@CRT@QVb{#ݸw)cLHy$04TM jNrŤ("éd(~N( X%Q[t#,Idi~ 'xѨ!V,RIIY=Zq濘&[USi#ŘAtdŸѤɜrWZiN뾱yA.zTdidp5Њoeׯ>c# -$I#JBZƂ7TSt:$_2kN@ڞMf:|?&,@ngXbR͒͹-E֡ܓz0*\yqV,\DfDP tZ'(5nBz,R}s&? ѫq2 agn]z ڨ! 6C pBoK f떊RiA܂Oyd,Pp 9]R| ھEM m_>-U8urJ, *<ޞoɍaf~0Fb' PtP`A0TT 5XCNӎ *=6u,7.e"ݗݮnTJM"(<6Z /6KrV$J|Ds)S41̃RrZgPBgc.f()H"!1<瞠rCF.¡@ElQ_BA0 pPĀtm2~ڱE.1zG=e+fmd^1|_Lu%%{dBٱhXL:cF[$HL~*p@29`̃xB)6bbҞ:hn@908~Ɲۢ~,תf6/7N{ޑYt"crfxvhlnC+> @)1h p[&7U١2_ Ud2m!aa14JC\TZS%4p$k|E6 rUn0C'(-HZ_nl8s_&טߺ:1-mSMŵIlπPǥ26Q$%$i44*j5cj9e X3v%a 1eUY 2!>[i_6sԴZgZh]cjfBduMپ0M]O'jdSZ!-4RӅ-&ȟ\((ZBѩ1_ *`s۪PO 2cԱ~-hkmE1)N닅Oh3VQ6fUbݴ9!,OA5)w||sKܕLY8VLmTZA">Yt R_vq.ue}"~;k%!GRnŞ*y- Μxg a7*T!T^nli3䧭#S3M'dTyx 吁7}9 :_/D__{jPkuT9yvt eێ*z?l+z,n2mr蔫=JS4Ns4T$",$a,.,`1@A0tkZ* DYJydJs+> J_SQ6?!9O{1gY[N;R1?0B"|͙ia|8Os@^<e%d6;}S6*n[vu NS΃IDf;бY[Ǔ!0$?Ny5ozhVcsiefAn"S6/?!FLEﲊ,s`+ݠs$;#9nA;MmYW+eS<#-s € 4H@` 2\`ÈThQbE:A>"ʎ!9T#I3G|Bcn^PC5z($]y)ӒG&x Ő6jijMB,kfS :vlɧU˕ڥ Ϛ܃OvTQkE]1fx6#^'7p|z9WƍWN5[=Lېo 26RݻbP:͙ro50ۗ= G 65avwdY!tBŴfwۙɱmŦ|qEu(;K >6kAㆃHarˍu4򫱸ʒ^ڏ290 EǓ. ,0Ճ2HɻN\xL"UL w%[/>"tȺ$m>Fs-DP<ɺ*1D(=3)|/ Ix(n%TlNG$3tT .PNNTB.Ѫ$SJ0?.PYs|( 4F 5WG^U<В<-0E7um;,DŽsUu[EXr+|݋dsOUn(iT5䠤:6! Ev'ldMޔ";#}W"$T3;HF]FrCuXZ5i7Ւs0MKTSMS033V3dGM0I 61] +;UI8""+.a&.E VeYgl -g;)7EME-c4X%u.S\puULqܘmg[ uBmg2&NN8u]zD/~d-VGdE-k-_KLO/SO_F@ybHr1<OY f$K:) #IPrӺ'hPIyQabAMI{1X:}/]y w##%.f4A&YzKjtGϫ 1ZXH 0WtFBg^D<gFu/9{3;kX]NNPШ?igTQ, ˈ>$OțmsrI /!4c .`R걉Z^*]Gq1,%355Q|Q1^Bh-p+TFăMs :ɗaNSvk]0$1YZ|qb%vGˣv7Xr}fI4a =Rg~^<J4mJ.)˦/=\9yOf%d!b$iGʒ c4F!UDL}VL)!Quq6:Zq$5줓L4KO D'mRHX>iQ5[ m֪yv]q*UhřsVɲ< ;zhR#.j$"z jg mDuPFj{Z̖Y `^Y8kXEXWxļ*fMj׏J%aJYn4cɵXKE%[j+rؘ3^rԵ?ciXOyb<0U=t!HHFl-[-QZĸH/ P\n6mT䧩}tqWAGWE$"Vu)[acazpk8uOۉRi̖dݨ\ȴCܙUe"G(.za0<\5&8Dt%7wC%J x#~s]!TȆĐj.*[^YMs(ɴrW,۔ ؅ZDFﶷnޚ5B9Ʀ'EV^` UmARcF\ϼPBv`[ރݛ<͇mb3ۛTj&T~Q߾5e {v9R+]f 无b,\6C-Y}`CAb{OQugv/"/'WQZ+-nv%U0 '嘦\LXfb6DawGU:{"2:f+4^E~檾zധ/Zlefjk6á *ĵjM> Hk^ hT *wl3i&kce i h1ϙb(/m(hHI2Lj"i邈tzi~Ez)z_0…^Ӱnr@H |F]-갂tilNcф"K`Ch -NX0*ŧLа΍Ϊ,7&VFʮ&rXN y|t$Y[ޢR\ZǪq " ;)d* b C(L*k5Rop6'`b `E.ͨ]PǸQY@ H)NN׆In{nقeN,FPD'lQd8%SHFp&2fmx7KI\HP":T5ZӸrr*D"30:5*`K!{jXKJ)ČNN 5ol*t#%pFd)gְvb,a`2Mg>+dD@ud? Tn+*U)}q&'B3Nh~ 1t-#PWeCi5GqlUD4AoH4DlQ3'Vcf`@J!9B ƠpI]S‚q}^$,DS}lu|1>0\%F_j#]Z>PF_H~k/,g*5RͪxE1)[v֗5:eh2+1<ypnM1RvROD!ei=Orfe *0RD0YGr+rZl~,!x:4ko{Wk/ ap?705'Neqk$DQ =&11s]ʤ`)ߵ^nחPc fB(SJ&t-E*fpho8+Vq&LVHxG=BSnyU9GۢqcӻfGbYrYb~I vѪ\Km"=D N]rN ƈU?X pnNY krmBg*Jg|W;8 LbksV rEM 8wl798:B Z8T9 vpmGE9R6?Gx]Nu[l Th5*pRe( b#gnTϵN -{ًo-a$ ͎sIXϦH{!U*P/f/&SkYj cOU!V[7oqp>6,Lf-GxŘ|P^ot9=W[\V R*G/G L&BtTldžB#5XkwaqY{aJb#Szؒ={(VbKiY'[ks[f0Dyr9u/l}eBXhVGX͢u-?wJO}JƮ/@yu;~ɤm^T_T2fzLitq(~E_O ģ-~#~VYN$̸ F<ݧӍDp ZA<"$ғ4M芃vLi];uO=Q`QO )ս};id[F=ھat7{綋A4"y-َ O0.V S',)}R[$ў-@pY\)xlT5q|lE m586E V+D%Wh\^TT*8>3qik YiҦgaSFQJsׯ 1 O-d 5 @T0P3Kڕ(<"{)*eMy6b`ynuajM!ieni`R6G"n\IgVsN(at=i[ڗL⩔CeI=HN-ڄ1_M/az>*]-cI7CY-Jj^@Ua1w*L:5aCtLz!E*JRZ|7JX}&*SnH{hy ݎ?YO>[jM72iZog`` Zj.氜z,FJ8hyJcj]Sqj+V^pcjdhl"sf.6Qh'\jEaŠxK&(!L9[G%< ϓy6VuǭLZ癤,"+2*SA?nucsŪ-`(sl`O>N+ȓlWj.f<ce'NfP*ܓL" v#lӳ=7PU6u{ &<[?B\ZQ?Aa!|VAỈqӘQU{$Em0WZٴU=fC֨ĥ hZc Qi;0|lh^B` $ĢD-83ʍ8EҪDj-u'I9r ClRT斜Rd()@`9.jIdl5R]!S JPA:DAEpA㕝eqӉ6#ә䎎 IͪZDO"w?K ~Rnj  2`Ts4-j"ymtnF9z m7vfzcnf?SwfNK:)ht \$T(Bk^DR' b4YԨ&f9KTa(+nip䈕&.bOsFBXp|$,RySlA>fz EG}|[>3kZ1UZ)GI+NYV4WHDQfZQ2/ʰvn]q8--;_RRV𺒽We+tb28U.`<e)0nէ'So½I~pr< 7pov^#UMy)2)ucTA*f}F54) 3XM`ðRAq ҕYQo'Y?|P̖}6ׇIl/;}PSտ^F6Lpf\hAش{&yuIO9tDCA\nE*\[=0nj&`T^T5 EI)28#Eߋ)T,6ǷڊȣgoD̦ql0/7"R%{MtxraW"[?gt.+Kqu˩kS \:B09)n^ )ck0cb4Ix m[Ԅޡim^*йB^F{SLj|;i9#l` {dE3W)k='7=kꝵHB2os^dhLn$p4OBRL&D2NO. {0ȮUp"-%^V.p%[Yic $gDT]6t"D 5!fP6+ܯ3I㑈`=kLұA}[Q~,O{{#H Bm'8Z [Ͼ{OOxϿXFXx 8 8Xge հ݀(3p Sl "8k )Â0x# Qp 5x#X)j @ APR@ƐZ0t K7;x.@%pm %i:i4(j KpZ6l  h/pJh2;Z nrx 8  HsHekX@0+Pg gq9g 8 p@xS &P 8(p3؊gь=PPgG܈Jhߘ'3p\`Q\0 0瘎8Z@戎jXg "(0gFXH )0?p xȨ+`98 ȑIeps D 0 P  LjEyI  P8 F9H90 P} 8E]pހ؄P@oI @ ȗ~ )@)O` 0->ziP &h /H ph ʹٜi!,,\IsU(AO\fpy`AAMH\AT _HlpAppOfiJyT`idAOJ\fpf\%y$AA.DoRWO,ˎ.aɕEґAԘAםMǑWܰoצhy..Dk dOpDfVuN^E+a1AAIqpfyiTғ޸ںշąҖԛˏǷ߼ܨȘč‘đŒŒœƔƔƕǕƖǘȘțʜʛɟ̞˞̚ɂ͓͢עΣΤϦϥϧШЦЫ¨ѩѩѫҫҬӪҮԯԯձղִ״׵ضطظٷٻۺݿݽܹڱƉކԟɼp H*\ȰÇ#JH +M @xV_Q k"r3)vȳϟ@ JѣHyPBb:TTP>:VBQ\ԫ@W #5N bGD˷߿ ^%U>YAp,7~,ʏ rRu1@l6yF ^  L5@۸sw]f< d@yңћ R!-p._%'rHһ(`  LIIX  3GEƬ@ !<150 BE\1 ťTViXp<4!MD+ˆVf~m& r@4V;H0( 4j!I@v5-Lq9Ē+N<)p姠*jpv $S)ZJ9֭)𬉫4<)BlCAU Ak[x"OS^i+`-(UWru:kP\p * 8@0ļ>R7&[ < pU+'T̐h*<#4`1N;@6 3C74ʆPC騧:Dh幹~ n5~/Do_|7?/_}g`oW4u5 ?@oh6[@ꇿPx Abcײ=![P3уȣA!ը0L$B\j#yHR)ФV 8 |)4W׼yݫ`82_(gS`,ŪQթJme:JQTQ&Ut:$(d+*ـZ|mAGP.ude-g:ډT=^+FaL`{*X/+Nt,M_S}(a pD1RwhyϟmYY).|WӍFVl6d2N*||k\Dt4[av4_I`SBpNxk`z}vk׍ʖֺ^sJl;YCxծ*@mOrKlG\6-rap%#:cʹL]t4&FحV+ۺd|Ov,m0/ݔ.8ƕt]mf]hWiumXhNӋqf=&3^2}Yo11.,qSˀrҙjy{om'4Q]rҕ7~-e~ϚV$[nz7﷔yYE/t%>*Kv1W.|7؋ŷU8V0Ks[x81NX|IoSӛx|׶\\VmN'X@Xe S% UzlskR{Vv_Ete~gg'g6g[F_ fUjl:boy~iG|ϥe9qփ$}7|܆Ryi ,jvkG[2~yCzyT`ggagWXT7su'vld}usׄwxEx&us8|JxeMet]%ĄoVb}wPf^cwp4`_z'cwOdx}flGEViwi~xtx h:&|h|yE lt sZjh~]c؉2y$Xrw_>6Xtt^̖`(nl8[l`>8zHGa'8qW` F`bO[7w/~ay~Wwv| i踔yQ' ~[Ymz琼Vw>W/&`]xz7H{mv`Ɲӈ2Q^|Wwm9)p )8W_ۧiژkvwGt9mmi6)zȌdI]fp9O6l"зWH9vye5GِI~BI{b dbn-yyyQHK9| 7/m%`3rv#Ȉm'H焆悤"0GjyogA| OtQM }Ĕakz9 DRz)1wlWI?Jp*9v*H檍:i.:8DXJ<\uiԌ&zYw*ȡU_`Z&PT8[n7v|vx[vXF xM*eFPBl^WΈT9p~w؝g2@:(ug\w9Ú(e `@9فzhuzŧuNloW[<+yzˊـَo0wSC*uQVf*yr+?ơ=!mIUYXjˬhKh.Xe0~pz牄:+dψ^⇨{$ʖŸ@Qۯ7*4* (τ}y6gǞwPlȋ.iB )s 9; ~_ǚ\Iژh { u5e PBwY~ *lrl9u{3yJkɗcke m:˸II8ܣ&>T[^ؠ̍akcTZ3ke4 Vf PB@т7 TKB{zﺭ![ٝx&yey@ZLy2+mmkm:k]F yJ뉮k8)h*נ 9y\_\撹Auȼ*K,U̘\|aotǺJA'twlzg1G¯:- ;f/H&7EFjYȘ@ ULy98lt7<=r4wr\p[` HvL ѭHw߽k⡭8;ϕR%lnH:lw<}gz:i᪾૽k-YamԘ]*׆| OjzKޔ-w{˂w6 Eѵ\Z6'ֆ^pʈ#\4,gK(\{X*{H:vPDZL-z|{{,wRz]MRȪt)f~xۊhhmwMO%ȷ X¾)l ?'A]m .ج iϓLY\\xяkѝŎGܾߕ}?5DZޕHN(ry~7)/9޸0.K)iCe/P _pQ.:5vSH٬PZ*+S\gM,,^붆oOٮW-8zlWڒ>n )<xȖfR`@ydjɚ.vG\8ұOÐVw-iq/\a`ٽ䲺^{4X0aƒ >TH…%6C^xbĆ/v옑ō-MZ$K WęS'N|O?- 4iPB*]*Q6mzեH"z֬aŎZT[re*U0g42M2I<2&ȿEΌ1o|8ݛv'X3w~ٳm\fת]kiٷZUvҥMW7jY5>a oalydvnɹ/9Wr:ㄯWh߷=7ࣧlF͵~Ͻ⻀h2 2Y3 ϻd$ͬm3SLvDAlһ֙G#y~w{9b%vWdcQL/]ܭ~uĚ u~/Tmzv^x<Q&$@ӂMǹO g4'<95Q/lZ!y}Ta0}<D1jQd Yƒ! 56-Qcf#L (x-Zi]m[$1-I/|禄 fC▶;nS /̈́:|'oI5,y-ussƩ usq|׷.A9iӀSٮeLv`ڹk聣 Wy lWTsr!WYk1rOZ*1cvf47+h'_Q0s+ܻ6ǚΰg aix^%XRֈ0ΡhR𽴹:l6 ?ϝZU֛4m3ivv¶HAH$" CtVuC뚿q2"vͮk =p^Ãџ ͤ 0BiŹN]r/^,rՃh>Kӊ߹:9]NsΠO=^ Sv8)A!mfރ~VWljk/k]ז0 aqmpi)ZLnԒzYқ7ҕ!]+KOމDK~7ؽڹR/UfY6N;P-I FmFGOSRW64.<|5zv2Dz *gdK/vV kΤb|V9n&r[r :NSo]Ӱz(kY9-|y~?yuK5Q2=')[7p')i/{,#+q[+9*Y?)T+Cg;![S5r9;><8hB 5ɩ*Kɖֳ%ү˲:^{:\Er{.1ҳSn:A;38_9A#"S,A:[:JA#'"'3ʭp2C$ npjd$GZHD8<ù'4Új ;GS5rD+A9Y֛Crq=. T&qvp$h3/Ҋ3s 50ӝs< S;g BVڛ*/_tBI/5WT.@# 0X0U ') 7Z8AF8K:2ltA k;3%!C%d \ 5ѡ2&|xH n:x~"1k;I!I8eaCC}kJK"Gj:QGAӥtha)0`h$b*&,E仫f=ӿ{#T4D-R;5JSBrCi9\3i"/͑5!DBZG4fXMkx+&.<5(EjeJ=:R (;t:)y 6ctNqZ=1f-\3R>g3cJB/*ʥ N"+ݢzCKwӓPӷ+nҩ8ϝJ@4BET>ϊl; HbK1y42<Ɠ Th +<-F\rTͰQYLOa{R] Rإ"`6k&HD\8*%Z\ū:G!NQ4}(LzGl]{P%)fc $MC|{8q}UيtȠˆL_6H-&u+2%C 9cAE]>$B3=DVUl0_.Iγ 5*=3T-%=[2 G(ydȽsڛGS<%UmDX-2$@qduERYXuN&b,'CC:XN"};TA4Us)_u%\ bS&$DQgIU5aL'nQ,!bGvF6^]FZmp3JV-#nb͢ɽ`6XNR gXKbd1,;1aM4Įv~mb:\V&zZ7L>N[DMa$h1iVR%_Ɯb4jkOL^DA` Bi ۺLCA*B"J"X<ݼ EJRTW;. |Vkk辍IB-6=e{ ^TlꤪJ ^fmM&ֳ "<2dj"rP0e-Fn<^H;U W룪F6&vsxq]yG3UfMhnW!BUi \Q3s hZt2~ڔF7F4=|:H. "{oE-ens=bZU^36F8B%NV^B{]3ߖ)] hwj,1h+ZW`{>~>*C 4s}l @43iSp]FK~D sgXeoUBi>4pa1)xIbKh?OrJgc?EmTfg_,Tސ X:~'쭞(ҽCAzEn msk{5:4IJN;h=Po؅_7 I^R@)X\uYybvg+pp՞bV@`kBCWr~di`+`rT HXmXWE7"<$nvYa-᎗\NmjjOEp,EKDmPU׋aZhOf~KFtw҆fhfAOћk!nwg^E?Ds:% `d^<4|P@ y||}LjnUVof*_`%GuvhLukMGiQ. do~coX=:"?2oWS7/[sZM(;cdgة';sOf% _p A C {E'FqD"5^,YI ;2dH*9t$Μ:wpA*ŸMtiңA"%:ӟL:mzR^+,Tg*Mi۴_ja٧s+3z$LJąQly8fNȊi`:k٧ؾYƕʗߥjnVNҰݶEַ謦KJ/zekwwQ 䙸fߴebS H,/~{?")_B.%[\7/6zt|4&+G{A [dJijZKڊj2uȍQ~yn wδl+=΍:}?{gpMz A/=mz8a1N^9éw76K.H{Okպ"u9;XRLdZx9F` o.4 V;6 ObXO)I !eK䡪Apr+܇G2+k)S5㦷Nu#.~ɰ|S(%*GVvLI^7WW!Ub@Lz cÕDLAkY^)ZW1t3sך4R 1<\WTj,= |^ 'A0_i_U4me"yLPn8[ԶLDeG@fzB^d@@s""x%G EeZd\$ +J*evs&ˈA]}H76ҕFEqSI<7φ e'44Hg +QxD2۾pzVCNG 85r4Gd`5 {' Ɓq3QCnь)}YOSŵ)e,ySQdG .4ǭZI(/+ URBIv\#Ɲj]>#F7]"Q2 6R'W 9pԚ.%8ۉNHh yJu,*Hw{P݄nQ_vԣd u!U۝4&Z4ȵbf7FzO hXUpͯ78撇|\nNAΜ]}wyFaքuqIE2%RFM&i=lnAI׾)Vxph{ߗL-";4BAEg0]h x՚$gV?]JEt1ѶY-,Y[02td W !'r.͹_XHmaG!vKfs56:=cd\}=*qy5!=٥hyWlʱŠA W]'7Һ4NB 0D8 -g&sXk2Ҋg f+ctn6׸֜-PKZX]oG%n+H]~usm㵦\8^ -䫤,s1Mlo8 2 ü:U5W}hׇ[K|yvza}ih7LQƺd4xg4ܣY?ǂG,tzK/֭yJH/s"@iAA%kn]X i]C_G_+;e{ I1EBA,i׺9oݚp<=əANaPqu) qdmuUܽUEd@E E`)pYTQɕYWR)Uh!aqW@u]F9^ՕipԂ}Ke`q$mq5۸5&iQSrUWҠ|^!n]Dȍ\IM)]ʍÝϽ`"RVzB!.I RI}|a!cI"p!d5!n[a'X%"*du^b] ݍAӝv!4ު%cj](*f]њ>\]$$YPeq-t#iaʘN ){b#}_]:2r @݈!ߨft*a^)Aޜ)L"'x61*+8~ 6D: ޽h0b)Ibڜ\wP*rWZ]d+ǂ4RF?zlf%4$AN,SfչX.=Qg>q3ck`/-dݦd&nhB 01vv7LCNp2^l(vn2h.,-+j([$_IWbf &¢.0A`EDA|wՙ'f6ߐZq!Ob{hf^w /`!#*گT SN`E `CfZ6"Rʙ*)N ^oT4 )XbYɈ'C DD(G5fZh-նjV*&yr~%޳hLY^nfl~.+@00Ck)>2s %r!òPTکm&ٷU؝\o.8R'vBv1) o sB lM3"2';al28nDh?` C }E3(9|HU;2H:PnI6TCV(E"'\K`Fn\=o,+%ۦoj OdZ2-9rWh;/~buki},P ږa&o'yC7]0nuzPwW._:JX.5٩C{{G#{m!>[R[Ia{:Sk)5C_7S^w0-oR[I[Hi;=/=R*yE|_ַBV,7cJ2C.)}P8)?wa7,b62w+ jO 5fW77C?%ajSlo?k |.ꮞi'i#}67OniTJmZ6bbm_χQv &D-pbS_@‚>*\ܡ3#(gSoOVo`4&{U?OZ5%pY7N$ X_K7"{yi=@x`4xP!… }8C#^QcF ;r4H"C"O~|qȋ [(fL/Ab%ɞSZQWGC4QJ{4uӨOFuU[~;lYzhg›q QΆ6JmimIwfȾ&nk߸Mo῍%|RsdƙANTbJ԰fUY۷-3ʠf1f _r<|nV>7vE3Tӷ]yDyN=vViӇU,ng&s.H4-3>So3sCċn pĚcAtY?cbϾfs 8DN%30=KP/G VҬ; 1I%k-iI,4/"C" y+sm2N L%7oA2!5P!cB#AF'S%CA_tiQ3uZ38JE;hIt/ePMe|NbϰuMORW2Ih!5/Rk#LIB4UC OPPzr`\mPчHvbXG oZWe%e\mGftMBsmTSռY2%م認 GTϖ/f[QqBW#$KEml06O3Mv0s43 WܿhUاyI0 V~5y\G.!A.4MRMiZO_Y s,/jfr⌍^Rx]4-fdߛĻaMm1]|1SQkk/rƱdHJ[".`js流ZXy/WO>U%oFcWeuKolqXcx438iTiGyE9g{y2bBQmz(jm[X>S˄s\ֿOƨa-S;Y;_BUgЇKsAg6 eA/g!d7*\x%liH5O뛌. .}DOZBB"~PjdȻ.oiB痾*撑LĮě%D$ޑ 9*mR$jV :"я 55딥)m͓p\N:npuh?w퐔*%u9MM谈鵲z4W@oܗ!9.yj"ҞvK0ȴ$}9덡&'9^.s(?,l(XS/i(o{J'[A#SrU2V őy*B{'O*b#'Č m?ɔXe$54Ôa4hGMaIi<%i26rguU/lM KnsdFBµBs"4WX lXi8tG-Q7ՒDb="e~M*+ Uj@RNU" wM̡ .O9cZYVU otY>t]:fЫ(̅c9;JT}B"h HE*8+(H kZ""|5QPE_yȺ(0SGOPs!VfkFj,[fL3_߯TT\zG? pM3Cf ZrA9w/Efצشc:]bɉ ;W0u½5#{1;9qNn3[Pi,$9?M]%7XNRF*'X[uBQ\o8Ć$gɪ_R;i3toK=}*n"h6#1V-ܛP#옟kk'.;b~/ µh4F ҹZ]+ORqS-HX~:mJ+-/0[bcx֐>yMRlm_\C)kfo_ѦT$PZtp͕.`;rځz9RskV{W'o)^'X).Rz8 <'3Vb^YI163hMd&:[~zrmdEn5<%;OD7f̺)6w^~u$]v<Ċ.N'<#..ӭ k"cCo Xj `s Fw.ƅ%xE^RWd^KE\ka}4h¯4@*ێΉTJK-Jʅ KMh ÞG8,(خKɽ gGkpPg ]- )0XF5*!/]*wQ31Np%os"!EpJJ129+S޸ aT{P3n0i đ9%Kjh"iad֠n"0>oAn ߔѩ I}Lki $ >n\AФK|)1*"҄ *M, 4q^t8'Iqsڲ0֫RG5ia Kh2= ~Ts2l 4q4.Q'6ޠD1 X2љdlO q#T*w.2LZTt# FR{C1~UCrN{?g2PFugQ/PAiR[ 4K{:mqԹL!Rˋ' 5,U2 Xnc2! PZ4ĩt /I~ Ht爆'QT3Up]ϐ/ LG]IQ|Qּcw).L\0mpeV 67Ms,6L-ÎQRbβ>)G*?D2aib/5l/qETD s7qNF5,74er(lY㾳jro^1bv^vxVϬT&g0'ŬT샄<ݑmt1R3D@^ې#z V)j/gs嫾~^/yW'bX" Og5x7sT567. ӬjGJr|t{gQB#A6W237]Bk -Į8TJ*q0TvlTߘ&87-wpа(LV}78/h5L1 vd60og?FlrZ-ØoYFxx+ӆqZ?%N8e]r (F`Ixb;Ahu*yYjHWSפvv=iv,x~4"s<  Vo;cƖb[x~1vS\1so38Iݬ>Rzy*O rpaYXJVlB2iZ03րNW)$QvG0N7moxj{V9Bgj(o¬RM+7@WP} W*ˌa9N -)51s};y x-ڣ9$cOnMUfYS3Xq5:I3YԤ[lwZti~:)Ҙz@ XAU8<*/o8N:8I7-ЧPa[e^e_wu;DyC⫧1mw;퐺\ [17˜ScmS!өrPe.D=з'M_u<uM5_ ,x!A.lPă F|8Bn#ȉ )6dhJx=z|eK1e3N.pN01.W v:lȫj}UKhqN^8 ޮ#u.l['Eqϒףkg*4W4zyҜ>f9iyARhiLbO 4 UZ2Q.Nc{uu)ރbe.zF$hXi)9)ar;ίt246}]ۘ|Amy Fs/WWGqe/{ݯ:̀E>DZ<~"c m<`ƳHaKԚۊ3O{y鞰9n/ "Oi 2w툗|%Q1vƤ?"js<[5B@rCrAlkӴ].2Lk$Mz=\Ц[L`2;̳up'E!6 \gw.nF(^2ML/.*ҫ&Ȧ|BJK/:n^:g~7_ybyNT\gN, ̴j<4Pj:EP9I9U4LwPSqĪA{U'$E.ۇW{jյojCWj$*1)P{.CԸ59Ȭ$:YDm$fcTTʌTOpzI<)dӨ/lvql9r/MPvuoTsGYRVW02-8qv,Rgb~2Qk"FX7T}gEH94c>YJ'bRfDgs$mNRQ{CdR{tboktlZ`cURGd,3zNUl6LҤ@ko3mN6A(xjxgG=}5xVvrrYq aPwV(1g1sp~dVBTd787ć[cm}fxv%[eֈ88ko:sX5V|xu`d*/XfGܔ$ U{JyZ,Qnڄ3 f}Ol\w'.Pc8T4xeq(sqH+#yͣ sׄfeeoxp„(Hbo7kS$A*ni8Ae~x>PXĊjLtHtsdrHlpo눑KI7~y!)yshSu;hQi BJ5^CאƏ]Xx^h|7Cz|I /x7GRwp;j2lY|0el_eXo['Da'7y |y4}JqX&CJYFdfs UDx$euy|Q>W4w#yoyhiqSF/%Ni1w2c}c9Ճ/yt&ǁ^hHP0X:݊kj^o|ȋm? IkwezQEfIzq[R2•6dY3d UNʲ~e[K@,\V[@ v<XM|ixkyiQOuYxuHG1ȣ?k(uؘZ 'xxiǵZGqZBfOxlTOg9Zڍt9['ǟhy8kJ #wj"Ӛo):c)\庱j*u寪X[VA ؽfMz~zuOLjQۮ jw*1fk(x`\ȉI%Y:ug7us ~ lY3Iy 5z !c|E(W$ͻɪ|ŝ\>$4Dt,y8yZvo]" e K˚ѩiȲg HÌp~k@ ֶ9#7) X}ډ, Ȉd:F. X], $2 Xz\&"4|`ōyKflIX4:<p"~$ib*T#nhwFHlnK8m ksj݅zd]yL4i:k?iJhkuH}}6=d˄p>WmN,,aV~71;;#] pVo zѰG'QT,گ0TiXѽ7|G2{*3*5 ]߁}VTǾ Y& ڌ$g uiR^Jd$rC_w(~YmOxd{ZQ Hc[{tEZCEm9g;=`+2(n//E]8#cT!V\P*< q\M&D=9B;Gn)4Y-%۔(쬩kj.5{d}tLs,G(Ύ\pفI/Eç+~^٠T?4آ_5SXzfxiH(n Y}; Gng <@ {ڃRWF uXin_Wc̶o3o5hG->iғaWM-Xw(r1>*2IhK-x>0eƩbx$/뎽,( ,7+ ?cQ<]BB2eF=,4"*SqA"mTW3FW4@#MkrQLm-:5>״Kمؠ&TGvS 1XD@XXuildu;t@r}cZ~s&M2 ͺvbuWaxԑ1J\YpMv pX#΍%@*7߼YG]m`(Xb ґڏi5RHI7Lum ziiǚ.Xa97źieoSH{M޹i{YGMP~zuOF9ze ]OJ}K']WީI^m MlCZO fqyZel;A[hf1Ehʻusq3&vw%61*͝akHt*g9.iU I B0iJR#XB e|-SójM6T%aS<.yEԸFl4Ed|9D#:WC:!YS\{w *<*@!}݋VDeSNQ4$6=[FZx;9m_3`.EE y!iKI;YF|c"Wq\+YF@*rR2=yr9smxR w63}"a̠/ॕc6sj;& w5nG-Rv0zʄ#3x} rxݼv71ڟ{^QIݞֈO9HJCД)szݨvTD$_$4K u rJ"IQyrh@)͝ tr)K=Сԯ6X,Og-8i)O۲ )3պ:Lj^gSXG'r<)83E <S TStڵPpi,>Na ٙ=oL%MZLwk/Q?uFR(il[$ِz VT*yxA *`诂wϤwv.w\זvrܐBtP&A>;Q>ĕ*Se%~ +vS쇸MU2-lъY6܋)mU\!|}[8U%GZ^7ɚm[PP0.5-ۮ>t^2=۱y/ QP11UwS-Ka;IWST3\~Ieq@9"G^BԵ{ /WN#%7HKU&OІF時);wn>>J=0rxx'lQJ@e7`Ό\Z$:46x[u;%*dr [O#S##{Bsɨ];l4*P^BA[+::81€NWkV'5&邝4A.okpJyzu1/.q~`=e֟b`N m-EwьJq-ksrWƍ܆,R!uʪ< {t fǧ4P?wӞMa-08 Z\O>#kxR#'9V~޸iwqRCYLQD"PaF!JOY0[o.d.yX\[d{~?/e>༏fڕ)1+_X6a@\"Iod#ʙ[gۥQsX3 1a?%*_'xʘ1H}b+R.Kz0c屸s[66HB 2;:CKŠ&cC!%l)ɪ 7>^*{s3*L32B$K9>*;8Թՙ5{ P>cc:_S85"@BBd:ӵ912k/[9Z?dDS:N$u0w+:BAlCCݠeAx;; ZCA60( rC=HLJ#.5<`6:_#?ĪF#WC<_X\z*9e,rK rš;Iq­*Q/kl ]#Ӭw /[-u|ſ{7K;KC'棴+sa<+0 vi9a9 |<3:)^B+ʎ%#b$7*/$=0㵁Q37z1HtwHVk=8E{J*4`٠",z9Uƺl۸M+̌F%v8IKҧ8]CB@)"K~$C36YrL¥MP9MC:+J>9 #8.A}%kIve\ǽf俦BdC||J$Q:HP3‡T>nL<#̫BS*!MNPwԮ6}6P$zcQ|TOX;Y&j$O\P"3DiGa0:-52%c I*ԥ 7<=,ABҌk9ó CR'2-KT6fZQ,0-dS$Ub$0bOD2G2Pga0Ldc3zۺ18ٳ]-4mSߴl%AG|7%D1gm J)T{-tBP[FU^G%_=UXW=]8tn/TJz=12XWHWEOE}K)[&R[wܣ[=9 +ʯ=am 3D دa !6(eNeJ}&eZF{UR>\(&VNNXTdΉ<5^ $PJ.?<ńS[_V8ó!;5M} }d6b7-_}̨g#;a1{uKIz98EV;P[s՘[}fOPQz2¸9]kC_2^ Vv|!`c;6dL^г-BT=gse߅6_ pgl5;wG Mͪd5O\-;J &8\=c"8^M[ NHBun# SSE:u~ں 5+FYƵ$)5ܴ )Xפ5Eݵ,W9\ T?r+Đ[fS' h.x&VI.}.g c;R{"ct2Ȑ3^L5ȆMd:ғ]%]f=ޒȁcQVVdkۭ 6׃Ma6P hЖV׭=l#5Ԗ$l ckm|Óbp]Onk&SSof7ΖS&k{ZBQhWg&sћJVKܼo+ ERPm˓2R i-*nOC&qMaB ŠIb] OȽ]d7 '1_C~o4[ꦻߝ +jC^tb=ǨZR M$rA9}SЧ{I3U-S0] ,&_m/\M$!k'=Z6=d=.HBHdG0[ uVr&I힆m5]ϋLojM%U Fb^iLdDX7\FY} eTb0F e`?<КEHw.m`=B?}CWjKo^{ԄUv$a,m-6Dv3Įj䦥YdגC$/X+gJ &EŴTNL4ITʒ^ aUic5y4 I >ntފbVȞ۸eivQ"00U@vR\Ԅ㞿oHz\?`)?.*e\$aۻ|^;MUGi8wl4Ԧ׷6N-S@UjyN:4.aOO }Vo.g=.yMpT3zojsRS35/v3Lw2F''QW!_wYŔ>ᐌhZ/\% 4p!C_>bƒN0!Ƃ72F 8"HGv2$G.Kf&ʝa4yЕa Fs^o*ժVb @g˞@ 3ؗN'ڮf<ڒؗolϓz¥fLϦL8ڣ_9X%[[wn)8!xxAxd|msd9/Vp(R,6@O8ԓF=RPHԛR)QdRU1Ħ*jfؚ߉Y"/5z}Q]\~* ?{2P5<`{H + T @R"нe ?G<1Ϡ0JQ:ccQ 0[e-ofLe,v `nLnuK`vlr !`sN ~pA2ұ,Q8^eU&QchI3qzUYIkjӛ4:)O{;scoop-0.7.1/doc/images/reduction.png000066400000000000000000000715341240127670500173740ustar00rootroot00000000000000PNG  IHDR ƒtEXtSoftwareAdobe ImageReadyqe<fiTXtXML:com.adobe.xmp wX(oIDATxUlnʦ'H@v`"DR_P_b * B@HۥHHMْ-3?gf{@,~F*.hے4Q`D$@3NHܰÉ]mP/?WM.qiU#?KE""BB`BHxIt~:?#23Kɸ$I XҮV^ҫ C[*DX+3>B"uuu Cԡ&8ޢvw^FRIgaW!cՖa?! BMtv0/x #bYUgvwkr?-{U ܖ5S?;[G3ګjf|d!Bс竽M F"}s$׫);j4i|N3S5.[W qJtBgi^Qg=H޴^zp_Iw-2 1+jKL<!# ?=SGQ$&"D"Kפ8c!`-%􋇜QJۑDCKDˎk(;L!R ݯqQ*>Z !3Y\$^QwDJDFXvCq hz`~H-JuxYºTdur*Kf#.&- ?^ʔ5la#D:J!.ۑfQtz>vBB!BD^ܺDۿ6O&. H-͂;d/ DEDGp8a@൰[ Zgv~Y#X6${y_~j̮Tú[U+<BH2˙RBd qN}랞VjϟG\XqK|TXFu9^^twMC u9[\( 1Z#SBHS]]]!/LtU27 36[cIQPSS#o 3KoKM+қUewlkh.;&y88kʾlZKB!0X [_vlM} Q]6Fm  {oصtf'?-bDϨ=%.*=#/Y!PBH`@nڹt`fH]_H ni:aΞ޴NL-:l,WH6Zٷ=G\jv3hj6gN!  JE7 r爛IxDfW֋I/Z6I“v׿w?r[f3~eypif8gܣHn@! ҟX^+0F),L赙DfF[˾aٯMfBx1gf)F!BVb3wzyC瀙tlzlx~xe[sQGA UxX4eK,1ψX=*!PB_b&:llDް"kqY#s>`Vba1}_s6=fb ]B(@!;\g9jNJa J&.@\EA0 F6f#VV9ߗ~6K\J߅}#9&:W uB!4ٗr)@Gdrt}27aqcD&^2hej9i byf՞!#BB!D΍@*|Og7=8^AqY@?[wHn A/Ad˴t{*e-zخB(@!$/YLC D`AK.Yc|KƉZ|raOZ {nawKzQl#2{DȽ# B3X,H}x &iSԶW+7?>"$->2lߧ̾vK a"o1[-n<&Y̊F! ҿ`Zܞ0lRw"6Ϊh } An>6MeieW 2YIh)N1{sa}¼A&PBH(7q 1,gۀTĪN0lXm/-GUeQ@xԋ;oAv/`=O a,i"NDŽ}T\ޔml! =сқ~q6,|.aiq)V $q{p #""elDY=6c6o2SHm7awws 1r.2#j%v Bwb683l6< ͼ}z!-.4"#JHB(@!O`fd,oA6;$lZot"Z"i6g ^~&ȱf##)F!I$nQWWǻJ ,A3 ,l.V˵Fgsq˓"2!> B?,4R@!8{o5!Y{p`:8a2!  lQ!:j%W}gs`؀ҿR[`ó8&!p̌ 2Y l,ž;Cmb}h|&FVQBBBMP1 l,`=o5!K+~B,|:y  BHĞf3s1ѱPDG;yϣ$F֚2G)F555z='?}ѧ{mmmo=NQFle2jAv;DD2a!F^Myâ~W?; k?Eo 2(1WZbd?NyuȄǴ,|&@qN,kHH^b g,Ӝklob.q3zSO BBAr4د0QA{f8c$(cm,L' 83戮dFmqmbHk{TK^Uڽ&F6*~PG#mFg8[ܬ>N6 mNXka42`9qȷu#k6DC{ޤO%:ABH6 IOr.e>c)'=)L~ s10k'Y}$ᵣl(Fk1۳fZC#E&8N{WG(ҁzqK0a_ɏ 9bdGlCO@"@pqS:3p>~<)6c8:|qYC"9( ::۵l/|l7鍗YYY\pD b>]_оlmߛLǙ[*O~"P_|`3J 3璈e-*B).-sl>XV^O!$ b3,%:^m}'}J\(D0aO=!#jўFc2!JuޜLNw*I}>0㷌O!$`#f 9ɽJص!^ COէyOP' t#>xb <ٷ6"FLZ'C\"Kut 'ȾL0ӲP&:БbVYa$BH,-F9:8lfoRR" B!mOG/XƘ e\).cKn&vBG58~uB#/nȽ@T;v i+~Udvp]]*EMM[3\V'~st(HmmPqYN@q Gz2 B },#n:`I+۠=㥝7,j m?ѾOO.g l%;V֧\Ζt ;; k_4q9B+0FrD3u>Fܹ#sVg^o-pI2~ʮ !İHq{qJn4ŎJ4zd#>WzaeLΖHDǽf'B}D$[!U"8)a|$!M~!.2r Iؽ{aUַLK(@1p5Д:Ҷ~M(Ajڃ&:ǀBϙP\D˴pPO}[7oDT"ַ}P"{_oblʟؽK7lDnDMr:Ct[m#B01i3b[m> !ɦ&Il"Pnk}/X˰!Y$.bY lo-͋ՙAHсB-ԙ}&Hb+ IPTj},]%P BDŝh;"||dsT-nC92$X*pfT;:ٸm[.N65$:#Ej& jZizkϺ7x!vް!AR JkY4!6Bh|v'.k?B\?ގ*( "9#[@Bh P$S!0$j˃ ~ B!B!"nI6$S!oq2-r9em®EBO"4ĝJVZY~! $,|589!ė`lD!$?Ȼ~;a?E(@H}ڶ_ʾN8D!OBBOj$RQZ!b?E 0ȱFmqacguB)SPc{&1ʾE%%#o!hT{Nm+!ʺj$"*TD´ ! &uEHIIjO[u gWg>rv<rGʒE'F{ E$(`{x5?U+E`¬Kjme]BO^McQt7&OҾSF{E|)RZzyJ(!Omm(q6(1sh|iWKrG+*bUU֮yV?B!HWϏUE$6z&MDvI4z?*klX2q.ҟnF' <yVHҲEIHI益Dsķo :[.GIeSՎR= iR eɘ#HiM"ݻ$g$vOСRU1;SBBآE6l]%"nvKH4z}rj}@xD'` 6@(ϱj@!?-C|/j'6c~;H|ȫ߳} da ~b  9QGjW >CEf;oފwM:٥֊KúXU<@ФVz_94RR⧣v0yBrj?S{ /aj]Z)ߛ-oX+;yV@x=#n}~y1%cŘiY yHmծU;g[BHN@ Z_ O*<5觤iC[T?ޫ9>ˉ磓=)@(@H9+Ј~[*>:=PEb}.frX#?KlSx4N8Pm%iM*DW;#Uʏվ ! p >߇H7ݦ⣡Jsڝ~i>o]QP 1XrqQ>qQzz1˄%G$. ޲^jO=YMj8 q?gYShqWFGČB_ƨ}RC|Qx*TGb"R|_$  $'nM>߇{QYWQql6G܌֮C `TT}C3Y8;^u^{  ::xx_c  !ؠh!dE=>lƬT|4v~3ocb> KbM(@|ÃI_Y̤|Z1h)3!)76Y\DOBFt.¸oou4kL &>R)5<962bo5V/ L5#!d>އ7d+/B5z "T'@(n)&N>Dj ^jLV:l|'d$ajq>8~*[¬MȁˢOESr!G^!ؠe\UZ5ó-xq%j+*3O! Antԣ{zS O`_X{ `b ~> ~i7dݫ d/c 5YgeNl, M]\j#<"N>}/Kv49*= Q>#8D*?B| qav߰~}ؾ}[yV{ ?'`vqB̘Ok}/(> m2`vo!􉣬QAϒ+dwJ- X{ Yv<:UdGi]tqy"֮"5,RW>"n9KH">އHu'q'BB|Sf 4f!l@p\,.%DDC.SRܡV^IGC2B!+?680P>R#-nB'] _2 39u6bo!D }L(.3x7viVVXa-{Pe[`:3.xָks;nNOSNaAAߋl}ϼU^I߷U|C?> -L}\܉Gx| E?`轼$ 7 N]ÉkM^A C)w>!.yC+@—٩p\wwHKeir8+\ot6wqkLN7K3TL,p_ӟuc'go}~H$;q_mSO/+HI,&t."`VVq3~(>|G lq֡~SI_a>t <Wd:YH"TeUw1r]{o^wu-uWU/~&}߮k؅nLHrO±rD R[0p f3ËxwSԧx|F\xѐr ;@/6a"7۠ xzEr:le:Fz˾(ϵ}tXI|BdvGAD7jO;fGqˈ-kog*pl0G|CS|d ^ Wxqf跎m,L)\FzϋE=,(no <{|6!2ѐ$ogh8$qDz><{̾=|k~{A|d`f"~`~\qٗk4Y  $9Ą;}>m0mt>cĥ>;LYauXb  ` 3-lG9>7I\_m!X:ڿXC'cZ*,ώ|B\ʷC%@҃˻ɾlY] {i= 3Y]qمKz(sx`h!`U 8gx{4I;˛ĝ~EL-! $@;2+a?G5X˨Gp@H,hH{LD)+dfp aH:#UL]ٺA}, _lC,T3PǍ庅J@IP ~\ XLR&46Rp^4u/걖2$noa+}Rxoc%cpHvVey&J{J%|WZf| d:f]؞iARW[[{#P1Þ), QLm'#C"a_M=+nRcN&3})II&{T@]b]O!6GhwZ`݇_̳A[{2\U"| |>KCl)6Gq3&x> #Ӈ6 nq뼍0鳓G Q3YH%&:>+S?'\&dޗ›m첉$p V /y2eaχ_,Iq{CH2џR$۸#Yc}{% 3~ d`:[w#ߏmG6yZQ1SJBH/(7!v ,xp HĘևy{/"e#y+)@H5z\E=и3,9}\il#!>8\\٪z$. 6(cV;[Yp`3}^\!ot󮙳rߺwϬ2c$r@roMϊP| Cĭ~)]S⬷ESns#3 Z?Ց=?>uՇ6泜TlD`H0k?.XFAWrsODmh&J2!2ؠ~ܨh/}U =H; yE ۤ~J t'0%ŧa?wv!=XXEL$X#`?#R5?(nla74u]_UX}D7J\ Ն؃Xd7n. n kl*dpψ[JSq|L_Hݮ5y!zW:ظIE0!SsyZ4O}]\B:[ςsjGAf" (@Ҡƙ smFXyqka]m⢩~bAw)L\+#.vECf?E.f|ORkX9L9%dihDϚRnaT1^\+L{KfVJI7oԞ4nmͫT"T}Js.ˁN%>&dgD 㽘E4nS!~* 7&.NVIA!ߗĭk\".56J3"s?`?F=wbsh  Iէ:I&Ԑ/|ߋMX&//{%iU, B?u7  N`٦ZJM*~\\t,Y~P҈z R f{QT ) _F4$>%䓧!! 9Hi?hҤJ,| l^v/kd$Gi>߇e;v,ØSy>lՃ@/*N\Ew+Sv{'[Z/ 8  -cVAq~{JN{okw;c""N]7,RQ!ui؀(¨?q0lky٬o/5-Oa?E q=hؑ 6ߏ}Nt[֮-)4Q"ŞDDxY=DXh2x=ro/>{[H65+h )BHGd,QmRIi=KbG$B#nAe#xNnk_/j=OVE䁻 \u,7Zd8DCЦ'd"!$Dȫ&<~+,1! |޳$n֭e5_y-9)`)!$#T2)*BDq-]!f @X;}!8M}p7C4>wԗHn6*Pʞ)nF iik\[+T(?N}@մWސ~Q6 !IW@qKeҾj$s\y&ұ4>纼OE k,\/aAh!OKs߾]: S|X^)n9=ɆURt^7?;Z~DHWصCܤ؅iAHN; CwHmƧ!>OX0T\T4͓}ki#):H֭*Be˒hlJ=C5jhNrAܞC4X9LH|i_Bvs 9P|R;vH+pM= TRL% )ѾeH2JO)~fZ;_/O?+WoL45} c)kthbM W۶JUF=M} kM޼mϿ|WmOkּgT[h[.5b? bE?p%9^?ZViYmڻ7nDhF=HDZ;jxD}Wk^{W|"iٗ_ י ~A\\+kZO)4">H,li K?xĵ&}P6cQ&KfY1vƟ|wblj:4Ek}Od<&LJҧE/H I̲2\NCT<~bq|ʮ@`яPSv T)= H SdV Q3apЧS>E")Hw䌐]o<Ǯ}ЧSЧ(@-6[±#cIαKyS\E-] 2,ƧyVG@~4`Sje(ALH !)Oѧ(@q!V(;auBO}H)W;HmDa]C9BO}HB48PiЧS>E">ETl:*n;Y]HΰMyS܈Nrm+FtcΧu#zG@JAp eB}>ES)BS VȄpG"v aBOB">E* !B! B,kH5q%)OѧOQ5%B| ;OѧOBO}K5jvЧS>E">EP[R*nZC/Y8Drȼw>eDHN8;k"$WO]F@Bתm 53K>E")!) Ӧ oeݮ}ЧSЧ(@ Q[%n#Oce5٦}ЧSЧ(@٢^F+;!!3>B"9LOЧH5ySpasjC6GYWXٞ}ЧSЧx۬2QVr׫=meZBO}@:k[UQBVGLkH- O1.  +>$`ާ5nzUc=F+VvBS)BOB .=6Y2sV!Y"|*m O1AOeק=Q0/ݣD" E"IVX_2xm7eL>EBSEO56jT4>OЎV~(F|86=PhԨHyMGU"A&S5ǯ.=gpd-{)tIO-8CEϧ$[O1AOg_m oh!R\0Zя &B#>qW|x-U Ne)+C;q$T\uuy1'|FM;X"i׵,E3-/]֡SѧHЩ- hUŇ ҲhvGGABӵCv΋+U oTsT]pağ|L>Qdƌpk$je@YP㫢}oo6KQ;ÆKьähLDr?P2,(SlҔYU$MIt)=K OiP?KOT$4L]|WeBͧ 8?c?ZN GņRmm ]&)A_O?NFI`.zLUd ̓!o9Kut/w :z >F:կ{3xHh\);F<4=7cFUE:zUUtL+{tU T]KF5t0.AJK%2JG1ˬ~*6cgMZ#AKŊ~JnӬFAXdg>X"%\~񩢢=M,ESէF-ʧ.W{LI@vHc?}+9VǎKy鳭+W6'EUeռE߄IoZI.,Q!6~޸ZE&nPm" D-Ŋ$ZWWkG5'ܬ5tO'Vl^:<-jM06C.6 X n2F_'6z>ͯ>o6uTUWlw IXdR{w]fBS*@ <+8(cMM̓a+ :~e  vyOOE'M{g{]A =01_k;9pW Q7x|CT7lc2K^KUw`gRrx?B:BLx7}|_|JEK}P&$u^*>n~J`ǝ-Xʅ(@-@N# ;R ޟ"w5{lk,itXWc7CB_qȷͿh\%!/VܢfV5`2y+_7P  _te*$X_CbSL`#|?Y;~cWe o>ǽj?ˡZVO#$[`ltQk\EzeXSƞEj^F²[?I j׉K慷۠`N'9m;:d3tuCYϫȤs[Pz|\'!}%}fq,RA~BVCi[e_̘ `SC,9'ȇH <b>!ޖmp<6nٓ){W<~83h"G6)"y0eo6Qկ[+`ED|J`)H')qfN4^>qg.}(?z$8UO6pG",%Lp>x]Jfހث}<7h h}o)@~S09Wm ^#L"^ ƣ*\a f1A$c/"4^II?WH9Qz{eWpu>[$ Џ~~zp(@:8_QL~y]dD y^@ u5؃OJ=gK,o\C35/+#]CmB9L2U?gcŝ!k=~3zFR X/5TzzcȮdAXy_fz$K:m!tj^[mɥX/ =fev_Ȯyfj4KY_UH Y+Ct8:4BB>d3y| a?__ejx")?K էGGp&)9x&>}^+ `it|LH/ƫK>ZemWp՛(@jWx=f/N5ceaO0W܌ڊeɐ^/y{34>*e&Z‰_azgQ!>9A2>Oƾ YPq0|*Q q3K^So*-]WG05/4VHu)Q uoA`ԼgWkўcIB~mziz&16룯khP'K L>"⦆8mLKHX u2!WҩyfT|$ɡ&K=N mG 摑g{>j6?L1-m$BB2 dA?\9,#Zܒ2`5 &>&w=xF~K"Zt0s#ĭB'מ|ק OP]Zyo5,>$g@K=Z>d#D:>wx|-,a<wcqx&&Խ[^,׉ _2}kpSytLK ."iGx|= `VskޏW~xgI3מ!p~6&^`wx=`~ #x񲸽zyx/2?f|HI }dJ>^cl5l 3"n4EB8<'QQY۷Q"8Ǚ!xS_ /v<'TbKS >PH@8&f^T[Pvs%D^wwz٨| D<>v7ؠjU~6bE|gC>^=A%ï{|, x eQf1⠏K굁Lbk :`ke.kyKh,8 >^$IZ|c}1G*x)~+[u?&Vuǫ `J?,nQ^SwO~W2RJYV3@=d)-yN8Lm_X^|:.(z3Iґl dCrzp.Լh+(Pkxo>jSޫڛCOoM'p1swkN^˯9):RO7.~[:UA#:8gcEe枂)NH=|Ǐ!<_u]U}*޳>~ꦺskl驞NM~>0 z4\YvT(}S=d:W,rפGT6#B]ooOeCDBl]$6$!va ;.fp*Mǵ˞E=/L~øQkmš*;qi'zWr͏cR|w\+SXhe5٣>Kw%6~yPdXqQ|ۀ'6ݦr5ݜ|*!$w|Xdw$WgN>}*>Uޡ wEv,SuTJ45ҧ*>S؀;WE.oPWpTuM\yDZ9/|j {ߕ=-~]DOʵOE,q;3Cd1~?̖:i 4Tض")m]1kOrgQEr J7뛺,fQ~` ֣[2$SkקI[oPۤO=>ZH׺L3\P[L->̊Oi#cGb݈6|Xt N!ѱRKQg0XuX 5sQTSrKʧfVD" qUVW%t]ݍOm5 nIV3J>4ڭm| J=pOE߉dd`V)SHDvʞǒw}*>U桟걞,1h1>̧*,GFOuS{OEFԧ촸ާzc\kJχDLejo*#U2XơRb85۴*/`7LI0 ٝܠuYEH>S`F|+CNPh$y֔r+oU:-HU#Sdt쐔 D0y&:OawuO5$ju*BR>5c]Ŭ^)4S$Q@,!mvDJMP#%Rep?5?}}r>4E7KsT)K"Mo6M?0$/H}>5\}j&_jy VV~>WMTȆTyP|ʯAa!8S`h!I;2,9A"9B Z EZ/aܥttC؞|UꓫtYrs*G@Nw5D-QChtg<~󩸴 y4Ʒ  =LǎDFJ"H R>u4&7|jDc@|'NyqYafeOiͷN0Fh~1"ϪXj貏Z}gSLDZﯗ:inH;Ɋ>詟)7>cLU,ÓQSe觰>6*:5SC! OmSZ>Tvdէ< o=n(_4O\B(1TqYm4%}!F~{uϒg8"[`R3Fj6 =Bx]<:{\'uVoǀTKA6_]#V&ʨG[M?E섻oܛzL>}k%ٕSY"5-X8Hgo?դS;#e A]j葿Bt슾!i@>=ES-229bD*PZVEC?i5:~]cMx֧< P=X\m ;UՐ#:|޲)ZObZt OW*CvIxxLSho@jNuKUljHבf;kt_XHUdzvS[9?kYVa]u|jSՁ^AF}>vJl_V&gYi-\$}*c'SmR+ +*I+M1~rK>>գ5_SLAuSzT >_e*֥HUR#;3Ȟy9Bĺ\|̧>&t=. Q|>H} vO.M?&\NOuϯ^H޾-tOt@ )+ܧޗ+jMAmT7>}}؋{"[sS^*ڃ@ r蛛tgYPM}#ue95L'>ROhz7ȬMѶ(GYJ ϧ^|Fھ[I|د0VO :R'ީv爕%YްBҲr] ާ~ <>QOD0ooJ+V6defe.D2]=Y Q|̅F慥V)+s|TG|ʕ{".dHvf~p)pz~]f]XO+uuHNA3<(0ʈ.(EBYWHB(Т I@]*(M CSR`3IOX$Tp>" Qd8J\Xglb()l#+!R>źb=RW{Ep(N6%J"BX!T(Q P摅H{>CYOR 3AOB䦺skrpTF$HSdV QTYg5+jn eI OͤOѧXO'֕HQ *ĝd8S•,j?Z!zꟺouUSSj_(UjՖb仧Tgt?/kb|-i&}ĺru[4Jܮ!VQ~.-]ʋsAlR^S rFZ:A  *> y_5{6oes3^;ktf-[Zz*?va^uTwN}ZקYSv.H q 2+D f:lºb=K̮3*-DnT;]" q~ߒ3>gAX+iRHŒ_;Br{,h9VXw}*`>zb=zYW erڈ_ʎc\XOR'uuPOu)>2C`t WtUzKGaZ}*H>zb=zWW &!J|YW'SԵRWKzU>/-7/%ZO wԵSm\ZS|ʥͻ* !cKS.|{mz^Օ]S^Օm@ϫzk;2LTw]a &>R PO{]}*>U{탨JҧBS*A#Xj,Q2||YW'S)kd]yXeOS+}**,̧IF2C[* aSXx5巪* +h4cL$:Ph N8pȟ CLF h€<M| -j*E g_Zo>[Jᶷg;>{oV dU#KaE"j"'r++,٤di&=䎝ȉj!jYoVvމv^HhT~b>{[alqPS9S^YYmhSh!Xvm ǣX5X;&nT_,$ HWF焴EQԕAS8mpں~75CANhjÞ Yf]sU 6zfܬ_f1V0U3!7;% m djBg%5枕;Ijb2o6Ԙ)rHSL~I5)9QS3&}M5 LX mԛs5h-=R$/9&YVRY͚g9xu!Ƭ QV8?;ĪWj#HkjɜZُkgxg5%QSs5$'gBԆ=jY7&LvLV#ga>ة/nBaM3x /p5 q8T4!i ƈ17ĿS /Yd͸H~)yDrUqoH)s uLcN;mmpE'D2!e1!d ) !'?" Vo՛UN]n4HNT&,&)"+?>rڄńx:ԣHA0'/K>/c+% H2+yC8pk꿆[ Ą&䁦Ĉ`>uJN~tY5jT9ƄDbB)}T;Y9b7MH( J>݆kl!Kh=tjYoDt4kʀY5i N'jbYՉU .IS;<+ǚF4pr:/1&+rbԇխ`*ձOpNkjM#'jANdU[V:4Ԣo$WHQ/H"'Flf۱ )N9)%jbY5 \L IENDB`scoop-0.7.1/doc/index.rst000066400000000000000000000045551240127670500152650ustar00rootroot00000000000000 .. image:: images/logo.png :align: center :height: 140px SCOOP (Scalable COncurrent Operations in Python) is a distributed task module allowing concurrent parallel programming on various environments, from heterogeneous grids to supercomputers. Philosophy ========== Our philosophy is based on these ideas: * The **future** is parallel; * **Simple** is beautiful; * **Parallelism** should be simpler. These tenets are translated concretely in a minimum number of functions allowing maximum parallel efficiency while keeping at minimum the inner knowledge required to use them. It is implemented with Python 3 in mind while being compatible with 2.6+ to allow fast prototyping without sacrificing efficiency and speed. Features ======== SCOOP has many features and advantages over `Futures `_, `multiprocessing `_ and similar modules, such as: * Harness the power of **multiple computers** over network; * Ability to spawn subtasks within tasks; * API compatible with :pep:`3148`; * Parallelizing serial programs with only minor modifications; * Efficient load-balancing. Anatomy of a SCOOPed program ---------------------------- SCOOP can handle multiple diversified multi-layered tasks. You can submit your different functions and data simultaneously and effortlessly while the framework executes them locally or remotely. Contrarily to most multiprocessing frameworks, it allows to launch subtasks within tasks. .. image:: images/introductory_tree.png :align: center :width: 600 px Through SCOOP, you can simultaneously execute tasks that are of different nature (Discs of different colors) or different by complexity (Discs radiuses). The module will handle the physical considerations of parallelization such as task distribution over your resources (load balancing), communications, etc. Applications ------------ The common applications of SCOOP consist of, but is not limited to: * Evolutionary Algorithms * Monte Carlo simulations * Data mining * Data processing * I/O processing * Graph traversal Manual ====== .. toctree:: :maxdepth: 2 install usage examples api contributing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` scoop-0.7.1/doc/install.rst000066400000000000000000000072331240127670500156200ustar00rootroot00000000000000Install ======= Dependencies ------------ The software requirements for SCOOP are as follows: * `Python `_ >= 2.6 or >= 3.2 * `Distribute `_ >= 0.6.2 or `setuptools `_ >= 0.7 * `Greenlet `_ >= 0.3.4 * `pyzmq `_ >= 13.1.0 and `libzmq `_ >= 3.2.0 * :program:`ssh` for remote execution Prerequisites ------------- Linux ~~~~~ You must have the Python headers (to compile pyzmq and greenlet) and pip installed. These should be simple to install using the package manager provided with your distribution. To get the prerequisites on an Ubuntu system, execute the following in a console:: sudo apt-get install python-dev python-pip Ensure that your compiler is GCC as it is the tested compiler for pyzmq and greenlet. Mac ~~~ The easiest way to get started is by using `Homebrew `_. Once you've brewed your Python version and ZeroMQ, you are ready to install SCOOP. Windows ~~~~~~~ Please download and install pyzmq before installing SCOOP. This can be done by using the binary installer provided at their `download page `_. These installers will provide libzmq alongside pyzmq. You can install pip on windows using either `Christoph Gohlke `_ windows installers or the get-pip.py script as shown in the `pip-installer.org webpage `_. Installation ------------ To install SCOOP, use `pip `_ as such:: pip install scoop POSIX Operating systems ~~~~~~~~~~~~~~~~~~~~~~~ Connection to remote hosts is done using SSH. An implementation of SSH must be installed in order to be able to use this feature. Windows Operating System ~~~~~~~~~~~~~~~~~~~~~~~~ On Windows, this will try to compile libzmq. You can skip this compilation by installing pyzmq using the installer available at their `download page `_. This installer installs libzmq alongside pyzmq. Furthermore, to be able to use the multi-system capabilities of SCOOP, a SSH implementation must be available. This may be done either by using `Cygwin `_ or `OpenSSH for Windows `_. Remote usage ------------ Because remote host connection needs to be done without a prompt, you must use ssh keys to allow **passwordless authentication between every computing node**. You should make sure that your public ssh key is contained in the ``~/.ssh/authorized_keys`` file on the remote systems (Refer to the `ssh manual `_). If you have a shared :file:`/home/` over your systems, you can do as such:: [~]$ mkdir ~/.ssh; cd ~/.ssh [.ssh]$ ssh-keygen -t dsa [.ssh]$ cat id_dsa.pub >> authorized_keys [.ssh]$ chmod 700 ~/.ssh ; chmod 600 ./id_dsa ; chmod 644 ./id_dsa.pub ./authorized_keys .. note:: If your remote hosts needs special configuration (non-default port, some specified username, etc.), you should do it in your ssh client configuration file (by default ``~/.ssh/config``). .. note:: The following parameters of ``ssh`` are used by SCOOP: * -x : Deactivates X forwarding * -n : Prevents reading from stdin (batch mode) * -oStrictHostKeyChecking=no : Allow the connection to hosts ``ssh`` sees for the first time. Without it, ``ssh`` interactively asks to accept the identity of the peer. scoop-0.7.1/doc/make.bat000066400000000000000000000111071240127670500150200ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SCOOP.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SCOOP.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end scoop-0.7.1/doc/usage.rst000066400000000000000000000430721240127670500152570ustar00rootroot00000000000000Usage ===== Nomenclature ------------ .. _Nomenclature-table: =========== ======================================================================================================================================= Keyword Description =========== ======================================================================================================================================= Future(s) The Future class encapsulates the asynchronous execution of a callable. Broker Process dispatching Futures. Worker Process executing Futures. Root The worker executing the root Future, your main program. =========== ======================================================================================================================================= Architecture diagram -------------------- The future(s) distribution over workers is done by a variation of the `Broker pattern `_. In such a pattern, workers act as independant elements that interact with a broker to mediate their communications. .. image:: images/architecture.png :height: 250px :align: center Mapping API ----------- The philosophy of SCOOP is loosely built around the *futures* module proposed by :pep:`3148`. It primarily defines a :meth:`~scoop.futures.map` and a :meth:`~scoop.futures.submit` function allowing asynchroneous computation that SCOOP will propagate to its workers. Map ~~~ A |map()|_ function applies multiple parameters to a single function. For example, if you want to apply the |abs()|_ function to every number of a list:: import random data = [random.randint(-1000, 1000) for r in range(1000)] # Without Map result = [] for i in data: result.append(abs(i)) # Using a Map result = list(map(abs, data)) .. |abs()| replace:: *abs()* .. _abs(): http://docs.python.org/library/functions.html#abs SCOOP's :meth:`~scoop.futures.map` returns a generator iterating over the results in the same order as its inputs. It can thus act as a parallel substitute to the standard |map()|_, for instance:: # Script to be launched with: python -m scoop scriptName.py import random from scoop import futures data = [random.randint(-1000, 1000) for r in range(1000)] if __name__ == '__main__': # Python's standard serial function dataSerial = list(map(abs, data)) # SCOOP's parallel function dataParallel = list(futures.map(abs, data)) assert dataSerial == dataParallel .. |map()| replace:: *map()* .. _map(): http://docs.python.org/library/functions.html#map .. _test-for-main-mandatory: .. warning:: In your root program, you *must* check ``if __name__ == '__main__'`` as shown above. Failure to do so will result in every worker trying to run their own instance of the program. This ensures that every worker waits for parallelized tasks spawned by the root worker. .. note:: Your callable function passed to SCOOP must be picklable in its entirety. Note that the pickle module is limited to **top level functions and classes** as stated in the `documentation `_. .. note:: Keep in mind that objects are not shared between workers and that changes made to an object in a function are not seen by other workers. Map_as_completed ~~~~~~~~~~~~~~~~ The :meth:`~scoop.futures.map_as_completed` function is used exactly in the same way as the :meth:`~scoop.futures.map` function. The only difference is that this function will yield results as soon as they are made available. Submit ~~~~~~ SCOOP's :meth:`~scoop.futures.submit` returns a :class:`~scoop._types.Future` instance. This allows a finer control over the Futures, such as out-of-order results retrieval. Reduction API ------------- mapReduce ~~~~~~~~~ The :meth:`~scoop.futures.mapReduce` function allows to parallelize a reduction function after applying the aforementioned :meth:`~scoop.futures.map` function. It returns a single element. A reduction function takes the map results and applies a function cumulatively to it. For example, applying ``reduce(lambda x, y: x+y, ["a", "b", "c", "d"])`` would execute ``(((("a")+"b")+"c")+"d")`` give you the result ``"abcd"``. More information is available in the `standard Python documentation on the reduce function `_. A common reduction usage consist of a sum as the following example:: # Script to be launched with: python -m scoop scriptName.py import random import operator from scoop import futures data = [random.randint(-1000, 1000) for r in range(1000)] if __name__ == '__main__': # Python's standard serial function serialSum = sum(map(abs, data)) # SCOOP's parallel function parallelSum = futures.mapReduce(abs, operator.add, data) assert serialSum == parallelSum .. note:: You can pass any arbitrary reduction function, not only operator ones. Architecture ~~~~~~~~~~~~ SCOOP will automatically generate a binary reduction tree and submit it. Every level of the tree contain reduction nodes except for the bottom-most which contains the mapped function. .. image:: images/reduction.png :height: 280px :align: center Utilities --------- Object sharing API ~~~~~~~~~~~~~~~~~~ Sharing constant objects between workers is available using the :mod:`~scoop.shared` module. Its functionnalities are summarised in this example:: from scoop import futures, shared def myParallelFunc(inValue): myValue = shared.getConst('myValue') return inValue + myValue if __name__ == '__main__': shared.setCont(myValue=5) print(list(futures.map(myParallelFunc, range(10)))) .. note:: A constant can only be defined once on the entire pool of workers. More information in the :ref:`api-shared-module` reference. Logging ~~~~~~~ You can use the `scoop.logger` to output useful information alongside your log messages such as the time, the worker name which emitted the message and the module in which the message was emitted. Here is a sample usage:: import scoop scoop.logger.warn("This is a warning!") How to launch SCOOP programs ---------------------------- Programs using SCOOP, such as the ones in the |exampleDirectory|_ directory, need to be launched with the :option:`-m scoop` parameter passed to Python, as such:: cd scoop/examples/ python -m scoop fullTree.py .. |exampleDirectory| replace:: :file:`examples/` .. _exampleDirectory: https://code.google.com/p/scoop/source/browse/examples/ .. note:: When using a Python version prior to 2.7, you must start SCOOP using `-m scoop.__main__` . You should also consider using an up-to-date version of Python. Launch in details ~~~~~~~~~~~~~~~~~ The SCOOP module spawns the needed broker(s) and worker(s) on the given list of computers, including remote ones via :program:`ssh`. Every worker imports your program with a `__name__` variable different than `__main__` then awaits orders given by the root node to execute available functions. This is necessary to have references over your functions and variables in the global scope. This means that everything (definitions, assignments, operations, etc.) in the global scope of your program will be executed by every worker. To ensure a section of your code is only executed once, you must place a conditional barrier such as this one: .. code-block:: python if __name__ == '__main__': Option list ~~~~~~~~~~~ Here is a list of the parameters that can be passed to SCOOP:: $ python -m scoop --help usage: python -m scoop [-h] [--hosts [Address [Address ...]] | --hostfile FileName] [--path PATH] [--nice NiceLevel] [--verbose] [--quiet] [--log FileName] [-n NumberOfWorkers] [--tunnel] [--broker-hostname Address] [--python-interpreter Path] [--pythonpath PYTHONPATH] [--profile] [executable] ... Starts a parallel program using SCOOP. positional arguments: executable The executable to start with SCOOP args The arguments to pass to the executable optional arguments: -h, --help show this help message and exit --hosts [Address [Address ...]], --host [Address [Address ...]] The list of hosts. The first host will execute the origin. (default is 127.0.0.1) --hostfile FileName The hostfile name --path PATH, -p PATH The path to the executable on remote hosts (default is local directory) --nice NiceLevel *nix niceness level (-20 to 19) to run the executable --verbose, -v Verbosity level of this launch script (-vv for more) --quiet, -q --log FileName The file to log the output. (default is stdout) -n NumberOfWorkers Total number of workers to launch on the hosts. Workers are spawned sequentially over the hosts. (ie. -n 3 with 2 hosts will spawn 2 workers on the first host and 1 on the second.) (default: Number of CPUs on current machine) --tunnel Activate ssh tunnels to route toward the broker sockets over remote connections (may eliminate routing problems and activate encryption but slows down communications) --broker-hostname Address The externally routable broker hostname / ip (defaults to the local hostname) --python-interpreter Path The python interpreter executable with which to execute the script --pythonpath PYTHONPATH The PYTHONPATH environment variable (default is current PYTHONPATH) --profile Turn on the profiling. SCOOP will call cProfile.run on the executable for every worker and will produce files in directory profile/ named workerX where X is the number of the worker. A remote workers example may be as follow:: python -m scoop --hostfile hosts -vv -n 6 your_program.py [your arguments] ================ ================================= Argument Meaning ================ ================================= -m scoop **Mandatory** Uses SCOOP to run program. --hostfile hosts is a file containing a list of host to launch SCOOP -vv Double verbosity flag. -n 6 Launch a total of 6 workers. your_program.py The program to be launched. [your arguments] The arguments that needs to be passed to your program. ================ ================================= .. note:: Your local hostname must be externally routable for remote hosts to be able to connect to it. If you don't have a DNS properly set up on your local network or a system hosts file, consider using the :option:`--broker-hostname` argument to provide your externally routable IP or DNS name to SCOOP. You may as well be interested in the :option:`-e` argument for testing purposes. Hostfile format ~~~~~~~~~~~~~~~ You can specify the hosts with a hostfile and pass it to SCOOP using the :option:`--hostfile` argument. The hostfile should use the following syntax:: hostname_or_ip 4 other_hostname 5 third_hostname 2 The name being the system hostname and the number being the number of workers to launch on this host. Using a list of host ~~~~~~~~~~~~~~~~~~~~ You can also use a list of host with the :option:`--host [...]` flag. In this case, you must put every host separated by a space the number of time you wish to have a worker on each of the node. For example:: python -m scoop --host machine_a machine_a machine_b machine_b your_program.py This example would start two workers on :option:`machine_a` and two workers on :option:`machine_b`. Choosing the number of workers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The number of workers started should be equal to the number of cores you have on each machine. If you wish to start more or less workers than specified in your hostfile or in your hostlist, you can use the :option:`-n` parameter. Be aware that tinkering with this parameter may hinder performances. .. note:: The :option:`-n` parameter overrides any previously specified worker amount. If :option:`-n` is less than the sum of workers specified in the hostfile or hostlist, the workers are launched in batch by host until the parameter is reached. This behavior may ignore latters hosts. If :option:`-n` is more than the sum of workers specified in the hostfile or hostlist, the remaining workers are distributed using a Round-Robin algorithm. Each host will increment its worker amount until the parameter is reached. Use with a scheduler -------------------- You must provide a startup script on systems using a scheduler such as supercomputers or laboratory grids. Here are some example startup scripts using different grid task managers. Some example startup scripts are available in the |submit_files_path|_ directory. .. |submit_files_path| replace:: :file:`examples/submit_files` .. _submit_files_path: https://code.google.com/p/scoop/source/browse/examples/submit_files/ SCOOP natively supports Sun Grid Engine (SGE), Torque (PBS-compatible, Moab, Maui) and SLURM. That means that a minimum launch file is needed while the framework recognizes automatically the nodes assigned to your task. .. note:: **These are only examples**. Refer to the documentation of your scheduler for the list of arguments needed to run the task on your grid or cluster. .. TODO Condor, Amazon EC2 using Boto & others Use on cloud services --------------------- Pitfalls -------- Program scope ~~~~~~~~~~~~~ As a good Python practice (see :pep:`395#what-s-in-a-name`), you should always wrap the executable part of your program using: .. code-block:: python if __name__ == '__main__': This is mandatory when using parallel frameworks such as multiprocessing or SCOOP. For an explanation why, read the `Launch in details`_ section. If your program lacks this conditional barrier, your whole program will be executed as many times as there are workers, meaning duplicate work is being done. Unpicklable Future ~~~~~~~~~~~~~~~~~~ Only functions or classes declared at the top level of your program are picklables. This is a limitation of `Python's pickle module `_. Here are some examples of non-working map invocations: .. code-block:: python from scoop import futures class myClass(object): @staticmethod def myFunction(x): return x if __name__ == '__main__': def mySecondFunction(x): return x # Both of these calls won't work because Python pickle won't be able to # pickle or unpickle the function references. wrongCall1 = futures.map(myClass.myFunction, [1, 2, 3, 4, 5]) wrongCall2 = futures.map(mySecondFunction, [1, 2, 3, 4, 5]) Launching a faulty program will result in this error being displayed:: [...] This element could not be pickled: [...] Mutable arguments ~~~~~~~~~~~~~~~~~ In standard programs, modifying a mutable function argument also modifies it in the caller scope because objects are passed by reference. This side-effect is not simulated in SCOOP. Function arguments are not serialized back along its answer. Lazy-like evaluation ~~~~~~~~~~~~~~~~~~~~ The :meth:`~scoop.futures.map` and :meth:`~scoop.futures.submit` will distribute their Futures both locally and remotely. Futures executed locally will be computed upon access (iteration for the :meth:`~scoop.futures.map` and :meth:`~scoop._types.Future.result` for :meth:`~scoop.futures.submit`). Futures distributed remotely will be executed right away. Large datasets ~~~~~~~~~~~~~~ Every parameter sent to a function by a :meth:`~scoop.futures.map` or :meth:`~scoop.futures.submit` gets serialized and sent within the Future to its worker. Sending large elements as parameter(s) to your function(s) results in slow speeds and network overload. You should consider using a global variable in your module scope for passing large elements. It will then be loaded on launch by every worker and won't overload your network. Unefficient:: from scoop import futures def mySum(inData): """The worker will receive all its data from network.""" return sum(inData) if __name__ == '__main__': data = [[i for i in range(x, x + 1000)] for x in range(0, 8001, 1000)] results = list(futures.map(mySum, data)) Better efficiency:: from scoop import futures data = [[i for i in range(x, x + 1000)] for x in range(0, 8001, 1000)] def mySum(inIndex): """The worker will only receive an index from network.""" return sum(data[inIndex]) if __name__ == '__main__': results = list(futures.map(mySum, range(len(data)))) SCOOP and greenlets ~~~~~~~~~~~~~~~~~~~ .. warning:: Since SCOOP uses greenlets to schedule and run futures, programs that use their own greenlets won't work with SCOOP. However, you should consider replacing the greenlets in your code by SCOOP functions. scoop-0.7.1/examples/000077500000000000000000000000001240127670500144645ustar00rootroot00000000000000scoop-0.7.1/examples/callback.py000066400000000000000000000024341240127670500165750ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Example of Future callback usage. """ from scoop import futures import time def myFunc(n): time.sleep(n) return n def doneElement(inFuture): print("Done: {0}".format(inFuture.result())) def main(): # Create launches launches = [futures.submit(myFunc, i + 1) for i in range(5)] # Add a callback on every launches for launch in launches: launch.add_done_callback(doneElement) # Wait for the launches to complete. [completed for completed in futures.as_completed(launches)] if __name__ == "__main__": main() scoop-0.7.1/examples/conditional_execution.py000066400000000000000000000021641240127670500214270ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Shows the conditional execution of a parallel Future. """ from scoop import futures import random first_type = lambda x: x + " World" second_type = lambda x: x + " Parallel World" if __name__ == '__main__': if random.random() < 0.5: my_future = futures.submit(first_type, "Hello") else: my_future = futures.submit(second_type, "Hello") print(my_future.result()) scoop-0.7.1/examples/conditional_import.py000066400000000000000000000022351240127670500207350ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Shows how to develop a program working with or without scoop launched providing serial map fallback. This example works even if SCOOP isn't installed. """ try: from scoop.futures import map as map_ except ImportError: map_ = map def helloWorld(value): return "Hello World from Future #{0}".format(value) if __name__ == "__main__": returnValues = list(map_(helloWorld, range(16))) print("\n".join(returnValues)) scoop-0.7.1/examples/deap_ga_evosn.py000066400000000000000000000145571240127670500176440ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import random from deap import algorithms from deap import base from deap import creator from deap import tools from scoop import futures from dependency import sortingnetwork as sn import logging import time import argparse import sys parser = argparse.ArgumentParser(description="Deap's evosn example.") parser.add_argument('--inputs', type=int, default=6) parser.add_argument('--cores', type=int, default=1) parser.add_argument('--filename') parser.add_argument('--population', type=int, default=300) parser.add_argument('--generations', type=int, default=40) args = parser.parse_args() INPUTS = args.inputs sizes = {11 : (29,39), 12 : (25,45), 13 : (39,51), 14 : (45,56), 15 : (51,60), 16 : (56,65), 17 : (60,69), 18 : (65,74), 19 : (69,78)} def evalEvoSN(individual, dimension): network = sn.SortingNetwork(dimension, individual) return network.assess(), network.length, network.depth def genWire(dimension): return (random.randrange(dimension), random.randrange(dimension)) def genNetwork(dimension, min_size, max_size): size = random.randint(min_size, max_size) return [genWire(dimension) for i in range(size)] def mutWire(individual, dimension, indpb): for index, elem in enumerate(individual): if random.random() < indpb: individual[index] = genWire(dimension) def mutAddWire(individual, dimension): index = random.randint(0, len(individual)) individual.insert(index, genWire(dimension)) def mutDelWire(individual): index = random.randrange(len(individual)) del individual[index] creator.create("FitnessMin", base.Fitness, weights=(-1.0, -1.0, -1.0)) creator.create("Individual", list, fitness=creator.FitnessMin) toolbox = base.Toolbox() # Gene initializer toolbox.register("network", genNetwork, dimension=INPUTS, min_size=sizes[INPUTS][0] if INPUTS in sizes else 25, max_size=sizes[INPUTS][1] if INPUTS in sizes else 35) # Structure initializers toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.network) toolbox.register("population", tools.initRepeat, list, toolbox.individual) toolbox.register("evaluate", evalEvoSN, dimension=INPUTS) toolbox.register("mate", tools.cxTwoPoints) toolbox.register("mutate", mutWire, dimension=INPUTS, indpb=0.05) toolbox.register("addwire", mutAddWire, dimension=INPUTS) toolbox.register("delwire", mutDelWire) toolbox.register("select", tools.selTournament, tournsize=3) toolbox.register("map", futures.map) #logging.warning("avant main") def main(): # test if file is ok before starting the test if args.filename: open(args.filename).close() random.seed(64) beginTime = time.time() evaluationTime = 0 population = toolbox.population(n=args.population) hof = tools.ParetoFront() stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("avg", tools.mean) stats.register("std", tools.std) stats.register("min", min) stats.register("max", max) logger = tools.EvolutionLogger(["gen", "evals", "time"] + [str(k) for k in stats.functions.keys()]) logger.logHeader() CXPB, MUTPB, ADDPB, DELPB, NGEN = 0.5, 0.2, 0.01, 0.01, args.generations evalBegin = time.time() # Evaluate every individuals fitnesses = toolbox.map(toolbox.evaluate, population) for ind, fit in zip(population, fitnesses): ind.fitness.values = fit evaluationTime += (time.time() - evalBegin) hof.update(population) stats.update(population) logger.logGeneration(gen=0, evals=len(population), stats=stats, time=evaluationTime) # Begin the evolution for g in range(1, NGEN): offspring = [toolbox.clone(ind) for ind in population] # Apply crossover and mutation for ind1, ind2 in zip(offspring[::2], offspring[1::2]): if random.random() < CXPB: toolbox.mate(ind1, ind2) del ind1.fitness.values del ind2.fitness.values # Note here that we have a different sheme of mutation than in the # original algorithm, we use 3 different mutations subsequently. for ind in offspring: if random.random() < MUTPB: toolbox.mutate(ind) del ind.fitness.values if random.random() < ADDPB: toolbox.addwire(ind) del ind.fitness.values if random.random() < DELPB: toolbox.delwire(ind) del ind.fitness.values # Evaluate the individuals with an invalid fitness invalid_ind = [ind for ind in offspring if not ind.fitness.valid] evalBegin = time.time() fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) for ind, fit in zip(invalid_ind, fitnesses): ind.fitness.values = fit evaluationTime += (time.time() - evalBegin) population = toolbox.select(population+offspring, len(offspring)) hof.update(population) stats.update(population) logger.logGeneration(gen=g, evals=len(invalid_ind), stats=stats, time=evaluationTime) best_network = sn.SortingNetwork(INPUTS, hof[0]) print(best_network) print(best_network.draw()) print("%i errors, length %i, depth %i" % hof[0].fitness.values) totalTime = time.time() - beginTime print("Total time: {0}\nEvaluation time: {1}".format(totalTime, evaluationTime)) if args.filename: f = open(args.filename, "a") f.write("{0};{1};{2};{3}\n".format(args.cores, INPUTS, totalTime, evaluationTime)) f.close() return population, stats, hof if __name__ == "__main__": main() scoop-0.7.1/examples/deap_ga_onemax.py000066400000000000000000000046201240127670500177670ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from __future__ import print_function import array import logging import random import sys logging.basicConfig(level=logging.INFO, stream=sys.stdout) import random try: from deap import algorithms from deap import base from deap import creator from deap import tools except Exception as e: raise Exception("This test needs DEAP to be installed.") from scoop import futures creator.create("FitnessMax", base.Fitness, weights=(1.0,)) creator.create("Individual", array.array, typecode='b', fitness=creator.FitnessMax) toolbox = base.Toolbox() # Attribute generator toolbox.register("attr_bool", random.randint, 0, 1) # Structure initializers toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, 100) toolbox.register("population", tools.initRepeat, list, toolbox.individual) def evalOneMax(individual): return sum(individual), toolbox.register("evaluate", evalOneMax) toolbox.register("mate", tools.cxTwoPoints) toolbox.register("mutate", tools.mutFlipBit, indpb=0.05) toolbox.register("select", tools.selTournament, tournsize=3) toolbox.register("map", futures.map) def main(): random.seed(64) pop = toolbox.population(n=300) hof = tools.HallOfFame(1) stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("avg", tools.mean) stats.register("std", tools.std) stats.register("min", min) stats.register("max", max) algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=40, stats=stats, halloffame=hof, verbose=True) logging.info("Best individual is %s, %s", hof[0], hof[0].fitness.values) return 0 if __name__ == "__main__": main() scoop-0.7.1/examples/dependency/000077500000000000000000000000001240127670500166025ustar00rootroot00000000000000scoop-0.7.1/examples/dependency/__init__.py000066400000000000000000000000001240127670500207010ustar00rootroot00000000000000scoop-0.7.1/examples/dependency/sortingnetwork.py000066400000000000000000000111641240127670500222560ustar00rootroot00000000000000# This file is part of DEAP. # # DEAP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # DEAP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with DEAP. If not, see . try: from itertools import product except ImportError: def product(*args, **kwds): # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 pools = map(tuple, args) * kwds.get('repeat', 1) result = [[]] for pool in pools: result = [x+[y] for x in result for y in pool] for prod in result: yield tuple(prod) class SortingNetwork(list): """Sorting network class. From Wikipedia : A sorting network is an abstract mathematical model of a network of wires and comparator modules that is used to sort a sequence of numbers. Each comparator connects two wires and sort the values by outputting the smaller value to one wire, and a larger value to the other. """ def __init__(self, dimension, connectors = []): self.dimension = dimension for wire1, wire2 in connectors: self.addConnector(wire1, wire2) def addConnector(self, wire1, wire2): """Add a connector between wire1 and wire2 in the network.""" if wire1 == wire2: return if wire1 > wire2: wire1, wire2 = wire2, wire1 try: last_level = self[-1] except IndexError: # Empty network, create new level and connector self.append([(wire1, wire2)]) return for wires in last_level: if wires[1] >= wire1 and wires[0] <= wire2: self.append([(wire1, wire2)]) return last_level.append((wire1, wire2)) def sort(self, values): """Sort the values in-place based on the connectors in the network.""" for level in self: for wire1, wire2 in level: if values[wire1] > values[wire2]: values[wire1], values[wire2] = values[wire2], values[wire1] def assess(self, cases=None): """Try to sort the **cases** using the network, return the number of misses. If **cases** is None, test all possible cases according to the network dimensionality. """ if cases is None: cases = product(range(2), repeat=self.dimension) misses = 0 ordered = [[0]*(self.dimension-i) + [1]*i for i in range(self.dimension+1)] for sequence in cases: sequence = list(sequence) self.sort(sequence) misses += (sequence != ordered[sum(sequence)]) return misses def draw(self): """Return an ASCII representation of the network.""" str_wires = [["-"]*7 * self.depth] str_wires[0][0] = "0" str_wires[0][1] = " o" str_spaces = [] for i in range(1, self.dimension): str_wires.append(["-"]*7 * self.depth) str_spaces.append([" "]*7 * self.depth) str_wires[i][0] = str(i) str_wires[i][1] = " o" for index, level in enumerate(self): for wire1, wire2 in level: str_wires[wire1][(index+1)*6] = "x" str_wires[wire2][(index+1)*6] = "x" for i in range(wire1, wire2): str_spaces[i][(index+1)*6+1] = "|" for i in range(wire1+1, wire2): str_wires[i][(index+1)*6] = "|" network_draw = "".join(str_wires[0]) for line, space in zip(str_wires[1:], str_spaces): network_draw += "\n" network_draw += "".join(space) network_draw += "\n" network_draw += "".join(line) return network_draw @property def depth(self): """Return the number of parallel steps that it takes to sort any input. """ return len(self) @property def length(self): """Return the number of comparison-swap used.""" return sum(len(level) for level in self) scoop-0.7.1/examples/exceptExample.py000066400000000000000000000051561240127670500176510ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ A simple example to show some case of exception handling using Scoop. The exception handling are done in the same way as they are in python. However, exception dealt by Scoop using python3 are a lot more clear than if python2 was used. """ from __future__ import print_function from scoop import futures def func0(n): # Task submission is asynchronous; It will return immediately. task = futures.submit(func1, n) # The call blocks here until it gets the result result = task.result() return result def func1(n): try: # The map alone doesn't throw the exception. The exception is raised # in the sum which calls the map generator. result = sum(futures.map(func2, [i+1 for i in range(n)])) except Exception as err: # We could do some stuff here raise Exception("This exception is normal") return result def func2(n): if n > 10: # This exception is treated in func1 raise Exception(10) launches = [] for i in range(n): launches.append(futures.submit(func3, i + 1)) result = futures.as_completed(launches) return sum(res.result() for res in result) def func3(n): result = [] try: result = list(futures.map(func4, [i+1 for i in range(n)])) except Exception as e: # We return what we can return e.args[0] + sum(result) # No exception was generated return sum(result) def func4(n): result = n*n if result > 20: # This exception is treated in func3 raise Exception(result) return result def main(): task = futures.submit(func0, 20) # You can wait for a result before continuing computing futures.wait([task], return_when=futures.ALL_COMPLETED) result = task.result() print(result) return result if __name__ == "__main__": main() scoop-0.7.1/examples/full_tree.py000066400000000000000000000040331240127670500170170ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ A simple example showing how to resolve a full balanced tree with multiples techniques using SCOOP. """ from scoop import futures def func4(n): # Example of calculus you may want to perform result = n*n return result def func3(n): # This call results in a generator function result = futures.map(func4, [i+1 for i in range(n)]) # The results are evaluated here when they are accessed. return sum(result) def func2(n): launches = [futures.submit(func3, i + 1) for i in range(n)] # Spawn a generator for each completion, unordered result = (a.result() for a in futures.as_completed(launches)) return sum(result) def func1(n): # To force an immediate evaluation, you can wrap your map in a list such as: result = list(futures.map(func2, [i+1 for i in range(n)])) return sum(result) def func0(n): # Task submission is asynchronous; It will return immediately. task = futures.submit(func1, n) # The call blocks here until it gets the result result = task.result() return result def main(): task = futures.submit(func0, 20) # You can wait for a result before continuing computing futures.wait([task], return_when=futures.ALL_COMPLETED) result = task.result() print(result) return result if __name__ == "__main__": main() scoop-0.7.1/examples/grtest.py000066400000000000000000000005121240127670500163440ustar00rootroot00000000000000from greenlet import greenlet from scoop import futures def test1(): print(12) gr2.switch() print(34) def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) if __name__ == '__main__': gr1.switch() #a = futures.submit(test1) #a.result() #gr1.switch() scoop-0.7.1/examples/image_resize.py000066400000000000000000000072321240127670500175050ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Example of parallel image resizing using PIL. """ import time import sys from collections import namedtuple from scoop import futures try: import Image except: raise Exception("This example uses PIL, the Python Imaging Library." "You must install this library before using this example.") # Set constants imageSize = namedtuple('imageSize', ['w', 'y'], verbose=False) TARGET_SIZE = imageSize(512, 384) DIVISION_HEIGHT = 2 DIVISION_WIDTH = 2 # Define a serialization for image format sImage = namedtuple('sImage', ['pixels', 'size', 'mode'], verbose=False) if len(sys.argv) < 2: raise Exception("This example needs an image file path as first parameter." "\nPlease re-execute it using :\n" " python -m scoop {0} yourImage.jpg".format(__file__)) originalImage = Image.open(sys.argv[-1]) def sliceImage(image, divWidth, divHeight): """Divide the received image in multiple tiles""" w, h = image.size tiles = [] for y in range(0, h - 1 , h/divHeight): my = min(y + h/divHeight, h) for x in range(0, w - 1, w/divWidth): mx = min(x + w/divWidth, w) tiles.append(image.crop((x, y, mx, my))) return tiles def resizeTile(index, size): """Apply Antialiasing resizing to tile""" resized = tiles[index].resize(size, Image.ANTIALIAS) return sImage(resized.tostring(), resized.size, resized.mode) # Generate image tiles on every workers ts = time.time() tiles = sliceImage(originalImage, divWidth=DIVISION_WIDTH, divHeight=DIVISION_HEIGHT) if __name__ == '__main__': # Resize the tiles resizedTiles = list(futures.map(resizeTile, range(len(tiles)), size=(TARGET_SIZE[0] // DIVISION_WIDTH, TARGET_SIZE[1] // DIVISION_HEIGHT))) # Create the new canvas that will receive the tiles resizedParallelImage = Image.new(originalImage.mode, TARGET_SIZE, "white") # Fusion the tiles together on the canvas imgPosition = imageSize(0, 0) for index, tile in enumerate(resizedTiles): # Convert the serialized image to a pastable image format imgTile = Image.fromstring(tile.mode, tile.size, tile.pixels) # Compute tile position in canvas imgPosition = ((index % DIVISION_WIDTH) * TARGET_SIZE[0] // DIVISION_WIDTH, (index // DIVISION_WIDTH) * TARGET_SIZE[1] // DIVISION_HEIGHT, ) # Paste the tile to the canvas resizedParallelImage.paste(imgTile, imgPosition) pts = time.time() - ts resizedParallelImage.show() # Serial computation of tree depth ts = time.time() resizedImage = originalImage.resize(TARGET_SIZE, Image.ANTIALIAS) sts = time.time() - ts resizedImage.show() print("Parallel time: {0:.5f}s\nSerial time: {1:.5f}s".format(pts, sts)) scoop-0.7.1/examples/interactive_shell.py000066400000000000000000000005051240127670500205420ustar00rootroot00000000000000from scoop import futures, shared import scoop import code # Usage example: # # >>> def myExample(inVal): # ... return inVal + 1 # ... # >>> shared.setConst(myExample=myExample) # >>> print(list(futures.map(myExample, range(64)))) # >>> exit() if __name__ == '__main__': code.interact(local=locals())scoop-0.7.1/examples/lambda.py000066400000000000000000000025151240127670500162610ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ SCOOP also works on lambda functions even if they aren't picklable by default. """ from scoop import futures, shared from math import cos import operator if __name__ == "__main__": # Standard lambda function myFunc = lambda x: x * 2 # Lambda function using a globally defined function myFunc2 = lambda x: cos(x) # Lambda function using a function through a module definition myFunc3 = lambda x: operator.add(x, 1) # Calls to SCOOP print(list(futures.map(myFunc, range(10)))) print(list(futures.map(myFunc2, range(10)))) print(list(futures.map(myFunc3, range(10)))) scoop-0.7.1/examples/map_as_completed.py000066400000000000000000000024731240127670500203400ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Shows the usage of the map_as_completed() function """ from scoop import futures def hello(input_): return input_ if __name__ == "__main__": print("Execution of map():") # Example of how to use a normal map function for out in futures.map(hello, range(10)): print("Hello from #{}!".format(out)) print("Execution of map_as_completed():") # Example of map_as_completed usage. Note that the results won't necessarily be ordered # like the previous for out in futures.map_as_completed(hello, range(10)): print("Hello from #{}!".format(out)) scoop-0.7.1/examples/map_doc.py000066400000000000000000000020641240127670500164420ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ This is an example usage of a map function using SCOOP. """ from __future__ import print_function from scoop import futures def helloWorld(value): return "Hello World from Future #{0}".format(value) if __name__ == "__main__": returnValues = list(futures.map(helloWorld, range(16))) print("\n".join(returnValues)) scoop-0.7.1/examples/map_reduce.py000066400000000000000000000031561240127670500171470ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ This is an example usage of a mapReduce function using SCOOP. """ import operator import time from scoop import futures def manipulateData(inData, chose=None): # Simulate a 10ms workload on every tasks time.sleep(0.01) return sum(inData) if __name__ == '__main__': scoopTime = time.time() res = futures.mapReduce( manipulateData, operator.add, list([a] * a for a in range(1000)), ) scoopTime = time.time() - scoopTime print("Executed parallely in: {0:.3f}s with result: {1}".format( scoopTime, res ) ) serialTime = time.time() res = sum( map( manipulateData, list([a] * a for a in range(1000)) ) ) serialTime = time.time() - serialTime print("Executed serially in: {0:.3f}s with result: {1}".format( serialTime, res ) ) scoop-0.7.1/examples/map_scan.py000066400000000000000000000032071240127670500166210ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ This is an example usage of a mapScan function using SCOOP. """ from scoop import futures import operator from itertools import accumulate import time def manipulateData(inData): # Simulate a 10ms workload on every tasks time.sleep(0.01) return sum(inData) if __name__ == '__main__': scoopTime = time.time() res = futures.mapScan( manipulateData, operator.add, list([a] * a for a in range(10)) ) scoopTime = time.time() - scoopTime print("Executed parallely in: {0:.3f}s with result: {1}".format( scoopTime, res ) ) serialTime = time.time() res = list(accumulate(map(manipulateData, list([a] * a for a in range(10))))) serialTime = time.time() - serialTime print("Executed serially in: {0:.3f}s with result: {1}".format( serialTime, res ) )scoop-0.7.1/examples/object.py000066400000000000000000000026201240127670500163040ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Example of dynamic parallel object manipulation. """ from scoop import futures class myClass(object): """An object with an instance variable.""" def __init__(self): self.myVar = 5 def modifyClass(myInstance): """Function modifying an instance variable.""" myInstance.myVar += 1 return myInstance def main(): # Create object instances myInstances = [myClass() for _ in range(20)] # Modify them parallely myAnswers = list(futures.map(modifyClass, myInstances)) # Each result is a new object with the modifications applied print(myAnswers) print([a.myVar for a in myAnswers]) if __name__ == "__main__": main() scoop-0.7.1/examples/pi_calc.py000066400000000000000000000031321240127670500164270ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Calculation of Pi using a Monte Carlo method. """ from math import hypot from random import random from scoop import futures from time import time # A range is used in this function for python3. If you are using python2, a # xrange might be more efficient. def test(tries): return sum(hypot(random(), random()) < 1 for _ in range(tries)) # Calculates pi with a Monte-Carlo method. This function calls the function # test "n" times with an argument of "t". Scoop dispatches these # functions interactively accross the available ressources. def calcPi(workers, tries): bt = time() expr = futures.map(test, [tries] * workers) piValue = 4. * sum(expr) / float(workers * tries) totalTime = time() - bt print("pi = " + str(piValue)) print("total time: " + str(totalTime)) return piValue if __name__ == "__main__": dataPi = calcPi(3000, 5000) scoop-0.7.1/examples/pi_calc_doc.py000066400000000000000000000023711240127670500172600ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Calculation of Pi using a Monte Carlo method. This version is used in the documentation and should be kept as clean as possible for study purposes. """ from math import hypot from random import random from scoop import futures def test(tries): return sum(hypot(random(), random()) < 1 for _ in range(tries)) def calcPi(nbFutures, tries): expr = futures.map(test, [tries] * nbFutures) return 4. * sum(expr) / float(nbFutures * tries) if __name__ == "__main__": print("pi = {}".format(calcPi(3000, 5000))) scoop-0.7.1/examples/recurse.py000066400000000000000000000023531240127670500165110ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ A very simple example of recursive nested tasks. Each task maps 2 others tasks, each of these 2 tasks maps 2 others, etc., up to RECURSIVITY_DEPTH. """ from scoop import futures RECURSIVITY_DEPTH = 12 def recursiveFunc(level): if level == 0: return 1 else: args = [level-1] * 2 s = sum(futures.map(recursiveFunc, args)) return s if __name__ == "__main__": result = recursiveFunc(RECURSIVITY_DEPTH) print("2^{RECURSIVITY_DEPTH} = {result}".format(**locals())) scoop-0.7.1/examples/rssDoc.py000066400000000000000000000045471240127670500163050ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Computing a residual sum of squares using SCOOP. """ from __future__ import print_function import random import operator import time from scoop import futures # The data is generated on every workers for the sake of this example. # This data could come from a file located on a shared hard drive or else. random.seed(31415926) leftSignal = [random.randint(-100, 100) for _ in range(200000)] rightSignal = [random.randint(-100, 100) for _ in range(200000)] # Set the size of each worker batch PARALLEL_SIZE = 25000 # Set the parallel function that will compute the Residual Sum of Squares # The index represent the element def RSS(index): # Get the data interval to compute on a given Future data = zip(leftSignal[index:index+PARALLEL_SIZE], rightSignal[index:index+PARALLEL_SIZE]) return sum(abs(y - x)**2 for y, x in data) if __name__ == "__main__": # Parallel with reduction call # Take a beginning timestamp ts = time.time() # Generate indexes to pass to futures indexes = range(0, len(leftSignal), PARALLEL_SIZE, ) # Execute the RSS computation parallely presult = futures.mapReduce(RSS, operator.add, indexes, ) ptime = time.time() - ts print("mapReduce result obtained in {0:03f}s".format(ptime)) # Serial ts = time.time() sresult = sum(abs(a - b)**2 for a, b in zip(leftSignal, rightSignal)) stime = time.time() - ts print("Serial result obtained in {0:03f}s".format(stime)) assert presult == sresult scoop-0.7.1/examples/shared_example.py000066400000000000000000000034511240127670500200220ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Example of shared constants use. This syntetic example only showcase the shared module API. """ from scoop import futures, shared # Import SCOOP to get access to its constants such as scoop.worker import scoop def myFunc(parameter): """This function will be executed on the remote host even if it was not available at launch.""" print('Hello World from {0}!'.format(scoop.worker)) # It is possible to get a constant anywhere print(shared.getConst('myVar')[2]) # Parameters are handled as usual return parameter + 1 if __name__ == "__main__": # Populate the shared constants shared.setConst(myVar={ 1: 'First element', 2: 'Second element', 3: 'Third element', }) shared.setConst(secondVar="Hello World!") shared.setConst(myFunc=myFunc) # Use the previously defined shared function print(list(futures.map(myFunc, range(10)))) # Un-commenting the following line will give a TypeError # since re-defining a constant is not allowed. #shared.setConst(myVar="Variable re-assignation") scoop-0.7.1/examples/shared_example_doc.py000066400000000000000000000044751240127670500206560ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Example of shared constants use. This is a synthetic partition and evaluation example that should only be analysed for its shared module API. """ from itertools import product import string from scoop import futures, shared # Create the hash to brute force HASH_TO_FIND = hash("SCO") def generateHashes(iterator): """Compute hashes of given iterator elements""" for combination in iterator: # Stop as soon as a worker finds the solution if shared.getConst('Done', timeout=0): return False # Compute the current combination hash currentString = "".join(combination).strip() if hash(currentString) == HASH_TO_FIND: # Share to every other worker that the solution has been found shared.setConst(Done=True) return currentString # Report that computing has not ended return False if __name__ == "__main__": # Generate possible characters possibleCharacters = [] possibleCharacters.extend(list(string.ascii_uppercase)) possibleCharacters.extend(' ') # Generate the solution space. stringIterator = product(possibleCharacters, repeat=3) # Partition the solution space into iterators # Keep in mind that it evaluates the whole solution space # making it pretty memory inefficient. SplittedIterator = [stringIterator for _ in range(1000)] # Parallelize the solution space evaluation results = futures.map(generateHashes, SplittedIterator) # Loop until a solution is found for result in results: if result: break print(result) scoop-0.7.1/examples/sorting.py000066400000000000000000000044571240127670500165350ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """Parallel merge sorting example using the futures""" import sys import time import random from scoop import futures, shared from itertools import repeat def merge(left, right): result = [] i, j = 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result += left[i:] result += right[j:] return result def mergesort(lst, current_depth=0, parallel_depth=0): if len(lst) <= 1: return lst middle = int(len(lst) / 2) if current_depth < parallel_depth: results = list( futures.map( mergesort, [ lst[:middle], lst[middle:], ], repeat(current_depth+1), repeat(parallel_depth), ) ) else: results = [] results.append(mergesort(lst[:middle])) results.append(mergesort(lst[middle:])) return merge(*results) if __name__ == "__main__": the_list = [random.randint(-sys.maxsize - 1, sys.maxsize) for r in range(10000)] shared.setConst(the_list=the_list) ts = time.time() parallel_result = mergesort(the_list, parallel_depth=1) pts = time.time() - ts ts = time.time() serial_result = mergesort(the_list) sts = time.time() - ts print("Parallel time: {0:.5f}s".format(pts)) print("Serial time: {0:.5f}s".format(sts)) assert serial_result == parallel_result scoop-0.7.1/examples/sum_multiples.py000066400000000000000000000025171240127670500177450ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ Sums the mutliples of 3 and 5 below 1000000 """ from time import time from scoop import futures from operator import add def multiples(n): return set(range(0, 1000000, n)) if __name__ == '__main__': bt = time() serial_result = sum(set.union(*map(multiples, [3, 5]))) serial_time = time() - bt bt = time() parallel_result = sum(futures.mapReduce(multiples, set.union, [3, 5])) parallel_reduce_time = time() - bt assert serial_result == parallel_result print("Serial time: {0:.4f} s\nParallel time: {1:.4f} s" "".format(serial_time, parallel_reduce_time) ) scoop-0.7.1/examples/testmut.py000066400000000000000000000021251240127670500165430ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ This is an example usage of a map function using SCOOP. """ from __future__ import print_function from scoop import futures def helloWorld(value): value = (value, lambda x: x) return "Hello World from Future #{0}".format(value) if __name__ == "__main__": returnValues = list(futures.map(helloWorld, range(16))) print("\n".join(returnValues)) scoop-0.7.1/examples/tree/000077500000000000000000000000001240127670500154235ustar00rootroot00000000000000scoop-0.7.1/examples/tree/Tree.py000066400000000000000000000102721240127670500166760ustar00rootroot00000000000000from __future__ import print_function import random import math import time import argparse try: import cPickle as pickle except ImportError: import pickle try: import pydot except ImportError: pydot = None GlobalTree = None mapfunc = None maxHeight = 0 maxDepth = 0 if pydot: g = pydot.Dot() h = 0 class Tree(): def __init__(self, height, minChildren, maxChildren, a, intMean, floatMean): global maxHeight, maxDepth, g, h import numpy.random self.intRange = int(numpy.random.weibull(a)*intMean) self.floatRange = int(numpy.random.weibull(a)*floatMean) # Record effective height of tree if maxHeight == 0: maxHeight = height maxDepth = height elif maxDepth > height: maxDepth = height self.children = [] self.nodes = 1 self.leaves = 0 # Generate a new node in graph if pydot: self.node = pydot.Node( h ) g.add_node( self.node ) h += 1 if height == 0: self.leaves = 1 return # Generate children n = random.randint(minChildren, maxChildren) self.children = [] for i in range(n): self.children.append(Tree(height - 1, minChildren, maxChildren, a, intMean, floatMean)) # Keep statistics if len(self.children) == 0: self.leaves = 1 for child in self.children: self.nodes += child.nodes self.leaves += child.leaves if pydot: g.add_edge( pydot.Edge( self.node, child.node ) ) del child.node self.height = maxHeight - maxDepth if pydot and height == maxHeight: del self.node def __str__(self): if pydot: g.write_png("graph.png") return ("height : {0}\n" "leaves : {1}\n" "nodes : {2}").format(self.height, self.leaves, self.nodes) def intCalc(self): x = 1 for i in range(self.intRange): x = (x + x * i) % 2**32 def floatCalc(self): x = 1.1 for i in range(self.floatRange): x = (x + x * i) % 2**32 def getTree(addresses): t = GlobalTree for index in addresses: t = t.children[index] return t def executeTree(address=[]): """This function executes a tree. To limit the size of the arguments passed to the function, the tree must be loaded in memory in every worker. To do this, simply call "Tree = importTree(filename)" before using the startup method of the parralisation library you are using""" global nodeDone # Get tree subsection localTree = getTree(address) # Execute tasks localTree.intCalc() localTree.floatCalc() # Select next nodes to be executed nextAddresses = [address + [i] for i in range(len(localTree.children))] if len(localTree.children) == 0: return 1 # Execute the children res = sum(mapfunc(executeTree, nextAddresses)) assert res == localTree.leaves, ( "Test failed: res = {0}, leaves = {1}").format(res, localTree.leaves) return res def exportTree(tree, filename): f = open(filename, 'wb') pickle.dump(tree, f) f.close() def importTree(filename): global GlobalTree f = open(filename, 'rb') GlobalTree = pickle.load(f) f.close() def registerMap(newMap): global mapfunc mapfunc = newMap def calibrate(meanTime): x = random.randint(1, 9) bt = time.time() for i in range(1000000): x = (x + x * i) % 2**32 total = time.time() - bt intValue = meanTime * 1000000 / total / 2 x = 1.1 * random.randint(1, 9) bt = time.time() for i in range(1000000): x = (x + x * i) % 2**32 total = time.time() - bt floatValue = meanTime * 1000000 / total / 2 print(intValue, floatValue) return intValue, floatValuescoop-0.7.1/examples/tree/TreeGen.py000066400000000000000000000033221240127670500173260ustar00rootroot00000000000000import argparse import Tree if __name__=="__main__": parser = argparse.ArgumentParser(description="Creates a random tree " "structure") parser.add_argument('--height', help='The height of the tree.', type=int, default=5) parser.add_argument('--minChildren', '-m', help="The minimum number of children by nodes", type=int, default=0) parser.add_argument('--maxChildren', '-M', help="The maximum number of children by nodes", type=int, default=8) parser.add_argument('--alpha', '-a', help="The 'a' parameter of the Weibull distribution " "used to create the task length.", type=float, default=1) parser.add_argument('--scale', '-s', help="The mean time of each task.", type = float, default=1) parser.add_argument('--filename', '-f', help="The filename to save the tree.", default="tree.txt") args = parser.parse_args() if args.minChildren > args.maxChildren: args.maxChildren = args.minChildren + 1 tree = Tree.Tree(args.height, args.minChildren, args.maxChildren, args.alpha, *Tree.calibrate(args.scale)) Tree.exportTree(tree, args.filename) print("Generated :\n{0}".format(tree)) scoop-0.7.1/examples/tree/dtm-tree.py000066400000000000000000000005461240127670500175230ustar00rootroot00000000000000# -*- coding: utf-8 -*- from Tree import * import sys import time from deap import dtm def main(): return executeTree() importTree(sys.argv[1] if len(sys.argv) > 1 else "tree.txt") registerMap(dtm.map) if __name__=="__main__": bt = time.time() dtm.start(main) totalTime = time.time() - bt print("total time : {}".format(totalTime))scoop-0.7.1/examples/tree/scoop_tree.py000066400000000000000000000005731240127670500201440ustar00rootroot00000000000000# -*- coding: utf-8 -*- from Tree import * import sys import time from scoop import futures, SIZE def main(): return executeTree() importTree(sys.argv[1] if len(sys.argv) > 1 else "tree.txt") registerMap(futures.map) if __name__=="__main__": bt = time.time() main() totalTime = time.time() - bt print("total time : {}\ncores : {}".format(totalTime, SIZE)) scoop-0.7.1/examples/tree/serial-tree.py000066400000000000000000000005051240127670500202110ustar00rootroot00000000000000# -*- coding: utf-8 -*- from Tree import * import sys import time def main(): executeTree() importTree(sys.argv[1] if len(sys.argv) > 1 else "tree.txt") registerMap(map) if __name__=="__main__": bt = time.time() main() totalTime = time.time() - bt print("Serial total time : {}".format(totalTime))scoop-0.7.1/examples/tree_traversal.py000066400000000000000000000104141240127670500200600ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """ This example shows a way to parallelize binary tree traversal. """ import random import sys from itertools import cycle from scoop import futures, shared def maxTreeDepthDivide(rootValue, currentDepth=0, parallelLevel=2): """Finds a tree node that represents rootValue and computes the max depth of this tree branch. This function will emit new futures until currentDepth=parallelLevel""" thisRoot = shared.getConst('myTree').search(rootValue) if currentDepth >= parallelLevel: return thisRoot.maxDepth(currentDepth) else: # Base case if not any([thisRoot.left, thisRoot.right]): return currentDepth if not all([thisRoot.left, thisRoot.right]): return thisRoot.maxDepth(currentDepth) # Parallel recursion return max( futures.map( maxTreeDepthDivide, [ thisRoot.left.payload, thisRoot.right.payload, ], cycle([currentDepth + 1]), cycle([parallelLevel]), ) ) class BinaryTreeNode(object): """A simple binary tree.""" def __init__(self, payload=None, left=None, right=None): self.payload = payload self.left = left self.right = right def insert(self, value): """Insert a value in the tree""" if not self.payload or value == self.payload: self.payload = value else: if value <= self.payload: if self.left: self.left.insert(value) else: self.left = BinaryTreeNode(value) else: if self.right: self.right.insert(value) else: self.right = BinaryTreeNode(value) def maxDepth(self, currentDepth=0): """Compute the depth of the longest branch of the tree""" if not any((self.left, self.right)): return currentDepth result = 0 for child in (self.left, self.right): if child: result = max(result, child.maxDepth(currentDepth + 1)) return result def search(self, value): """Find an element in the tree""" if self.payload == value: return self else: if value <= self.payload: if self.left: return self.left.search(value) else: if self.right: return self.right.search(value) return None if __name__ == '__main__': import time print("Beginning Tree generation.") # Generate the same tree on every workers. random.seed(314159265) exampleTree = BinaryTreeNode(0) for _ in range(128000): exampleTree.insert(random.randint(-sys.maxsize - 1, sys.maxsize)) shared.setConst(myTree=exampleTree) print("Tree generation done.") # Splits the tree in two and process the left and right branches parallely ts = time.time() presult = max( futures.map( maxTreeDepthDivide, [exampleTree.payload], parallelLevel=2, ) ) pts = time.time() - ts # Serial computation of tree depth ts = time.time() sresult = exampleTree.maxDepth() sts = time.time() - ts print("Parallel result: {0}".format(presult)) print("Serial result: {0}".format(sresult)) print("Parallel time: {0:.5f}s".format(pts)) print("Serial time: {0:.5f}s".format(sts)) assert presult == sresult scoop-0.7.1/examples/url_fetch.py000066400000000000000000000051671240127670500170220ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """This examples imports a list of 100 web sites and returns the sizes in bytes of every web site. It compares the speed to do this task of the regular python map, futures.map and the as_completed function.""" import urllib.request import urllib.error from scoop import futures import socket import time def getSize(string): """ This functions opens a web sites and then calculate the total size of the page in bytes. This is for the sake of the example. Do not use this technique in real code as it is not a very bright way to do this.""" try: # We open the web page with urllib.request.urlopen(string, None, 1) as f: return sum(len(line) for line in f) except (urllib.error.URLError, socket.timeout) as e: return 0 if __name__ == "__main__": # The pageurl variable contains a link to a list of web sites. It is # commented for security's sake. #pageurl = "http://httparchive.org/lists/Fortune%20500.txt" pageurl = "http://www.example.com" with urllib.request.urlopen(pageurl) as pagelist: pages = [page.decode() for page in pagelist][:30] # This will apply the getSize function serially on every item # of the pages list. for res in map(getSize, pages): time.sleep(0.1) # Work on the data ... print(res) # This will apply the getSize function on every item of the pages list # in parallel. The results will be treated in the same order as the # pages. for res in futures.map(getSize, pages): time.sleep(0.1) # Work on the data ... print(res) # This will apply the getSize function on every item of the pages list # in parallel. The results will be treated as they are produced. fut = [futures.submit(getSize, page) for page in pages] for f in futures.as_completed(fut): time.sleep(0.1) # Work on the data print(f.result()) scoop-0.7.1/examples/url_fetch_doc.py000066400000000000000000000042461240127670500176440ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """This examples imports a list of 100 web sites and returns the sizes in bytes of every web site. It compares the speed to do this task of the regular python map, futures.map and the as_completed function.""" import urllib.request import urllib.error from scoop import futures import socket import time def getSize(string): """ This functions opens a web sites and then calculate the total size of the page in bytes. This is for the sake of the example. Do not use this technique in real code as it is not a very bright way to do this.""" try: # We open the web page with urllib.request.urlopen(string, None, 1) as f: return sum(len(line) for line in f) except (urllib.error.URLError, socket.timeout) as e: return 0 if __name__ == "__main__": # The pageurl variable contains a link to a list of web sites. It is # commented for security's sake. pageurl = "http://httparchive.org/lists/Fortune%20500.txt" #pageurl = "http://www.example.com" with urllib.request.urlopen(pageurl) as pagelist: pages = [page.decode() for page in pagelist][:30] # This will apply the getSize function on every item of the pages list # in parallel. The results will be treated as they are produced. fut = [futures.submit(getSize, page) for page in pages] for f in futures.as_completed(fut): time.sleep(0.1) # Work on the data print(f.result()) scoop-0.7.1/scoop.egg-info/000077500000000000000000000000001240127670500154635ustar00rootroot00000000000000scoop-0.7.1/scoop.egg-info/PKG-INFO000066400000000000000000000021661240127670500165650ustar00rootroot00000000000000Metadata-Version: 1.1 Name: scoop Version: 0.7.1.release Summary: Scalable COncurrent Operations in Python Home-page: http://scoop.googlecode.com Author: SCOOP Development Team Author-email: scoop-users@googlegroups.com License: LGPL Download-URL: http://code.google.com/p/scoop/downloads/list Description: SCOOP (Scalable COncurrent Operations in Python) is a distributed task module allowing concurrent parallel programming on various environments, from heterogeneous grids to supercomputers. See https://scoop.googlecode.com/ for documentation, informations, bug reporting and more. Keywords: distributed algorithms,parallel programming,Concurrency,Cluster programming,greenlet,zmq Platform: any Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Classifier: Topic :: Software Development scoop-0.7.1/scoop.egg-info/SOURCES.txt000066400000000000000000000046111240127670500173510ustar00rootroot00000000000000CHANGELOG.txt LICENSE.txt MANIFEST.in README.txt setup.py doc/Makefile doc/api.rst doc/blu.diff doc/conf.py doc/contributing.rst doc/examples.rst doc/index.rst doc/install.rst doc/make.bat doc/usage.rst doc/_static/copybutton.js doc/_static/logo.png doc/_static/sidebar.js doc/_template/globaltoc.html doc/_template/indexcontent.html doc/_template/indexsidebar.html doc/_template/page.html doc/_themes/pydoctheme/theme.conf doc/_themes/pydoctheme/static/pydoctheme.css doc/images/architecture.png doc/images/introductory_tree.png doc/images/logo.png doc/images/monteCarloPiExample.gif doc/images/reduction.png examples/callback.py examples/conditional_execution.py examples/conditional_import.py examples/deap_ga_evosn.py examples/deap_ga_onemax.py examples/exceptExample.py examples/full_tree.py examples/grtest.py examples/image_resize.py examples/interactive_shell.py examples/lambda.py examples/map_as_completed.py examples/map_doc.py examples/map_reduce.py examples/map_scan.py examples/object.py examples/pi_calc.py examples/pi_calc_doc.py examples/recurse.py examples/rssDoc.py examples/shared_example.py examples/shared_example_doc.py examples/sorting.py examples/sum_multiples.py examples/testmut.py examples/tree_traversal.py examples/url_fetch.py examples/url_fetch_doc.py examples/dependency/__init__.py examples/dependency/sortingnetwork.py examples/tree/Tree.py examples/tree/TreeGen.py examples/tree/dtm-tree.py examples/tree/scoop_tree.py examples/tree/serial-tree.py scoop/__init__.py scoop/__main__.py scoop/_control.py scoop/_debug.py scoop/_types.py scoop/encapsulation.py scoop/fallbacks.py scoop/futures.py scoop/launcher.py scoop/shared.py scoop/utils.py scoop.egg-info/PKG-INFO scoop.egg-info/SOURCES.txt scoop.egg-info/dependency_links.txt scoop.egg-info/requires.txt scoop.egg-info/top_level.txt scoop/_comm/__init__.py scoop/_comm/scoopexceptions.py scoop/_comm/scooptcp.py scoop/_comm/scoopzmq.py scoop/backports/__init__.py scoop/backports/dictconfig.py scoop/backports/newCollections.py scoop/backports/runpy.py scoop/bootstrap/__init__.py scoop/bootstrap/__main__.py scoop/broker/__init__.py scoop/broker/__main__.py scoop/broker/brokertcp.py scoop/broker/brokerzmq.py scoop/broker/structs.py scoop/discovery/__init__.py scoop/discovery/minusconf.py scoop/launch/__init__.py scoop/launch/brokerLaunch.py scoop/launch/workerLaunch.py test/tests.py test/tests_parser.py test/tests_stat.py test/tests_stopwatch.pyscoop-0.7.1/scoop.egg-info/dependency_links.txt000066400000000000000000000000011240127670500215310ustar00rootroot00000000000000 scoop-0.7.1/scoop.egg-info/requires.txt000066400000000000000000000001011240127670500200530ustar00rootroot00000000000000greenlet>=0.3.4 pyzmq>=13.1.0 argparse>=1.1 [nice] psutil>=0.6.1scoop-0.7.1/scoop.egg-info/top_level.txt000066400000000000000000000000061240127670500202110ustar00rootroot00000000000000scoop scoop-0.7.1/scoop/000077500000000000000000000000001240127670500137715ustar00rootroot00000000000000scoop-0.7.1/scoop/__init__.py000066400000000000000000000022301240127670500160770ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # __author__ = ("Marc Parizeau", "Olivier Gagnon", "Marc-Andre Gardner", "Yannick Hold-Geoffroy", "Felix-Antoine Fortin", "Francois-Michel de Rainville") __version__ = "0.7.1" __revision__ = "release" import logging # In case SCOOP was not initialized correctly CONFIGURATION = {} DEBUG = False IS_RUNNING = False logger = logging.getLogger() SHUTDOWN_REQUESTED = False TIME_BETWEEN_PARTIALDEBUG = 30 scoop-0.7.1/scoop/__main__.py000066400000000000000000000014771240127670500160740ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from .launcher import main if __name__ == "__main__": main()scoop-0.7.1/scoop/_comm/000077500000000000000000000000001240127670500150635ustar00rootroot00000000000000scoop-0.7.1/scoop/_comm/__init__.py000066400000000000000000000017441240127670500172020ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from .scoopexceptions import Shutdown import scoop if scoop.CONFIGURATION.get('backend', 'ZMQ') == 'ZMQ': from .scoopzmq import ZMQCommunicator as Communicator else: from .scooptcp import TCPCommunicator as Communicatorscoop-0.7.1/scoop/_comm/scoopexceptions.py000066400000000000000000000017641240127670500206720ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """This file contains the generic communication exceptions that can be raised by SCOOP""" class Shutdown(Exception): pass class ReferenceBroken(Exception): """An object could not be unpickled (dereferenced) on a worker""" passscoop-0.7.1/scoop/_comm/scooptcp.py000066400000000000000000000354451240127670500173020ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import time import sys import random import socket import copy import logging import asyncore import array import threading try: import cPickle as pickle except ImportError: import pickle import scoop from .. import shared, encapsulation, utils from ..shared import SharedElementEncapsulation from .scoopexceptions import Shutdown, ReferenceBroken try: _chr = unichr except NameError: scoop.logger.warn('NameError on scooptcp.') _chr = chr def serialize(*data): #sendData = ''.join(data) #sendData = _chr(len(sendData)) + sendData #return array.array('b', sendData).tobytes() return pickle.dumps(data) def deserialize(data): #return array.frombytes(data) return pickle.loads(data) class EchoHandler(asyncore.dispatcher_with_send): def handle_read(self): data = self.recv(8192) if data: self.send(data) class DirectSocketServer(asyncore.dispatcher): def __init__(self, host, port): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind((host, port)) self.listen(1) def handle_accept(self): pair = self.accept() if pair is not None: sock, addr = pair print('Incoming connection from %s' % repr(addr)) handler = EchoHandler(sock) class TCPCommunicator(object): """This class encapsulates the communication features toward the broker.""" def __init__(self): # TODO number of broker self.number_of_broker = float('inf') self.broker_set = set() # Get the current address of the interface facing the broker s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((scoop.BROKER.externalHostname, scoop.BROKER.task_port)) external_addr = s.getsockname()[0] s.close() if external_addr in utils.loopbackReferences: external_addr = scoop.BROKER.externalHostname # Create an inter-worker socket self.direct_socket_peers = [] self.direct_socket = DirectSocketServer('', 0) self.direct_socket_port = self.direct_socket.getsockname()[1] scoop.worker = "{addr}:{port}".format( addr=external_addr, port=self.direct_socket_port, ).encode() # Update the logger to display our name try: scoop.logger.handlers[0].setFormatter( logging.Formatter( "[%(asctime)-15s] %(module)-9s ({0}) %(levelname)-7s " "%(message)s".format(scoop.worker) ) ) except IndexError: scoop.logger.debug( "Could not set worker name into logger ({0})".format( scoop.worker ) ) # socket for the futures, replies and request self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # socket for the shutdown signal #self.infoSocket = CreateZMQSocket(zmq.SUB) # Set poller #self.poller = zmq.Poller() #self.poller.register(self.socket, zmq.POLLIN) #self.poller.register(self.direct_socket, zmq.POLLIN) #self.poller.register(self.infoSocket, zmq.POLLIN) self._addBroker(scoop.BROKER) # Send an INIT to get all previously set variables and share # current configuration to broker self.socket.send(serialize( b"INIT", pickle.dumps(scoop.CONFIGURATION), )) scoop.CONFIGURATION.update(pickle.loads(self.socket.recv())) inboundVariables = pickle.loads(self.socket.recv()) shared.elements = dict([ (pickle.loads(key), dict([(pickle.loads(varName), pickle.loads(varValue)) for varName, varValue in value.items() ])) for key, value in inboundVariables.items() ]) for broker in pickle.loads(self.socket.recv()): # Skip already connected brokers if broker in self.broker_set: continue self._addBroker(broker) self.OPEN = True self.loop_thread = threading.Thread(target=asyncore.loop, name="Asyncore Loop") self.loop_thread.daemon = True self.loop_thread.start() def addPeer(self, peer): if peer not in self.direct_socket_peers: self.direct_socket_peers.append(peer) new_peer = "tcp://{0}".format(peer.decode("utf-8")) self.direct_socket.connect(new_peer) def _addBroker(self, brokerEntry): # Add a broker to the socket and the infosocket. broker_address = "tcp://{hostname}:{port}".format( hostname=brokerEntry.hostname, port=brokerEntry.task_port, ) meta_address = "tcp://{hostname}:{port}".format( hostname=brokerEntry.hostname, port=brokerEntry.info_port, ) self.socket.connect(broker_address) self.infoSocket.connect(meta_address) self.infoSocket.setsockopt(zmq.SUBSCRIBE, b"") self.broker_set.add(brokerEntry) def _poll(self, timeout): self.pumpInfoSocket() return self.poller.poll(timeout) def _recv(self): # Prioritize answers over new tasks if self.direct_socket.poll(0): router_msg = self.direct_socket.recv_multipart() # Remove the sender address msg = router_msg[1:] + [router_msg[0]] else: msg = self.socket.recv_multipart() try: thisFuture = pickle.loads(msg[1]) except AttributeError as e: scoop.logger.error( "An instance could not find its base reference on a worker. " "Ensure that your objects have their definition available in " "the root scope of your program.\n{error}".format( error=e, ) ) raise ReferenceBroken(e) if msg[0] == b"TASK": # Try to connect directly to this worker to send the result # afterwards if Future is from a map. if thisFuture.sendResultBack: self.addPeer(thisFuture.id.worker) isCallable = callable(thisFuture.callable) isDone = thisFuture._ended() if not isCallable and not isDone: # TODO: Also check in root module globals for fully qualified name try: module_found = hasattr(sys.modules["__main__"], thisFuture.callable) except TypeError: module_found = False if module_found: thisFuture.callable = getattr(sys.modules["__main__"], thisFuture.callable) else: raise ReferenceBroken("This element could not be pickled: " "{0}.".format(thisFuture)) return thisFuture def pumpInfoSocket(self): while self.infoSocket.poll(0): msg = self.infoSocket.recv_multipart() if msg[0] == b"SHUTDOWN": if scoop.IS_ORIGIN is False: raise Shutdown("Shutdown received") if not scoop.SHUTDOWN_REQUESTED: scoop.logger.error( "A worker exited unexpectedly. Read the worker logs " "for more information. SCOOP pool will now shutdown." ) raise Shutdown("Unexpected shutdown received") elif msg[0] == b"VARIABLE": key = pickle.loads(msg[3]) varValue = pickle.loads(msg[2]) varName = pickle.loads(msg[1]) shared.elements.setdefault(key, {}).update({varName: varValue}) self.convertVariable(key, varName, varValue) elif msg[0] == b"BROKER_INFO": # TODO: find out what to do here ... if len(self.broker_set) == 0: # The first update self.broker_set.add(pickle.loads(msg[1])) if len(self.broker_set) < self.number_of_broker: brokers = pickle.loads(msg[2]) needed = self.number_of_broker - len(self.broker_set) try: new_brokers = random.sample(brokers, needed) except ValueError: new_brokers = brokers self.number_of_broker = len(self.broker_set) + len(new_brokers) scoop.logger.warning(("The number of brokers could not be set" " on worker {0}. A total of {1} worker(s)" " were set.".format(scoop.worker, self.number_of_broker))) for broker in new_brokers: broker_address = "tcp://" + broker.hostname + broker.task_port meta_address = "tcp://" + broker.hostname + broker.info_port self._addBroker(broker_address, meta_address) self.broker_set.update(new_brokers) def convertVariable(self, key, varName, varValue): """Puts the function in the globals() of the main module.""" if isinstance(varValue, encapsulation.FunctionEncapsulation): result = varValue.getFunction() # Update the global scope of the function to match the current module mainModule = sys.modules["__main__"] result.__name__ = varName result.__globals__.update(mainModule.__dict__) setattr(mainModule, varName, result) shared.elements[key].update({ varName: result, }) def recvFuture(self): while self._poll(0): received = self._recv() if received: yield received def sendFuture(self, future): """Send a Future to be executed remotely.""" try: if shared.getConst(hash(future.callable), timeout=0): # Enforce name reference passing if already shared future.callable = SharedElementEncapsulation(hash(future.callable)) self.socket.send_multipart([b"TASK", pickle.dumps(future, pickle.HIGHEST_PROTOCOL)]) except pickle.PicklingError as e: # If element not picklable, pickle its name # TODO: use its fully qualified name scoop.logger.warn("Pickling Error: {0}".format(e)) previousCallable = future.callable future.callable = hash(future.callable) self.socket.send_multipart([b"TASK", pickle.dumps(future, pickle.HIGHEST_PROTOCOL)]) future.callable = previousCallable def sendResult(self, future): """Send a terminated future back to its parent.""" future = copy.copy(future) # Remove the (now) extraneous elements from future class future.callable = future.args = future.kargs = future.greenlet = None if not future.sendResultBack: # Don't reply back the result if it isn't asked future.resultValue = None self._sendReply( future.id.worker, pickle.dumps( future, pickle.HIGHEST_PROTOCOL, ), ) def _sendReply(self, destination, *args): """Send a REPLY directly to its destination. If it doesn't work, launch it back to the broker.""" # Try to send the result directly to its parent self.addPeer(destination) try: self.direct_socket.send_multipart([ destination, b"REPLY", ] + list(args), flags=zmq.NOBLOCK) except zmq.error.ZMQError as e: # Fallback on Broker routing if no direct connection possible scoop.logger.debug( "{0}: Could not send result directly to peer {1}, routing through " "broker.".format(scoop.worker, destination) ) self.socket.send_multipart([ b"REPLY", ] + list(args) + [ destination, ]) def sendVariable(self, key, value): self.socket.send_multipart([b"VARIABLE", pickle.dumps(key), pickle.dumps(value, pickle.HIGHEST_PROTOCOL), pickle.dumps(scoop.worker, pickle.HIGHEST_PROTOCOL)]) def taskEnd(self, groupID, askResults=False): self.socket.send_multipart([ b"TASKEND", pickle.dumps( askResults, pickle.HIGHEST_PROTOCOL ), pickle.dumps( groupID, pickle.HIGHEST_PROTOCOL ), ]) def sendRequest(self): for _ in range(len(self.broker_set)): self.socket.send(b"REQUEST") def workerDown(self): self.socket.send(b"WORKERDOWN") def shutdown(self): """Sends a shutdown message to other workers.""" if self.OPEN: self.OPEN = False scoop.SHUTDOWN_REQUESTED = True self.socket.send(b"SHUTDOWN") self.socket.close() self.infoSocket.close() time.sleep(0.3) scoop-0.7.1/scoop/_comm/scoopzmq.py000066400000000000000000000356701240127670500173230ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import time import sys import random import socket import copy import logging try: import cPickle as pickle except ImportError: import pickle import zmq import scoop from .. import shared, encapsulation, utils from ..shared import SharedElementEncapsulation from .scoopexceptions import Shutdown, ReferenceBroken def CreateZMQSocket(sock_type): """Create a socket of the given sock_type and deactivate message dropping""" sock = ZMQCommunicator.context.socket(sock_type) sock.setsockopt(zmq.LINGER, 1000) # Remove message dropping sock.setsockopt(zmq.SNDHWM, 0) sock.setsockopt(zmq.RCVHWM, 0) # Don't accept unroutable messages if sock_type == zmq.ROUTER: sock.setsockopt(zmq.ROUTER_BEHAVIOR, 1) return sock class ZMQCommunicator(object): """This class encapsulates the communication features toward the broker.""" context = zmq.Context() def __init__(self): # TODO number of broker self.number_of_broker = float('inf') self.broker_set = set() # Get the current address of the interface facing the broker s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((scoop.BROKER.externalHostname, scoop.BROKER.task_port)) external_addr = s.getsockname()[0] s.close() if external_addr in utils.loopbackReferences: external_addr = scoop.BROKER.externalHostname # Create an inter-worker socket self.direct_socket_peers = [] self.direct_socket = CreateZMQSocket(zmq.ROUTER) # TODO: This doesn't seems to be respected in the ROUTER socket self.direct_socket.setsockopt(zmq.SNDTIMEO, 0) # Code stolen from pyzmq's bind_to_random_port() from sugar/socket.py for i in range(100): try: self.direct_socket_port = random.randrange(49152, 65536) # Set current worker inter-worker socket name to its addr:port scoop.worker = "{addr}:{port}".format( addr=external_addr, port=self.direct_socket_port, ).encode() self.direct_socket.setsockopt(zmq.IDENTITY, scoop.worker) self.direct_socket.bind("tcp://*:{0}".format( self.direct_socket_port, )) except: # Except on ZMQError with a check on EADDRINUSE should go here # but its definition is not consistent in pyzmq over multiple # versions pass else: break else: raise Exception("Could not create direct connection socket") # Update the logger to display our name try: scoop.logger.handlers[0].setFormatter( logging.Formatter( "[%(asctime)-15s] %(module)-9s ({0}) %(levelname)-7s " "%(message)s".format(scoop.worker) ) ) except IndexError: scoop.logger.debug( "Could not set worker name into logger ({0})".format( scoop.worker ) ) # socket for the futures, replies and request self.socket = CreateZMQSocket(zmq.DEALER) self.socket.setsockopt(zmq.IDENTITY, scoop.worker) # socket for the shutdown signal self.infoSocket = CreateZMQSocket(zmq.SUB) # Set poller self.poller = zmq.Poller() self.poller.register(self.socket, zmq.POLLIN) self.poller.register(self.direct_socket, zmq.POLLIN) self.poller.register(self.infoSocket, zmq.POLLIN) self._addBroker(scoop.BROKER) # Send an INIT to get all previously set variables and share # current configuration to broker self.socket.send_multipart([ b"INIT", pickle.dumps(scoop.CONFIGURATION) ]) scoop.CONFIGURATION.update(pickle.loads(self.socket.recv())) inboundVariables = pickle.loads(self.socket.recv()) shared.elements = dict([ (pickle.loads(key), dict([(pickle.loads(varName), pickle.loads(varValue)) for varName, varValue in value.items() ])) for key, value in inboundVariables.items() ]) for broker in pickle.loads(self.socket.recv()): # Skip already connected brokers if broker in self.broker_set: continue self._addBroker(broker) self.OPEN = True def addPeer(self, peer): if peer not in self.direct_socket_peers: self.direct_socket_peers.append(peer) new_peer = "tcp://{0}".format(peer.decode("utf-8")) self.direct_socket.connect(new_peer) def _addBroker(self, brokerEntry): # Add a broker to the socket and the infosocket. broker_address = "tcp://{hostname}:{port}".format( hostname=brokerEntry.hostname, port=brokerEntry.task_port, ) meta_address = "tcp://{hostname}:{port}".format( hostname=brokerEntry.hostname, port=brokerEntry.info_port, ) self.socket.connect(broker_address) self.infoSocket.connect(meta_address) self.infoSocket.setsockopt(zmq.SUBSCRIBE, b"") self.broker_set.add(brokerEntry) def _poll(self, timeout): self.pumpInfoSocket() return self.poller.poll(timeout) def _recv(self): # Prioritize answers over new tasks if self.direct_socket.poll(0): router_msg = self.direct_socket.recv_multipart() # Remove the sender address msg = router_msg[1:] + [router_msg[0]] else: msg = self.socket.recv_multipart() try: thisFuture = pickle.loads(msg[1]) except AttributeError as e: scoop.logger.error( "An instance could not find its base reference on a worker. " "Ensure that your objects have their definition available in " "the root scope of your program.\n{error}".format( error=e, ) ) raise ReferenceBroken(e) if msg[0] == b"TASK": # Try to connect directly to this worker to send the result # afterwards if Future is from a map. if thisFuture.sendResultBack: self.addPeer(thisFuture.id.worker) isCallable = callable(thisFuture.callable) isDone = thisFuture._ended() if not isCallable and not isDone: # TODO: Also check in root module globals for fully qualified name try: module_found = hasattr(sys.modules["__main__"], thisFuture.callable) except TypeError: module_found = False if module_found: thisFuture.callable = getattr(sys.modules["__main__"], thisFuture.callable) else: raise ReferenceBroken("This element could not be pickled: " "{0}.".format(thisFuture)) return thisFuture def pumpInfoSocket(self): while self.infoSocket.poll(0): msg = self.infoSocket.recv_multipart() if msg[0] == b"SHUTDOWN": if scoop.IS_ORIGIN is False: raise Shutdown("Shutdown received") if not scoop.SHUTDOWN_REQUESTED: scoop.logger.error( "A worker exited unexpectedly. Read the worker logs " "for more information. SCOOP pool will now shutdown." ) raise Shutdown("Unexpected shutdown received") elif msg[0] == b"VARIABLE": key = pickle.loads(msg[3]) varValue = pickle.loads(msg[2]) varName = pickle.loads(msg[1]) shared.elements.setdefault(key, {}).update({varName: varValue}) self.convertVariable(key, varName, varValue) elif msg[0] == b"BROKER_INFO": # TODO: find out what to do here ... if len(self.broker_set) == 0: # The first update self.broker_set.add(pickle.loads(msg[1])) if len(self.broker_set) < self.number_of_broker: brokers = pickle.loads(msg[2]) needed = self.number_of_broker - len(self.broker_set) try: new_brokers = random.sample(brokers, needed) except ValueError: new_brokers = brokers self.number_of_broker = len(self.broker_set) + len(new_brokers) scoop.logger.warning(("The number of brokers could not be set" " on worker {0}. A total of {1} worker(s)" " were set.".format(scoop.worker, self.number_of_broker))) for broker in new_brokers: broker_address = "tcp://" + broker.hostname + broker.task_port meta_address = "tcp://" + broker.hostname + broker.info_port self._addBroker(broker_address, meta_address) self.broker_set.update(new_brokers) def convertVariable(self, key, varName, varValue): """Puts the function in the globals() of the main module.""" if isinstance(varValue, encapsulation.FunctionEncapsulation): result = varValue.getFunction() # Update the global scope of the function to match the current module mainModule = sys.modules["__main__"] result.__name__ = varName result.__globals__.update(mainModule.__dict__) setattr(mainModule, varName, result) shared.elements[key].update({ varName: result, }) def recvFuture(self): while self._poll(0): received = self._recv() if received: yield received def sendFuture(self, future): """Send a Future to be executed remotely.""" try: if shared.getConst(hash(future.callable), timeout=0): # Enforce name reference passing if already shared future.callable = SharedElementEncapsulation(hash(future.callable)) self.socket.send_multipart([b"TASK", pickle.dumps(future, pickle.HIGHEST_PROTOCOL)]) except pickle.PicklingError as e: # If element not picklable, pickle its name # TODO: use its fully qualified name scoop.logger.warn("Pickling Error: {0}".format(e)) previousCallable = future.callable future.callable = hash(future.callable) self.socket.send_multipart([b"TASK", pickle.dumps(future, pickle.HIGHEST_PROTOCOL)]) future.callable = previousCallable def sendResult(self, future): """Send a terminated future back to its parent.""" future = copy.copy(future) # Remove the (now) extraneous elements from future class future.callable = future.args = future.kargs = future.greenlet = None if not future.sendResultBack: # Don't reply back the result if it isn't asked future.resultValue = None self._sendReply( future.id.worker, pickle.dumps( future, pickle.HIGHEST_PROTOCOL, ), ) def _sendReply(self, destination, *args): """Send a REPLY directly to its destination. If it doesn't work, launch it back to the broker.""" # Try to send the result directly to its parent self.addPeer(destination) try: self.direct_socket.send_multipart([ destination, b"REPLY", ] + list(args), flags=zmq.NOBLOCK) except zmq.error.ZMQError as e: # Fallback on Broker routing if no direct connection possible scoop.logger.debug( "{0}: Could not send result directly to peer {1}, routing through " "broker.".format(scoop.worker, destination) ) self.socket.send_multipart([ b"REPLY", ] + list(args) + [ destination, ]) def sendVariable(self, key, value): self.socket.send_multipart([b"VARIABLE", pickle.dumps(key), pickle.dumps(value, pickle.HIGHEST_PROTOCOL), pickle.dumps(scoop.worker, pickle.HIGHEST_PROTOCOL)]) def taskEnd(self, groupID, askResults=False): self.socket.send_multipart([ b"TASKEND", pickle.dumps( askResults, pickle.HIGHEST_PROTOCOL ), pickle.dumps( groupID, pickle.HIGHEST_PROTOCOL ), ]) def sendRequest(self): for _ in range(len(self.broker_set)): self.socket.send(b"REQUEST") def workerDown(self): self.socket.send(b"WORKERDOWN") def shutdown(self): """Sends a shutdown message to other workers.""" if self.OPEN: self.OPEN = False scoop.SHUTDOWN_REQUESTED = True self.socket.send(b"SHUTDOWN") self.socket.close() self.infoSocket.close() time.sleep(0.3) scoop-0.7.1/scoop/_control.py000066400000000000000000000210631240127670500161640ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from collections import deque, defaultdict import os import time import tempfile import sys import math from functools import partial import greenlet from ._types import Future, FutureId, FutureQueue, CallbackType import scoop # Backporting collection features if sys.version_info < (2, 7): from scoop.backports.newCollections import deque # Future currently running in greenlet current = None # Dictionary of existing futures futureDict = {} # Queue of futures pending execution execQueue = None # Execution Statistics class _stat(deque): def __init__(self, *args, **kargs): self._sum = 0 self._squared_sum = 0 super(_stat, self).__init__(*args, maxlen=10, **kargs) def appendleft(self, x): if len(self) >= self.maxlen: self._sum -= self[-1] self._squared_sum -= self[-1] ** 2 self._sum += x self._squared_sum += x ** 2 super(_stat, self).appendleft(x) def mean(self): ourSize = len(self) if ourSize > 3: return self._sum / ourSize return float("inf") def std(self): ourSize = len(self) if ourSize > 3: return math.sqrt(len(self) * self._squared_sum - self._sum ** 2) / len(self) return float("inf") def advertiseBrokerWorkerDown(exctype, value, traceback): """Hook advertizing the broker if an impromptu shutdown is occuring.""" if not scoop.SHUTDOWN_REQUESTED: execQueue.socket.shutdown() sys.__excepthook__(exctype, value, traceback) def init_debug(): """Initialise debug_stats and QueueLength (this is not a reset)""" global debug_stats global QueueLength if debug_stats is None: list_defaultdict = partial(defaultdict, list) debug_stats = defaultdict(list_defaultdict) QueueLength = [] def delFutureById(futureId, parentId): """Delete future on id basis""" try: del futureDict[futureId] except KeyError: pass try: toDel = [a for a in futureDict[parentId].children if a.id == futureId] for f in toDel: del futureDict[parentId].children[f] except KeyError: pass def delFuture(afuture): """Delete future afuture""" try: del futureDict[afuture.id] except KeyError: pass try: del futureDict[afuture.parentId].children[afuture] except KeyError: pass def runFuture(future): """Callable greenlet in charge of running tasks.""" global debug_stats global QueueLength if scoop.DEBUG: init_debug() # in case _control is imported before scoop.DEBUG was set debug_stats[future.id]['start_time'].append(time.time()) future.waitTime = future.stopWatch.get() future.stopWatch.reset() # Get callback Group ID and assign the broker-wide unique executor ID try: uniqueReference = [cb.groupID for cb in future.callback][0] except IndexError: uniqueReference = None future.executor = (scoop.worker, uniqueReference) try: future.resultValue = future.callable(*future.args, **future.kargs) except BaseException as err: import traceback scoop.logger.debug( "The following error occurend on a worker:\n{err}\n{tb}".format( err=err, tb=traceback.format_exc(), ) ) future.exceptionValue = err future.executionTime = future.stopWatch.get() future.isDone = True # Update the worker inner work statistics if future.executionTime != 0. and hasattr(future.callable, '__name__'): execStats[future.callable.__name__].appendleft(future.executionTime) # Set debugging informations if needed if scoop.DEBUG: t = time.time() debug_stats[future.id]['end_time'].append(t) debug_stats[future.id].update({ 'executionTime': future.executionTime, 'worker': scoop.worker, 'creationTime': future.creationTime, 'callable': str(future.callable.__name__) if hasattr(future.callable, '__name__') else 'No name', 'parent': future.parentId }) QueueLength.append((t, len(execQueue), execQueue.timelen(execQueue))) # Run callback (see http://www.python.org/dev/peps/pep-3148/#future-objects) future._execute_callbacks(CallbackType.universal) # Delete references to the future future._delete() return future def runController(callable_, *args, **kargs): """Callable greenlet implementing controller logic.""" global execQueue # initialize and run root future rootId = FutureId(-1, 0) # initialise queue if execQueue is None: execQueue = FutureQueue() sys.excepthook = advertiseBrokerWorkerDown # TODO: Make that a function # Wait until we received the main module if we are a headless slave headless = scoop.CONFIGURATION.get("headless", False) if not scoop.MAIN_MODULE: # If we're not the origin and still don't have our main_module, # wait for it and then import it as module __main___ main = scoop.shared.getConst('__MAIN_MODULE__', timeout=float('inf')) directory_name = tempfile.mkdtemp() os.chdir(directory_name) scoop.MAIN_MODULE = main.writeFile(directory_name) from .bootstrap.__main__ import Bootstrap as SCOOPBootstrap newModule = SCOOPBootstrap.setupEnvironment() sys.modules['__main__'] = newModule elif scoop.IS_ORIGIN and headless and scoop.MAIN_MODULE: # We're the origin, share our main_module scoop.shared.setConst( __MAIN_MODULE__=scoop.encapsulation.ExternalEncapsulation( scoop.MAIN_MODULE, ) ) # TODO: use modulefinder to share every local dependency of # main module # launch future if origin or try to pickup a future if slave worker if scoop.IS_ORIGIN: future = Future(rootId, callable_, *args, **kargs) else: future = execQueue.pop() future.greenlet = greenlet.greenlet(runFuture) future = future._switch(future) if scoop.DEBUG: lastDebugTs = time.time() while not scoop.IS_ORIGIN or future.parentId != rootId or not future._ended(): if scoop.DEBUG and time.time() - lastDebugTs < scoop.TIME_BETWEEN_PARTIALDEBUG: from scoop import _debug _debug.writeWorkerDebug( debug_stats, QueueLength, "debug/partial-{0}".format( round(time.time(), -1) ) ) # process future if future._ended(): # future is finished if future.id.worker != scoop.worker: # future is not local execQueue.sendResult(future) future = execQueue.pop() else: # future is local, parent is waiting if future.index is not None: parent = futureDict[future.parentId] if parent.exceptionValue is None: future = parent._switch(future) else: future = execQueue.pop() else: future = execQueue.pop() else: # future is in progress; run next future from pending execution queue. future = execQueue.pop() if not future._ended() and future.greenlet is None: # initialize if the future hasn't started future.greenlet = greenlet.greenlet(runFuture) future = future._switch(future) execQueue.shutdown() if future.exceptionValue: raise future.exceptionValue return future.resultValue execStats = defaultdict(_stat) debug_stats = None QueueLength = None if scoop.DEBUG: init_debug() scoop-0.7.1/scoop/_debug.py000066400000000000000000000026301240127670500155710ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import os try: import cPickle as pickle except ImportError: import pickle import scoop def getDebugIdentifier(): return scoop.worker.decode().replace(":", "_") def writeWorkerDebug(debugStats, queueLength, path="debug"): import os try: os.makedirs(path) except: pass statsFilename = os.path.join( path, "worker-{0}-STATS".format(getDebugIdentifier()) ) lengthFilename = os.path.join( path, "worker-{0}-QUEUE".format(getDebugIdentifier()) ) with open(statsFilename, 'wb') as f: pickle.dump(debugStats, f) with open(lengthFilename, 'wb') as f: pickle.dump(queueLength, f) scoop-0.7.1/scoop/_types.py000066400000000000000000000361641240127670500156600ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from collections import namedtuple, deque import itertools import time import sys import greenlet import scoop from scoop._comm import Communicator, Shutdown # Backporting collection features if sys.version_info < (2, 7): from scoop.backports.newCollections import Counter else: from collections import Counter class CallbackType: """Type of groups enumeration.""" standard = "standard" universal = "universal" # This class encapsulates a stopwatch that returns elapse time in seconds. class StopWatch(object): # initialize stopwatch. def __init__(self): self.totalTime = 0 self.startTime = time.time() self.halted = False # return elapse time. def get(self): if self.halted: return self.totalTime else: return self.totalTime + time.time() - self.startTime # halt stopWatch. def halt(self): self.halted = True self.totalTime += time.time() - self.startTime # resume stopwatch. def resume(self): self.halted = False self.startTime = time.time() # set stopwatch to zero. def reset(self): self.__init__() class CancelledError(Exception): """The Future was cancelled.""" pass class TimeoutError(Exception): """The operation exceeded the given deadline.""" pass FutureId = namedtuple('FutureId', ['worker', 'rank']) callbackEntry = namedtuple('callbackEntry', ['func', 'callbackType', 'groupID']) class Future(object): """This class encapsulates an independent future that can be executed in parallel. A future can spawn other parallel futures which themselves can recursively spawn other futures.""" rank = itertools.count() def __init__(self, parentId, callable, *args, **kargs): """Initialize a new Future.""" self.id = FutureId(scoop.worker, next(Future.rank)) self.executor = None # id of executor self.parentId = parentId # id of parent self.index = None # parent index for result self.callable = callable # callable object self.args = args # arguments of callable self.kargs = kargs # key arguments of callable self.creationTime = time.ctime() # future creation time self.stopWatch = StopWatch() # stop watch for measuring time self.greenlet = None # cooperative thread for running future self.resultValue = None # future result self.exceptionValue = None # exception raised by callable self.sendResultBack = True self.isDone = False self.callback = [] # set callback self.children = {} # set children list of the callable (dict for speedier delete) # insert future into global dictionary scoop._control.futureDict[self.id] = self def __lt__(self, other): """Order futures by creation time.""" return self.creationTime < other.creationTime def __repr__(self): """Convert future to string.""" try: return "{0}:{1}{2}={3}".format(self.id, self.callable.__name__, self.args, self.resultValue) except AttributeError: return "{0}:{1}{2}={3}".format(self.id, "partial", self.args, self.resultValue) def _switch(self, future): """Switch greenlet.""" scoop._control.current = self assert self.greenlet is not None, ("No greenlet to switch to:" "\n{0}".format(self.__dict__)) return self.greenlet.switch(future) def cancel(self): """If the call is currently being executed or sent for remote execution, then it cannot be cancelled and the method will return False, otherwise the call will be cancelled and the method will return True.""" if self in scoop._control.execQueue.movable: self.exceptionValue = CancelledError() scoop._control.futureDict[self.id]._delete() scoop._control.execQueue.remove(self) return True return False def cancelled(self): """Returns True if the call was successfully cancelled, False otherwise.""" return isinstance(self.exceptionValue, CancelledError) def running(self): """Returns True if the call is currently being executed and cannot be cancelled.""" return not self._ended() and self not in scoop._control.execQueue def done(self): """Returns True if the call was successfully cancelled or finished running, False otherwise. This function updates the executionQueue so it receives all the awaiting message.""" # Flush the current future in the local buffer (potential deadlock # otherwise) try: scoop._control.execQueue.remove(self) scoop._control.execQueue.socket.sendFuture(self) except ValueError as e: # Future was not in the local queue, everything is fine pass # Process buffers scoop._control.execQueue.updateQueue() return self._ended() def _ended(self): """True if the call was successfully cancelled or finished running, False otherwise. This function does not update the queue.""" # TODO: Replace every call to _ended() to .isDone return self.isDone def result(self, timeout=None): """Return the value returned by the call. If the call hasn't yet completed then this method will wait up to ''timeout'' seconds. More information in the :doc:`usage` page. If the call hasn't completed in timeout seconds then a TimeoutError will be raised. If timeout is not specified or None then there is no limit to the wait time. If the future is cancelled before completing then CancelledError will be raised. If the call raised an exception then this method will raise the same exception. :returns: The value returned by the callable object.""" if not self._ended(): return scoop.futures._join(self) if self.exceptionValue is not None: raise self.exceptionValue return self.resultValue def exception(self, timeout=None): """Return the exception raised by the call. If the call hasn't yet completed then this method will wait up to timeout seconds. More information in the :doc:`usage` page. If the call hasn't completed in timeout seconds then a TimeoutError will be raised. If timeout is not specified or None then there is no limit to the wait time. If the future is cancelled before completing then CancelledError will be raised. If the call completed without raising then None is returned. :returns: The exception raised by the call.""" return self.exceptionValue def add_done_callback(self, callable_, inCallbackType=CallbackType.standard, inCallbackGroup=None): """Attach a callable to the future that will be called when the future is cancelled or finishes running. Callable will be called with the future as its only argument. Added callables are called in the order that they were added and are always called in a thread belonging to the process that added them. If the callable raises an Exception then it will be logged and ignored. If the callable raises another BaseException then behavior is not defined. If the future has already completed or been cancelled then callable will be called immediately.""" self.callback.append(callbackEntry(callable_, inCallbackType, inCallbackGroup)) # If already completed or cancelled, execute it immediately if self._ended(): self.callback[-1].func(self) def _execute_callbacks(self, callbackType=CallbackType.standard): for callback in self.callback: isUniRun = (self.parentId.worker == scoop.worker and callbackType == CallbackType.universal) if isUniRun or callback.callbackType == callbackType: try: callback.func(self) except: pass def _delete(self): # TODO: Do we need this? if self.id in scoop._control.execQueue.inprogress: del scoop._control.execQueue.inprogress[self.id] for child in self.children: child.exceptionValue = CancelledError() scoop._control.delFuture(self) class FutureQueue(object): """This class encapsulates a queue of futures that are pending execution. Within this class lies the entry points for future communications.""" def __init__(self): """Initialize queue to empty elements and create a communication object.""" self.movable = deque() self.ready = deque() self.inprogress = {} self.socket = Communicator() if scoop.SIZE == 1 and not scoop.CONFIGURATION.get('headless', False): self.lowwatermark = float("inf") self.highwatermark = float("inf") else: # TODO: Make it dependent on the network latency self.lowwatermark = 0.01 self.highwatermark = 0.01 def __iter__(self): """Iterates over the selectable (cancellable) elements of the queue.""" return itertools.chain(self.movable, self.ready) def __len__(self): """Returns the length of the queue, meaning the sum of it's elements lengths.""" return len(self.movable) + len(self.ready) def timelen(self, queue_): stats = scoop._control.execStats times = Counter(hash(f.callable) for f in queue_) return sum(stats[f].mean() * occur for f, occur in times.items()) def append(self, future): """Append a future to the queue.""" if future._ended() and future.index is None: self.inprogress[future.id] = future elif future._ended() and future.index is not None: self.ready.append(future) elif future.greenlet is not None: self.inprogress.append(future) else: self.movable.append(future) # Send the oldest future in the movable deque until under the hwm over_hwm = self.timelen(self.movable) > self.highwatermark while over_hwm and len(self.movable) > 1: sending_future = self.movable.popleft() if sending_future.id.worker != scoop.worker: sending_future._delete() self.socket.sendFuture(sending_future) def pop(self): """Pop the next future from the queue; in progress futures have priority over those that have not yet started; higher level futures have priority over lower level ones; """ self.updateQueue() # If our buffer is underflowing, request more Futures if self.timelen(self) < self.lowwatermark: self.requestFuture() # If an unmovable Future is ready to be executed, return it if len(self.ready) != 0: return self.ready.popleft() # Then, use Futures in the movable queue elif len(self.movable) != 0: return self.movable.popleft() else: # Otherwise, block until a new task arrives while len(self) == 0: # Block until message arrives self.socket._poll(-1) self.updateQueue() if len(self.ready) != 0: return self.ready.popleft() elif len(self.movable) != 0: return self.movable.popleft() def flush(self): """Empty the local queue and send its elements to be executed remotely. """ for elem in self: if elem.id.worker != scoop.worker: elem._delete() self.socket.sendFuture(elem) self.ready.clear() self.movable.clear() def requestFuture(self): """Request futures from the broker""" self.socket.sendRequest() def updateQueue(self): """Process inbound communication buffer. Updates the local queue with elements from the broker.""" for future in self.socket.recvFuture(): if future._ended(): # If the answer is coming back, update its entry try: thisFuture = scoop._control.futureDict[future.id] except KeyError: # Already received? scoop.logger.warn('{0}: Received an unexpected future: ' '{1}'.format(scoop.worker, future.id)) continue thisFuture.resultValue = future.resultValue thisFuture.exceptionValue = future.exceptionValue thisFuture.executor = future.executor thisFuture.isDone = future.isDone # Execute standard callbacks here (on parent) thisFuture._execute_callbacks(CallbackType.standard) self.append(thisFuture) future._delete() elif future.id not in scoop._control.futureDict: scoop._control.futureDict[future.id] = future self.append(scoop._control.futureDict[future.id]) else: self.append(scoop._control.futureDict[future.id]) to_remove = [] for future in self.inprogress.values(): if future.index is not None: self.ready.append(future) to_remove.append(future) for future in to_remove: del self.inprogress[future.id] def remove(self, future): """Remove a future from the queue. The future must be cancellable or this method will raise a ValueError.""" self.movable.remove(future) def sendResult(self, future): """Send back results to broker for distribution to parent task.""" # Greenlets cannot be pickled future.greenlet = None assert future._ended(), "The results are not valid" self.socket.sendResult(future) def shutdown(self): """Shutdown the ressources used by the queue""" self.socket.shutdown() scoop-0.7.1/scoop/backports/000077500000000000000000000000001240127670500157615ustar00rootroot00000000000000scoop-0.7.1/scoop/backports/__init__.py000066400000000000000000000000001240127670500200600ustar00rootroot00000000000000scoop-0.7.1/scoop/backports/dictconfig.py000066400000000000000000000545761240127670500204650ustar00rootroot00000000000000# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved. # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose and without fee is hereby granted, # provided that the above copyright notice appear in all copies and that # both that copyright notice and this permission notice appear in # supporting documentation, and that the name of Vinay Sajip # not be used in advertising or publicity pertaining to distribution # of the software without specific, written prior permission. # VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING # ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL # VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR # ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER # IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import logging.handlers import re import sys import types IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I) def valid_ident(s): m = IDENTIFIER.match(s) if not m: raise ValueError('Not a valid Python identifier: %r' % s) return True # # This function is defined in logging only in recent versions of Python # try: from logging import _checkLevel except ImportError: def _checkLevel(level): if isinstance(level, int): rv = level elif str(level) == level: if level not in logging._levelNames: raise ValueError('Unknown level: %r' % level) rv = logging._levelNames[level] else: raise TypeError('Level not an integer or a ' 'valid string: %r' % level) return rv # The ConvertingXXX classes are wrappers around standard Python containers, # and they serve to convert any suitable values in the container. The # conversion converts base dicts, lists and tuples to their wrapped # equivalents, whereas strings which match a conversion format are converted # appropriately. # # Each wrapper should have a configurator attribute holding the actual # configurator to use for conversion. class ConvertingDict(dict): """A converting dictionary wrapper.""" def __getitem__(self, key): value = dict.__getitem__(self, key) result = self.configurator.convert(value) #If the converted value is different, save for next time if value is not result: self[key] = result if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple): result.parent = self result.key = key return result def get(self, key, default=None): value = dict.get(self, key, default) result = self.configurator.convert(value) #If the converted value is different, save for next time if value is not result: self[key] = result if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple): result.parent = self result.key = key return result def pop(self, key, default=None): value = dict.pop(self, key, default) result = self.configurator.convert(value) if value is not result: if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple): result.parent = self result.key = key return result class ConvertingList(list): """A converting list wrapper.""" def __getitem__(self, key): value = list.__getitem__(self, key) result = self.configurator.convert(value) #If the converted value is different, save for next time if value is not result: self[key] = result if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple): result.parent = self result.key = key return result def pop(self, idx=-1): value = list.pop(self, idx) result = self.configurator.convert(value) if value is not result: if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple): result.parent = self return result class ConvertingTuple(tuple): """A converting tuple wrapper.""" def __getitem__(self, key): value = tuple.__getitem__(self, key) result = self.configurator.convert(value) if value is not result: if type(result) in (ConvertingDict, ConvertingList, ConvertingTuple): result.parent = self result.key = key return result class BaseConfigurator(object): """ The configurator base class which defines some useful defaults. """ CONVERT_PATTERN = re.compile(r'^(?P[a-z]+)://(?P.*)$') WORD_PATTERN = re.compile(r'^\s*(\w+)\s*') DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*') INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*') DIGIT_PATTERN = re.compile(r'^\d+$') value_converters = { 'ext' : 'ext_convert', 'cfg' : 'cfg_convert', } # We might want to use a different one, e.g. importlib importer = __import__ def __init__(self, config): self.config = ConvertingDict(config) self.config.configurator = self def resolve(self, s): """ Resolve strings to objects using standard import and attribute syntax. """ name = s.split('.') used = name.pop(0) try: found = self.importer(used) for frag in name: used += '.' + frag try: found = getattr(found, frag) except AttributeError: self.importer(used) found = getattr(found, frag) return found except ImportError: e, tb = sys.exc_info()[1:] v = ValueError('Cannot resolve %r: %s' % (s, e)) v.__cause__, v.__traceback__ = e, tb raise v def ext_convert(self, value): """Default converter for the ext:// protocol.""" return self.resolve(value) def cfg_convert(self, value): """Default converter for the cfg:// protocol.""" rest = value m = self.WORD_PATTERN.match(rest) if m is None: raise ValueError("Unable to convert %r" % value) else: rest = rest[m.end():] d = self.config[m.groups()[0]] #print d, rest while rest: m = self.DOT_PATTERN.match(rest) if m: d = d[m.groups()[0]] else: m = self.INDEX_PATTERN.match(rest) if m: idx = m.groups()[0] if not self.DIGIT_PATTERN.match(idx): d = d[idx] else: try: n = int(idx) # try as number first (most likely) d = d[n] except TypeError: d = d[idx] if m: rest = rest[m.end():] else: raise ValueError('Unable to convert ' '%r at %r' % (value, rest)) #rest should be empty return d def convert(self, value): """ Convert values to an appropriate type. dicts, lists and tuples are replaced by their converting alternatives. Strings are checked to see if they have a conversion format and are converted if they do. """ if not isinstance(value, ConvertingDict) and isinstance(value, dict): value = ConvertingDict(value) value.configurator = self elif not isinstance(value, ConvertingList) and isinstance(value, list): value = ConvertingList(value) value.configurator = self elif not isinstance(value, ConvertingTuple) and\ isinstance(value, tuple): value = ConvertingTuple(value) value.configurator = self elif isinstance(value, basestring): # str for py3k m = self.CONVERT_PATTERN.match(value) if m: d = m.groupdict() prefix = d['prefix'] converter = self.value_converters.get(prefix, None) if converter: suffix = d['suffix'] converter = getattr(self, converter) value = converter(suffix) return value def configure_custom(self, config): """Configure an object with a user-supplied factory.""" c = config.pop('()') if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: c = self.resolve(c) props = config.pop('.', None) # Check for valid identifiers kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) result = c(**kwargs) if props: for name, value in props.items(): setattr(result, name, value) return result def as_tuple(self, value): """Utility function which converts lists to tuples.""" if isinstance(value, list): value = tuple(value) return value class DictConfigurator(BaseConfigurator): """ Configure logging using a dictionary-like object to describe the configuration. """ def configure(self): """Do the configuration.""" config = self.config if 'version' not in config: raise ValueError("dictionary doesn't specify a version") if config['version'] != 1: raise ValueError("Unsupported version: %s" % config['version']) incremental = config.pop('incremental', False) EMPTY_DICT = {} logging._acquireLock() try: if incremental: handlers = config.get('handlers', EMPTY_DICT) # incremental handler config only if handler name # ties in to logging._handlers (Python 2.7) if sys.version_info[:2] == (2, 7): for name in handlers: if name not in logging._handlers: raise ValueError('No handler found with ' 'name %r' % name) else: try: handler = logging._handlers[name] handler_config = handlers[name] level = handler_config.get('level', None) if level: handler.setLevel(_checkLevel(level)) except StandardError, e: raise ValueError('Unable to configure handler ' '%r: %s' % (name, e)) loggers = config.get('loggers', EMPTY_DICT) for name in loggers: try: self.configure_logger(name, loggers[name], True) except StandardError, e: raise ValueError('Unable to configure logger ' '%r: %s' % (name, e)) root = config.get('root', None) if root: try: self.configure_root(root, True) except StandardError, e: raise ValueError('Unable to configure root ' 'logger: %s' % e) else: disable_existing = config.pop('disable_existing_loggers', True) logging._handlers.clear() del logging._handlerList[:] # Do formatters first - they don't refer to anything else formatters = config.get('formatters', EMPTY_DICT) for name in formatters: try: formatters[name] = self.configure_formatter( formatters[name]) except StandardError, e: raise ValueError('Unable to configure ' 'formatter %r: %s' % (name, e)) # Next, do filters - they don't refer to anything else, either filters = config.get('filters', EMPTY_DICT) for name in filters: try: filters[name] = self.configure_filter(filters[name]) except StandardError, e: raise ValueError('Unable to configure ' 'filter %r: %s' % (name, e)) # Next, do handlers - they refer to formatters and filters # As handlers can refer to other handlers, sort the keys # to allow a deterministic order of configuration handlers = config.get('handlers', EMPTY_DICT) for name in sorted(handlers): try: handler = self.configure_handler(handlers[name]) handler.name = name handlers[name] = handler except StandardError, e: raise ValueError('Unable to configure handler ' '%r: %s' % (name, e)) # Next, do loggers - they refer to handlers and filters #we don't want to lose the existing loggers, #since other threads may have pointers to them. #existing is set to contain all existing loggers, #and as we go through the new configuration we #remove any which are configured. At the end, #what's left in existing is the set of loggers #which were in the previous configuration but #which are not in the new configuration. root = logging.root existing = root.manager.loggerDict.keys() #The list needs to be sorted so that we can #avoid disabling child loggers of explicitly #named loggers. With a sorted list it is easier #to find the child loggers. existing.sort() #We'll keep the list of existing loggers #which are children of named loggers here... child_loggers = [] #now set up the new ones... loggers = config.get('loggers', EMPTY_DICT) for name in loggers: if name in existing: i = existing.index(name) prefixed = name + "." pflen = len(prefixed) num_existing = len(existing) i = i + 1 # look at the entry after name while (i < num_existing) and\ (existing[i][:pflen] == prefixed): child_loggers.append(existing[i]) i = i + 1 existing.remove(name) try: self.configure_logger(name, loggers[name]) except StandardError, e: raise ValueError('Unable to configure logger ' '%r: %s' % (name, e)) #Disable any old loggers. There's no point deleting #them as other threads may continue to hold references #and by disabling them, you stop them doing any logging. #However, don't disable children of named loggers, as that's #probably not what was intended by the user. for log in existing: logger = root.manager.loggerDict[log] if log in child_loggers: logger.level = logging.NOTSET logger.handlers = [] logger.propagate = True elif disable_existing: logger.disabled = True # And finally, do the root logger root = config.get('root', None) if root: try: self.configure_root(root) except StandardError, e: raise ValueError('Unable to configure root ' 'logger: %s' % e) finally: logging._releaseLock() def configure_formatter(self, config): """Configure a formatter from a dictionary.""" if '()' in config: factory = config['()'] # for use in exception handler try: result = self.configure_custom(config) except TypeError, te: if "'format'" not in str(te): raise #Name of parameter changed from fmt to format. #Retry with old name. #This is so that code can be used with older Python versions #(e.g. by Django) config['fmt'] = config.pop('format') config['()'] = factory result = self.configure_custom(config) else: fmt = config.get('format', None) dfmt = config.get('datefmt', None) result = logging.Formatter(fmt, dfmt) return result def configure_filter(self, config): """Configure a filter from a dictionary.""" if '()' in config: result = self.configure_custom(config) else: name = config.get('name', '') result = logging.Filter(name) return result def add_filters(self, filterer, filters): """Add filters to a filterer from a list of names.""" for f in filters: try: filterer.addFilter(self.config['filters'][f]) except StandardError, e: raise ValueError('Unable to add filter %r: %s' % (f, e)) def configure_handler(self, config): """Configure a handler from a dictionary.""" formatter = config.pop('formatter', None) if formatter: try: formatter = self.config['formatters'][formatter] except StandardError, e: raise ValueError('Unable to set formatter ' '%r: %s' % (formatter, e)) level = config.pop('level', None) filters = config.pop('filters', None) if '()' in config: c = config.pop('()') if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType: c = self.resolve(c) factory = c else: klass = self.resolve(config.pop('class')) #Special case for handler which refers to another handler if issubclass(klass, logging.handlers.MemoryHandler) and\ 'target' in config: try: config['target'] = self.config['handlers'][config['target']] except StandardError, e: raise ValueError('Unable to set target handler ' '%r: %s' % (config['target'], e)) elif issubclass(klass, logging.handlers.SMTPHandler) and\ 'mailhost' in config: config['mailhost'] = self.as_tuple(config['mailhost']) elif issubclass(klass, logging.handlers.SysLogHandler) and\ 'address' in config: config['address'] = self.as_tuple(config['address']) factory = klass kwargs = dict([(k, config[k]) for k in config if valid_ident(k)]) try: result = factory(**kwargs) except TypeError, te: if "'stream'" not in str(te): raise #The argument name changed from strm to stream #Retry with old name. #This is so that code can be used with older Python versions #(e.g. by Django) kwargs['strm'] = kwargs.pop('stream') result = factory(**kwargs) if formatter: result.setFormatter(formatter) if level is not None: result.setLevel(_checkLevel(level)) if filters: self.add_filters(result, filters) return result def add_handlers(self, logger, handlers): """Add handlers to a logger from a list of names.""" for h in handlers: try: logger.addHandler(self.config['handlers'][h]) except StandardError, e: raise ValueError('Unable to add handler %r: %s' % (h, e)) def common_logger_config(self, logger, config, incremental=False): """ Perform configuration which is common to root and non-root loggers. """ level = config.get('level', None) if level is not None: logger.setLevel(_checkLevel(level)) if not incremental: #Remove any existing handlers for h in logger.handlers[:]: logger.removeHandler(h) handlers = config.get('handlers', None) if handlers: self.add_handlers(logger, handlers) filters = config.get('filters', None) if filters: self.add_filters(logger, filters) def configure_logger(self, name, config, incremental=False): """Configure a non-root logger from a dictionary.""" logger = logging.getLogger(name) self.common_logger_config(logger, config, incremental) propagate = config.get('propagate', None) if propagate is not None: logger.propagate = propagate def configure_root(self, config, incremental=False): """Configure a root logger from a dictionary.""" root = logging.getLogger() self.common_logger_config(root, config, incremental) dictConfigClass = DictConfigurator def dictConfig(config): """Configure logging using a dictionary.""" dictConfigClass(config).configure() scoop-0.7.1/scoop/backports/newCollections.py000066400000000000000000000163271240127670500213340ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from collections import deque from operator import itemgetter from heapq import nlargest from itertools import repeat, ifilter # maxlen functionnality of deque appeared in 2.7. Backport it if necessary. class deque(deque): def __init__(self, iterable=(), maxlen=None): super(deque, self).__init__(iterable, maxlen) self._maxlen = maxlen @property def maxlen(self): return self._maxlen # The Counter class appeared in 2.7 class Counter(dict): '''Dict subclass for counting hashable objects. Sometimes called a bag or multiset. Elements are stored as dictionary keys and their counts are stored as dictionary values. >>> Counter('zyzygy') Counter({'y': 3, 'z': 2, 'g': 1}) ''' def __init__(self, iterable=None, **kwds): '''Create a new, empty Counter object. And if given, count elements from an input iterable. Or, initialize the count from another mapping of elements to their counts. >>> c = Counter() # a new, empty counter >>> c = Counter('gallahad') # a new counter from an iterable >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping >>> c = Counter(a=4, b=2) # a new counter from keyword args ''' self.update(iterable, **kwds) def __missing__(self, key): return 0 def most_common(self, n=None): '''List the n most common elements and their counts from the most common to the least. If n is None, then list all element counts. >>> Counter('abracadabra').most_common(3) [('a', 5), ('r', 2), ('b', 2)] ''' if n is None: return sorted(self.iteritems(), key=itemgetter(1), reverse=True) return nlargest(n, self.iteritems(), key=itemgetter(1)) def elements(self): '''Iterator over elements repeating each as many times as its count. >>> c = Counter('ABCABC') >>> sorted(c.elements()) ['A', 'A', 'B', 'B', 'C', 'C'] If an element's count has been set to zero or is a negative number, elements() will ignore it. ''' for elem, count in self.iteritems(): for _ in repeat(None, count): yield elem # Override dict methods where the meaning changes for Counter objects. @classmethod def fromkeys(cls, iterable, v=None): raise NotImplementedError( 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') def update(self, iterable=None, **kwds): '''Like dict.update() but add counts instead of replacing them. Source can be an iterable, a dictionary, or another Counter instance. >>> c = Counter('which') >>> c.update('witch') # add elements from another iterable >>> d = Counter('watch') >>> c.update(d) # add elements from another counter >>> c['h'] # four 'h' in which, witch, and watch 4 ''' if iterable is not None: if hasattr(iterable, 'iteritems'): if self: self_get = self.get for elem, count in iterable.iteritems(): self[elem] = self_get(elem, 0) + count else: dict.update(self, iterable) # fast path when counter is empty else: self_get = self.get for elem in iterable: self[elem] = self_get(elem, 0) + 1 if kwds: self.update(kwds) def copy(self): 'Like dict.copy() but returns a Counter instance instead of a dict.' return Counter(self) def __delitem__(self, elem): 'Like dict.__delitem__() but does not raise KeyError for missing values.' if elem in self: dict.__delitem__(self, elem) def __repr__(self): if not self: return '%s()' % self.__class__.__name__ items = ', '.join(map('%r: %r'.__mod__, self.most_common())) return '%s({%s})' % (self.__class__.__name__, items) # Multiset-style mathematical operations discussed in: # Knuth TAOCP Volume II section 4.6.3 exercise 19 # and at http://en.wikipedia.org/wiki/Multiset # # Outputs guaranteed to only include positive counts. # # To strip negative and zero counts, add-in an empty counter: # c += Counter() def __add__(self, other): '''Add counts from two counters. >>> Counter('abbb') + Counter('bcc') Counter({'b': 4, 'c': 2, 'a': 1}) ''' if not isinstance(other, Counter): return NotImplemented result = Counter() for elem in set(self) | set(other): newcount = self[elem] + other[elem] if newcount > 0: result[elem] = newcount return result def __sub__(self, other): ''' Subtract count, but keep only results with positive counts. >>> Counter('abbbc') - Counter('bccd') Counter({'b': 2, 'a': 1}) ''' if not isinstance(other, Counter): return NotImplemented result = Counter() for elem in set(self) | set(other): newcount = self[elem] - other[elem] if newcount > 0: result[elem] = newcount return result def __or__(self, other): '''Union is the maximum of value in either of the input counters. >>> Counter('abbb') | Counter('bcc') Counter({'b': 3, 'c': 2, 'a': 1}) ''' if not isinstance(other, Counter): return NotImplemented _max = max result = Counter() for elem in set(self) | set(other): newcount = _max(self[elem], other[elem]) if newcount > 0: result[elem] = newcount return result def __and__(self, other): ''' Intersection is the minimum of corresponding counts. >>> Counter('abbb') & Counter('bcc') Counter({'b': 1}) ''' if not isinstance(other, Counter): return NotImplemented _min = min result = Counter() if len(self) < len(other): self, other = other, self for elem in ifilter(self.__contains__, other): newcount = _min(self[elem], other[elem]) if newcount > 0: result[elem] = newcount return result if __name__ == '__main__': import doctest print doctest.testmod()scoop-0.7.1/scoop/backports/runpy.py000066400000000000000000000247611240127670500175220ustar00rootroot00000000000000"""runpy.py - locating and running Python code using the module namespace Provides support for locating and running Python scripts using the Python module namespace instead of the native filesystem. This allows Python code to play nicely with non-filesystem based PEP 302 importers when locating support scripts as well as when importing modules. """ # Written by Nick Coghlan # to implement PEP 338 (Executing Modules as Scripts) import sys import imp from pkgutil import read_code try: from imp import get_loader except ImportError: from pkgutil import get_loader __all__ = [ "run_module", "run_path", ] class _TempModule(object): """Temporarily replace a module in sys.modules with an empty namespace""" def __init__(self, mod_name): self.mod_name = mod_name self.module = imp.new_module(mod_name) self._saved_module = [] def __enter__(self): mod_name = self.mod_name try: self._saved_module.append(sys.modules[mod_name]) except KeyError: pass sys.modules[mod_name] = self.module return self def __exit__(self, *args): if self._saved_module: sys.modules[self.mod_name] = self._saved_module[0] else: del sys.modules[self.mod_name] self._saved_module = [] class _ModifiedArgv0(object): def __init__(self, value): self.value = value self._saved_value = self._sentinel = object() def __enter__(self): if self._saved_value is not self._sentinel: raise RuntimeError("Already preserving saved value") self._saved_value = sys.argv[0] sys.argv[0] = self.value def __exit__(self, *args): self.value = self._sentinel sys.argv[0] = self._saved_value def _run_code(code, run_globals, init_globals=None, mod_name=None, mod_fname=None, mod_loader=None, pkg_name=None): """Helper to run code in nominated namespace""" if init_globals is not None: run_globals.update(init_globals) run_globals.update(__name__ = mod_name, __file__ = mod_fname, __loader__ = mod_loader, __package__ = pkg_name) exec code in run_globals return run_globals def _run_module_code(code, init_globals=None, mod_name=None, mod_fname=None, mod_loader=None, pkg_name=None): """Helper to run code in new namespace with sys modified""" with _ModifiedArgv0(mod_fname): with _TempModule(mod_name) as temp_module: mod_globals = temp_module.module.__dict__ _run_code(code, mod_globals, init_globals, mod_name, mod_fname, mod_loader, pkg_name) # Copy the globals of the temporary module, as they # may be cleared when the temporary module goes away return mod_globals.copy() # This helper is needed due to a missing component in the PEP 302 # loader protocol (specifically, "get_filename" is non-standard) # Since we can't introduce new features in maintenance releases, # support was added to zipimporter under the name '_get_filename' def _get_filename(loader, mod_name): for attr in ("get_filename", "_get_filename"): meth = getattr(loader, attr, None) if meth is not None: return meth(mod_name) return None # Helper to get the loader, code and filename for a module def _get_module_details(mod_name): loader = get_loader(mod_name) if loader is None: raise ImportError("No module named %s" % mod_name) if loader.is_package(mod_name): if mod_name == "__main__" or mod_name.endswith(".__main__"): raise ImportError("Cannot use package as __main__ module") try: pkg_main_name = mod_name + ".__main__" return _get_module_details(pkg_main_name) except ImportError, e: raise ImportError(("%s; %r is a package and cannot " + "be directly executed") %(e, mod_name)) code = loader.get_code(mod_name) if code is None: raise ImportError("No code object available for %s" % mod_name) filename = _get_filename(loader, mod_name) return mod_name, loader, code, filename def _get_main_module_details(): # Helper that gives a nicer error message when attempting to # execute a zipfile or directory by invoking __main__.py main_name = "__main__" try: return _get_module_details(main_name) except ImportError as exc: if main_name in str(exc): raise ImportError("can't find %r module in %r" % (main_name, sys.path[0])) raise # This function is the actual implementation of the -m switch and direct # execution of zipfiles and directories and is deliberately kept private. # This avoids a repeat of the situation where run_module() no longer met the # needs of mainmodule.c, but couldn't be changed because it was public def _run_module_as_main(mod_name, alter_argv=True): """Runs the designated module in the __main__ namespace Note that the executed module will have full access to the __main__ namespace. If this is not desirable, the run_module() function should be used to run the module code in a fresh namespace. At the very least, these variables in __main__ will be overwritten: __name__ __file__ __loader__ __package__ """ try: if alter_argv or mod_name != "__main__": # i.e. -m switch mod_name, loader, code, fname = _get_module_details(mod_name) else: # i.e. directory or zipfile execution mod_name, loader, code, fname = _get_main_module_details() except ImportError as exc: msg = "%s: %s" % (sys.executable, str(exc)) sys.exit(msg) pkg_name = mod_name.rpartition('.')[0] main_globals = sys.modules["__main__"].__dict__ if alter_argv: sys.argv[0] = fname return _run_code(code, main_globals, None, "__main__", fname, loader, pkg_name) def run_module(mod_name, init_globals=None, run_name=None, alter_sys=False): """Execute a module's code without importing it Returns the resulting top level namespace dictionary """ mod_name, loader, code, fname = _get_module_details(mod_name) if run_name is None: run_name = mod_name pkg_name = mod_name.rpartition('.')[0] if alter_sys: return _run_module_code(code, init_globals, run_name, fname, loader, pkg_name) else: # Leave the sys module alone return _run_code(code, {}, init_globals, run_name, fname, loader, pkg_name) # XXX (ncoghlan): Perhaps expose the C API function # as imp.get_importer instead of reimplementing it in Python? def _get_importer(path_name): """Python version of PyImport_GetImporter C API function""" cache = sys.path_importer_cache try: importer = cache[path_name] except KeyError: # Not yet cached. Flag as using the # standard machinery until we finish # checking the hooks cache[path_name] = None for hook in sys.path_hooks: try: importer = hook(path_name) break except ImportError: pass else: # The following check looks a bit odd. The trick is that # NullImporter throws ImportError if the supplied path is a # *valid* directory entry (and hence able to be handled # by the standard import machinery) try: importer = imp.NullImporter(path_name) except ImportError: return None cache[path_name] = importer return importer def _get_code_from_file(fname): # Check for a compiled file first with open(fname, "rb") as f: code = read_code(f) if code is None: # That didn't work, so try it as normal source code with open(fname, "rU") as f: code = compile(f.read(), fname, 'exec') return code def run_path(path_name, init_globals=None, run_name=None): """Execute code located at the specified filesystem location Returns the resulting top level namespace dictionary The file path may refer directly to a Python script (i.e. one that could be directly executed with execfile) or else it may refer to a zipfile or directory containing a top level __main__.py script. """ if run_name is None: run_name = "" importer = _get_importer(path_name) if isinstance(importer, imp.NullImporter): # Not a valid sys.path entry, so run the code directly # execfile() doesn't help as we want to allow compiled files code = _get_code_from_file(path_name) return _run_module_code(code, init_globals, run_name, path_name) else: # Importer is defined for path, so add it to # the start of sys.path sys.path.insert(0, path_name) try: # Here's where things are a little different from the run_module # case. There, we only had to replace the module in sys while the # code was running and doing so was somewhat optional. Here, we # have no choice and we have to remove it even while we read the # code. If we don't do this, a __loader__ attribute in the # existing __main__ module may prevent location of the new module. main_name = "__main__" saved_main = sys.modules[main_name] del sys.modules[main_name] try: mod_name, loader, code, fname = _get_main_module_details() finally: sys.modules[main_name] = saved_main pkg_name = "" with _ModifiedArgv0(path_name): with _TempModule(run_name) as temp_module: mod_globals = temp_module.module.__dict__ return _run_code(code, mod_globals, init_globals, run_name, fname, loader, pkg_name).copy() finally: try: sys.path.remove(path_name) except ValueError: pass if __name__ == "__main__": # Run the module specified as the next command line argument if len(sys.argv) < 2: print >> sys.stderr, "No module specified for execution" else: del sys.argv[0] # Make the requested module sys.argv[0] _run_module_as_main(sys.argv[0]) scoop-0.7.1/scoop/bootstrap/000077500000000000000000000000001240127670500160065ustar00rootroot00000000000000scoop-0.7.1/scoop/bootstrap/__init__.py000066400000000000000000000000001240127670500201050ustar00rootroot00000000000000scoop-0.7.1/scoop/bootstrap/__main__.py000066400000000000000000000256131240127670500201070ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import sys import os import functools import argparse import logging try: import psutil except ImportError: psutil = None if sys.version_info < (3, 3): from imp import load_source as importFunction FileNotFoundError = IOError else: import importlib.machinery importFunction = lambda name, path: importlib.machinery.SourceFileLoader(name, path).load_module() import scoop from ..broker.structs import BrokerInfo from .. import discovery, utils if sys.version_info < (2, 7): import scoop.backports.runpy as runpy else: import runpy class Bootstrap(object): """Set up SCOOP communication links and launches the client module""" def __init__(self): self.parser = None self.args = None self.verbose = 1 def main(self): """Bootstrap an arbitrary script. If no agruments were passed, use discovery module to search and connect to a broker.""" if self.args is None: self.parse() self.log = utils.initLogging(self.verbose) # Change to the desired directory if self.args.workingDirectory: os.chdir(self.args.workingDirectory) if not self.args.brokerHostname: self.log.info("Discovering SCOOP Brokers on network...") pools = discovery.Seek() if not pools: self.log.error("Could not find a SCOOP Broker broadcast.") sys.exit(-1) self.log.info("Found a broker named {name} on {host} port " "{ports}".format( name=pools[0].name, host=pools[0].host, ports=pools[0].ports, )) self.args.brokerHostname = pools[0].host self.args.taskPort = pools[0].ports[0] self.args.metaPort = pools[0].ports[0] self.log.debug("Using following addresses:\n{brokerAddress}\n" "{metaAddress}".format( brokerAddress=self.args.brokerAddress, metaAddress=self.args.metaAddress, )) self.args.origin = True self.setScoop() self.run() def makeParser(self): """Generate the argparse parser object containing the bootloader accepted parameters """ self.parser = argparse.ArgumentParser(description='Starts the executable.', prog=("{0} -m scoop.bootstrap" ).format(sys.executable)) self.parser.add_argument('--origin', help="To specify that the worker is the origin", action='store_true') self.parser.add_argument('--brokerHostname', help="The routable hostname of a broker", default="") self.parser.add_argument('--externalBrokerHostname', help="Externally routable hostname of local " "worker", default="") self.parser.add_argument('--taskPort', help="The port of the broker task socket", type=int) self.parser.add_argument('--metaPort', help="The port of the broker meta socket", type=int) self.parser.add_argument('--size', help="The size of the worker pool", type=int, default=1) self.parser.add_argument('--nice', help="Adjust the niceness of the process", type=int, default=0) self.parser.add_argument('--debug', help="Activate the debug", action='store_true') self.parser.add_argument('--profile', help="Activate the profiler", action='store_true') self.parser.add_argument('--echoGroup', help="Echo the process Group ID before launch", action='store_true') self.parser.add_argument('--workingDirectory', help="Set the working directory for the " "execution", default=None) self.parser.add_argument('--backend', help="Choice of communication backend", choices=['ZMQ', 'TCP'], default='ZMQ') self.parser.add_argument('executable', nargs='?', help='The executable to start with scoop') self.parser.add_argument('args', nargs=argparse.REMAINDER, help='The arguments to pass to the executable', default=[]) self.parser.add_argument('--verbose', '-v', action='count', help=("Verbosity level of this launch script" "(-vv for " "more)"), default=1) self.parser.add_argument('--quiet', '-q', action='store_true', help="Suppress the output") def parse(self): """Generate a argparse parser and parse the command-line arguments""" if self.parser is None: self.makeParser() self.args = self.parser.parse_args() self.verbose = self.args.verbose if not self.args.quiet else 0 def setScoop(self): """Setup the SCOOP constants.""" scoop.IS_RUNNING = True scoop.IS_ORIGIN = self.args.origin scoop.BROKER = BrokerInfo( self.args.brokerHostname, self.args.taskPort, self.args.metaPort, self.args.externalBrokerHostname if self.args.externalBrokerHostname else self.args.brokerHostname, ) scoop.SIZE = self.args.size scoop.DEBUG = self.args.debug scoop.MAIN_MODULE = self.args.executable scoop.CONFIGURATION = { 'headless': not bool(self.args.executable), 'backend': self.args.backend, } scoop.logger = self.log if self.args.nice: if not psutil: scoop.logger.error("psutil not installed.") raise ImportError("psutil is needed for nice functionnality.") p = psutil.Process(os.getpid()) p.set_nice(self.args.nice) if scoop.DEBUG or self.args.profile: from scoop import _debug @staticmethod def setupEnvironment(self=None): """Set the environment (argv, sys.path and module import) of scoop.MAIN_MODULE. """ # get the module path in the Python path sys.path.append(os.path.dirname(os.path.abspath(scoop.MAIN_MODULE))) # Add the user arguments to argv sys.argv = sys.argv[:1] if self: sys.argv += self.args.args try: user_module = importFunction( "SCOOP_WORKER", scoop.MAIN_MODULE, ) except FileNotFoundError as e: # Could not find file sys.stderr.write('{0}\nFile: {1}\nIn path: {2}\n'.format( str(e), scoop.MAIN_MODULE, sys.path[-1], ) ) sys.stderr.flush() sys.exit(-1) globs = {} try: attrlist = user_module.__all__ except AttributeError: attrlist = dir(user_module) for attr in attrlist: globs[attr] = getattr(user_module, attr) if self: return globs return user_module def run(self, globs=None): """Import user module and start __main__ passing globals() is required when subclassing in another module """ # Without this, the underneath import clashes with the top-level one global scoop if globs is None: globs = globals() # Show the current process Group ID if asked if self.args.echoGroup: sys.stdout.write(str(os.getpgrp()) + "\n") sys.stdout.flush() scoop.logger.info("Worker(s) launched using {0}".format( os.environ['SHELL'], ) ) # import the user module if scoop.MAIN_MODULE: globs.update(self.setupEnvironment(self)) # Start the user program from scoop import futures def futures_startup(): """Execute the user code. Wraps futures._startup (SCOOP initialisation) over the user module. Needs """ return futures._startup( functools.partial( runpy.run_path, scoop.MAIN_MODULE, init_globals=globs, run_name="__main__" ) ) if self.args.profile: import cProfile # runctx instead of run is required for local function try: os.makedirs("profile") except: pass cProfile.runctx( "futures_startup()", globs, locals(), "./profile/{0}.prof".format(os.getpid()) ) else: try: futures_startup() finally: # Must reimport (potentially not there after bootstrap) import scoop # Ensure a communication queue exists (may happend when a # connection wasn't established such as cloud-mode wait). if scoop._control.execQueue: scoop._control.execQueue.shutdown() if __name__ == "__main__": b = Bootstrap() b.main() scoop-0.7.1/scoop/broker/000077500000000000000000000000001240127670500152555ustar00rootroot00000000000000scoop-0.7.1/scoop/broker/__init__.py000066400000000000000000000000001240127670500173540ustar00rootroot00000000000000scoop-0.7.1/scoop/broker/__main__.py000066400000000000000000000076251240127670500173610ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import os from signal import signal, SIGTERM, SIGINT import argparse try: import psutil except: psutil = None if __name__ == "__main__": parser = argparse.ArgumentParser(description="Starts the broker on the" "current computer") parser.add_argument('--tPort', help='The port of the task socket', default="*") parser.add_argument('--mPort', help="The port of the info socket", default="*") parser.add_argument('--nice', help="Adjust the process niceness", type=int, default=0) parser.add_argument('--debug', help="Activate the debug", action='store_true') parser.add_argument('--path', help="Path to run the broker", default="") parser.add_argument('--backend', help="Choice of communication backend", choices=['ZMQ', 'TCP'], default='ZMQ') parser.add_argument('--headless', help="Enforce headless (cloud-style) operation", action='store_true') parser.add_argument('--echoGroup', help="Echo the process Group ID before launch", action='store_true') parser.add_argument('--echoPorts', help="Echo the listening ports", action='store_true') args = parser.parse_args() if args.echoGroup: import os import sys sys.stdout.write(str(os.getpgrp()) + "\n") sys.stdout.flush() if args.path: import os try: os.chdir(args.path) except OSError: sys.stderr.write('Could not chdir in {0}.'.format(args.path)) sys.stderr.flush() if args.backend == 'ZMQ': from ..broker.brokerzmq import Broker else: from ..broker.brokertcp import Broker thisBroker = Broker("tcp://*:" + args.tPort, "tcp://*:" + args.mPort, debug=args.debug, headless=args.headless, ) signal(SIGTERM, lambda signum, stack_frame: thisBroker.shutdown()) signal(SIGINT, lambda signum, stack_frame: thisBroker.shutdown()) # Handle nicing functionnality if args.nice: if not psutil: scoop.logger.error("Tried to nice but psutil is not installed.") raise ImportError("psutil is needed for nice functionality.") p = psutil.Process(os.getpid()) p.set_nice(args.nice) if args.echoPorts: import os import sys sys.stdout.write("{0},{1}\n".format( thisBroker.tSockPort, thisBroker.infoSockPort, )) sys.stdout.flush() thisBroker.logger.info("Using name {workerName}".format( workerName=thisBroker.getName(), )) try: thisBroker.run() finally: thisBroker.shutdown() scoop-0.7.1/scoop/broker/brokertcp.py000066400000000000000000000247411240127670500176320ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from collections import deque, defaultdict import time import sys import threading import array import socket import copy import asyncore import logging try: import cPickle as pickle except ImportError: import pickle import scoop from .. import discovery, utils from .structs import BrokerInfo # Worker requests INIT = b"INIT" REQUEST = b"REQUEST" TASK = b"TASK" REPLY = b"REPLY" SHUTDOWN = b"SHUTDOWN" VARIABLE = b"VARIABLE" BROKER_INFO = b"BROKER_INFO" # Broker interconnection CONNECT = b"CONNECT" class LaunchingError(Exception): pass def serialize(*data): #sendData = ''.join(data) #sendData = _chr(len(sendData)) + sendData #return array.array('b', sendData).tobytes() return pickle.dumps(data) def deserialize(data): #return array.frombytes(data) return pickle.loads(data) class TaskHandler(asyncore.dispatcher_with_send): def handle_read(self): data = self.recv(8192) if data: msg = deserialize(data) msg_type = msg[1] if self.debug: self.stats.append((time.time(), msg_type, len(self.unassignedTasks), len(self.availableWorkers))) # New task inbound if msg_type in TASK: task = msg[2] try: address = self.availableWorkers.popleft() except IndexError: self.unassignedTasks.append(task) else: self.taskSocket.send_multipart([address, TASK, task]) # Request for task elif msg_type == REQUEST: address = msg[0] try: task = self.unassignedTasks.pop() except IndexError: self.availableWorkers.append(address) else: self.taskSocket.send_multipart([address, TASK, task]) # Answer needing delivery elif msg_type == REPLY: destination = msg[-1] origin = msg[0] self.taskSocket.send_multipart([destination] + msg[1:] + [origin]) # Shared variable to distribute elif msg_type == VARIABLE: address = msg[4] value = msg[3] key = msg[2] self.sharedVariables[address].update( {key: value}, ) self.infoSocket.send_multipart([VARIABLE, key, value, address]) # Initialize the variables of a new worker elif msg_type == INIT: address = msg[0] try: self.processConfig(pickle.loads(msg[2])) except pickle.PickleError: return self.taskSocket.send_multipart([ address, pickle.dumps(self.config, pickle.HIGHEST_PROTOCOL), pickle.dumps(self.sharedVariables, pickle.HIGHEST_PROTOCOL), ]) self.taskSocket.send_multipart([ address, pickle.dumps(self.clusterAvailable, pickle.HIGHEST_PROTOCOL), ]) # Add a given broker to its fellow list elif msg_type == CONNECT: try: connect_brokers = pickle.loads(msg[2]) except pickle.PickleError: self.logger.error("Could not understand CONNECT message.") return self.logger.info("Connecting to other brokers...") self.addBrokerList(connect_brokers) # Shutdown of this broker was requested elif msg_type == SHUTDOWN: self.logger.debug("SHUTDOWN command received.") raise asyncore.ExitNow('Server is quitting!') class TaskServer(asyncore.dispatcher): def __init__(self, host, port): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind((host, port)) self.listen(1) def handle_accept(self): pair = self.accept() if pair is not None: sock, addr = pair print('Incoming connection from %s' % repr(addr)) handler = TaskHandler(sock) class InfoServer(asyncore.dispatcher): def __init__(self, host, port): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind((host, port)) self.listen(1) def handle_accept(self): pair = self.accept() if pair is not None: sock, addr = pair print('Incoming connection from %s' % repr(addr)) handler = TaskHandler(sock) class Broker(object): def __init__(self, tSock="tcp://*:*", mSock="tcp://*:*", debug=False, headless=False, hostname="127.0.0.1"): """This function initializes a broker. :param tSock: Task Socket Address. Must contain protocol, address and port information. :param mSock: Meta Socket Address. Must contain protocol, address and port information. """ self.debug = debug self.hostname = hostname self.tSockPort = 0 addr, port = tSock[6:].split(":", 1) if port == "*": port = 0 else: port = int(port) if addr == "*": addr = "" self.taskSocket = TaskServer(addr, port) self.tSockPort = self.taskSocket.socket.getsockname()[1] # Create identifier for this broker self.name = "{0}:{1}".format(hostname, self.tSockPort) # Initialize broker logging self.logger = utils.initLogging(2 if debug else 0) self.logger.handlers[0].setFormatter( logging.Formatter( "[%(asctime)-15s] %(module)-9s ({0}) %(levelname)-7s " "%(message)s".format(self.name) ) ) self.infoSockPort = 0 addr, port = mSock[6:].split(":", 1) if port == "*": port = 0 else: port = int(port) if addr == "*": addr = "" self.infoSocket = InfoServer(addr, port) self.mSockPort = self.taskSocket.socket.getsockname()[1] # Init connection to fellow brokers #self.clusterSocket = self.context.socket(zmq.DEALER) #self.clusterSocket.setsockopt_string(zmq.IDENTITY, self.getName()) #self.cluster = [] #self.clusterAvailable = set() # Init statistics if self.debug: self.stats = [] # Two cases are important and must be optimised: # - The search of unassigned task # - The search of available workers # These represent when the broker must deal the communications the # fastest. Other cases, the broker isn't flooded with urgent messages. # Initializing the queue of workers and tasks # The busy workers variable will contain a dict (map) of workers: task self.availableWorkers = deque() self.unassignedTasks = deque() self.groupTasks = defaultdict(list) # Shared variables containing {workerID:{varName:varVal},} self.sharedVariables = defaultdict(dict) # Start a worker-like communication if needed self.execQueue = None # Handle cloud-like behavior self.discoveryThread = None self.config = defaultdict(bool) self.processConfig({'headless': headless}) def addBrokerList(self, aBrokerInfoList): """Add a broker to the broker cluster available list. Connects to the added broker if needed.""" self.clusterAvailable.update(set(aBrokerInfoList)) # If we need another connection to a fellow broker # TODO: only connect to a given number for aBrokerInfo in aBrokerInfoList: self.clusterSocket.connect( "tcp://{hostname}:{port}".format( hostname=aBrokerInfo.hostname, port=aBrokerInfo.task_port, ) ) self.cluster.append(aBrokerInfo) def processConfig(self, worker_config): """Update the pool configuration with a worker configuration. """ self.config['headless'] |= worker_config.get("headless", False) if self.config['headless']: # Launch discovery process if not self.discoveryThread: self.discoveryThread = discovery.Advertise( port=",".join(str(a) for a in self.getPorts()), ) def run(self): """Redirects messages until a shutdown message is received. """ asyncore.loop(timeout=0) self.shutdown() def getPorts(self): return (self.tSockPort, self.infoSockPort) def getName(self): import sys if sys.version < '3': return unicode(self.name) return self.name def shutdown(self): # This send may raise an ZMQError # Looping over it until it gets through for i in range(100): try: self.infoSocket.send(SHUTDOWN) except zmq.ZMQError: time.sleep(0.01) else: break time.sleep(0.1) self.taskSocket.close() self.infoSocket.close() #self.context.term() # Write down statistics about this run if asked if self.debug: import os import pickle try: os.mkdir('debug') except: pass name = self.name.replace(":", "_") with open("debug/broker-{name}".format(**locals()), 'wb') as f: pickle.dump(self.stats, f) scoop-0.7.1/scoop/broker/brokerzmq.py000066400000000000000000000247511240127670500176540ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from collections import deque, defaultdict, namedtuple import time import zmq import sys import threading import copy import logging try: import cPickle as pickle except ImportError: import pickle import scoop from scoop import TIME_BETWEEN_PARTIALDEBUG from .. import discovery, utils from .structs import BrokerInfo # Worker requests INIT = b"INIT" REQUEST = b"REQUEST" TASK = b"TASK" REPLY = b"REPLY" SHUTDOWN = b"SHUTDOWN" VARIABLE = b"VARIABLE" BROKER_INFO = b"BROKER_INFO" # Broker interconnection CONNECT = b"CONNECT" class LaunchingError(Exception): pass class Broker(object): def __init__(self, tSock="tcp://*:*", mSock="tcp://*:*", debug=False, headless=False, hostname="127.0.0.1"): """This function initializes a broker. :param tSock: Task Socket Address. Must contain protocol, address and port information. :param mSock: Meta Socket Address. Must contain protocol, address and port information. """ # Initialize zmq self.context = zmq.Context(1) self.debug = debug self.hostname = hostname # zmq Socket for the tasks, replies and request. self.taskSocket = self.context.socket(zmq.ROUTER) self.taskSocket.setsockopt(zmq.ROUTER_BEHAVIOR, 1) self.taskSocket.setsockopt(zmq.LINGER, 1000) self.tSockPort = 0 if tSock[-2:] == ":*": self.tSockPort = self.taskSocket.bind_to_random_port(tSock[:-2]) else: self.taskSocket.bind(tSock) self.tSockPort = tSock.split(":")[-1] # Create identifier for this broker self.name = "{0}:{1}".format(hostname, self.tSockPort) # Initialize broker logging self.logger = utils.initLogging(2 if debug else 0, name=self.name) self.logger.handlers[0].setFormatter( logging.Formatter( "[%(asctime)-15s] %(module)-9s ({0}) %(levelname)-7s " "%(message)s".format(self.name) ) ) # zmq Socket for the pool informations self.infoSocket = self.context.socket(zmq.PUB) self.infoSocket.setsockopt(zmq.LINGER, 1000) self.infoSockPort = 0 if mSock[-2:] == ":*": self.infoSockPort = self.infoSocket.bind_to_random_port(mSock[:-2]) else: self.infoSocket.bind(mSock) self.infoSockPort = mSock.split(":")[-1] self.taskSocket.setsockopt(zmq.SNDHWM, 0) self.taskSocket.setsockopt(zmq.RCVHWM, 0) self.infoSocket.setsockopt(zmq.SNDHWM, 0) self.infoSocket.setsockopt(zmq.RCVHWM, 0) # Init connection to fellow brokers self.clusterSocket = self.context.socket(zmq.DEALER) self.clusterSocket.setsockopt_string(zmq.IDENTITY, self.getName()) self.clusterSocket.setsockopt(zmq.RCVHWM, 0) self.clusterSocket.setsockopt(zmq.SNDHWM, 0) self.cluster = [] self.clusterAvailable = set() # Init statistics if self.debug: self.stats = [] self.lastDebugTs = time.time() # Two cases are important and must be optimised: # - The search of unassigned task # - The search of available workers # These represent when the broker must deal the communications the # fastest. Other cases, the broker isn't flooded with urgent messages. # Initializing the queue of workers and tasks # The busy workers variable will contain a dict (map) of workers: task self.availableWorkers = deque() self.unassignedTasks = deque() self.groupTasks = defaultdict(list) # Shared variables containing {workerID:{varName:varVal},} self.sharedVariables = defaultdict(dict) # Start a worker-like communication if needed self.execQueue = None # Handle cloud-like behavior self.discoveryThread = None self.config = defaultdict(bool) self.processConfig({'headless': headless}) def addBrokerList(self, aBrokerInfoList): """Add a broker to the broker cluster available list. Connects to the added broker if needed.""" self.clusterAvailable.update(set(aBrokerInfoList)) # If we need another connection to a fellow broker # TODO: only connect to a given number for aBrokerInfo in aBrokerInfoList: self.clusterSocket.connect( "tcp://{hostname}:{port}".format( hostname=aBrokerInfo.hostname, port=aBrokerInfo.task_port, ) ) self.cluster.append(aBrokerInfo) def processConfig(self, worker_config): """Update the pool configuration with a worker configuration. """ self.config['headless'] |= worker_config.get("headless", False) if self.config['headless']: # Launch discovery process if not self.discoveryThread: self.discoveryThread = discovery.Advertise( port=",".join(str(a) for a in self.getPorts()), ) def run(self): """Redirects messages until a shutdown message is received. """ while True: if not self.taskSocket.poll(-1): continue msg = self.taskSocket.recv_multipart() msg_type = msg[1] if self.debug: self.stats.append((time.time(), msg_type, len(self.unassignedTasks), len(self.availableWorkers))) if time.time() - self.lastDebugTs > TIME_BETWEEN_PARTIALDEBUG: self.writeDebug("debug/partial-{0}".format( round(time.time(), -1) )) self.lastDebugTs = time.time() # New task inbound if msg_type in TASK: task = msg[2] try: address = self.availableWorkers.popleft() except IndexError: self.unassignedTasks.append(task) else: self.taskSocket.send_multipart([address, TASK, task]) # Request for task elif msg_type == REQUEST: address = msg[0] try: task = self.unassignedTasks.popleft() except IndexError: self.availableWorkers.append(address) else: self.taskSocket.send_multipart([address, TASK, task]) # Answer needing delivery elif msg_type == REPLY: destination = msg[-1] origin = msg[0] self.taskSocket.send_multipart([destination] + msg[1:] + [origin]) # Shared variable to distribute elif msg_type == VARIABLE: address = msg[4] value = msg[3] key = msg[2] self.sharedVariables[address].update( {key: value}, ) self.infoSocket.send_multipart([VARIABLE, key, value, address]) # Initialize the variables of a new worker elif msg_type == INIT: address = msg[0] try: self.processConfig(pickle.loads(msg[2])) except pickle.PickleError: continue self.taskSocket.send_multipart([ address, pickle.dumps(self.config, pickle.HIGHEST_PROTOCOL), pickle.dumps(self.sharedVariables, pickle.HIGHEST_PROTOCOL), ]) self.taskSocket.send_multipart([ address, pickle.dumps(self.clusterAvailable, pickle.HIGHEST_PROTOCOL), ]) # Add a given broker to its fellow list elif msg_type == CONNECT: try: connect_brokers = pickle.loads(msg[2]) except pickle.PickleError: self.logger.error("Could not understand CONNECT message.") continue self.logger.info("Connecting to other brokers...") self.addBrokerList(connect_brokers) # Shutdown of this broker was requested elif msg_type == SHUTDOWN: self.logger.debug("SHUTDOWN command received.") self.shutdown() break def getPorts(self): return (self.tSockPort, self.infoSockPort) def getName(self): import sys if sys.version < '3': return unicode(self.name) return self.name def shutdown(self): # This send may raise an ZMQError # Looping over it until it gets through for i in range(100): try: self.infoSocket.send(SHUTDOWN) except zmq.ZMQError: time.sleep(0.01) else: break time.sleep(0.1) self.taskSocket.close() self.infoSocket.close() #self.context.term() # Write down statistics about this run if asked if self.debug: self.writeDebug() def writeDebug(self, path="debug"): import os import pickle try: os.makedirs(path) except: pass name = self.name.replace(":", "_") with open(os.path.join( path, "broker-{name}".format(**locals())), 'wb') as f: pickle.dump(self.stats, f) scoop-0.7.1/scoop/broker/structs.py000066400000000000000000000017671240127670500173510ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from collections import namedtuple BrokerInfo = namedtuple('BrokerInfo', ['hostname', 'task_port', 'info_port', 'externalHostname'])scoop-0.7.1/scoop/discovery/000077500000000000000000000000001240127670500160005ustar00rootroot00000000000000scoop-0.7.1/scoop/discovery/__init__.py000066400000000000000000000054621240127670500201200ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from . import minusconf import scoop SERVICES_DISCOVERED = [] class SCOOPool(object): def __init__(self, host, ports, name): self._host = host self._ports = ports self._name = name @property def host(self): """Get address of discovered broker. Will convert back to IPv4 accordingly.""" # TODO Is %interfacename working? host = self._host if host[:7] == "::ffff:": # IPv4 address mapped to IPv6, convert back host = host[7:] return host @property def ports(self): return self._ports.split(",") @property def name(self): return self._name def _print_error(seeker, opposite, error_str): import sys sys.stderr.write("Error from {opposite}: {error_str}\n".format( opposite=opposite, error_str=error_str, )) def _seekerCallback(seeker, svca): global SERVICES_DISCOVERED SERVICES_DISCOVERED.append( SCOOPool( svca.addr, svca.port, svca.aname, ) ) scoop.logger.info("Discovery seeker has found a broker.") def Advertise(port, stype="SCOOP", sname="Broker", advertisername="Broker", location=""): """ stype = always SCOOP port = comma separated ports sname = broker unique name location = routable location (ip or dns) """ scoop.logger.info("Launching advertiser...") service = minusconf.Service(stype, port, sname, location) advertiser = minusconf.ThreadAdvertiser([service], advertisername) advertiser.start() scoop.logger.info("Advertiser launched.") return advertiser def Seek(stype="SCOOP", sname="Broker", advertisername=""): scoop.logger.info("Launching discovery seeker...") se = minusconf.Seeker(stype=stype, aname=advertisername, sname=sname, find_callback=_seekerCallback, error_callback=_print_error, ) se.run() # TODO: Remove the timeout and return as soon as something is found. return SERVICES_DISCOVERED scoop-0.7.1/scoop/discovery/minusconf.py000077500000000000000000000560221240127670500203630ustar00rootroot00000000000000#!/usr/bin/env python """ Implementation of the minusconf protocol. See http://code.google.com/p/minusconf/ for details. Apache License 2.0, see the LICENSE file for details. Most users will want a (Thread)Advertiser to advertise their server's location and a Seeker to find out the locations of servers and report them to a client program. """ import struct import socket import threading import time _PORT = 6376 _ADDRESS_4 = '239.45.99.98' _ADDRESS_6 = 'ff08:0:0:6d69:6e75:7363:6f6e:6600' _ADDRESSES = [_ADDRESS_4] if socket.has_ipv6: _ADDRESSES.append(_ADDRESS_6) _CHARSET = 'UTF-8' VERSION='1.0' # Compatibility functions try: if bytes != str: # Python 3+ _compat_bytes = lambda bytestr: bytes(bytestr, 'charmap') else: # 2.6+ _compat_bytes = str except NameError: # <2.6 _compat_bytes = str try: _compat_str = unicode except NameError: # Python 3+ _compat_str = str _MAGIC = _compat_bytes('\xad\xc3\xe6\xe7') _OPCODE_QUERY = _compat_bytes('\x01') _OPCODE_ADVERTISEMENT = _compat_bytes('\x65') _OPCODE_ERROR = _compat_bytes('\x6f') _STRING_TERMINATOR = _compat_bytes('\x00') _TTL = None _MAX_PACKET_SIZE = 2048 # Biggest packet size this implementation will accept""" _SEEKER_TIMEOUT = 2.0 # Timeout for seeks in s class MinusconfError(Exception): def __init__(self, msg=''): super(MinusconfError, self).__init__() self.msg = msg def send(self, sock, to): _send_packet(sock, to, _OPCODE_ERROR, _encode_string(self.msg)) class _ImmutableStruct(object): """ Helper structure for immutable objects """ def __setattr__(self, *args): raise TypeError("This structure is immutable") __delattr__ = __setattr__ def __init__(self, **kwargs): for k,v in kwargs.items(): super(_ImmutableStruct, self).__setattr__(k, v) def __eq__(self, other): return self.__dict__ == other.__dict__ def __ne__(self, other): return self.__dict__ != other.__dict__ def __lt__(self, other): return self.__dict__ < other.__dict__ def __le__(self, other): return self.__dict__ <= other.__dict__ def __gt__(self, other): return self.__dict__ > other.__dict__ def __ge__(self, other): return self.__dict__ >= other.__dict__ def __hash__(self): return hash(sum((hash(i) for i in self.__dict__.items()))) class _MinusconfImmutableStruct(_ImmutableStruct): def __init__(self, **kwargs): for v in kwargs.values(): _check_val(v) super(_MinusconfImmutableStruct, self).__init__(**kwargs) class Service(_MinusconfImmutableStruct): """ Helper structure for a service.""" def __init__(self, stype, port, sname='', location=''): super(Service, self).__init__(stype=stype, port=_compat_str(port), sname=sname, location=location) def matches_query(self, stype, sname): return _string_match(stype, self.stype) and _string_match(sname, self.sname) def __str__(self): res = self.stype + ' service at ' if self.sname != '': res += self.sname + ' ' res += self.location + ':' + self.port return res def __repr__(self): return ('Service(' + repr(self.stype) + ', ' + repr(self.port) + ', ' + repr(self.sname) + ', ' + repr(self.location) + ')') class ServiceAt(_MinusconfImmutableStruct): """ A service returned by an advertiser""" def __init__(self, aname, stype, sname, location, port, addr): super(ServiceAt, self).__init__( aname=aname, stype=stype, sname=sname, location=location, port=port, addr=addr ) def matches_query_at(self, aname, stype, sname): return _string_match(stype, self.stype) and _string_match(sname, self.sname) and _string_match(aname, self.aname) @property def effective_location(self): return self.location if self.location != "" else self.addr def __str__(self): return ( self.stype + ' service at ' + ((self.sname + ' ') if self.sname != '' else '') + self.location + ':' + self.port + ' (advertiser "' + self.aname + '" at ' + self.addr + ')' ) def __repr__(self): return ('ServiceAt(' + repr(self.aname) + ', ' + repr(self.stype) + ', ' + repr(self.sname) + ', ' + repr(self.location) + ', ' + repr(self.port) + ', ' + repr(self.addr) + ')') class Advertiser(object): """ Generic implementation of a -conf advertiser. You will probably want to use one of the subclasses. If ignore_unavailable is set, unsupported addresses (typically IPv6) are silently ignored """ def __init__(self, services=[], aname=None, ignore_unavailable=True): super(Advertiser, self).__init__() self.services = services self.aname = aname if aname != None else socket.gethostname() self.port = _PORT self.addresses = _ADDRESSES self.ignore_unavailable = ignore_unavailable def _set_aname(self, aname): _check_val(aname) self._aname = aname aname = property(fget=lambda self:self._aname, fset=_set_aname) def run(self): self._init_advertiser() while True: rawdata,sender = self._sock.recvfrom(_MAX_PACKET_SIZE) self._handle_packet(rawdata, sender) def _init_advertiser(self): sock = _find_sock() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, struct.pack('@I', 1)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, struct.pack('@I', 1)) if sock.family == socket.AF_INET6: sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, struct.pack('@I', 1)) sock.bind(('', self.port)) addrs = _resolve_addrs(self.addresses, None, self.ignore_unavailable, (sock.family,)) for fam,to,orig_fam,orig_addr in addrs: try: _multicast_join_group(sock, orig_fam, orig_addr) except socket.error: if not self.ignore_unavailable: raise self._sock = sock def _handle_packet(self, rawdata, sender): try: opcode, data = _parse_packet(rawdata) if opcode == _OPCODE_QUERY: self._handle_query(sender, data) elif opcode == _OPCODE_ERROR: pass # Explicitely prevent bouncing errors elif opcode == None: raise MinusconfError('Minusconf magic missing. See http://code.google.com/p/minusconf/source/browse/trunk/protocol.txt for details.') else: raise MinusconfError('Invalid or unsupported opcode ' + str(struct.unpack('!B', opcode)[0])) # Comment out for verbose error handling #except MinusconfError, mce: #mce.send(self._sock, sender) except MinusconfError: pass def services_matching(self, stype, sname): return filter(lambda svc: svc.matches_query(stype, sname), self.services) def _handle_query(self, sender, qrydata): qaname,p = _decode_string(qrydata, 0) qstype,p = _decode_string(qrydata, p) qsname,p = _decode_string(qrydata, p) if _string_match(qaname, self.aname): for svc in self.services_matching(qstype, qsname): rply = ( _encode_string(self.aname) + _encode_string(svc.stype) + _encode_string(svc.sname) + _encode_string(svc.location) + _encode_string(svc.port) ) _send_packet(self._sock, sender, _OPCODE_ADVERTISEMENT, rply) class ConcurrentAdvertiser(Advertiser): # Subclasses must set _cav_started to an event def start_blocking(self): """ Start the advertiser in the background, but wait until it is ready """ self._cav_started.clear() self.start() self._cav_started.wait() def _init_advertiser(self): try: super(ConcurrentAdvertiser, self)._init_advertiser() finally: self._cav_started.set() def wait_until_ready(self, timeout=None): self._cav_started.wait(timeout) def stop(self): raise NotImplementedError() def stop_blocking(self): raise NotImplementedError() class ThreadAdvertiser(ConcurrentAdvertiser, threading.Thread): def __init__(self, services=[], aname=None, ignore_unavailable=True, daemon=True): ConcurrentAdvertiser.__init__(self, services, aname, ignore_unavailable) threading.Thread.__init__(self) self.setDaemon(daemon) self._cav_started = self._createEvent() self._ta_should_stop = self._createEvent() def run(self): self._ta_should_stop.clear() self._init_advertiser() while True: rawdata,sender = self._sock.recvfrom(_MAX_PACKET_SIZE) if self._ta_should_stop.is_set(): break self._handle_packet(rawdata, sender) def stop(self): self._ta_should_stop.set() def stop_blocking(self): """ Stop the service and wait for it to be cleaned up. """ self.stop() # The thread will be there, but will terminate upon the next message @staticmethod def _createEvent(): res = threading.Event() if not hasattr(res, 'is_set'): # Python<2.6 res.is_set = res.isSet return res try: import multiprocessing class MultiprocessingAdvertiser(ConcurrentAdvertiser, multiprocessing.Process): """ multiprocessing is only available for Python 2.6+. See http://code.google.com/p/python-multiprocessing/ for a backport. """ def __init__(self, services=[], aname=None, ignore_unavailable=True, daemon=True): ConcurrentAdvertiser.__init__(self, services, aname, ignore_unavailable) multiprocessing.Process.__init__(self) self.daemon = daemon self._cav_started = multiprocessing.Event() self._mpa_manager = multiprocessing.Manager() self.services = self._mpa_manager.list(services) def stop(self): self.terminate() def stop_blocking(self): self.stop() self.join() except ImportError: pass class Seeker(threading.Thread): """ find_callback is called with (this_seeker,found_service_at) error_callback is called with (this seeker, sender, error message) """ def __init__(self, stype='', aname='', sname='', timeout=_SEEKER_TIMEOUT, port=_PORT, addresses=_ADDRESSES, find_callback=None, error_callback=None, daemonized=True, ignore_senderrors=True): super(Seeker, self).__init__() self.timeout = timeout self.port = port self.addresses = addresses self.find_callback = find_callback self.error_callback = error_callback self.setDaemon(daemonized) self.ignore_senderrors = ignore_senderrors self.reset(stype, aname, sname) def reset(self, stype='', aname='', sname=''): self.stype = stype self.aname = aname self.sname = sname def _set_stype(self, stype): _check_val(stype) self._stype = stype stype = property(fget=lambda self:self._stype, fset=_set_stype) def _set_aname(self, aname): _check_val(aname) self._aname = aname aname = property(fget=lambda self:self._aname, fset=_set_aname) def _set_sname(self, sname): _check_val(sname) self._sname = sname sname = property(fget=lambda self:self._sname, fset=_set_sname) def run(self): self._init_seeker() if self._send_queries() > 0: self._read_replies() def run_forever(self): self.timeout = None self.run() def _init_seeker(self): self.results = set() self._sock = _find_sock() _multicast_configure_sender(self._sock, _TTL) def _send_queries(self): """ Sends queries to multiple addresses. Returns the number of successful queries. """ res = 0 addrs = _resolve_addrs(self.addresses, self.port, self.ignore_senderrors, [self._sock.family]) for addr in addrs: try: self._send_query(addr[1]) res += 1 except: if not self.ignore_senderrors: raise return res def _send_query(self, to): binqry = _encode_string(self.aname) binqry += _encode_string(self.stype) binqry += _encode_string(self.sname) _send_packet(self._sock, to, _OPCODE_QUERY, binqry) def _read_replies(self): if self.timeout == None: self._sock.settimeout(None) else: starttime = time.time() while True: if self.timeout != None: timeout = self.timeout - (time.time() - starttime) if timeout < 0: break self._sock.settimeout(timeout) try: rawdata,sender = self._sock.recvfrom(_MAX_PACKET_SIZE) except socket.timeout: break self._handle_packet(rawdata, sender) def _handle_packet(self, rawdata, sender): try: opcode,data = _parse_packet(rawdata) if opcode == _OPCODE_ADVERTISEMENT: self._handle_advertisement(data, sender) elif opcode == _OPCODE_ERROR: try: error_str = _decode_string(data, 0)[0] except: error_str = '[Error when trying to read error message ' + repr(data) + ']' if self.error_callback != None: self.error_callback(self, sender, error_str) else: # Invalid opcode pass except MinusconfError: # Invalid packet pass def _handle_advertisement(self, bindata, sender): aname,p = _decode_string(bindata, 0) stype,p = _decode_string(bindata, p) sname,p = _decode_string(bindata, p) location,p = _decode_string(bindata, p) port,p = _decode_string(bindata, p) if stype == '': # servicetype must be non-empty return svca = ServiceAt(aname, stype, sname, location, port, sender[0]) if svca.matches_query_at(self.aname, self.stype, self.sname): self._found_result(svca) def _found_result(self, result): if not (result in self.results): self.results.add(result) if self.find_callback != None: self.find_callback(self, result) def _send_packet(sock, to, opcode, data): sock.sendto(_MAGIC + opcode + data, 0, to) def _parse_packet(rawdata): """ Returns a tupel (opcode, minusconf-data). opcode is None if this isn't a -conf packet.""" if (len(rawdata) < len(_MAGIC) + 1) or (_MAGIC != rawdata[:len(_MAGIC)]): # Wrong protocol return (None, None) opcode = rawdata[len(_MAGIC):len(_MAGIC)+1] payload = rawdata[len(_MAGIC)+1:] return (opcode, payload) def _check_val(val): """ Checks whether a minusconf value contains any NUL bytes. """ try: if val.find('\x00') >= 0: raise ValueError(repr(val) + ' contains a NUL byte') except AttributeError: # Not a string or compatible pass def _encode_string(val): return val.encode(_CHARSET) + _STRING_TERMINATOR def _decode_string(buf, pos): """ Decodes a string in the buffer buf, starting at position pos. Returns a tupel of the read string and the next byte to read. """ for i in range(pos, len(buf)): if buf[i:i+1] == _compat_bytes('\x00'): try: return (buf[pos:i].decode(_CHARSET), i+1) # Uncomment the following two lines for detailled information #except UnicodeDecodeError as ude: # raise MinusconfError(str(ude)) except UnicodeDecodeError: raise MinusconfError('Not a valid ' + _CHARSET + ' string: ' + repr(buf[pos:i])) raise MinusconfError("Premature end of string (Forgot trailing \\0?), buf=" + repr(buf)) def _string_match(query, value): return query == "" or query == value def _multicast_configure_sender(sock, ttl=None): if ttl != None: ttl_bin = struct.pack('@I', ttl) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl_bin) if sock.family == socket.AF_INET6: sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, ttl_bin) def _multicast_join_group(sock, family, addr): group_bin = _inet_pton(family, addr) if family == socket.AF_INET: # IPv4 mreq = group_bin + struct.pack('=I', socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) elif family == socket.AF_INET6: # IPv6 mreq = group_bin + struct.pack('@I', 0) sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) else: raise ValueError('Unsupported protocol family ' + family) def _resolve_addrs(straddrs, port, ignore_unavailable=False, protocols=[socket.AF_INET, socket.AF_INET6]): """ Returns a tupel of tupels of (family, to, original_addr_family, original_addr). If ignore_unavailable is set, addresses for unavailable protocols are ignored. protocols determines the protocol family indices supported by the socket in use. """ res = [] for sa in straddrs: try: ais = socket.getaddrinfo(sa, port) for ai in ais: if ai[0] in protocols: res.append((ai[0], ai[4], ai[0], ai[4][0])) break else: # Try to convert from IPv4 to IPv6 ai = ais[0] if ai[0] == socket.AF_INET and socket.AF_INET6 in protocols: to = socket.getaddrinfo('::ffff:' + ai[4][0], port, socket.AF_INET6)[0][4] res.append((socket.AF_INET6, to, ai[0], ai[4][0])) except socket.gaierror: if not ignore_unavailable: raise return res def _find_sock(): """ Create a UDP socket """ if socket.has_ipv6: try: return socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) except socket.gaierror: pass # Platform lied about IPv6 support return socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def _main(): """ CLI interface """ import sys if len(sys.argv) < 2: _usage('Expected at least one parameter!') sc = sys.argv[1] options = sys.argv[2:] if sc == 'a' or sc == 'advertise': if len(options) > 5 or len(options) < 2: _usage() stype,port = options[:2] advertisername = options[2] if len(options) > 2 else None sname = options[3] if len(options) > 3 else '' slocation = options[4] if len(options) > 4 else '' service = Service(stype, port, sname, slocation) advertiser = Advertiser([service], advertisername) advertiser.run() elif sc == 's' or sc == 'seek': if len(options) > 4: _usage() aname = options[0] if len(options) > 0 else '' stype = options[1] if len(options) > 1 else '' sname = options[2] if len(options) > 2 else '' se = Seeker(aname, stype, sname, find_callback=_print_result, error_callback=_print_error) se.run() else: _usage('Unknown subcommand "' + sys.argv[0] + '"') def _print_result(seeker, svca): print ("Found " + str(svca)) def _print_error(seeker, opposite, error_str): import sys sys.stderr.write("Error from " + str(opposite) + ": " + error_str + "\n") def _usage(note=None, and_exit=True): import sys if note != None: print("Error: " + note + "\n") print("Usage: " + sys.argv[0] + " subcommand options...") print("\ta[dvertise] servicetype port [advertisername [servicename [location]]]") print("\ts[eek] [servicetype [advertisername [servicename]]]") print('Use "" for default/any value.') print("Examples:") print("\t" + sys.argv[0] + " advertise http 80 fastmachine Apache") print("\t" + sys.argv[0] + ' seek http "" Apache') if and_exit: sys.exit(0) def _compat_inet_pton(family, addr): """ socket.inet_pton for platforms that don't have it """ if family == socket.AF_INET: # inet_aton accepts some strange forms, so we use our own res = _compat_bytes('') parts = addr.split('.') if len(parts) != 4: raise ValueError('Expected 4 dot-separated numbers') for part in parts: intval = int(part, 10) if intval < 0 or intval > 0xff: raise ValueError("Invalid integer value in IPv4 address: " + str(intval)) res = res + struct.pack('!B', intval) return res elif family == socket.AF_INET6: wordcount = 8 res = _compat_bytes('') # IPv4 embedded? dotpos = addr.find('.') if dotpos >= 0: v4start = addr.rfind(':', 0, dotpos) if v4start == -1: raise ValueException("Missing colons in an IPv6 address") wordcount = 6 res = socket.inet_aton(addr[v4start+1:]) addr = addr[:v4start] + '!' # We leave a marker that the address is not finished # Compact version? compact_pos = addr.find('::') if compact_pos >= 0: if compact_pos == 0: addr = '0' + addr compact_pos += 1 if compact_pos == len(addr)-len('::'): addr = addr + '0' addr = (addr[:compact_pos] + ':' + ('0:' * (wordcount - (addr.count(':') - '::'.count(':')) - 2)) + addr[compact_pos + len('::'):]) # Remove any dots we left if addr.endswith('!'): addr = addr[:-len('!')] words = addr.split(':') if len(words) != wordcount: raise ValueError('Invalid number of IPv6 hextets, expected ' + str(wordcount) + ', got ' + str(len(words))) for w in reversed(words): # 0x and negative is not valid here, but accepted by int(,16) if 'x' in w or '-' in w: raise ValueError("Invalid character in IPv6 address") intval = int(w, 16) if intval > 0xffff: raise ValueError("IPv6 address componenent too big") res = struct.pack('!H', intval) + res return res else: raise ValueError("Unknown protocol family " + family) # Cover for socket_pton inavailability on some systems (non-IPv6 or Windows) try: import ipaddr if hasattr(ipaddr.IPv4, 'packed'): def _inet_pton(family, addr): if family == socket.AF_INET: return ipaddr.IPv4(addr).packed elif family == socket.AF_INET6: return ipaddr.IPv6(addr).packed else: raise ValueError("Unknown protocol family " + family) except: pass if not '_inet_pton' in dir(): if hasattr(socket, 'inet_pton'): _inet_pton = socket.inet_pton else: _inet_pton = _compat_inet_pton if __name__ == '__main__': _main() scoop-0.7.1/scoop/encapsulation.py000066400000000000000000000136011240127670500172110ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import marshal import tempfile import types import os from inspect import ismodule from functools import partial try: import cPickle as pickle except ImportError: import pickle try: import copyreg from io import BytesIO as FileLikeIO from io import BufferedReader as FileType except ImportError: # Support for Python 2.X import copy_reg as copyreg from StringIO import StringIO as FileLikeIO from types import FileType as FileType import scoop def functionFactory(in_code, name, defaults, globals_, imports): """Creates a function at runtime using binary compiled inCode""" def generatedFunction(): pass generatedFunction.__code__ = marshal.loads(in_code) generatedFunction.__name__ = name generatedFunction.__defaults = defaults generatedFunction.__globals__.update(pickle.loads(globals_)) for key, value in imports.items(): imported_module = __import__(value) scoop.logger.debug("Dynamically loaded module {0}".format(value)) generatedFunction.__globals__.update({key: imported_module}) return generatedFunction class FunctionEncapsulation(object): """Encapsulates a function in a serializable way. This is used by the sharing module (setConst). Used for lambda functions and function defined on-the-fly (interactive shell)""" def __init__(self, in_func, name): """Creates a serializable (picklable) object of a function""" self.code = marshal.dumps(in_func.__code__) self.name = name self.defaults = in_func.__defaults__ # Pickle references to functions used in the function used_globals = {} # name: function used_modules = {} # used name: origin module name for key, value in in_func.__globals__.items(): if key in in_func.__code__.co_names: if ismodule(value): used_modules[key] = value.__name__ else: used_globals[key] = value self.globals = pickle.dumps(used_globals, pickle.HIGHEST_PROTOCOL) self.imports = used_modules def __call__(self, *args, **kwargs): """Called by local worker (which doesn't _communicate this class)""" return self.getFunction()(*args, **kwargs) def __name__(self): return self.name def getFunction(self): """Called by remote workers. Useful to populate main module globals() for interactive shells. Retrieves the serialized function.""" return functionFactory( self.code, self.name, self.defaults, self.globals, self.imports, ) class ExternalEncapsulation(object): """Encapsulates an arbitrary file in a serializable way""" def __init__(self, in_filepath): """Creates a serializable (picklable) object of inFilePath""" self.filename = os.path.basename(in_filepath) with open(in_filepath, "rb") as fhdl: self.data = pickle.dumps(fhdl, pickle.HIGHEST_PROTOCOL) def writeFile(self, directory=None): """Writes back the file to a temporary path (optionaly specified)""" if directory: # If a directory was specified full_path = os.path.join(directory, self.filename) with open(full_path, 'wb') as f: f.write(pickle.loads(self.data).read()) return full_path # if no directory was specified, create a temporary file this_file = tempfile.NamedTemporaryFile(delete=False) this_file.write(pickle.loads(self.data).read()) this_file.close() return this_file.name # The following block handles callables pickling and unpickling # TODO: Make a factory to generate unpickling functions def unpickleLambda(pickled_callable): # TODO: Set globals to user module return types.LambdaType(marshal.loads(pickled_callable), globals()) def unpickleMethodType(pickled_callable): # TODO: Set globals to user module return types.MethodType(marshal.loads(pickled_callable), globals()) def pickleCallable(callable_, unpickle_func): # TODO: Pickle also argdefs and closure return unpickle_func, (marshal.dumps(callable_.__code__), ) pickle_lambda = partial(pickleCallable, unpickle_func=unpickleLambda) pickle_method = partial(pickleCallable, unpickle_func=unpickleMethodType) def makeLambdaPicklable(lambda_function): """Take input lambda function l and makes it picklable.""" if isinstance(lambda_function, type(lambda: None)) and lambda_function.__name__ == '': def __reduce_ex__(proto): # TODO: argdefs, closure return unpickleLambda, (marshal.dumps(lambda_function.__code__), ) lambda_function.__reduce_ex__ = __reduce_ex__ return lambda_function # The following block handles file-like objects pickling and unpickling def unpickleFileLike(position, data): file_ = FileLikeIO(data) file_.seek(position) return file_ def pickleFileLike(file_): position = file_.tell() file_.seek(0) data = file_.read() file_.seek(position) return unpickleFileLike, (position, data) copyreg.pickle(FileType, pickleFileLike, unpickleFileLike) scoop-0.7.1/scoop/fallbacks.py000066400000000000000000000053041240127670500162670ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # """Module containing builins fallbacks or exceptions when SCOOP is not started properly.""" import sys import warnings from functools import wraps class NotStartedProperly(Exception): """SCOOP was not started properly""" pass def ensureScoopStartedProperlyMapFallback(func): @wraps(func) def wrapper(*args, **kwargs): futures_not_loaded = 'scoop.futures' not in sys.modules controller_not_started = not ( sys.modules['scoop.futures'].__dict__.get("_controller", None) ) if futures_not_loaded or controller_not_started: if not hasattr(ensureScoopStartedProperlyMapFallback, "already"): warnings.warn( "SCOOP was not started properly.\n" "Be sure to start your program with the " "'-m scoop' parameter. You can find " "further information in the " "documentation.\n" "Your map call has been replaced by the builtin " "serial Python map().", RuntimeWarning ) ensureScoopStartedProperlyMapFallback.already = True return map(*args, **kwargs) return func(*args, **kwargs) return wrapper def ensureScoopStartedProperly(func): def wrapper(*args, **kwargs): futures_not_loaded = 'scoop.futures' not in sys.modules controller_not_started = not ( sys.modules['scoop.futures'].__dict__.get("_controller", None) ) if futures_not_loaded or controller_not_started: raise NotStartedProperly("SCOOP was not started properly.\n" "Be sure to start your program with the " "'-m scoop' parameter. You can find " "further information in the " "documentation.") return func(*args, **kwargs) return wrapperscoop-0.7.1/scoop/futures.py000066400000000000000000000441041240127670500160430ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import os from inspect import ismethod from collections import namedtuple, Iterable from functools import partial, reduce import itertools import copy import time import scoop from ._types import Future, CallbackType from . import _control as control from .fallbacks import ( ensureScoopStartedProperlyMapFallback, ensureScoopStartedProperly, NotStartedProperly ) # Constants stated by PEP 3148 (http://www.python.org/dev/peps/pep-3148/#module-functions) FIRST_COMPLETED = 'FIRST_COMPLETED' FIRST_EXCEPTION = 'FIRST_EXCEPTION' ALL_COMPLETED = 'ALL_COMPLETED' _AS_COMPLETED = '_AS_COMPLETED' # This is the greenlet for running the controller logic. _controller = None callbackGroupID = itertools.count() def _startup(rootFuture, *args, **kargs): """Initializes the SCOOP environment. :param rootFuture: Any callable object (function or class object with *__call__* method); this object will be called once and allows the use of parallel calls inside this object. :param args: A tuple of positional arguments that will be passed to the callable object. :param kargs: A dictionary of additional keyword arguments that will be passed to the callable object. :returns: The result of the root Future. Be sure to launch your root Future using this method.""" import greenlet global _controller _controller = greenlet.greenlet(control.runController) try: result = _controller.switch(rootFuture, *args, **kargs) except scoop._comm.Shutdown: result = None control.execQueue.shutdown() if scoop.DEBUG: from scoop import _debug _debug.writeWorkerDebug(control.debug_stats, control.QueueLength) return result def _mapFuture(callable_, *iterables): """Similar to the built-in map function, but each of its iteration will spawn a separate independent parallel Future that will run either locally or remotely as `callable(*args)`. :param callable: Any callable object (function or class object with *__call__* method); this object will be called to execute each Future. :param iterables: A tuple of iterable objects; each will be zipped to form an iterable of arguments tuples that will be passed to the callable object as a separate Future. :returns: A list of Future objects, each corresponding to an iteration of map. On return, the Futures are pending execution locally, but may also be transfered remotely depending on global load. Execution may be carried on with any further computations. To retrieve the map results, you need to either wait for or join with the spawned Futures. See functions waitAny, waitAll, or joinAll. Alternatively, You may also use functions mapWait or mapJoin that will wait or join before returning.""" childrenList = [] for args in zip(*iterables): childrenList.append(submit(callable_, *args)) return childrenList def _mapGenerator(futures): """Generator function that iterates through the results in-order.""" for future in _waitAll(*futures): yield future.resultValue @ensureScoopStartedProperlyMapFallback def map(func, *iterables, **kwargs): """map(func, *iterables) Equivalent to `map(func, \*iterables, ...) `_ but *func* is executed asynchronously and several calls to func may be made concurrently. This non-blocking call returns an iterator which raises a TimeoutError if *__next__()* is called and the result isn't available after timeout seconds from the original call to *map()*. If timeout is not specified or None then there is no limit to the wait time. If a call raises an exception then that exception will be raised when its value is retrieved from the iterator. :param func: Any picklable callable object (function or class object with *__call__* method); this object will be called to execute the Futures. The callable must return a value. :param iterables: Iterable objects; each will be zipped to form an iterable of arguments tuples that will be passed to the callable object as a separate Future. :param timeout: The maximum number of seconds to wait. If None, then there is no limit on the wait time. :returns: A generator of map results, each corresponding to one map iteration.""" # TODO: Handle timeout futures = _mapFuture(func, *iterables) return _mapGenerator(futures) def map_as_completed(func, *iterables, **kwargs): """map_as_completed(func, *iterables) Equivalent to map, but the results are returned as soon as they are made available. :param func: Any picklable callable object (function or class object with *__call__* method); this object will be called to execute the Futures. The callable must return a value. :param iterables: Iterable objects; each will be zipped to form an iterable of arguments tuples that will be passed to the callable object as a separate Future. :param timeout: The maximum number of seconds to wait. If None, then there is no limit on the wait time. :returns: A generator of map results, each corresponding to one map iteration.""" # TODO: Handle timeout for future in as_completed(_mapFuture(func, *iterables)): yield future.resultValue def _recursiveReduce(mapFunc, reductionFunc, scan, *iterables): """Generates the recursive reduction tree. Used by mapReduce.""" if iterables: half = min(len(x) // 2 for x in iterables) data_left = [list(x)[:half] for x in iterables] data_right = [list(x)[half:] for x in iterables] else: data_left = data_right = [[]] # Submit the left and right parts of the reduction out_futures = [None, None] out_results = [None, None] for index, data in enumerate([data_left, data_right]): if any(len(x) <= 1 for x in data): out_results[index] = mapFunc(*list(zip(*data))[0]) else: out_futures[index] = submit( _recursiveReduce, mapFunc, reductionFunc, scan, *data ) # Wait for the results for index, future in enumerate(out_futures): if future: out_results[index] = future.result() # Apply a scan if needed if scan: last_results = copy.copy(out_results) if type(out_results[0]) is not list: out_results[0] = [out_results[0]] else: last_results[0] = out_results[0][-1] if type(out_results[1]) is list: out_results[0].extend(out_results[1][:-1]) last_results[1] = out_results[1][-1] out_results[0].append(reductionFunc(*last_results)) return out_results[0] return reductionFunc(*out_results) @ensureScoopStartedProperly def mapScan(mapFunc, reductionFunc, *iterables, **kwargs): """Exectues the :meth:`~scoop.futures.map` function and then applies a reduction function to its result while keeping intermediate reduction values. This is a blocking call. :param mapFunc: Any picklable callable object (function or class object with *__call__* method); this object will be called to execute the Futures. The callable must return a value. :param reductionFunc: Any picklable callable object (function or class object with *__call__* method); this object will be called to reduce pairs of Futures results. The callable must support two parameters and return a single value. :param iterables: Iterable objects; each will be zipped to form an iterable of arguments tuples that will be passed to the mapFunc object as a separate Future. :param timeout: The maximum number of seconds to wait. If None, then there is no limit on the wait time. :returns: Every return value of the reduction function applied to every mapped data sequentially ordered.""" return submit( _recursiveReduce, mapFunc, reductionFunc, True, *iterables ).result() @ensureScoopStartedProperly def mapReduce(mapFunc, reductionFunc, *iterables, **kwargs): """Exectues the :meth:`~scoop.futures.map` function and then applies a reduction function to its result. The reduction function will cumulatively merge the results of the map function in order to get a single final value. This call is blocking. :param mapFunc: Any picklable callable object (function or class object with *__call__* method); this object will be called to execute the Futures. The callable must return a value. :param reductionFunc: Any picklable callable object (function or class object with *__call__* method); this object will be called to reduce pairs of Futures results. The callable must support two parameters and return a single value. :param iterables: Iterable objects; each will be zipped to form an iterable of arguments tuples that will be passed to the callable object as a separate Future. :param timeout: The maximum number of seconds to wait. If None, then there is no limit on the wait time. :returns: A single value.""" return submit( _recursiveReduce, mapFunc, reductionFunc, False, *iterables ).result() def _createFuture(func, *args): """Helper function to create a future.""" assert callable(func), ( "The provided func parameter is not a callable." ) # If function is a lambda or class method, share it (or its parent object) # beforehand lambdaType = type(lambda: None) funcIsLambda = isinstance(func, lambdaType) and func.__name__ == '' # Determine if function is a method. Methods derived from external # languages such as C++ aren't detected by ismethod. funcIsMethod = ismethod(func) if funcIsLambda or funcIsMethod: from .shared import SharedElementEncapsulation func = SharedElementEncapsulation(func) return Future(control.current.id, func, *args) @ensureScoopStartedProperly def submit(func, *args): """Submit an independent asynchronous :class:`~scoop._types.Future` that will either run locally or remotely as `func(*args)`. :param func: Any picklable callable object (function or class object with *__call__* method); this object will be called to execute the Future. The callable must return a value. :param args: A tuple of positional arguments that will be passed to the func object. :returns: A future object for retrieving the Future result. On return, the Future can be pending execution locally but may also be transfered remotely depending on load or on remote distributed workers. You may carry on with any further computations while the Future completes. Result retrieval is made via the :meth:`~scoop._types.Future.result` function on the Future.""" child = _createFuture(func, *args) control.futureDict[control.current.id].children[child] = None control.execQueue.append(child) return child def _waitAny(*children): """Waits on any child Future created by the calling Future. :param children: A tuple of children Future objects spawned by the calling Future. :return: A generator function that iterates on futures that are done. The generator produces results of the children in a non deterministic order that depends on the particular parallel execution of the Futures. The generator returns a tuple as soon as one becomes available.""" n = len(children) # check for available results and index those unavailable for index, future in enumerate(children): if future.exceptionValue: raise future.exceptionValue if future._ended(): future._delete() yield future n -= 1 else: future.index = index future = control.current while n > 0: # wait for remaining results; switch to controller future.stopWatch.halt() childFuture = _controller.switch(future) future.stopWatch.resume() if childFuture.exceptionValue: raise childFuture.exceptionValue # Only yield if executed future was in children, otherwise loop if childFuture in children: childFuture._delete() yield childFuture n -= 1 def _waitAll(*children): """Wait on all child futures specified by a tuple of previously created Future. :param children: A tuple of children Future objects spawned by the calling Future. :return: A generator function that iterates on Future results. The generator produces results in the order that they are specified by the children argument. Because Futures are executed in a non deterministic order, the generator may have to wait for the last result to become available before it can produce an output. See waitAny for an alternative option.""" for future in children: for f in _waitAny(future): yield f def wait(fs, timeout=-1, return_when=ALL_COMPLETED): """Wait for the futures in the given sequence to complete. Using this function may prevent a worker from executing. :param fs: The sequence of Futures to wait upon. :param timeout: The maximum number of seconds to wait. If negative or not specified, then there is no limit on the wait time. :param return_when: Indicates when this function should return. The options are: =============== ================================================ FIRST_COMPLETED Return when any future finishes or is cancelled. FIRST_EXCEPTION Return when any future finishes by raising an exception. If no future raises an exception then it is equivalent to ALL_COMPLETED. ALL_COMPLETED Return when all futures finish or are cancelled. =============== ================================================ :return: A named 2-tuple of sets. The first set, named 'done', contains the futures that completed (is finished or cancelled) before the wait completed. The second set, named 'not_done', contains uncompleted futures.""" DoneAndNotDoneFutures = namedtuple('DoneAndNotDoneFutures', 'done not_done') if timeout < 0: # Negative timeout means blocking. if return_when == FIRST_COMPLETED: next(_waitAny(*fs)) elif return_when in [ALL_COMPLETED, FIRST_EXCEPTION]: for _ in _waitAll(*fs): pass done = set(f for f in fs if f.done()) not_done = set(fs) - done return DoneAndNotDoneFutures(done, not_done) elif timeout == 0: # Zero-value entry means non-blocking control.execQueue.flush() control.execQueue.updateQueue() done = set(f for f in fs if f._ended()) not_done = set(fs) - done return DoneAndNotDoneFutures(done, not_done) else: # Any other value means blocking for a given time. done = set() start_time = time.time() while time.time() - start_time < timeout: # Flush futures on local queue (to be executed remotely) control.execQueue.flush() # Block until data arrives (to free CPU time) control.execQueue.socket._poll(time.time() - start_time) # Update queue control.execQueue.updateQueue() for f in fs: if f._ended(): done.add(f) not_done = set(fs) - done if return_when == FIRST_COMPLETED and len(done) > 0: break if len(not_done) == 0: break return DoneAndNotDoneFutures(done, not_done) def as_completed(fs, timeout=None): """Iterates over the given futures that yields each as it completes. This call is blocking. :param fs: The sequence of Futures to wait upon. :param timeout: The maximum number of seconds to wait. If None, then there is no limit on the wait time. :return: An iterator that yields the given Futures as they complete (finished or cancelled). """ #TODO: Handle timeout return _waitAny(*fs) def _join(child): """This private function is for joining the current Future with one of its child Future. :param child: A child Future object spawned by the calling Future. :return: The result of the child Future. Only one Future can be specified. The function returns a single corresponding result as soon as it becomes available.""" for future in _waitAny(child): return future.resultValue def _joinAll(*children): """This private function is for joining the current Future with all of the children Futures specified in a tuple. :param children: A tuple of children Future objects spawned by the calling Future. :return: A list of corresponding results for the children Futures. This function will wait for the completion of all specified child Futures before returning to the caller.""" return [_join(future) for future in _waitAll(*children)] def shutdown(wait=True): """This function is here for compatibility with `futures` (PEP 3148) and doesn't have any behavior. :param wait: Unapplied parameter.""" pass scoop-0.7.1/scoop/launch/000077500000000000000000000000001240127670500152435ustar00rootroot00000000000000scoop-0.7.1/scoop/launch/__init__.py000066400000000000000000000014741240127670500173620ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from .workerLaunch import Host from .brokerLaunch import localBroker, remoteBroker scoop-0.7.1/scoop/launch/brokerLaunch.py000066400000000000000000000210611240127670500202340ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from threading import Thread import subprocess import shlex import sys import os try: import cPickle as pickle except ImportError: import pickle import scoop try: import psutil except ImportError: psutil = None class localBroker(object): def __init__(self, debug, nice=0, backend='ZMQ'): """Starts a broker on random unoccupied ports""" self.backend = backend if backend == 'ZMQ': from ..broker.brokerzmq import Broker else: from ..broker.brokertcp import Broker if nice: if not psutil: scoop.logger.error("'nice' used while psutil not installed.") raise ImportError("psutil is needed for nice functionnality.") p = psutil.Process(os.getpid()) p.set_nice(nice) self.localBroker = Broker(debug=debug) self.brokerPort, self.infoPort = self.localBroker.getPorts() self.broker = Thread(target=self.localBroker.run) self.broker.daemon = True self.broker.start() scoop.logger.debug("Local broker launched on ports {0}, {1}" ".".format(self.brokerPort, self.infoPort)) def sendConnect(self, data): """Send a CONNECT command to the broker :param data: List of other broker main socket URL""" # Imported dynamically - Not used if only one broker if self.backend == 'ZMQ': import zmq self.context = zmq.Context() self.socket = self.context.socket(zmq.DEALER) self.socket.setsockopt(zmq.IDENTITY, b'launcher') self.socket.connect( "tcp://127.0.0.1:{port}".format( port=self.brokerPort, ) ) self.socket.send_multipart([b"CONNECT", pickle.dumps(data, pickle.HIGHEST_PROTOCOL)]) else: # TODO pass def getHost(self): return "127.0.0.1" def getPorts(self): return (self.brokerPort, self.infoPort) def close(self): scoop.logger.debug('Closing local broker.') class remoteBroker(object): BASE_SSH = ['ssh', '-x', '-n', '-oStrictHostKeyChecking=no'] def __init__(self, hostname, pythonExecutable, debug=False, nice=0, backend='ZMQ'): """Starts a broker on the specified hostname on unoccupied ports""" self.backend = backend brokerString = ("{pythonExec} -m scoop.broker.__main__ " "--echoGroup " "--echoPorts " "--backend {backend} ".format( pythonExec=pythonExecutable, backend=backend, ) ) if nice: brokerString += "--nice {nice} ".format(nice=nice) if debug: brokerString += "--debug --path {path} ".format( path=os.getcwd() ) self.hostname = hostname for i in range(5000, 10000, 2): cmd = self.BASE_SSH + [ hostname, brokerString.format(brokerPort=i, infoPort=i + 1, pythonExec=pythonExecutable, ) ] scoop.logger.debug("Launching remote broker: {cmd}" "".format(cmd=" ".join(cmd))) self.shell = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # TODO: This condition is not doing what it's supposed if self.shell.poll() is not None: continue else: self.brokerPort, self.infoPort = i, i + 1 break else: raise Exception("Could not successfully launch the remote broker.") # Get remote process group ID try: self.remoteProcessGID = int(self.shell.stdout.readline().strip()) except ValueError: self.remoteProcessGID = None # Get remote ports receivedLine = self.shell.stdout.readline() try: ports = receivedLine.decode().strip().split(",") self.brokerPort, self.infoPort = ports except ValueError: # Following line for Python 2.6 compatibility (instead of [as e]) e = sys.exc_info()[1] # Terminate the process, otherwide reading from stderr may wait # undefinitely self.shell.terminate() stderr = self.shell.stderr.read() raise Exception("Could not successfully launch the remote broker.\n" "Requested remote broker ports, received:\n" "{receivedLine}\n" "Port number decoding error:\n{e}\n" "SSH process stderr:\n{stderr}".format(**locals())) scoop.logger.debug("Foreign broker launched on ports {0}, {1} of host {2}" ".".format(self.brokerPort, self.infoPort, hostname, ) ) def sendConnect(self, data): """Send a CONNECT command to the broker :param data: List of other broker main socket URL""" # Imported dynamically - Not used if only one broker if self.backend == 'ZMQ': import zmq self.context = zmq.Context() self.socket = self.context.socket(zmq.DEALER) if sys.version_info < (3,): self.socket.setsockopt_string(zmq.IDENTITY, unicode('launcher')) else: self.socket.setsockopt_string(zmq.IDENTITY, 'launcher') self.socket.connect( "tcp://{hostname}:{port}".format( port=self.brokerPort, hostname = self.hostname ) ) self.socket.send_multipart([b"CONNECT", pickle.dumps(data, pickle.HIGHEST_PROTOCOL)]) else: # TODO pass def getHost(self): return self.hostname def getPorts(self): return (self.brokerPort, self.infoPort) def isLocal(self): """Is the current broker on the localhost?""" # This exists for further fusion with localBroker return False # return self.hostname in utils.localHostnames def close(self): """Connection(s) cleanup.""" # TODO: DRY with workerLaunch.py # Ensure everything is cleaned up on exit scoop.logger.debug('Closing broker on host {0}.'.format(self.hostname)) # Terminate subprocesses try: self.shell.terminate() except OSError: pass # Send termination signal to remaining workers if not self.isLocal() and self.remoteProcessGID is None: scoop.logger.warn( "Zombie process(es) possibly left on host {0}!" "".format(self.hostname) ) elif not self.isLocal(): command = ("python -c " "'import os, signal; os.killpg({0}, signal.SIGKILL)' " ">&/dev/null").format(self.remoteProcessGID) subprocess.Popen(self.BASE_SSH + [self.hostname] + [command], ).wait() # Output child processes stdout and stderr to console sys.stdout.write(self.shell.stdout.read().decode("utf-8")) sys.stdout.flush() sys.stderr.write(self.shell.stderr.read().decode("utf-8")) sys.stderr.flush() scoop-0.7.1/scoop/launch/workerLaunch.py000066400000000000000000000244431240127670500202700ustar00rootroot00000000000000# # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # # Global imports from collections import namedtuple import logging import sys import subprocess # Local import scoop from scoop import utils class Host(object): """Represents an accessible computing resource. Can be remote (ssh via netowrk) or represent localhost.""" BOOTSTRAP_MODULE = 'scoop.bootstrap.__main__' BASE_SSH = ['ssh', '-x', '-n', '-oStrictHostKeyChecking=no'] LAUNCHING_ARGUMENTS = namedtuple( 'launchingArguments', [ 'pythonPath', 'path', 'nice', 'pythonExecutable', 'size', 'origin', 'brokerHostname', 'brokerPorts', 'debug', 'profiling', 'executable', 'verbose', 'args', 'prolog', 'backend' ] ) def __init__(self, hostname="localhost"): self.workersArguments = [] self.hostname = hostname self.subprocesses = [] self.remoteProcessGID = None def __repr__(self): return "{0} ({1} workers)".format( self.hostname, len(self.workersArguments) ) def isLocal(self): """Is the current host the localhost?""" return self.hostname in utils.localHostnames def addWorker(self, *args, **kwargs): """Add a worker assignation arguments and order to pass are defined in LAUNCHING_ARGUMENTS Using named args is advised. """ try: la = self.LAUNCHING_ARGUMENTS(*args, **kwargs) except TypeError as e: scoop.logger.error(("addWorker failed to convert args %s and kwargs %s " "to namedtuple (requires %s arguments (names %s)") % (args, kwargs, len(self.LAUNCHING_ARGUMENTS._fields), self.LAUNCHING_ARGUMENTS._fields)) self.workersArguments.append(la) def _WorkerCommand_environment(self, worker): """Return list of shell commands to prepare the environment for bootstrap""" c = [] if worker.prolog: c.extend([ "source", worker.prolog, "&&", ]) if worker.pythonPath: # Tried to make it compliant to all shell variants. c.extend([ "export", "PYTHONPATH={0}:$PYTHONPATH".format(worker.pythonPath), ">&/dev/null", "||", "setenv", "PYTHONPATH", "{0}:$PYTHONPATH".format(worker.pythonPath), "&&", ]) return c def _WorkerCommand_bootstrap(self, worker): """Return list commands to start the bootstrap process""" c = [] c.extend([worker.pythonExecutable, '-m', self.BOOTSTRAP_MODULE]) return c def _WorkerCommand_options(self, worker, workerID): """Return list of options for bootstrap""" c = [] # If broker is on localhost if self.hostname == worker.brokerHostname: broker = "127.0.0.1" else: broker = worker.brokerHostname # If host is not localhost, echo group process if not self.isLocal() and workerID == 0: c.append("--echoGroup ") if worker.nice is not None: c.extend(['--nice', str(worker.nice)]) c.extend(['--size', str(worker.size)]) c.extend(['--workingDirectory', str(worker.path)]) c.extend(['--brokerHostname', broker]) c.extend(['--externalBrokerHostname', worker.brokerHostname]) c.extend(['--taskPort', str(worker.brokerPorts[0])]) c.extend(['--metaPort', str(worker.brokerPorts[1])]) if worker.origin and worker.executable: c.append('--origin') if worker.debug: c.append('--debug') if worker.profiling: c.append('--profile') if worker.backend: c.append('--backend={0}'.format(worker.backend)) if worker.verbose == 0: c.append('-q') elif worker.verbose >= 2: c.append('-v') return c def _WorkerCommand_executable(self, worker): """Return executable and any options to be executed by bootstrap""" c = [] if worker.executable: c.append(worker.executable) # This trick is used to parse correctly quotes # (ie. myScript.py 'arg1 "arg2" arg3') # Because shell=True is set with Popen, every quote gets re-interpreted # It replaces simple quotation marks with \\\" which gets evaluated to # \" by the second shell which prints it out as a double quote. if worker.args: if self.isLocal(): # If local, no shell is used so no escaping needed c.extend([ '{0}'.format(a) for a in worker.args ]) else: c.extend([ '"{0}"'.format(a.replace('"', '\\\"')) for a in worker.args ]) return c def _getWorkerCommandList(self, workerID): """Generate the workerCommand as list""" worker = self.workersArguments[workerID] c = [] if not self.isLocal(): c.extend(self._WorkerCommand_environment(worker)) c.extend(self._WorkerCommand_bootstrap(worker)) c.extend(self._WorkerCommand_options(worker, workerID)) c.extend(self._WorkerCommand_executable(worker)) return c def getWorkerCommand(self, workerID=None): """Retrieves the working launching shell command.""" c = " ".join(self._getWorkerCommandList(workerID)) return c def getCommand(self): """Retrieves the shell command to launch every worker on this host.""" # All this parenthesis insanity is to start subshells (workers) in the # correct monitoring mode (no background job echo). # Output: ( [launch command 1] & ) && ( [launch command 2] & ) [...] command = [] for workerID, worker in enumerate(self.workersArguments): command.append("(" + self.getWorkerCommand(workerID)) command[-1] += ")" return " & ) && ".join(command) def launch(self, tunnelPorts=None, stdPipe=False): """Launch every worker assigned on this host.""" if self.isLocal(): # Launching local workers for workerID, workerToLaunch in enumerate(self.workersArguments): # Launch one per subprocess c = self._getWorkerCommandList(workerID) self.subprocesses.append( subprocess.Popen(c) ) else: # Launching remotely sshCmd = self.BASE_SSH if tunnelPorts is not None: sshCmd += [ '-R {0}:127.0.0.1:{0}'.format(tunnelPorts[0]), '-R {0}:127.0.0.1:{0}'.format(tunnelPorts[1]), ] self.subprocesses.append( subprocess.Popen(sshCmd + [self.hostname, self.getCommand()], stdout=subprocess.PIPE if stdPipe else None, stderr=subprocess.PIPE if stdPipe else None, ) ) # Get group id from remote connections receivedLine = self.subprocesses[-1].stdout.readline() try: textGID = receivedLine.decode().strip() self.remoteProcessGID = int(textGID) except ValueError: # Following line for Python 2.6 compatibility (instead of [as e]) e = sys.exc_info()[1] # Terminate the process, otherwide reading from stderr may wait # undefinitely self.subprocesses[-1].terminate() stderr = self.subprocesses[-1].stderr.read() hostname = self.hostname scoop.logger.warning("Could not successfully launch the remote " "worker on {hostname}.\n" "Requested remote group process id, " "received:\n{receivedLine}\n" "Group id decoding error:\n{e}\n" "SSH process stderr:\n{stderr}" "".format(**locals())) return self.subprocesses def close(self): """Connection(s) cleanup.""" # Ensure everything is cleaned up on exit scoop.logger.debug('Closing workers on {0}.'.format(self)) # Output child processes stdout and stderr to console for process in self.subprocesses: if process.stdout is not None: sys.stdout.write(process.stdout.read().decode("utf-8")) sys.stdout.flush() if process.stderr is not None: sys.stderr.write(process.stderr.read().decode("utf-8")) sys.stderr.flush() # Terminate subprocesses for process in self.subprocesses: try: process.terminate() except OSError: pass # Send termination signal to remaining workers if not self.isLocal() and self.remoteProcessGID is None: scoop.logger.warn("Zombie process(es) possibly left on " "host {0}!".format(self.hostname)) elif not self.isLocal(): command = ("python -c " "'import os, signal; os.killpg({0}, signal.SIGKILL)' " ">&/dev/null").format(self.remoteProcessGID) subprocess.Popen(self.BASE_SSH + [self.hostname] + [command], ).wait() scoop-0.7.1/scoop/launcher.py000066400000000000000000000467711240127670500161630ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # # Global imports import argparse import os import sys import socket import subprocess import time import logging import traceback import signal from threading import Thread # Local imports from scoop import utils from scoop.launch import Host from scoop.launch.brokerLaunch import localBroker, remoteBroker from .broker.structs import BrokerInfo import scoop try: signal.signal(signal.SIGQUIT, utils.KeyboardInterruptHandler) except AttributeError: # SIGQUIT doesn't exist on Windows signal.signal(signal.SIGTERM, utils.KeyboardInterruptHandler) class ScoopApp(object): """SCOOP application. Coordinates the broker and worker launches.""" LAUNCH_HOST_CLASS = Host def __init__(self, hosts, n, b, verbose, python_executable, externalHostname, executable, arguments, tunnel, path, debug, nice, env, profile, pythonPath, prolog, backend): # Assure setup sanity assert type(hosts) == list and hosts, ("You should at least " "specify one host.") self.workersLeft = n self.createdSubprocesses = [] # launch information self.python_executable = python_executable[0] self.pythonpath = pythonPath self.prolog = prolog self.n = n self.b = b self.tunnel = tunnel self.executable = executable self.args = arguments self.verbose = verbose self.path = path self.debug = debug self.nice = nice self.profile = profile self.backend = backend self.errors = None # Logging configuration if self.verbose > 2: self.verbose = 2 scoop.logger = utils.initLogging( verbosity=self.verbose, name="launcher", ) # Show runtime information (useful for debugging) scoop.logger.info("SCOOP {0} {1} on {2} using Python {3}, API: {4}".format( scoop.__version__, scoop.__revision__, sys.platform, sys.version.replace("\n", ""), sys.api_version, ) ) if env in ["SLURM","PBS", "SGE"]: scoop.logger.info("Detected {0} environment.".format(env)) scoop.logger.info("Deploying {0} worker(s) over {1} " "host(s).".format( n, len(hosts) ) ) # Handling External Hostname self.externalHostname = '127.0.0.1' if self.tunnel else externalHostname scoop.logger.debug('Using hostname/ip: "{0}" as external broker ' 'reference.'.format(self.externalHostname)) scoop.logger.debug('The python executable to execute the program with is: ' '{0}.'.format(self.python_executable)) # Create launch lists self.broker_hosts = self.divideHosts(hosts[:], self.b) self.worker_hosts = self.divideHosts(hosts, self.n) # Logging of worker distribution warnings maximumWorkers = sum(host[1] for host in hosts) if self.n > maximumWorkers: scoop.logger.debug("The -n flag is set at {0} workers, which is higher " "than the maximum number of workers ({1}) specified " "by the hostfile.\nThis behavior may degrade the " "performances of scoop for cpu-bound operations." "".format(qty, maximumWorkers)) elif self.n < maximumWorkers: scoop.logger.debug("The -n flag is set at {0} workers, which is lower " "than the maximum number of workers ({1}) specified " "by the hostfile." "".format(qty, maximumWorkers)) # Display self.showHostDivision(headless=not executable) self.workers = [] self.brokers = [] def initLogging(self): """Configures the logger.""" verbose_levels = { 0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG, } logging.basicConfig( level=verbose_levels[self.verbose], format="[%(asctime)-15s] %(module)-9s %(levelname)-7s %(message)s" ) return logging.getLogger(self.__class__.__name__) def divideHosts(self, hosts, qty): """Divide processes among hosts.""" maximumWorkers = sum(host[1] for host in hosts) # If specified amount of workers is greater than sum of each specified. if qty > maximumWorkers: index = 0 while qty > maximumWorkers: hosts[index] = (hosts[index][0], hosts[index][1] + 1) index = (index + 1) % len(hosts) maximumWorkers += 1 # If specified amount of workers if lower than sum of each specified. elif qty < maximumWorkers: while qty < maximumWorkers: maximumWorkers -= hosts[-1][1] if qty > maximumWorkers: hosts[-1] = (hosts[-1][0], qty - maximumWorkers) maximumWorkers += hosts[-1][1] else: del hosts[-1] # Checking if the broker if externally routable if self.externalHostname in utils.loopbackReferences and \ len(hosts) > 1 and \ not self.tunnel: raise Exception("\n" "Could not find route from external worker to the " "broker: Unresolvable hostname or IP address.\n " "Please specify your externally routable hostname " "or IP using the --external-hostname parameter or " " use the --tunnel flag.") hosts.reverse() return hosts def showHostDivision(self, headless): """Show the worker distribution over the hosts""" scoop.logger.info('Worker distribution: ') for worker, number in reversed(self.worker_hosts): first_worker = (worker == self.worker_hosts[-1][0]) scoop.logger.info(' {0}:\t{1} {2}'.format( worker, number - 1 if first_worker or headless else str(number), "+ origin" if first_worker or headless else "", ) ) def _addWorker_args(self, workerinfo): """Create the arguments to pass to the addWorker call. The returned args and kwargs must ordered/named according to the namedtuple in LAUNCH_HOST_CLASS.LAUNCHING_ARGUMENTS both args and kwargs are supported for full flexibilty, but usage of kwargs only is strongly advised workerinfo is a dict with information that can be used to start the worker """ args = [] kwargs = { 'pythonPath': self.pythonpath, 'prolog': self.prolog, 'path': self.path, 'nice': self.nice, 'pythonExecutable': self.python_executable, 'size': self.n, 'origin': self.workersLeft == 1, 'brokerHostname': self.externalHostname, 'brokerPorts': (self.brokers[0].brokerPort, self.brokers[0].infoPort), 'debug': self.debug, 'profiling': self.profile, 'executable': self.executable, 'verbose': self.verbose, 'backend': self.backend, 'args': self.args, } return args, kwargs def addWorkerToHost(self, workerinfo): """Adds a worker to current host""" hostname = workerinfo['hostname'] scoop.logger.debug('Initialising {0}{1} worker {2} [{3}].'.format( "local" if hostname in utils.localHostnames else "remote", " origin" if self.workersLeft == 1 else "", self.workersLeft, hostname, ) ) add_args, add_kwargs = self._addWorker_args(workerinfo) self.workers[-1].addWorker(*add_args, **add_kwargs) def run(self): """Launch the broker(s) and worker(s) assigned on every hosts.""" # Launch the brokers for hostname, nb_brokers in self.broker_hosts: for ind in range(nb_brokers): # Launching the broker(s) if self.externalHostname in utils.localHostnames: self.brokers.append(localBroker( debug=self.debug, nice=self.nice, backend=self.backend, )) else: self.brokers.append(remoteBroker( hostname=hostname, pythonExecutable=self.python_executable, debug=self.debug, nice=self.nice, backend=self.backend, )) # Share connection information between brokers if self.b > 1: for broker in self.brokers: # Only send data of other brokers to a given broker connect_data = [ BrokerInfo( x.getHost(), *x.getPorts(), externalHostname=x.getHost() ) for x in self.brokers if x is not broker ] broker.sendConnect(connect_data) # Launch the workers for hostname, nb_workers in self.worker_hosts: self.workers.append(self.LAUNCH_HOST_CLASS(hostname)) total_workers_host = min(nb_workers, self.workersLeft) for worker_idx_host in range(total_workers_host): workerinfo = { 'hostname': hostname, 'total_workers_host': total_workers_host, 'worker_idx_host': worker_idx_host, } self.addWorkerToHost(workerinfo) self.workersLeft -= 1 # Launch every workers at the same time scoop.logger.debug( "{0}: Launching '{1}'".format( hostname, self.workers[-1].getCommand(), ) ) shells = self.workers[-1].launch( (self.brokers[0].brokerPort, self.brokers[0].infoPort) if self.tunnel else None, stdPipe=not self.workers[-1].isLocal(), ) if self.workersLeft <= 0: # We've launched every worker we needed, so let's exit the loop rootProcess = shells[-1] break # Wait for the root program if self.workers[-1].isLocal(): self.errors = self.workers[-1].subprocesses[-1].wait() else: # Process stdout first, then the whole stderr at the end for outStream, inStream in [(sys.stdout, rootProcess.stdout), (sys.stderr, rootProcess.stderr)]: data = inStream.read(1) while len(data) > 0: # Should not rely on utf-8 codec outStream.write(data.decode("utf-8")) outStream.flush() data = inStream.read(1) self.errors = rootProcess.wait() scoop.logger.info('Root process is done.') return self.errors def close(self): """Subprocess cleanup.""" # Give time to flush data if debug was on if self.debug: time.sleep(5) # Terminate workers for host in self.workers: host.close() # Terminate the brokers for broker in self.brokers: try: broker.close() except AttributeError: # Broker was not started (probably mislaunched) pass scoop.logger.info('Finished cleaning spawned subprocesses.') def makeParser(): """Create the SCOOP module arguments parser.""" # TODO: Add environment variable (all + selection) parser = argparse.ArgumentParser( description="Starts a parallel program using SCOOP.", prog="{0} -m scoop".format(sys.executable), ) group = parser.add_mutually_exclusive_group() group.add_argument('--hosts', '--host', help="The list of hosts. The first host will execute " "the origin. (default is 127.0.0.1)", metavar="Address", nargs='*') group.add_argument('--hostfile', help="The hostfile name", metavar="FileName") parser.add_argument('--path', '-p', help="The path to the executable on remote hosts " "(default is local directory)", default=os.getcwd()) parser.add_argument('--nice', type=int, metavar="NiceLevel", help="*nix niceness level (-20 to 19) to run the " "executable") parser.add_argument('--verbose', '-v', action='count', help="Verbosity level of this launch script (-vv for " "more)", default=1) parser.add_argument('--quiet', '-q', action='store_true') parser.add_argument('-n', help="Total number of workers to launch on the hosts. " "Workers are spawned sequentially over the hosts. " "(ie. -n 3 with 2 hosts will spawn 2 workers on " "the first host and 1 on the second.) (default: " "Number of CPUs on current machine)", type=int, metavar="NumberOfWorkers") parser.add_argument('-b', help="Total number of brokers to launch on the hosts. " "Brokers are spawned sequentially over the hosts. " "(ie. -b 3 with 2 hosts will spawn 2 brokers on " "the first host and 1 on the second.) (default: " "1)", type=int, default=1, metavar="NumberOfBrokers") parser.add_argument('--tunnel', help="Activate ssh tunnels to route toward the broker " "sockets over remote connections (may eliminate " "routing problems and activate encryption but " "slows down communications)", action='store_true') parser.add_argument('--external-hostname', nargs=1, help="The externally routable hostname / ip of this " "machine. (defaults to the local hostname)", metavar="Address") parser.add_argument('--python-interpreter', nargs=1, help="The python interpreter executable with which to " "execute the script", default=[sys.executable], metavar="Path") parser.add_argument('--pythonpath', nargs=1, help="The PYTHONPATH environment variable (default is " "current PYTHONPATH)", default=[os.environ.get('PYTHONPATH', '')]) parser.add_argument('--prolog', nargs=1, help="Absolute Path to a shell script or executable " "that will be executed at the launch of every " "worker", default=[None]) parser.add_argument('--debug', help=argparse.SUPPRESS, action='store_true') parser.add_argument('--profile', help=("Turn on the profiling. SCOOP will call " "cProfile.run on the executable for every worker and" " will produce files in directory profile/ named " "workerX where X is the number of the worker."), action='store_true') parser.add_argument('--backend', help="Choice of communication backend", choices=['ZMQ', 'TCP'], default='ZMQ') parser.add_argument('executable', nargs='?', help='The executable to start with SCOOP') parser.add_argument('args', nargs=argparse.REMAINDER, help='The arguments to pass to the executable', default=[], metavar="args") return parser def main(): """Execution of the SCOOP module. Parses its command-line arguments and launch needed resources.""" # Generate a argparse parser and parse the command-line arguments parser = makeParser() args = parser.parse_args() # Get a list of resources to launch worker(s) on hosts = utils.getHosts(args.hostfile, args.hosts) if args.n: n = args.n else: n = utils.getWorkerQte(hosts) assert n > 0, ("Scoop couldn't determine the number of worker to start.\n" "Use the '-n' flag to set it manually.") if not args.external_hostname: args.external_hostname = [utils.externalHostname(hosts)] # Launch SCOOP thisScoopApp = ScoopApp(hosts, n, args.b, args.verbose if not args.quiet else 0, args.python_interpreter, args.external_hostname[0], args.executable, args.args, args.tunnel, args.path, args.debug, args.nice, utils.getEnv(), args.profile, args.pythonpath[0], args.prolog[0], args.backend) rootTaskExitCode = False interruptPreventer = Thread(target=thisScoopApp.close) try: rootTaskExitCode = thisScoopApp.run() except Exception as e: logging.error('Error while launching SCOOP subprocesses:') logging.error(traceback.format_exc()) rootTaskExitCode = -1 finally: # This should not be interrupted (ie. by a KeyboadInterrupt) # The only cross-platform way to do it I found was by using a thread. interruptPreventer.start() interruptPreventer.join() # Exit with the proper exit code if rootTaskExitCode: sys.exit(rootTaskExitCode) if __name__ == "__main__": main() scoop-0.7.1/scoop/shared.py000066400000000000000000000141261240127670500156150ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import itertools from inspect import ismethod from functools import reduce import time from . import encapsulation, utils import scoop from .fallbacks import ensureScoopStartedProperly, NotStartedProperly elements = None def _ensureAtomicity(fn): """Ensure atomicity of passed elements on the whole worker pool""" @ensureScoopStartedProperly def wrapper(*args, **kwargs): """setConst(**kwargs) Set a constant that will be shared to every workers. This call blocks until the constant has propagated to at least one worker. :param \*\*kwargs: One or more combination(s) key=value. Key being the variable name and value the object to share. :returns: None. Usage: setConst(name=value) """ # Note that the docstring is the one of setConst. # This is because of the documentation framework (sphinx) limitations. from . import _control # Enforce retrieval of currently awaiting constants _control.execQueue.socket.pumpInfoSocket() for key, value in kwargs.items(): # Object name existence check if key in itertools.chain(*(elem.keys() for elem in elements.values())): raise TypeError("This constant already exists: {0}.".format(key)) # Retry element propagation until it is returned while all(key in elements.get(scoop.worker, []) for key in kwargs.keys()) is not True: scoop.logger.debug("Sending global variables {0}...".format( list(kwargs.keys()) )) # Call the function fn(*args, **kwargs) # Enforce retrieval of currently awaiting constants _control.execQueue.socket.pumpInfoSocket() # TODO: Make previous blocking instead of sleep time.sleep(0.1) # Atomicity check elementNames = list(itertools.chain(*(elem.keys() for elem in elements.values()))) if len(elementNames) != len(set(elementNames)): raise TypeError("This constant already exists: {0}.".format(key)) return wrapper @_ensureAtomicity def setConst(**kwargs): """setConst(**kwargs) Set a constant that will be shared to every workers. :param **kwargs: One or more combination(s) key=value. Key being the variable name and value the object to share. :returns: None. Usage: setConst(name=value) """ from . import _control sendVariable = _control.execQueue.socket.sendVariable for key, value in kwargs.items(): # Propagate the constant # for file-like objects, see encapsulation.py where copyreg was # used to overload standard pickling. if callable(value): sendVariable(key, encapsulation.FunctionEncapsulation(value, key)) else: sendVariable(key, value) def getConst(name, timeout=0.1): """Get a shared constant. :param name: The name of the shared variable to retrieve. :param timeout: The maximum time to wait in seconds for the propagation of the constant. :returns: The shared object. Usage: value = getConst('name') """ from . import _control import time timeStamp = time.time() while True: # Enforce retrieval of currently awaiting constants _control.execQueue.socket.pumpInfoSocket() # Constants concatenation constants = dict(reduce( lambda x, y: x + list(y.items()), elements.values(), [] )) timeoutHappened = time.time() - timeStamp > timeout if constants.get(name) is not None or timeoutHappened: return constants.get(name) time.sleep(0.01) class SharedElementEncapsulation(object): """Encapsulates a reference to an element available in the shared module. This is used by Futures (map on lambda, for instance).""" def __init__(self, element): self.isMethod = False if utils.isStr(element): # Already shared element assert getConst(element, timeout=0) != None, ( "Element must already be shared." ) self.uniqueID = element else: # Element to share # Determine if function is a method. Methods derived from external # languages such as C++ aren't detected by ismethod. if ismethod(element): # Must share whole object before ability to use its method self.isMethod = True self.methodName = element.__name__ element = element.__self__ # Lambda-like or unshared code to share uniqueID = str(scoop.worker) + str(id(element)) + str(hash(element)) self.uniqueID = uniqueID if getConst(uniqueID, timeout=0) == None: funcRef = {uniqueID: element} setConst(**funcRef) def __repr__(self): return self.uniqueID def __call__(self, *args, **kwargs): if self.isMethod: wholeObj = getConst( self.__repr__(), timeout=float("inf"), ) return getattr(wholeObj, self.methodName)(*args, **kwargs) else: return getConst(self.__repr__(), timeout=float("inf"))(*args, **kwargs) def __name__(self): return self.__repr__() scoop-0.7.1/scoop/utils.py000066400000000000000000000175441240127670500155160ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # from multiprocessing import cpu_count from itertools import groupby import os import re import sys import socket import logging if sys.version_info < (2, 7): from scoop.backports.dictconfig import dictConfig else: from logging.config import dictConfig loopbackReferences = [ "127.0.0.1", "localhost", "::1", ] localHostnames = loopbackReferences + [ socket.getfqdn().split('.')[0], ] localHostnames.extend([ ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")][:1] ) loggingConfig = {} def initLogging(verbosity=0, name="SCOOP"): """Creates a logger.""" global loggingConfig verbose_levels = { -2: "CRITICAL", -1: "ERROR", 0: "WARNING", 1: "INFO", 2: "DEBUG", 3: "NOSET", } log_handlers = { "console": { "class": "logging.StreamHandler", "formatter": "{name}Formatter".format(name=name), "stream": "ext://sys.stdout", }, } loggingConfig.update({ "{name}Logger".format(name=name): { "handlers": ["console"], "level": verbose_levels[verbosity], }, }) dict_log_config = { "version": 1, "handlers": log_handlers, "loggers": loggingConfig, "formatters": { "{name}Formatter".format(name=name): { "format": "[%(asctime)-15s] %(module)-9s " "%(levelname)-7s %(message)s", }, }, } dictConfig(dict_log_config) return logging.getLogger("{name}Logger".format(name=name)) def externalHostname(hosts): """Ensure external hostname is routable.""" hostname = hosts[0][0] if hostname in localHostnames and len(hosts) > 1: hostname = socket.getfqdn().split(".")[0] try: socket.getaddrinfo(hostname, None) except socket.gaierror: raise Exception("\nThe first host (containing a broker) is not" " routable.\nMake sure the address is correct.") return hostname def groupTogether(in_list): # TODO: This algorithm is not efficient, use itertools.groupby() return_value = [] already_done = [] for index, element in enumerate(in_list): if element not in already_done: how_much = in_list[index + 1:].count(element) return_value += [element]*(how_much + 1) already_done.append(element) return return_value def getCPUcount(): """Try to get the number of cpu on the current host.""" try: return cpu_count() except NotImplementedError: return 1 def getEnv(): """Return the launching environnement""" if "SLURM_NODELIST" in os.environ: return "SLURM" elif "PBS_ENVIRONMENT" in os.environ: return "PBS" elif "PE_HOSTFILE" in os.environ: return "SGE" else: return "other" def getHosts(filename=None, hostlist=None): """Return a list of host depending on the environment""" if filename: return getHostsFromFile(filename) elif hostlist: return getHostsFromList(hostlist) elif "SLURM_NODELIST" in os.environ: return getHostsFromSLURM() elif "PBS_ENVIRONMENT" in os.environ: return getHostsFromPBS() elif "PE_HOSTFILE" in os.environ: return getHostsFromSGE() else: return getDefaultHosts() def getHostsFromFile(filename): """Parse a file to return a list of hosts.""" valid_hostname = r"^[^ /\t=\n]+" workers = r"\d+" hostname_re = re.compile(valid_hostname) worker_re = re.compile(workers) hosts = [] with open(filename) as f: for line in f: # check to see if it is a SLURM grouping instead of a # regular list of hosts if re.search('[\[\]]',line): hosts = hosts + parseSLURM(line.strip()) else: host = hostname_re.search(line.strip()) if host: hostname = host.group() n = worker_re.search(line[host.end():]) if n: n = n.group() else: n = 1 hosts.append((hostname, int(n))) return hosts def getHostsFromList(hostlist): """Return the hosts from the command line""" # check to see if it is a SLURM grouping instead of a # regular list of hosts if any(re.search('[\[\]]', x) for x in hostlist): return parseSLURM(str(hostlist)) # Counter would be more efficient but: # 1. Won't be Python 2.6 compatible # 2. Won't be ordered hostlist = groupTogether(hostlist) retVal = [] for key, group in groupby(hostlist): retVal.append((key, len(list(group)))) return retVal def parseSLURM(string): """Return a host list from a SLURM string""" bunchedlist = re.findall('([^ /\t=\n\[,]+)(?=\[)(.*?)(?<=\])', string) hosts = [] # parse out the name followd by range (ex. borgb[001-002,004-006] for h,n in bunchedlist: block = re.findall('([^\[\],]+)', n) for rng in block: bmin,bmax = rng.split('-') fill_width = max(len(bmin),len(bmax)) for i in range(int(bmin),int(bmax)+1): hostname = str(h)+str(i).zfill(fill_width) hosts.append((hostname, int(1))) return hosts def getHostsFromSLURM(): """Return a host list from a SLURM environment""" return parseSLURM(os.environ["SLURM_NODELIST"]) def getHostsFromPBS(): """Return a host list in a PBS environment""" # See above comment about Counter with open(os.environ["PBS_NODEFILE"], 'r') as hosts: hostlist = groupTogether(hosts.read().split()) retVal = [] for key, group in groupby(hostlist): retVal.append((key, len(list(group)))) return retVal def getHostsFromSGE(): """Return a host list in a SGE environment""" with open(os.environ["PE_HOSTFILE"], 'r') as hosts: return [(host.split()[0], int(host.split()[1])) for host in hosts] def getWorkerQte(hosts): """Return the number of workers to launch depending on the environment""" if "SLURM_NTASKS" in os.environ: return int(os.environ["SLURM_NTASKS"]) elif "PBS_NP" in os.environ: return int(os.environ["PBS_NP"]) elif "NSLOTS" in os.environ: return int(os.environ["NSLOTS"]) else: return sum(host[1] for host in hosts) def KeyboardInterruptHandler(signum, frame): """This is use in the interruption handler""" raise KeyboardInterrupt("Shutting down!") def getDefaultHosts(): """This is the default host for a simple SCOOP launch""" return [('127.0.0.1', getCPUcount())] try: # Python 2.X fallback basestring # attempt to evaluate basestring def isStr(string): return isinstance(string, basestring) except NameError: def isStr(string): return isinstance(string, str) scoop-0.7.1/setup.cfg000066400000000000000000000000731240127670500144670ustar00rootroot00000000000000[egg_info] tag_date = 0 tag_build = tag_svn_revision = 0 scoop-0.7.1/setup.py000066400000000000000000000034121240127670500143600ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup import scoop import sys # Backports installation extraPackages = [] if sys.version_info < (2, 7): extraPackages = ['scoop.backports'] setup(name='scoop', version="{ver} {rev}".format( ver=scoop.__version__, rev=scoop.__revision__, ), description='Scalable COncurrent Operations in Python', long_description=open('README.txt').read(), author='SCOOP Development Team', author_email='scoop-users@googlegroups.com', url='http://scoop.googlecode.com', download_url='http://code.google.com/p/scoop/downloads/list', install_requires=['greenlet>=0.3.4', 'pyzmq>=13.1.0', 'argparse>=1.1', ], extras_require = {'nice': ['psutil>=0.6.1'], }, packages=['scoop', 'scoop.bootstrap', 'scoop.launch', 'scoop.broker', 'scoop._comm', 'scoop.discovery', ] + extraPackages, platforms=['any'], keywords=['distributed algorithms', 'parallel programming', 'Concurrency', 'Cluster programming', 'greenlet', 'zmq', ], license='LGPL', classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: GNU Library or Lesser General Public ' 'License (LGPL)', 'Programming Language :: Python', 'Topic :: Scientific/Engineering', 'Topic :: Software Development', ], ) scoop-0.7.1/test/000077500000000000000000000000001240127670500136255ustar00rootroot00000000000000scoop-0.7.1/test/tests.py000066400000000000000000000475341240127670500153560ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of Scalable COncurrent Operations in Python (SCOOP). # # SCOOP is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # SCOOP is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with SCOOP. If not, see . # import scoop scoop.DEBUG = False import unittest import subprocess import time import copy import os import sys import operator import signal import math from tests_parser import TestUtils from tests_stat import TestStat from tests_stopwatch import TestStopWatch from scoop import futures, _control, utils, shared from scoop._types import FutureQueue from scoop.broker.structs import BrokerInfo subprocesses = [] def cleanSubprocesses(): [a.kill() for a in subprocesses] try: signal.signal(signal.SIGQUIT, cleanSubprocesses) except AttributeError: # SIGQUIT doesn't exist on Windows signal.signal(signal.SIGTERM, cleanSubprocesses) def func0(n): task = futures.submit(func1, n) result = task.result() return result def func1(n): result = futures.map(func2, [i+1 for i in range(n)]) return sum(result) def func2(n): launches = [] for i in range(n): launches.append(futures.submit(func3, i + 1)) result = futures.as_completed(launches) return sum(r.result() for r in result) def func3(n): result = list(futures.map(func4, [i+1 for i in range(n)])) return sum(result) def func4(n): result = n * n return result def funcLambda(n): lambda_func = lambda x : x*x result = list(futures.map(lambda_func, [i+1 for i in range(n)])) return sum(result) def funcLambdaSubfuncNotGlobal(n): """Tests a lambda function containing a call to a function that is not in the globals().""" my_mul = operator.mul lambda_func = lambda x : my_mul(x, x) result = list(futures.map(lambda_func, [i+1 for i in range(n)])) return sum(result) def funcCos(): result = list(futures.map(math.cos, [i for i in range(10)])) return sum(result) def funcCallback(): f = futures.submit(func4, 100) def callBack(future): future.was_callabacked = True f.add_done_callback(callBack) if len(f.callback) == 0: return False futures.wait((f,)) try: return f.was_callabacked except: return False def funcCancel(): f = futures.submit(func4, 100) f.cancel() return f.cancelled() def funcCompleted(n): launches = [] for i in range(n): launches.append(futures.submit(func4, i + 1)) result = futures.as_completed(launches) return sum(r.result() for r in result) def funcDone(): f = futures.submit(func4, 100) futures.wait((f,)) done = f.done() if done != True: return done res = f.result() done = f.done() return done def funcWait(timeout): fs = [futures.submit(func4, i) for i in range(1000)] done, not_done = futures.wait(fs, timeout=timeout) return done, not_done def funcExcept(n): f = futures.submit(funcRaise, n) try: f.result() except: return True return False def funcRaise(n): raise Exception("Test exception") def funcSub(n): f = futures.submit(func4, n) return f.result() def funcMapScan(l): resultat = futures.mapScan(func4, operator.add, l) time.sleep(0.3) _control.execQueue.socket.pumpInfoSocket() return resultat def funcMapReduce(l): resultat = futures.mapReduce(func4, operator.add, l) time.sleep(0.3) _control.execQueue.socket.pumpInfoSocket() return resultat def funcDoubleMapReduce(l): resultat = futures.mapReduce(func4, operator.add, l) resultat2 = futures.mapReduce(func4, operator.add, l) time.sleep(0.3) _control.execQueue.socket.pumpInfoSocket() return resultat == resultat2 def funcUseSharedConstant(): # Tries on a mutable and an immutable object assert shared.getConst('myVar') == { 1: 'Example 1', 2: 'Example 2', 3: 'Example 3', } assert shared.getConst('secondVar') == "Hello World!" return True def funcUseSharedFunction(): assert shared.getConst('myRemoteFunc')(5) == 5 * 5 assert shared.getConst('myRemoteFunc')(25) == 25 * 25 return True def funcSharedConstant(): shared.setConst(myVar={1: 'Example 1', 2: 'Example 2', 3: 'Example 3', }) shared.setConst(secondVar="Hello World!") result = True for _ in range(100): try: result &= futures.submit(funcUseSharedConstant).result() except AssertionError: result = False return result def funcSharedFunction(): shared.setConst(myRemoteFunc=func4) result = True for _ in range(100): try: result &= futures.submit(funcUseSharedFunction).result() except AssertionError: result = False return result def funcMapAsCompleted(n): result = list(futures.map_as_completed(func4, [i+1 for i in range(n)])) return sum(result) def funcIter(n): result = list(futures.map(func4, (i+1 for i in range(n)))) return sum(result) def main(n): task = futures.submit(func0, n) futures.wait([task], return_when=futures.ALL_COMPLETED) result = task.result() return result def main_simple(n): task = futures.submit(func3, n) futures.wait([task], return_when=futures.ALL_COMPLETED) result = task.result() return result def submit_get_queues_size(n): task = futures.submit(func4, n) result = task.result() return [ len(scoop._control.execQueue.inprogress), len(scoop._control.execQueue.ready), len(scoop._control.execQueue.movable), len(scoop._control.futureDict) - 1, # - 1 because the current function is a future too ] def map_get_queues_size(n): result = list(map(func4, [n for n in range(n)])) return [ len(scoop._control.execQueue.inprogress), len(scoop._control.execQueue.ready), len(scoop._control.execQueue.movable), len(scoop._control.futureDict) - 1, # - 1 because the current function is a future too ] def port_ready(port, socket): """Checks if a given port is already binded""" try: socket.connect(('127.0.0.1', port)) except IOError: return False else: socket.shutdown(2) socket.close() return True class TestScoopCommon(unittest.TestCase): def __init__(self, *args, **kwargs): # Parent initialization super(TestScoopCommon, self).__init__(*args, **kwargs) def multiworker_set(self): global subprocesses worker = subprocess.Popen([sys.executable, "-m", "scoop.bootstrap", "--brokerHostname", "127.0.0.1", "--taskPort", "5555", "--metaPort", "5556", "tests.py"]) subprocesses.append(worker) return worker def setUp(self): global subprocesses # Start the server self.server = subprocess.Popen([sys.executable, "-m", "scoop.broker", "--tPort", "5555", "--mPort", "5556"]) import socket, datetime, time s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) begin = datetime.datetime.now() while not port_ready(5555, s): if (datetime.datetime.now() - begin > datetime.timedelta(seconds=3)): raise Exception('Could not start server!') pass subprocesses.append(self.server) scoop.IS_RUNNING = True scoop.IS_ORIGIN = True scoop.WORKER_NAME = 'origin'.encode() scoop.BROKER_NAME = 'broker'.encode() scoop.BROKER = BrokerInfo("127.0.0.1", 5555, 5556, "127.0.0.1") scoop.worker = (scoop.WORKER_NAME, scoop.BROKER_NAME) scoop.MAIN_MODULE = "tests.py" scoop.VALID = True scoop.DEBUG = False scoop.SIZE = 2 scoop._control.execQueue = FutureQueue() def tearDown(self): global subprocesses scoop._control.futureDict.clear() try: self.w.kill() except: pass # Destroy the server if self.server.poll() == None: try: self.server.kill() except: pass # Stabilise zmq after a deleted socket del subprocesses[:] time.sleep(0.1) class TestMultiFunction(TestScoopCommon): def __init__(self, *args, **kwargs): # Parent initialization super(TestMultiFunction, self).__init__(*args, **kwargs) self.main_func = main self.small_result = 77 self.large_result = 76153 def test_small_uniworker(self): _control.FutureQueue.highwatermark = 10 _control.FutureQueue.lowwatermark = 5 result = futures._startup(self.main_func, 4) self.assertEqual(result, self.small_result) def test_small_no_lowwatermark_uniworker(self): _control.FutureQueue.highwatermark = 9999999999999 _control.FutureQueue.lowwatermark = 1 result = futures._startup(self.main_func, 4) self.assertEqual(result, self.small_result) def test_small_foreign_uniworker(self): _control.FutureQueue.highwatermark = 1 result = futures._startup(self.main_func, 4) self.assertEqual(result, self.small_result) def test_small_local_uniworker(self): _control.FutureQueue.highwatermark = 9999999999999 result = futures._startup(self.main_func, 4) self.assertEqual(result, self.small_result) def test_large_uniworker(self): _control.FutureQueue.highwatermark = 9999999999999 result = futures._startup(self.main_func, 20) self.assertEqual(result, self.large_result) def test_large_no_lowwatermark_uniworker(self): _control.FutureQueue.lowwatermark = 1 _control.FutureQueue.highwatermark = 9999999999999 result = futures._startup(self.main_func, 20) self.assertEqual(result, self.large_result) def test_large_foreign_uniworker(self): _control.FutureQueue.highwatermark = 1 result = futures._startup(self.main_func, 20) self.assertEqual(result, self.large_result) def test_large_local_uniworker(self): _control.FutureQueue.highwatermark = 9999999999999 result = futures._startup(self.main_func, 20) self.assertEqual(result, self.large_result) def test_small_local_multiworker(self): self.w = self.multiworker_set() _control.FutureQueue.highwatermark = 9999999999999 Backupenv = os.environ.copy() result = futures._startup(self.main_func, 4) self.assertEqual(result, self.small_result) os.environ = Backupenv def test_small_foreign_multiworker(self): self.w = self.multiworker_set() _control.FutureQueue.highwatermark = 1 Backupenv = os.environ.copy() result = futures._startup(self.main_func, 4) self.assertEqual(result, self.small_result) os.environ = Backupenv def test_execQueue_multiworker(self): self.w = self.multiworker_set() result = futures._startup(func0, 6) self.assertEqual(len(scoop._control.execQueue.inprogress), 0) self.assertEqual(len(scoop._control.execQueue.ready), 0) self.assertEqual(len(scoop._control.execQueue.movable), 0) self.assertEqual(len(scoop._control.futureDict), 0) def test_execQueue_uniworker(self): result = futures._startup(func0, 6) self.assertEqual(len(scoop._control.execQueue.inprogress), 0) self.assertEqual(len(scoop._control.execQueue.ready), 0) self.assertEqual(len(scoop._control.execQueue.movable), 0) self.assertEqual(len(scoop._control.futureDict), 0) def test_execQueue_submit_uniworker(self): result = futures._startup(submit_get_queues_size, 6) self.assertEqual( result, [0 for _ in range(len(result))], "Buffers are not empty after future completion" ) def test_execQueue_map_uniworker(self): result = futures._startup(map_get_queues_size, 6) self.assertEqual( result, [0 for _ in range(len(result))], "Buffers are not empty after future completion" ) def test_execQueue_submit_multiworker(self): self.w = self.multiworker_set() result = futures._startup(submit_get_queues_size, 6) self.assertEqual( result, [0 for _ in range(len(result))], "Buffers are not empty after future completion" ) def test_execQueue_map_multiworker(self): self.w = self.multiworker_set() result = futures._startup(map_get_queues_size, 6) self.assertEqual( result, [0 for _ in range(len(result))], "Buffers are not empty after future completion" ) def test_partial(self): """This function removes some attributes (such as __name__).""" from functools import partial result = futures._startup(partial(self.main_func), 4) self.assertEqual(result, self.small_result) class TestSingleFunction(TestMultiFunction): def __init__(self, *args, **kwargs): # Parent initialization super(TestSingleFunction, self).__init__(*args, **kwargs) self.main_func = main_simple self.small_result = 30 self.large_result = 2870 class TestApi(TestScoopCommon): def __init(self, *args, **kwargs): super(TestApi, self).__init(*args, **kwargs) def test_as_Completed_single(self): result = futures._startup(funcCompleted, 30) self.assertEqual(result, 9455) def test_as_Completed_multi(self): self.w = self.multiworker_set() result = futures._startup(funcCompleted, 30) self.assertEqual(result, 9455) def test_map_single(self): result = futures._startup(func3, 30) self.assertEqual(result, 9455) def test_map_multi(self): self.w = self.multiworker_set() result = futures._startup(func3, 30) self.assertEqual(result, 9455) def test_map_lambda(self): self.w = self.multiworker_set() result = futures._startup(funcLambda, 30) self.assertEqual(result, 9455) # This test is complex to handle and has many implications # Bundle a closure with the future? # How to manage side-effects of variables in closure? #def test_map_lambda_subfunc_not_global(self): # self.w = self.multiworker_set() # result = futures._startup(funcLambdaSubfuncNotGlobal, 30) # self.assertEqual(result, 9455) def test_map_imported_func(self): self.w = self.multiworker_set() result = futures._startup(funcCos) self.assertGreater(result, 0.4) self.assertLess(result, 0.5) def test_submit_single(self): result = futures._startup(funcSub, 10) self.assertEqual(result, 100) def test_submit_multi(self): self.w = self.multiworker_set() result = futures._startup(funcSub, 10) self.assertEqual(result, 100) def test_exception_single(self): result = futures._startup(funcExcept, 19) self.assertTrue(result) def test_exception_multi(self): self.w = self.multiworker_set() result = futures._startup(funcExcept, 19) self.assertTrue(result) def test_done(self): result = futures._startup(funcDone) self.assertTrue(result) def test_cancel(self): self.assertTrue(futures._startup(funcCancel)) def test_callback(self): self.assertTrue(futures._startup(funcCallback)) def test_wait_no_timeout(self): done, not_done = futures._startup(funcWait, -1) self.assertTrue(len(done) == 1000) self.assertTrue(len(not_done) == 0) def test_wait_with_timeout(self): done, not_done = futures._startup(funcWait, 0.1) self.assertTrue((len(done) + len(not_done)) == 1000) def test_wait_nonblocking(self): done, not_done = futures._startup(funcWait, 0) self.assertTrue((len(done) + len(not_done)) == 1000) def test_map_as_completed_single(self): result = futures._startup(funcMapAsCompleted, 30) self.assertEqual(result, 9455) def test_map_as_completed_multi(self): self.w = self.multiworker_set() result = futures._startup(funcMapAsCompleted, 30) self.assertEqual(result, 9455) def test_from_generator_single(self): result = futures._startup(funcIter, 30) self.assertEqual(result, 9455) def test_from_generator_multi(self): self.w = self.multiworker_set() result = futures._startup(funcIter, 30) self.assertEqual(result, 9455) class TestCoherent(TestScoopCommon): def __init(self, *args, **kwargs): super(TestCoherent, self).__init(*args, **kwargs) def test_mapReduce(self): result = futures._startup(funcMapReduce, [10, 20, 30]) self.assertEqual(result, 1400) def test_doubleMapReduce(self): result = futures._startup(funcDoubleMapReduce, [10, 20, 30]) self.assertTrue(result) def test_mapScan(self): result = futures._startup(funcMapScan, [10, 20, 30]) self.assertEqual(max(result), 1400) class TestShared(TestScoopCommon): def __init(self, *args, **kwargs): super(TestShared, self).__init(*args, **kwargs) def test_shareConstant(self): result = futures._startup(funcSharedFunction) self.assertEqual(result, True) def test_shareFunction(self): result = futures._startup(funcSharedConstant) self.assertEqual(result, True) if __name__ == '__main__' and os.environ.get('IS_ORIGIN', "1") == "1": utSimple = unittest.TestLoader().loadTestsFromTestCase(TestSingleFunction) utComplex = unittest.TestLoader().loadTestsFromTestCase(TestMultiFunction) utApi = unittest.TestLoader().loadTestsFromTestCase(TestApi) utUtils = unittest.TestLoader().loadTestsFromTestCase(TestUtils) utCoherent = unittest.TestLoader().loadTestsFromTestCase(TestCoherent) utShared = unittest.TestLoader().loadTestsFromTestCase(TestShared) utStat = unittest.TestLoader().loadTestsFromTestCase(TestStat) utStopWatch = unittest.TestLoader().loadTestsFromTestCase(TestStopWatch) if len(sys.argv) > 1: if sys.argv[1] == "simple": unittest.TextTestRunner(verbosity=2).run(utSimple) elif sys.argv[1] == "complex": unittest.TextTestRunner(verbosity=2).run(utComplex) elif sys.argv[1] == "api": unittest.TextTestRunner(verbosity=2).run(utApi) elif sys.argv[1] == "utils": unittest.TextTestRunner(verbosity=2).run(utUtils) elif sys.argv[1] == "coherent": unittest.TextTestRunner(verbosity=2).run(utCoherent) elif sys.argv[1] == "shared": unittest.TextTestRunner(verbosity=2).run(utShared) elif sys.argv[1] == "stat": unittest.TextTestRunner(verbosity=2).run(utStat) elif sys.argv[1] == "stopwatch": unittest.TextTestRunner(verbosity=2).run(utStopWatch) elif sys.argv[1] == "verbose": sys.argv = sys.argv[0:1] unittest.main(verbosity=2) else: unittest.main() elif __name__ == '__main__': futures._startup(main_simple) scoop-0.7.1/test/tests_parser.py000066400000000000000000000047141240127670500167230ustar00rootroot00000000000000from scoop import utils import unittest import os # This is a subset of a SGE environment for testing SGE_ENV = {'NHOSTS':'2', 'PE_HOSTFILE':"sgehostssim.txt", 'NSLOTS': '16', 'PE': 'default', 'ENVIRONMENT': 'BATCH'} # This is a subset of a PBS environment for testing PBS_ENV = {'ENVIRONMENT': 'BATCH', 'PBS_ENVIRONMENT': 'PBS_BATCH', 'MOAB_PROCCOUNT': '16', 'PBS_NUM_NODES': '2', 'PBS_NP': '16', 'PBS_NODEFILE': 'pbshostssim.txt'} # This is the logical content of the hostfiles hosts = [("host1", 8), ("host2", 4), ("host3", 2), ("host4", 2)] class TestUtils(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestUtils, self).__init__(*args, **kwargs) def setUp(self): self.backup = os.environ.copy() def tearDown(self): os.environ = self.backup def test_getCpuCount(self): # Since we don't know how many cores has the computer on which these # tests will be run, we check that at least, getCPUcount() returns an # int larger than 0. self.assertIsInstance(utils.getCPUcount(), int) self.assertTrue(utils.getCPUcount() > 0) def test_getEnvPBS(self): os.environ.update(PBS_ENV) self.assertEqual(utils.getEnv(), "PBS") def test_getEnvSGE(self): os.environ.update(SGE_ENV) self.assertEqual(utils.getEnv(), "SGE") def test_getEnvOther(self): self.assertEqual(utils.getEnv(), "other") def test_getHostsPBS(self): os.environ.update(PBS_ENV) # We used the set because the order of the hosts is not important. self.assertEqual(set(utils.getHosts()), set(hosts)) def test_getHostsSGE(self): os.environ.update(SGE_ENV) # We used the set because the order of the hosts is not important. self.assertEqual(set(utils.getHosts()), set(hosts)) def test_getHostsFile(self): self.assertEqual(set(utils.getHosts("hostfilesim.txt")), set(hosts)) def test_getWorkerQtePBS(self): os.environ.update(PBS_ENV) self.assertEqual(utils.getWorkerQte(utils.getHosts()), 16) def test_getWorkerQteSGE(self): os.environ.update(SGE_ENV) self.assertEqual(utils.getWorkerQte(utils.getHosts()), 16) def test_getWorkerQteFile(self): self.assertEqual(utils.getWorkerQte(utils.getHosts("hostfilesim.txt")), 16) if __name__ == "__main__": t = unittest.TestLoader().loadTestsFromTestCase(TestUtils) unittest.TextTestRunner(verbosity=2).run(t) scoop-0.7.1/test/tests_stat.py000066400000000000000000000025321240127670500163760ustar00rootroot00000000000000from scoop._control import _stat import unittest class TestStat(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestStat, self).__init__(*args, **kwargs) def test_appendleft(self): stats = _stat() data = list(range(5)) for i in data[::-1]: stats.appendleft(i) for i, j in enumerate(stats): self.assertEqual(data[i], j) def test_maxlen(self): stats = _stat() data = list(range(15)) for i in data[::-1]: stats.appendleft(i) for i, j in enumerate(stats): self.assertEqual(data[i], j) self.assertEqual(len(stats), 10) def test_mean(self): stats = _stat() data = list(range(15)) for i in data: stats.appendleft(float(i)) self.assertEqual(stats.mean(), 9.5) stats.appendleft(1000) self.assertEqual(stats.mean(), 109.0) def test_std(self): stats = _stat() data = list(range(10)) for i in data: stats.appendleft(float(i)) self.assertAlmostEqual(stats.std(), 2.87228132327) stats.appendleft(1000) self.assertAlmostEqual(stats.std(), 298.510050082) if __name__ == "__main__": t = unittest.TestLoader().loadTestsFromTestCase(TestStat) unittest.TextTestRunner(verbosity=2).run(t) scoop-0.7.1/test/tests_stopwatch.py000066400000000000000000000023451240127670500174410ustar00rootroot00000000000000from scoop._types import StopWatch import unittest import time class TestStopWatch(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestStopWatch, self).__init__(*args, **kwargs) def test_get(self): watch = StopWatch() first = watch.get() time.sleep(0.1) second = watch.get() # *nix tend to overshoot a tiny bit, Windows tend to always be under # by max. 1ms self.assertAlmostEqual(second - first, 0.1, places=2) def test_halt(self): watch = StopWatch() watch.halt() first = watch.get() time.sleep(0.1) second = watch.get() self.assertEqual(first, second) def test_resume(self): watch = StopWatch() watch.halt() first = watch.get() watch.resume() time.sleep(0.1) second = watch.get() # See test_get self.assertAlmostEqual(second - first, 0.1, places=2) def test_reset(self): watch = StopWatch() time.sleep(0.1) watch.reset() self.assertLess(watch.get(), 0.001) if __name__ == "__main__": t = unittest.TestLoader().loadTestsFromTestCase(TestStopWatch) unittest.TextTestRunner(verbosity=2).run(t)