pax_global_header00006660000000000000000000000064123236274650014524gustar00rootroot0000000000000052 comment=3140ba04c35734b5ae69f4b1bf2b7877e2dd1547 pyres-1.5/000077500000000000000000000000001232362746500125335ustar00rootroot00000000000000pyres-1.5/.gitignore000066400000000000000000000001111232362746500145140ustar00rootroot00000000000000*.pyc build/ .coverage *.report *.egg-info logs/ dist/ *.swp *.swo .tox/ pyres-1.5/.travis.yml000066400000000000000000000003311232362746500146410ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "3.3" # - "pypy" # command to install dependencies install: - python setup.py install # command to run tests script: python setup.py test services: - redis-server pyres-1.5/AUTHORS.md000066400000000000000000000006061232362746500142040ustar00rootroot00000000000000## Authors * Matt George * Craig Hawco * Michael Russo * Chris Song * Whit Morriss * Joe Shaw * Yashwanth Nelapati * Cezar Sa Espinola * Alex Ezell * Christy O'Reilly * Kevin McConnell * Bernardo Heynemann * David Schoonover * Rob Hudson * Salimane Adjao Moustapha * John Hobbs * James M. Henderson * Iraê Carvalho * Fabien Reboia * Peter Teichman Inspired by Resque, by Chris Wanstrath pyres-1.5/CHANGES.txt000066400000000000000000000002221232362746500143400ustar00rootroot000000000000002011-03-01 whit * Added hooks for the worker to allow worker subclasses to insert code before and after forking pyres-1.5/HISTORY.md000066400000000000000000000056141232362746500142240ustar00rootroot00000000000000##1.4.2 (2013-06-21) * __str__ returns correctly with dsn * worker_pids returns correct set of workers * workers are re-registered on every job * add exception metadata for after_perform method * logger no longer overrides root logger * support for redis db in dsn ##1.4.1 (2012-07-30) * fix for non existent system signal for linux * cleanup of setup.py and requirements ##1.4 (2012-06-?) * added hooks for before and after perform methods * fixed logging *fixed problems with password authentication ##1.3 (2012-06-01) * remove resweb from pyres * resweb is now available at http://github.com/Pyres/resweb or on pypi ##1.2 * release with changes from pull requests ##1.1 (2011-06-16) * api change based on redis-py * setproctitle requirements fix * change exception logging in worker ##1.0.1 (2011-04-12) * fixed bug with tempaltes and media in resweb * call to redis-py disconnect was failing, switched to connection.disconnect * interval cast to int for pyres_worker script command ## 0.9.1 (2010-10-15) * fixing issues #45, #46. * #45 - resweb not working in chrome * #46 - delayed_queue_schedule_size() returns incorrect value * updated version requirement for redis-py * added Failure docs from Alex._ ## 0.9 (2010-08-05) * added better logging to the project ## 0.8 (2010-04-24) * added the pyres_manager and the horde module. This allows a more prefork like model for processing jobs. * setproctitle usage. Allows better process titles when viewing via ps * ability to delete and requeue failed items ## 0.7.5.1 (2010-03-18) * fixed the pyres_scheduler script * changed download link to remove v from version number ## 0.7.5 (2010-03-18) * added feature to retry jobs based on a class attribute ## 0.7.1 (2010-03-16) * bug fix for pruning workers. ## 0.7.0 (2010-03-05) * delayed tasks * resweb pagination * switch stored timestamps to a unix timestamp * updated documentation * upgraded to redis-py 1.34.1 * switched from print statements to the logging module * import errors on jobs are now reported in the failed queue * prune dead workers * small bugfixes in the resweb package * improved failure formatting * datetime json parser ## 0.5.0 (2010-0114) * added new documentation to the project * update setup.py * preparing for semantic versioning ## 0.4.1 (2010-01-06) * fixed issue with new failure package in distutils sdist * changed setup.py to remove camel case, because it's ugly ## 0.4.0 (2010-01-06) * added the basics of failure backend support ## 0.3.1 (2009-12-16) * minor bug fix in worker.py * merged in some setup.py niceties from dsc fork * merged in better README info from dsc fork ## 0.3.0 (2009-12-10) * updated setup.py * refactored package for better testing * resque namespacing by fakechris * smarter import from string by fakechris ## 0.2.0 (2009-12-09) * Better web interface via resweb * Updated the api to be more inline with resque * More tests. ## 0.1.0 (2009-12-01) * First release. pyres-1.5/LICENSE000066400000000000000000000020441232362746500135400ustar00rootroot00000000000000Copyright (c) 2009-2013 Matt George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyres-1.5/MANIFEST.in000066400000000000000000000000311232362746500142630ustar00rootroot00000000000000include requirements.txt pyres-1.5/README.markdown000066400000000000000000000030401232362746500152310ustar00rootroot00000000000000Pyres - a Resque clone ====================== [Resque](http://github.com/defunkt/resque) is a great implementation of a job queue by the people at github. It's written in ruby, which is great, but I primarily work in python. So I took on the task of porting over the code to python and PyRes was the result ## Project Goals Because of some differences between ruby and python, there are a couple of places where I chose speed over correctness. The goal will be to eventually take the application and make it more pythonic without sacrificing the awesome functionality found in resque. At the same time, I hope to stay within the bounds of the original api and web interface. ## Travis CI Currently, pyres is being tested via travis ci for python version 2.6, 2.7, and 3.3: [![Build Status](https://secure.travis-ci.org/binarydud/pyres.png)](http://travis-ci.org/binarydud/pyres) ## Running Tests 1. Install nose: `$ easy_install nose` 2. Start redis: `$ redis-server [PATH_TO_YOUR_REDIS_CONFIG]` 3. Run nose: `$ nosetests` Or more verbosely: `$ nosetests -v` ##Mailing List To join the list simply send an email to . This will subscribe you and send you information about your subscription, include unsubscribe information. The archive can be found at . ## Information * Code: `git clone git://github.com/binarydud/pyres.git` * Home: * Docs: * Bugs: * List: pyres-1.5/docs/000077500000000000000000000000001232362746500134635ustar00rootroot00000000000000pyres-1.5/docs/Makefile000066400000000000000000000056361232362746500151350ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex 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 " 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 " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @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 build/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html @echo @echo "Build finished. The HTML pages are in build/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml @echo @echo "Build finished. The HTML pages are in build/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in build/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in build/qthelp, like this:" @echo "# qcollectiongenerator build/qthelp/PyRes.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile build/qthelp/PyRes.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex @echo @echo "Build finished; the LaTeX files are in build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes @echo @echo "The overview file is in build/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in build/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in build/doctest/output.txt." pyres-1.5/docs/make.bat000066400000000000000000000055731232362746500151020ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation set SPHINXBUILD=sphinx-build set ALLSPHINXOPTS=-d build/doctrees %SPHINXOPTS% source 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. 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. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 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 (build\*) do rmdir /q /s %%i del /q /s build\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% build/html echo. echo.Build finished. The HTML pages are in build/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% build/dirhtml echo. echo.Build finished. The HTML pages are in build/dirhtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% build/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% build/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% build/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in build/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% build/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in build/qthelp, like this: echo.^> qcollectiongenerator build\qthelp\PyRes.qhcp echo.To view the help file: echo.^> assistant -collectionFile build\qthelp\PyRes.ghc goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% build/latex echo. echo.Build finished; the LaTeX files are in build/latex. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% build/changes echo. echo.The overview file is in build/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% build/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in build/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% build/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in build/doctest/output.txt. goto end ) :end pyres-1.5/docs/source/000077500000000000000000000000001232362746500147635ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/000077500000000000000000000000001232362746500162245ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/ADCTheme/000077500000000000000000000000001232362746500175765ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/ADCTheme/README.rst000066400000000000000000000016651232362746500212750ustar00rootroot00000000000000============== How To Install ============== Install in Sphinx ----------------- Copy this directory into the ``sphinx/templates`` directory where Shpinx is installed. For example, a standard install of sphinx on Mac OS X is at ``/Library/Python/2.6/site-packages/Sphinx-0.6.3-py2.6.egg/`` Install Somewhere Else ---------------------- If you want to install this theme somewhere else, you will have to modify the ``conf.py`` file. :: templates_path = ['/absolute/path/to/dir/','relative/path/'] Making Sphinx Use the Theme --------------------------- Edit the ``conf.py`` file and make the following setting: :: html_theme = 'ADCtheme' Screen Shots ------------ .. image:: http://github.com/coordt/ADCtheme/raw/master/static/scrn1.png .. image:: http://github.com/coordt/ADCtheme/raw/master/static/scrn2.png To Do ----- * Gotta get the javascript working so the Table of Contents is hide-able. * Probably lots of css cleanup.pyres-1.5/docs/source/_theme/ADCTheme/layout.html000066400000000000000000000124421232362746500220040ustar00rootroot00000000000000{% extends "basic/layout.html" %} {%- block doctype -%} {%- endblock %} {%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} {%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} {%- block linktags %} {%- if hasdoc('about') %} {%- endif %} {%- if hasdoc('genindex') %} {%- endif %} {%- if hasdoc('search') %} {%- endif %} {%- if hasdoc('copyright') %} {%- endif %} {%- if parents %} {%- endif %} {%- if next %} {%- endif %} {%- if prev %} {%- endif %} {%- endblock %} {%- block extrahead %} {% endblock %} {%- block header %}{% endblock %} {%- block relbar1 %}

{{docstitle}}

{% endblock %} {%- block sidebar1 %} {%- if not embedded %}{% if not theme_nosidebar|tobool %}
{%- block sidebarlogo %} {%- if logo %} {%- endif %} {%- endblock %} {%- block sidebartoc %} {{ toctree() }} {%- endblock %} {%- block sidebarrel %} {%- endblock %} {%- block sidebarsourcelink %} {%- if show_source and has_source and sourcename %}

{{ _('This Page') }}

{%- endif %} {%- endblock %} {%- if customsidebar %} {% include customsidebar %} {%- endif %} {%- block sidebarsearch %} {%- if pagename != "search" %} {%- endif %} {%- endblock %}
{%- endif %}{% endif %} {% endblock %} {%- block document %}
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
{%- endif %}{% endif %}
{% block body %} {% endblock %}
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
{%- endif %}{% endif %}
{%- endblock %} {%- block sidebar2 %}{% endblock %} {%- block relbar2 %}{% endblock %} {%- block footer %} {%- endblock %} pyres-1.5/docs/source/_theme/ADCTheme/static/000077500000000000000000000000001232362746500210655ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/ADCTheme/static/adctheme.css000066400000000000000000000320431232362746500233530ustar00rootroot00000000000000/** * Sphinx stylesheet -- basic theme * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* -- main layout ----------------------------------------------------------- */ div.clearer { clear: both; } /* -- header ---------------------------------------------------------------- */ #header #title { background:#29334F url(title_background.png) repeat-x scroll 0 0; border-bottom:1px solid #B6B6B6; height:25px; overflow:hidden; } #headerButtons { position: absolute; list-style: none outside; top: 26px; left: 0px; right: 0px; margin: 0px; padding: 0px; border-top: 1px solid #2B334F; border-bottom: 1px solid #EDEDED; height: 20px; font-size: 8pt; overflow: hidden; background-color: #D8D8D8; } #headerButtons li { background-repeat:no-repeat; display:inline; margin-top:0; padding:0; } .headerButton { display: inline; height:20px; } .headerButton a { text-decoration: none; float: right; height: 20px; padding: 4px 15px; border-left: 1px solid #ACACAC; font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; color: black; } .headerButton a:hover { color: white; background-color: #787878; } li#toc_button { text-align:left; } li#toc_button .headerButton a { width:198px; padding-top: 4px; font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; color: black; float: left; padding-left:15px; border-right:1px solid #ACACAC; background:transparent url(triangle_closed.png) no-repeat scroll 4px 6px; } li#page_buttons { position:absolute; right:0; } #breadcrumbs { color: black; background-image:url(breadcrumb_background.png); border-top:1px solid #2B334F; bottom:0; font-size:10px; height:15px; left:0; overflow:hidden; padding:3px 10px 0; position:absolute; right:0; white-space:nowrap; z-index:901; } #breadcrumbs a { color: black; text-decoration: none; } #breadcrumbs a:hover { text-decoration: underline; } /* -- sidebar --------------------------------------------------------------- */ #sphinxsidebar { position: absolute; top: 84px; bottom: 19px; left: 0px; width: 229px; background-color: #E4EBF7; border-right: 1px solid #ACACAC; border-top: 1px solid #2B334F; overflow-x: hidden; overflow-y: auto; padding: 0px 0px 0px 0px; font-size:11px; } div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } #sphinxsidebar li { margin: 0px; padding: 0px; font-weight: normal; margin: 0px 0px 7px 0px; overflow: hidden; text-overflow: ellipsis; font-size: 11px; } #sphinxsidebar ul { list-style: none; margin: 0px 0px 0px 0px; padding: 0px 5px 0px 5px; } #sphinxsidebar ul ul, #sphinxsidebar ul.want-points { list-style: square; } #sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } #sphinxsidebar form { margin-top: 10px; } #sphinxsidebar input { border: 1px solid #787878; font-family: sans-serif; font-size: 1em; } img { border: 0; } #sphinxsidebar li.toctree-l1 a { font-weight: bold; color: #000; text-decoration: none; } #sphinxsidebar li.toctree-l2 a { font-weight: bold; color: #4f4f4f; text-decoration: none; } /* -- search page ----------------------------------------------------------- */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #888; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } #sphinxsidebar input.prettysearch {border:none;} input.searchbutton { float: right; } .search-wrapper {width: 100%; height: 25px;} .search-wrapper input.prettysearch { border: none; width:200px; height: 16px; background: url(searchfield_repeat.png) center top repeat-x; border: 0px; margin: 0; padding: 3px 0 0 0; font: 11px "Lucida Grande", "Lucida Sans Unicode", Arial, sans-serif; } .search-wrapper input.prettysearch { width: 184px; margin-left: 20px; *margin-top:-1px; *margin-right:-2px; *margin-left:10px; } .search-wrapper .search-left { display: block; position: absolute; width: 20px; height: 19px; background: url(searchfield_leftcap.png) left top no-repeat; } .search-wrapper .search-right { display: block; position: relative; left: 204px; top: -19px; width: 10px; height: 19px; background: url(searchfield_rightcap.png) right top no-repeat; } /* -- index page ------------------------------------------------------------ */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* -- general index --------------------------------------------------------- */ table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #f2f2f2; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } /* -- general body styles --------------------------------------------------- */ .document { border-top:1px solid #2B334F; overflow:auto; padding-left:2em; padding-right:2em; position:absolute; z-index:1; top:84px; bottom:19px; right:0; left:230px; } a.headerlink { visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } p.rubric { margin-top: 30px; font-weight: bold; } /* -- sidebars -------------------------------------------------------------- */ /*div.sidebar { margin: 0 0 0.5em 1em; border: 1px solid #ddb; padding: 7px 7px 0 7px; background-color: #ffe; width: 40%; float: right; } p.sidebar-title { font-weight: bold; } */ /* -- topics ---------------------------------------------------------------- */ div.topic { border: 1px solid #ccc; padding: 7px 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* -- admonitions ----------------------------------------------------------- */ .admonition { border: 1px solid #a1a5a9; background-color: #f7f7f7; margin: 20px; padding: 0px 8px 7px 9px; text-align: left; } .warning { background-color:#E8E8E8; border:1px solid #111111; margin:30px; } .admonition p { font: 12px 'Lucida Grande', Geneva, Helvetica, Arial, sans-serif; margin-top: 7px; margin-bottom: 0px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; padding-top: 3px; } div.body p.centered { text-align: center; margin-top: 25px; } /* -- tables ---------------------------------------------------------------- */ table.docutils { border-collapse: collapse; border-top: 1px solid #919699; border-left: 1px solid #919699; border-right: 1px solid #919699; font-size:12px; padding:8px; text-align:left; vertical-align:top; } table.docutils td, table.docutils th { padding: 8px; font-size: 12px; text-align: left; vertical-align: top; border-bottom: 1px solid #919699; } table.docutils th { font-weight: bold; } /* This alternates colors in up to six table rows (light blue for odd, white for even)*/ .docutils tr { background: #F0F5F9; } .docutils tr + tr { background: #FFFFFF; } .docutils tr + tr + tr { background: #F0F5F9; } .docutils tr + tr + tr + tr { background: #FFFFFF; } .docutils tr + tr + tr +tr + tr { background: #F0F5F9; } .docutils tr + tr + tr + tr + tr + tr { background: #FFFFFF; } .docutils tr + tr + tr + tr + tr + tr + tr { background: #F0F5F9; } table.footnote td, table.footnote th { border: 0 !important; } th { text-align: left; padding-right: 5px; } /* -- other body styles ----------------------------------------------------- */ dl { margin-bottom: 15px; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } dt:target, .highlight { background-color: #fbe54e; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } .field-list ul { vertical-align: top; margin: 0; padding-bottom: 0; list-style: none inside; } .field-list ul li { margin-top: 0; } .field-list p { margin: 0; } .refcount { color: #060; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } .system-message { background-color: #fda; padding: 5px; border: 3px solid red; } .footnote:target { background-color: #ffa } /* -- code displays --------------------------------------------------------- */ pre { overflow: auto; background-color:#F1F5F9; border:1px solid #C9D1D7; border-spacing:0; font-family:"Bitstream Vera Sans Mono",Monaco,"Lucida Console",Courier,Consolas,monospace; font-size:11px; padding: 10px; } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } /* -- math display ---------------------------------------------------------- */ img.math { vertical-align: middle; } div.body div.math p { text-align: center; } span.eqno { float: right; } /* -- printout stylesheet --------------------------------------------------- */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0; width: 100%; } div.sphinxsidebar, div.related, div.footer, #top-link { display: none; } } body { font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; } dl.class dt { padding: 3px; border-top: 2px solid #999; } tt.descname { font-size: 1em; } em.property { font-style: normal; } dl.class dd p { } dl.class dd dl.exception dt { padding: 3px; background-color: #FFD6D6; border-top: none; } dl.class dd dl.method dt { padding: 3px; background-color: #e9e9e9; border-top: none; } dl.function dt { padding: 3px; border-top: 2px solid #999; } ul { list-style-image:none; list-style-position:outside; list-style-type:square; margin:0 0 0 30px; padding:0 0 12px 6px; } #docstitle { height: 36px; background-image: url(header_sm_mid.png); left: 0; top: 0; position: absolute; width: 100%; } #docstitle p { padding:7px 0 0 45px; margin: 0; color: white; text-shadow:0 1px 0 #787878; background: transparent url(documentation.png) no-repeat scroll 10px 3px; height: 36px; font-size: 15px; } #header { height:45px; left:0; position:absolute; right:0; top:36px; z-index:900; } #header h1 { font-size:10pt; margin:0; padding:5px 0 0 10px; text-shadow:0 1px 0 #D5D5D5; white-space:nowrap; } h1 { -x-system-font:none; color:#000000; font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; font-size:30px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:normal; margin-bottom:25px; margin-top:1em; } .footer { border-top:1px solid #DDDDDD; clear:both; padding-top:9px; width:100%; font-size:10px; } p { -x-system-font:none; font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; font-size:12px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal; line-height:normal; margin-bottom:10px; margin-top:0; } h2 { border-bottom:1px solid #919699; color:#000000; font-size:24px; margin-top:2.5em; padding-bottom:2px; } a:link:hover { color:#093D92; text-decoration:underline; } a:link { color:#093D92; text-decoration:none; } ol { list-style-position:outside; list-style-type:decimal; margin:0 0 0 30px; padding:0 0 12px 6px; } li { margin-top:7px; font-family:'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; font-size:12px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal; line-height:normal; } li > p { display:inline; } li p { margin-top:8px; }pyres-1.5/docs/source/_theme/ADCTheme/static/breadcrumb_background.png000066400000000000000000000002101232362746500260710ustar00rootroot00000000000000PNG  IHDRQ g pHYs  :IDAT=1 CQh<҆Xt;v,Q/ig7d&ޫ 8;kIENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/documentation.png000066400000000000000000000006341232362746500244470ustar00rootroot00000000000000PNG  IHDRvcIDATH햱J\A@5* 1EH)!` U$]]VXe;+-UnvXy 3sgνs z>5 8_%d;ࣺπQ`؉ܹY0L@hGPvQoRJ?mu5ͯW[הޔu= BD}*}GI?ak\*A+=L\SgcQg)Њ1)`#; >]4]40,3vK>.TVߦ~$~\R:ZGK}f~Q?қ>U9FrYȦ=IENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/header_sm_mid.png000066400000000000000000000002371232362746500243550ustar00rootroot00000000000000PNG  IHDR$tEXtSoftwareAdobe ImageReadyqe<AIDATxb Ϫe, c/VXDPīKpFW_;X]FZ&l`8cH#>IENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/scrn1.png000066400000000000000000003230161232362746500226260ustar00rootroot00000000000000PNG  IHDR0 qiCCPICC ProfilexZuTo~gNr8twwЍtw(4X X ҈6* R53{eޙy@gDD  1"::9 8ؾOhW5.?'1]1`}voD 6;C5C=D_.bO(ؖ{^OO^{#ّ1`| p=b vwDr{m׷0#߲0}-5b}b~'/D$& ?Kvv6Oبlz@@H9 ԁ0 'AQHi(@=@+h 0&3̂` MpX!nHd!H 2  P(J2<TBP Ac5-C0 SŒ0',Kʰl ®p!|kp< O "P( 2@9|Q$T(uu5zF}Dh:47Z6Dۡ=$t$,* İcD1#Ĕb0W0}q[7, ˈ*b Xl6[mŽb_cP8&NgsE2pqq7q7<ρ | ,~HGDaFAKGQCqb5wOP%X| I# ncgJ %7e"e)e#esoD"QIt q"Jʎ**4U'cej"5ڍ:zzQq)iyG rӪ:-mDG3ˠ{EMEAB@_N):*CC9M [\ZO332Ę̘BZ&~03k2{0g0cfBϢĒre+ k2k" ;[&[wv.v= bvi,GG7NN=Σ8縨ScQS;@JL=WVC_#CWLJѺݭttuuu1F9d"ْ\BcsԫЛ?ɀdcaece8lDmdkTf4m,`hljnfrǔ޴tL,Ҭ?hذ$[XNZq[[XmZX~GpO؞6h+rY[Y$!;F;KvG_9H:$8˸kێ N*N&c\\]Z]Qu+nnܕsݟܗõ#'gguoUO/ٷw p h D2*:&:\-4|1(6b3!re_TO4gt\xLL~XؚحN080'> .NHJNLN|T|3#%)EfjuAiiYiM33"2322<?%.#+'-g.<%9/)o&8 `и(hk\YşKJ)9QSzcb>nz '~:9ZVv3gϞ:G{.yO.^li8ԘӸy1KF46W]}y%}[+]W^&x-jo_xsf7ojl$u6U}viuWn7;{grohB_LNSX޹7d:ta{{=uG{=|hp{cSϧܧf>^xdiӍgQs^P(}zZpZޘqy뻄Yx6ay>|)oy/_Ɩ\c_ְ2j{kk w767[e?~6~7 s\o.........+@!Gra JVGmBbKh@f t@0d .r]8Tt̜,BbҤ*fVi%} aC#/ 6拖VZ{lmBms{M0q+rxz%z| h\ VIN!e?6́kqo֓hESRƧLoϘ\>-E@B"zR%L%GfKm8sI2r J*jS5:IugF6=}A"Assּ+_+n+jHx#fxg-N]z=J} ݼ8\6/~ب1>?81I}Lxӱg7׿(|3J5 [wwKsK}H k>F~t13 ×oK|=-io5uS[2X viar58Ph `pU+.1jց~Q m56Qv}\ܶ<6|~тiBGDD_I0IIYKdd_+*U(M%^i?ՙ'1240DCfC#_yV rumig >Ypt  K?Qy*)vXqLr ։aI)'SEdxgfQcG/Lv_Trģh'\NZIUN՚W;c~\  /74^oiͺy5ZZ[F{;& Ǐ!OCh惼#+o6ykjȓO'=~=Z͌۠wQqsS2?~,TXҗ_IVھkY7^l~z!v8v ?I`C pJ5ޏ|v#)\d2, - 6<C=c$V1l9L99nrx_(F OԉFiCCVRLRSeH rr;m !Jʋ*Tm0jE{54?i]>c+Jҫҏ1036ԙf[[b-g$۸jqww^vpvqsur3w'Aīۻѧ7/?2/9"X/D9T<#"|5Mxdtlo&.%IJLLz`YOlNƃ̺,7ss/Jm-/;Vzh1lߟh?_SN$VU-TOyT;zznWϝ=_b雷[-4|W䯒Yٵ8z}&gۭS_vqP݊ᾑOEGo=:>9uI3r\HXl!;5I_e §5vXD\E~"̵)b@D8^0:Dr! CYP%th&p.BѠtQ1lNE0L-fˉuVa_pA:_C!KEH(!,RZP6"j2)j,u83#k´'6c03|f`\`rae6`D&V 6 Zw8p ZaE>BBKOTPY )72eXl<9%-Ƥ&a顕]3QO3c!{Sob&GV;{H6!~:Y5CkW?(8T5&2`jLD⁠O QɇSv;gٖ9[y 1~X]jxt孕շjn\_w|JCE&W:ntVV5,`,n\y)ʗ^[͜񏣋K,_Wؾ~|X,A "0 dt!O؆ErAz CHO{p \߃Q)t-ba~`U^g;W)) ( VJ,%;.UiNLAQqɐb2j-m=&W7]ɣ7wj/DAbG%IKIKxIra 抚JZ*:jfZ:u=Ol{ M0YY^mf[ox'*87֞z|G9GCÎoFD{V\[Bdɔi2,3_Ρȭ(x\]L_rH"Jzq֙sh/5i7o_).1sdmBPOIπïu<8Q?UYiW?\z:'/8b EdX2eh`Z:"򋈱Gg`""?mOM)҃ykT=~`_0hh1ٍň7 VB~_t|0'!f?!_dG^ G;/@?rO$ƮnwtŮ_!5H[ƄFˡd&Z xhv V@uhuD ;ij1m2<^!^u#??'S]'{o[on E&}%yM¼%yH$U_5^x pHYs   IDATx\SOHBe((( Tp+:Vq]kR:jm몕jq TPP@({ 2n&!u=&|O_{.'&$@H $xO5ʼn"$@H `EIY[ zRV^RZ^ $@H "xzDy\;=:lhaa HT[p;>)2Σ\v>mZpqrmd6̜Ԥԇ ܯ t $@H#6i^ݳlaڴrD"w^JNۺ@Xoˤ<sGo-"$@H  s̷CNuiPU'ï4 6FH $@ϑrcsTຝ5ފM!$@H <R^:ka<0X` $@H xJe=R㞎 [ $@H HOAsV+"'ٶo$~zEYvڂH3n$<ƎՋ:Rq Qơ @H $@C<4 }5YuE%)}x!Ҁk>dcɒc֯Fιk\,CA~zoG6A=Z6F!ϩݛTb-^x8$@(We;9T0_LBs+nt7 >*C~Wv7#Qi/59/ߟ둺onj\ZZ S>d| DOqZK._Wwݟ2FH $^IO?`P_$XAt*\%gs ;O섈ٲ"łW+J2NJqq y$keV̞ Jwyϱ4Mޝcۧ]%~pjҍ&L{Ӥktw"Dp6^soNYq6wCpG#:s綒92L&,\4+=KB R"|: hAw$ YxUfO hOjRi6OzQ}/[),$YPt=. F(=esIK:POR\r_R%?N=nH=Կ5[od׭mߜȢR2սt_LH $PHZ^;sg\!TP8@e+%)/ שY7S&F<G;K"8iZYcE > =ͬ͌,J* .G}wg4y~ժ ޜp5ҭ#oԆ&`k@J&_~LwzoFÃk*t~.\+L fʣ?#J)! -} v_u`٣|Caţ[#Y9*)Fwjd*7jRe{t;秿:@^0jeN^-{Qe/Mjm br(Gr%G0jDhD,͘5s027*Lhiʳ1vl{ι ׋+Z5xo]ؗG_5=ȼڴS7&oKW* &iEN㦽wܜ6&ܒDž\P6ӿ5[cB{K.SKoH $P=$}FH}#"8jHej ."?>͢[Ҳ SFfjᲴ~ ˩TYcϦO?3o.WZ0B!sRwsy{W$9/&|dŽHar幗ĉ5n5ikRf\JE.'mԦ<Ri#ܫ;WZLmn?N\[>R@*6.[_!0?9rN]ON+̈m?.+Ml)}3S AW2˒iKtxIsc7L>]x&҆1^ϊ5dV+]|OŞdPAy߷'19v˯r/b*7[0п5[1TĄ@H mOk9"M-A0 GI°}sLĔ3ãp#+ym.)2r\Bv:ȳ/MICܖNб9)_7&5JgH9ʍg-(Jg<ؽvtPoƝ>QM[.̎h,=$bDrJzȸTBƮm]aG'fг-8v`N^Bz_1iS'/+ف s6zv?S('Ign r,Fb6_eLwTcJIv_Ľ iB16 kVInԀ7U8!ke3ee>Q*c@H @cLSrvze;k ª48ΓҤ蛑%VzicUQI9tĘ>B.+(WIضAF+ s{XYˁ85!B/Oe3"yTt;ӄ  '-;ژx̻) @bťj* 9˸!b1 JbL+/WcgBϗ>xº͑Q%T7++Б5.fG69`8%+MXzU EV+n).gr sH $ O9"*X)!Ue\_UB쁨R{n OpRqA> )sêPcά V&D#f~s6̬ۿ {Wn.L+5-v@1+gA"d*+OJG)iɆTަ̹wI: ZZ ɾ3k#]LXt ȓ&ML^N𞟖xYFlͬ|cm}[  ̱8Kh6c\cg{ @Y e4j쒂}mv]gt]֝3_|AH $:FnxLPѬ/*dDfl[1~VDZ}3hX3ZNnvH8+s Dc޶ӓM- 2.|9ľM&ˈQVY*bmg]P0x`ccCfRxD6>͇.bۻ>-b91R 材߾=ݨSHC7CZpLȫ/%?,{4|J:`YVuf23/zplB~ԉ E-<|d5Ǫϥ~5h37"]cT;F9Weҳվ[TsH $K){9yNmjըeRF|Ø$M~o%j@GO\o6Ǵ!pFEk7_;;Z$"i%*F {2 ^=uA+{@73|ArweTV `݂ײ̓F0+Kx~&~ۡߠ YwN$- `k&Z']Bs9;tRN\2SB9&BԪd% ~󠎭O1Gw*ϕ}k fyδث ٨^{p1XKSVlTuI%Pa7Ԡ@'EfX!{ReUH}R[P'\V[:W VkyRJQ=W>bBH $6ow?0v ?;yk v<$=)LM(`#O4O;fMrkįss{iqn fmD.'zx[LǦ%'%v{7ohp׎mgk-IPŵhNM iy؄LԴ{#{2ԣ]Uɟd\G6]}[YJt,1kٳ=4%ݺQOqnJHD2&עRDŽ-[3fߏ[>d1B*,LO#1l̯uA&?Z 뱙2bܸUW;hO{0F-:9󉬸(JMwK ̬n0ޚun^9s̵h٥eaZ\l6鹎ebֺ֥]„@H wAcqӧLACvko׿WeG5qԧOg{A 7zD.[ XoY7o%>(q6jUDh f 6Z,=b1=)-c"5X @mJJ/ZMyFƴDŽ*լ yp\,!ᛙ L%``&4yRRUBSeԱZV`s2U@H 7c \l7Y)7x~'*OJv$Od{eyqn50{ͬ!*SP9bIbqm!iPzįڪzR[]QZ_G3*BP:VZKuL! $x|_sjD0zQ1% BKK |[I?613#e%Ye7ǁ; Ҳ_4"$@H 7Dz)iq3?(ccc>:ٮ),*r52nB~~>Hd{;;6;wl =K _|3L; ⌈ p??P $@H& vǟD껮ZɓĤ:OGǎܼ|MLݺ`tng SҚ^ui\N M@H $4.qcE Tq\޹PG3## A†; @H $ %0㣠`> A|)$@H |00E6|pH $x t $@HYvg}@H $^k'\fL^:i^?n mҽyaT>+Z7i 0aDH $xxMמ#GojJ϶I[nR }$-m{)'_<2Cx' H $x ԽcaDwv]zjɟ?6wACKWW ڈPenIhuRw~ͿΡ˜Eܛ i@H $8?j`gC?:Fb3%y\:yAjlRrzN~oڪcg܌N&nܸN: sgY^x%VķnߥM/{tbD(kJ|{ZX+Mrfj۫Y9ۀRWtZLHF=:3kh#}qrдq >.Iiԥ!gD_~TN<8[>RԨ;6E7ηv$"Ҫ Wv\qv7 DRw~,ZxUĞJ(pKC\z-ԫZӮ^z".>R# s=}ܸ\@Q^r/2">5WF |m!iN%߷O]|EH $@p9_{C_8Omb &؟,O "|k˴1@H}_k/}6qZlNH]L!}vDzG#RphƗՅ{9!u.y}kGt|=.\&O8s-q/FD1d}M,Xړ;x[ * 1)ӯBys" 1UŐmn_C6]mD/3/xSG_c|pkR-p9u59#>0ny$@H D)uo 48U4qgX FfM L@ [}फGs%ytbEe"( YvQ[*E^E L$Xp Y1F>h=/GvueČEyN$]%~:50(8Qnυw~팙[qzg1 wu)QVayH)1bl$3[)l=k)=GHvXx`Լܢrr\Ob r*\O1 xѾA^?&uƻ;;׃C~*;w#/a!1Nf h%,:6N˨w6]<8y·KӓEJScf2@H @7Ʃ&TÕU[#$ ؔ+Zض`\ok.(7QqzvQܲ&$.WinvpN66u0o2-a wxwk4!ITH8 f/N*#l8Qt!dur5&g+cm҈NRP*' ^uH}?7Uٙ6sneye| N8 \9[IRRB0agپ/]JIduR(9bF^c3cEiyt̲ГTH*``wRٿ2<@hr{:9dKЋ @H $ru`bbt.K*/їŜP,?ރ 9L "AIrS("`aԝ\P*E"13="`WDmDOcTjLBd5%*pF` JQF Nx\zKX˖TJh W(! pVeJj0R.'LQㆾcv ƚR4wCH8911fG1H _\\ (ڟ IDAT9O“vJ/^KU;1UEe'Um3b֪ P`L}N8^e+i TlҰ01cmLsN]-)1RbT*R=LBb B3>6)#WG.T(^b94# v` caBH $W$ #.3a`KUm޶м~^m}̎t18$QZ%\&ՓfRYw&a˔:\T}ֻa~\VԁAs{GX E~c![C8v_q<ȜjZu`є%.f|Ă6{㧟_M:vB*ɺ\&Lu ߒ7'oiN-vx\/ؿK']Sp`޷{-|2[P;{ & ?#wJc6vo1%S iZs@oH $[M)"UON|gN16}V|6+;w;3zhiV#$]M6XhB_•Lˍ[3oG`K/U*[ym]y^tڍ3ߟćGѿl޼P-9㷜O;F"[ xp2(EoX̸oB[ըI\ ٹp2qiجr;~KtB!㡂S/,rm׌Og =;6wT><b<}e؆nW/VmH $x*9vn}G|5&4CrosL@H $xAeЧ=40q[4q474 ܂\ڀ.M ޽ۮ]6H $co?R/]>?pH $@He@2H $@/EH $xPL86@H $K!"`A@H $^&'%GIM*J8WxU!TѥЭ+W]:򊊊JE "$@H%b/ O}d͂R%^ѣjmCk"%֓&l7*7gqTvu%97F>Z:5BwUK/m nӵg׮]}}۵#X@H ל"bJk"nO\]T{]psrSP]G>Wy &p."| 3ޛ"&ϴ~#Đǭ^3$Ē_sKGtl#+@$2y m/Xpb,е^q]#wW5tb-@H $j%``jB h+#NvnS(u::F&|bbPCG^TvǏ˳spp7$~6yѡT1O}AJsc2々2jyIVZ06;aed!9GHϡӱ [ i.Bs>=2Z4"LZ4CqzBKѧ2+÷$Q~ BɹޭB·2^~J;r×;VJ$i[@ȑG>DU8W(!$@o%If9/Eߟ9LgvŖޅLBcY$ GHBI9tn_+ Lasn1ZK򐶜\Cw]X[y$U5̇hS^IcWsZ')D_Ǧ.d9'[qlkϢ+#D5puE9P[qVEY{Y]Î۝;wVTԗGy-""N<糬;L}sӞ-+,,%p$mEl82$F-!M nbmŎ}hAi90 B.!!į_G_|?YSPxđ-d߅$*iԱs<7ٴ`ūDDyE;!Xɚ g O@v,@%~GF _8(t؎"qjھ2_j$@HĒ؊ٛ3h#o,Z5ݗU<Qt7`cN|kF:U^bi‘ %ĔEV1B*c+**uE!X#w\ K+E;9]'Oʮ-f1sBNHf$gkK|Fyxεc}mbR8`Gԑ _f$QQQK.=~8{YKq=Z[۲Gֲd¦e}|z _/q9KKRŕdztG"*n*rlp;;sRJc;m40Nz*IB-έW{fCGGÑzu0'7ifkbdd֛1f'w} "hxwnWMfzwkо ] $x |aCwʞ<@6E,b8>iMŢ?v1ӗ6D4f0Ng)nlY"]:'lvyvX zг"$:ֳw믇zo~:aq㕉_%V|*K|xEY&GAT`/xBX{R}Ok#Z UYҐERܣ)-dtEՓ~kֱ3T>=Uns.hu3wЩ3&(6)V3ݠ{8_;O\:k_@H $f6x [_yBVJ iBrF+Ue9ڕ|V(-}ާ7%L&8zv7roAGzdtUfLvvz=~=Fy_)?q:6$[9cKO!K!D[R=|h!Z3 , _QkѤf; 6B$q2'NFf劈=6%Ӱ*٪C}abbw> Rɩ3YSu^v>1g^at]Rww$@H& zwYB/\-hҔPy41uP3:[:-%C[lmq4)}'A7R3( A⎌'=񥽉t cY?tx1--^xРARA%Z|'4t$J.bC|Ύ]aK$iO"Qi^rrO'QfQ(sĽsaiCu\$8,8-_N-F %!k$IҴx5f|SHD;MEIi^Z|ꆿj=RHsneHhKiEm &=f?,AH $ܼY\}< vln"#üc.\̘=(͈M9SwٓǛJ 2A>SS- Ĵ GBBNy[G}z='yѱW;7J8l r:R$U&B1, {|',fM;CЮkh+}O lbm܂l! KEhĮ'{ 쳘v_gKgFL՗]]gj;f?D88Dv {M?!B!`"*.%Gtݱ )GI$}9x`uiCĮ$xb5KÆ}B w&/H $F@;ig<Χ!7r_gzc' 'N2-g<^&XbQ1;Hy[б޹6ƦIHM,cCg^lc͔U͛|խo]zS=[ϭʿݜ?zo[7ƼIüܻ[_CJ΢DD EQ<8*$ܼ+%*[ˠЏ~ r`b&@H $x rW>Kh x1ŏ# $@pLfii)2#|>lrxĵk=FH $x upphڴኊ PÐg0 ~?.8y$@H @ƅ]xqyy9HaC!Բ ")oƺ,@H $VC/`@R `J*h"X 3H $@+LbP)Qka $@HM#7>>^}!3W67mq>H $@@Ç[lyRԎpP $@HM bW{˃plvڬ0@H $ZX/$QQQIIIp~MV!6UM5}0@H $^/ sY n \BVE뵾-@H $~ O+((4*++;~F  $@H AVbCCCFŷn}Ms@H $x Tdp CUHpCDl[cG[U*9gF%)KY3ˏnVeVY^!$@H E@[Hh9 Mnt]3%0!#C.k)k+qaA< sR<Χ $@H B@V`h5,"sj}~3}]_du(ˍǟGݻ͎p{OնԿېO;lۻW8u򫙣{ '3 $y!6Q 1!$@H  zRi6JKKE"aHd 53o/fr q^3QEeoħgle#/?%!;٣[^5)b"G%\+S&\,(R~}oV5o/>Wc3H $@o`p0pdd 'm۶ .04he5'vN:CF`ΝsO]aR!\= 8!_j O޾<גH>87UR$- /[0icLDqœ}CȐ/Ә@H $ xݨ(_4S4Y6l}jrrr޽[jnScGC,X2ſ1]_YM ! 6 iMvKDV=oNeLU@%}We? Zm6.*Wd޻nҪ,]T5@H $6p6𩣩#M*+ mm&?= (`B!ZA9y%Un-ψ'<+A DZկ%XmOriӣC  [ZXNt ޿oE!KmBӷgc [$0!$@H Uboe?cz®v+>[4vq*mNBN[9Ұ=ff' =)\1wb-hI6.$-̽q}$үZYB-j6fzĐ1~31BGV-imՃɶ^.m[;`M*CH $xC n FO[ӑV&NV&;Z5n۹X㬬,`y_P@TX3&c$4ҳ#cI lj YTR΃ /zx܏Awxx3éj<)nh(ף}#Fx=|E/@H ӧ^)bod{XFRdܸgfLPe???9TA4M3g܄LF;pa5V)`(wé;@H $&pW\ZI6 SXA?[,-&g@.Onm3gv+(SԶJu#H $@2%Xhl@ɉ1Pafĵp܈ $O"a~D0 ƨO{ }CH $x.`cj78D\16$B_G\@$@H 7Ko+pG1W)rḅB.E?97t7N $@HM"%iK@I.\ H8$@H “ȥR"rRPF<>_` "7 bC0$@H 7(% %D|)HD"'29 a s07dqH $@ K t0h__WxWsH $@58Ai1u0ǘI#po^]NPI;@H $NC̿ IDAT@kOPDph ꗧ3!4(0@H $qDbh3s<hb>5`q2JD|kv4ꕑ<.#̈́jQ9Ij5tT KvGH $GAÂil`H([y\bGCX+05hȬD-3OecUl ݘdCP?}i@H $^*8"x:_d\,”V$X+TKϢpUGW>/?To}3m%ZٶƎ*a8|ꆻdR(ccGwѱ`WcBH $x (9JnvF@]pc\j)_Ϋ5ss-ϢKSK: smzo ʃ{Y}ПY3IK۟ GT~ӫ7;_>Iц$=mmg g7ҥUI|}pAQf%Of _nXH $Pr `˃~ 8|ϥW * EyϿGUo&$nk}=.Vʠ5a3kѬk6p1k͌Ux%ֻG w.y\6WGbKHNbt/rGpVʉǫ7Dg{t!杧V^܈Ȅ߈OEF^~LTqjȂ{Ȳ5#uQ ˎ_>| E z[}뿷#Zcc @?;e=UqkzRV@~$eYv%֮#~;5 3no35@/d0m.C.n'Ӂa4I+)%#cv3H*h-.^~u ĠVOֱ1OvoIvz1"mA':yvrl4= 8!ҮxEylQT'Y?* 05=f tځKϴ{ɽ?L*rc3|uۦr'ѱޫNr>h!]llOg63fs\bz{ms;2p!=Z;m\/vn|cmH$h_UNy dS,S]y$.>W *XϊS(erTItqLQҊ6 lצ=4G2G`ʱаG o.wE1#- $Ħ[Ewoo $kUP.jCʪ1݇4׳{s|ZU=[pm7bwhl/c JOo_٦:zWS󕏽|{alMosă<(NPK)(J. <+0N ]fB-ߕ(I^Z\TأWK >FvP"F&p09q~M\jjh$KW:лmn.dMTέd\#<+z U[/>˳"JԔ'MvJm:wk/Zذz߳.<&$P4wƧ[Tgư R1]y:_o,gjOB H 5x62!psA(  Qeu(iUI9^/oۮvZﵪff<קvI>ݝ^!i{.'۶5O޽dÐ珦0&3JЇiiէ~'N_fyKiXyeU\y d笙ܗEfn΂m˿:<}UHH~.?iI%tknl Y0`_L>6-6.w#F'ia;W> awOf+J& k$m'M,lR9q 6IeayyU̞Ț` 7RIVrd\AG]29sళegljjZS{HPr1HՆKOhMTʥƭ[S.>{[U#\t$`BS-kD`JOKxB heF=WI,W@#F2kn@d3Lj[:mObȘֺexU?EHe!gho. bEoyƟY'|h8oT&pԩ-ݢIccJAD2Si&V&Ԃ6%76lXQQѳe&0jAkV/ZU~n yRe )` @WsAj,MZ[R=sU=zzaG@g#Uk9*FFcw. SQ{7I 菑DB\'g@\{\M?>xy80mXPЇzj5fG|&QZ,!gˊ9K׆vpj/7蓙'9ݶGiI 6}v{cǏ5Ω_V#^^q>Q߳Tv'Wf ٥*17W/Y{hIey[*QOtZ4*ZDWccȦ' NɮsւK^yO[2xֈ좱ũwm۴MZ 5nXk*/ضɺwۅ0۽sMIk}HYbF^rjl\8//88 a5Me-̟~Um@Y*ȦYAݳ1~AL3H r8  r r)` nBrY"U_XNT/CdަJFAEE3^F1!|•t퉕#Kѐ>36MGi8f>İ5|TuWΗm^+]4Ur-V\x֒뢋fvϊyllj!,qІFK!*@W͔?Kw ˁVJŬ7 > e1̵k]Q*e1}}4GsE/4֪2sÔ ͺiϪ4sR~ꪹd3Nn3P)k{jlj!2KBV |kY!FY>l/󥹗bTMZjK$^1l$xfv8!LJ-,--8!q*܀˙ -m5n dc=gv@S{ȗy!4@^7s~TY0ډ/4#^ ]&P'˲߯ECCmۏw^:0"MNiv@4R$jH j+=.;خ. i_ jQjg4F'M<_CKWOӕ݋2v =&׽MњNVi\_ԚzK,2$?ǠٰISI?'IX+D@!Ws1dXI$ NQp{S(gΈP;`{^m܂zy,#mAօLy ֞Iv_V%d韛uKVh-E_MTm5mĔs~НA 6G-_ Ai:ßzkLaj.~fیZ6zvsla4zB/Z^}1 l~ֆuh̔jS;?osDMCU%4 Zjom޼n42Iкaq91rc5Өe5311, r)۵4b$^- ǐq9O?7'eP+"Z9u#`ֹH /sAr׊C:9: ]o".Ȣ=2(nQuG²T鱺?/ K =k>#DQ :./|ENNDmuZz;ݺqFر"խվzfҝ ]k} 3qነXY^fSš7a,U*e68mU@ǵT\"ZZTPR*"}]D 2ܙ$$!A@@r.IɝBJO@2iF|\`֣YߊrGTfmx;| BMJuI 03 P(HPok+WPmSE9qIuJK_ԯ{eL^ A{s#b7$W.nMMq_Œ\џyGGLSB#u䅱d,9Sr\ì>I)Wud8@-L`gR`MM8U-rorA *̻RxO$i(*}vDƝS1&Tu]~(.t\~~fbBo@־{xOW:mLʗeD˗dޠ™>er2v:+J+=;BŰT )3x} 5%厙b.,7,f!6OIe^QFu;˱?Nc(tlMQ>1/ XU#,7GyoБXAY *9Ѩy^[]SȽ7ErUt6ǠƁa}\-W4srZ/YL.>n@p㪷5HKBo!Pmc~J$OŸRQ_7x=Z'Ϟt3m}K*a7t)\/2boѯRX$^`߄dpQRH{5Ov^G`-3;ɇe ,˛7[Ȭ7 $ШWx/ajjȰ! w%4zʶg}ϨkYSSlkqQBݲ滓P?3Щ-6Z+_ϾWv xcHƍW/] c]u ꮇQrv7SQ8c%IwJmQJZB̘ (:I)f|nSIm/\%_at]~ wٲ|HcV-Q.t`14d]w#7%pObIYebH’;Gu/W׶BX;s]O7k]m||U&_ۢ`;yQwkH颒2Hw3N 8׎4$Ce37:()㗟.*Wf+c~#,ٕt$> 3{fff˯)ey*{eLa@72!pr\ 0ɵC0NUV".8+sO2L7FQDŽTEM2y$~8`VINnƭAX]$Q?p-5%89m:/h"U2A$(} ɳ'޾-$=퇺,:uC5l۾0qs`_7CQM3}kJ/0f*la@Z}j9U5FʝdBev?oM/(>Սڰ>dl 'nEW縯9q1҆]F8X ߿^%hz'^NvL -jp̽a kV^J @W]?Ĵx&(`ån%sƬ;GEبmEv#T=2d%z,ly^W fd0CZ[_#}*笢TMT ꛛZ~j*=-|SQ"Qk0͹S ycJ=Άb\zez9% }ű3@V bVT<p$Xd$pIa}Di;N I!<7+Yu0TZuY};Gew -\-V4fGHO")ܸ+ ""`% ߜ-{c3 ]X6TAG°Q?&nQ1~b\>t;ѻ^_@1C+qDҹul'z*?i,9xcHv0 r IDAT1bcϵI/qQ%L]s#fY4 C8c>pP捴eKlNZOTؖv :sP5X-FW 14po&2E F֌fzuM>_~A˻;(^\Yz;Zg³[ԃ'qH=$[!]ϠzGcb7(Hzyr=A;_%%nP<Ȝ ,+1$fኩY⮩oV[>أStxh<׀KC0 kUk ]\M/^-RT7?ogCv{eWhP`ԵŮmtJKPXQ*JLc*/nE*!N'wM[XLܗE~3\ݙChK[:˕HMHjrhЖt5=guVh○|ZMtnảHS׈ ?[hDܰcjk5춱P=Ry;ʫIc~L~߱Kn& @ICEyVm:v|Q;9.ю`.wn߻Cjkhض<v!Ӓ'AKMhnՍUhmEǶc]imЬ}8206V/{L, >?!U("QUtfSPmu0 #*PxTn̬±հґijasA4ux Hm,Р1`FiUUK@Xe*2:m@iسGvzՄ|.ܻѦ6}kM,W;p+L27 NGϦiމ5< "U@a1ֆy?D~˄(qc*uGo?7_%zp_j 48tqR5+30UyUX4!q~Ȫ~S=?2eo.#NߵXzdګ}g00fkh^QbL5%S+aXowBV*W03f%=4F162n ,+XS@A,GH%VN +"}ʁ8-PQb >1otfeV:/$ݗ-}fT*=ⅳ~kf.*=|4-vx z=g&ۦ@O蠘 ӧG6j8$6yNqήs+HĹy%]7oVmĬb$[%su %.?QQQ恟f* ݧK{z@H 7MyV4৻7h-OjljiHp]y!F&ms[%DP~՟rG[%3˖UwpƼ'?xCQ;zrX.cdPZB)">hkub=#}ipmv+Dbѫl\ ̌Tx‘ FTj>9l]4_y/Ɛg}G $ >pIQFf-(`fdi"32ѓdxrZZy[|?=FZ?_nČqB;yHڃ ƈ˰[J '7@ T]Sԗ)nhoo{&y@H wOҠZ9kj&M'aU45CK̳MHIBeOߤpl<恘vhҨ2N3H1 ]6y SέA^۪MXvuٔ3iIJ+ KO3$@ A2XSa`,i-7ML6b7iNNDgt5/5}捃>Xƶ C#=V ױA/5Z iDCZuu5. $Cp]]\+,րE$pk Œ2=87K@G@_Qæ7 GGH $jɚ5 `SKNe[ 4@H $^#ZV ~u!4TS) `vqPH $@@-=#aY] h`'` $@H ýV>%58aC~4z@H $ #7JC< y055 d{Ł?i K $@H &ɄqlWU50D EnN(X` $@H&P[K5ըfjӈ0]Q ׋euu,0@H $Nh0 vk5jZ1NsHpSD0Sbz/~W;?r s‚{E\\p $@H5x`T1ϫuI ,uPe'f<}1(j^wsr֎ m2`wGÉ~j1e}esWdK.Z}.F\I!Ǿt;;,`5P "a $@HЬɤ9|NʪkEϘյ*"Q%`C@|j9a㭔UVY.N~ 7/6Nx42:*KXsb<輶ס!S@/fCD'r#re\KQ83=32q^6m߳J|׮=MTd>}u#W^ON)#bЍT-!]gJsd?eHEv0e lHĠܬ; 6\ j{`}BW|OQ~K@H $$!󬂩P?OTǕD$""2U+* U}`!7P`+i *yKG#;lM&q .C%]<CF5!_rQU㨋"ץl?SmQn=+Yu0E xGPT]ry*+3B.!%!D-HD? AmO8m $@HuiZJQe ,~x?U5F\US]EB<~P01GMFg7엛nR^cGX5͑8)5k7Bkym7fX[יg+eHUͺstv%tE*zIP'V2'=z5B f\;sΟ֬cȬ?u|Y$@H E͙G|~=aY3GiJ -'y`0ґ`qU B/f'D'D| hM\tv$dhgxةCݟ U:M诿n0jlr)X}FI0 {$jV?O]) B;]c|V5J=ⷳ`۱ c*-ׯ,ʭG>ODH $hXǾ~ N^25UWT>>-A!ˊ`=qlWs Yu"gxڋ Bv߉gKutgTэ Ņ,N:okfJ[q|:J%f R;oD?jmE_B 7KZx۹G^=!¼^=^9gƄ@H $hjj:i"=~Fd?eV>+ZH(~_jnnndd^j*|EV d<=F\LXTFt$Fm\ն[$=oϕmƚS#HTQzJTf*D#2~ZXgS'LМ $x /{y\e[b#m j_?@xUvj/-)#{Ӊ  x MꞵT W1 W@H $^/81 ;tGgnV<'#|^6̤owdllEo!0_t4b雴{}q$@H At0\JEi6m@n6?m`YG:m^p $@H DӃ`_Ȁkkk!F^@@H $. A 5pKe6 n!pH$@H $ P啚VH $@?>"$@H (@7@H $(}3DH $P""X n"$@H Pg@H $DeD0S~X۱)./WRy;A/@H $8`Y&F?S kGW_|*w9ߏ%C|M} >}Wљ ;wdgκSwٯC1@H $hLиn;eӬC-ZdW}%7J9MQq'b6 c0REܦ_Np6yzJ<7*>:{s[f@H $>`M.=#$7 >s6l]!ⓛ&IkU^$QG׏Ya'9uv+X]$Q;{Ü wp^*t;r6s֡DHIN>` ׽#ȌtԌGg5] }i>PA{c1L I[:-ֆc‡GINH 'P}-[~066 :o$+oݝ[ Bϔ `f`Mk]Y.99E6aYII4ɇ^79FXRR"͗qy9D\ )_A,lmH#2j jNT7JG&ܾa 9r~vL8xƠ~HzO e(m^mm %^&}cwJ,!Co|?OHg$ThV =4?!ζ]3(v7'P֘tgJ^PcR -Uqr%Ү#7?Ǒ%G-:jYAkOh aC ۃXl?vc+DHnR;eM]]]C%ܥvsrw_cesokhT0>c@>̕eںLu23|Bmce+ˊ4|:vunFϕw9^J{jo03}!0"r"{T0+UmOkH= rn,$vs$ұ8wP=hhJoz6lpf=P*%aB*7w&0tߕ%O3LѮx ;W,$ɬ*-z5 5H>Wj8 aet:}vr?jµ =8+u>dU'h sQL黖ҍǺ.'lPcdlȑ\y|zĒXBb7UN\W֌XP"΍ktב4 Y F:qO}D /ǘ.?P~R$SҤvvvv!awt݀g1hgN~x#$hNo敬|?6BW,XI#q8qaEF*dulb8 )VX~`>Rܽ܄Ga=m$=䞪ḙ攒[%W wۮ#`FvWkvvQGҐ&$ÇN'%%iϦCrr7,o0F>yw vX!Y]z3sS|u(MN ˢX̑&D\.K^/=@[>eb41M( ڝe>ra0ao4")9BRvz`bd&YSɱ׽88ӾT٩Ee}U{cih/UIL͛׋LL&.2׭UP﫞Ò~u}3̅zz܃zakRr% Dz T{ڠq'; X.n͌]~!⼟[w4T41̍ML<$MNDv H0SC`C~js=hǴذ4ztOh۰I{WQv%2z\<,69!=,v%~{sQuNN[t{yfSNڻ#6,૰ (.L^>qEsd9^?{ݟ%יp2QyI~]Q~ȌCF[@d`o:iujTui6x' =9 ؖ#&B5&ܽ Y_L0!$h&[^ɶNiCRb.HNmJO =֩[m~L$Iv+슜`k]iNb(idoY^ǡsڐ[̰kL <Ⱥ gkNS ]<#mPp&s]8gCr'yAsAkr/)YxH.m?[{r4 AxᦼCa7Kn%_|P*j3%$pi+fr"A>:-_Dڎr'yy$$@{xX;\6]6yTGTF72)y]= =,Cr+bU8@${GnY8 igYт IDAT:}is͔^0kGQ]&MJު>oD wJ.ybFQYvj2Mj}6q9Cl=&ԃI KħNC<KtBꏓMܾ)& G4d_2K l_-&%;a[x/ 0J~3(b +AܡecCC?}lk|AehF%Qb|[H\- {~U/I/U Ii5Qn@N毘1!$Rh ~!%!*ޭ02(-`)">Rub=#}qd_o}e˪;|kcޓ+#v+DbQvOw)ΊGm+""~p.b}ж݃3(Z#'r!>M!h<?E&rƻM?3$uk\/.+ )4$Ul-)M;l6Ԭԭ}[*vqa{=Z!k:`Ɔ~d݀XuH;~i98_e}[K{Am+<,-9V&:4e|TVPEa#í;+f=dC:dU"D0Ǫ}])y֠pYǕ{wȔ;76JC V{7 i^ׄv n<$mf}ѮVuxPRTPyڐ{9 Ħ .># –kUXWI];|9^ 5.W\ׅ-D'S]޼-~ɳY6J݁N@{x&lg|7H U VF̘>Lbda. P&Gzζ=W7;+`5^P1jYO9~^ >j3#cĢq2ռsx`Ib4s04qA&E6S]R|u<2E > Xۋ݃w:mш3O(Zrp"[A~4Ir8"&wRUhҡ mY#ڞ)6}V2cՆZoz`ђŸ\|Qa|}3oXSe{3o(%sjkpA$O8eYrNAEpU\[i )I@UGl=Nq.Xɞ͘l <.1&̃Xxz"4Ƞ aP. յhT:قk$}d^ה<+>2Qy^}!B]2J{,Π_uT Xн+ST&.`'?g#$DeEӗg;Ա ))S|!/m4Mg,hjolA`;k.H˾F?*i x_7;}:ij86{ỊgdzAnlߐ#M֣H^>t?D֌L5I¡lzt،l_FnUAH ؛iҹCլ)ct0/ՇO>/iPe49wƹ먈S8dԯӳz~.pY(8^v"cVjOLpfx^Vâvu4iiIکN869W_0Ժ{w{_<>aH{Q $v<[ݠ$blB|9:Ã,OH IpE79>MlJ 4 AV,őKNol?tќ,Ho.61#H:̀wL5]q!OlDW4*e75o(:$BF\&56VV (y[MxIⲲjc߅eeأBXqߪNiu3`[7`%B slۡp1PvMՍBsKO:*=q]ޜV\#F,e):ǺOm lSV%ͨ{Q9J/hHЬMA#n񤼣Zdg$}7c=%roΓV/;ݱ % C 6|>,nd/^QSS7g{:{Og`Ҍ_ ]E+(`hrG6η&Փ}ފg Gn֫R‹f$LJ* y:/Ժ'*̨х/in %9T%mU^nVre `FXxm(]V!I.N(`(ȏncvF8ômdj.k/ifOYƨi X9n!F߮4@ୌ7/[%[߮iZEfՋ,) |#MTDV;ןTvꦢR],3.=(Yb^i3p_w**,ciak$+"W"f00+_Q165n>ª74XjnZ 2} !$,>1ͺ#$@H @tf $@H'"߁>@H $@ n:3@H $h%ҘBFKҫ nK/Kkb޵ X(^H״QsOIyXܦ?0GH $xU ~d?0GmThv!+n(j @H $h.>4kaN$'xO4 70h3gדn]{ y δ0эkwbâv~yօIƘG|BV^}aq ";OPy:89:챊ѩG@H $cMqۈ / ( MqfJJeN1S|ų!Σt%<+F]|יC{{ATySq`fA~#{[Wdpq͉ b;m9 -C+nL&GrewqIVG4bȍ»%.]]BH $x4I3ib^t g!mϭ#$g| S?3W{2ml(?GH $IEqH $ ? $@$[3wkvDƝzދ5⻨:(@H w@SD,&들ޱIdzL[|3jjLƦq]Mɹ*d q $@H@@SD߶]'@cnoiP$f@ 1KFf fx,o]wC#*/}XIruIL6I|~ C]p*/$L š5Mhfk87ZRˀZ5 vCH $A9"$M޾>΄- \ 5q]tdnֲ }T><΋L|hC !#yLyp>} 8]}+'u2qr0̱uO;ldZGW;Px-<ԁQ^4:us!+ }?9dUyQLFE_ϖ ; 77j3CRgZ'9#k\lFȧlHJCpܣ*bD-HcդSGBnd)y S8@H $"4L-5}4 ܐr`cT|-l&DFŮi=i'@Uƅ~l$ͮfAYQW^H C0.ª 1DiIMɪg^nQБtܵ-4X``naa?Q1nNЊ 9WJmJ (-ȅgXSuz:DϔF[3?9F;;L=x4;!mo??s^qօk@Ye_[nr bTrZ;ӣGvl;! ʪd9ڐtBEuNcmX 'g@+λz[<:C׎'Bj 3n!u!$&&;M)cs8#?CH $h taG 3"1/,|A:}ЌX;re2ְ0ܺGbUwkQ}nGön]}G ,mM O~e(K;iN ~NRHvnok>h׮Ntæ.$hiIGjKpE';{cM"XNڝgKut<76}׉=mHjP̅rm=mrB Eңգ#Bh٫Gmv!hKFjI5.̅ jw:q;k? +1!$@H 4L@VIe-TˈEVOpW@ظ^6GT'SK%H[UD`^O+䆠 cRꞙ \iAxtL|]A2zF|@]ovm}I4aďELbwlJ Uo$@HUHKKՅGHy<jjj6EEEs䯲ETOQ7 Qyy]Pl]WB~u^ۨnrsuɫOGhrU֫(eT4x:m ն $@H @ӖC 4$@H $@D@H $@n/l@H $hI\faMz #vߦg8wf1!$@H %В"~|%T !KܧW(c.<\@H $hYM%p387}.sZ 1G[ֿ|C]k%p ;iu#mݕm"$@H he|er(^؛Oyt~};Zc⁠@V|nI&\6[uLףY@cߏc8,`5*on)1x[<{ Qw&}Ԡs[⪼(qs&\M(!$@HC'w\)_9>MMBZ7!9g.6eJsd? NCqKtqAᛢKǤ80cC":7kM9r_ݾsjAq"ȕj┴Й5r[hAHסdʸ&:DTZ7X@H $h-!k&ۘw6_m/ub}0svtHe. IDATEBF ~0řGiE!k;XmٚY =h4}*e?%0ʷ[H $@H@@E0'7(m2.X="pϿ΄O[;r4hݡ{7rJ@ێmU**!r}Z + Ptr6,8^I1,BH $@4MA$[fLĐCZS&æo%xؐk$ t331KZx۹G^=`kh7auĪ.=7N6(} u>!$@H eJ*Zfӗ_TĄ#߲%͔=`D*a3ݕZ77tɻzFPj,)&LxgGǑ@H WH --MKKK[[[WW!&xla"O[_@+ξ~b=eT4"f+GV @H $8NmκV0 DH $xwW"# ;S$@H 7@i'ƽqH$@H $P4Q@H $@"t $@H nih $@H'"E @H $@K@D@H $[OE[A$@H &"=$@H @o!.:BԢM;cն}f魟/:@H $HD;G̑xb,Y\"X f@H $^MYQ~n&U6*U"uB!FZ->KFT^PDs;Ziqh $@H$\xzO!#Wۡ+ צc3#X#Lt;莘LHXBLuD[tnI\\1S&|헳v&dc#~ܶI#G$F,M- U}pH$x{ 4A=yn`6Ys [NXNW|я{!}kzAc(aH JNF,KZ e|mSx7.&Nz9u-?2NP b’;YY4)){L>}0}HmY YSTwg4 K@H%\]*%=l%Daff|NH\F~G4r|4hiEHxLa}Di;:p <~ jK0t^kF9d +" JaZ``o/ɺgwmuvv>Wו8Ã>=CX%K-c $@ hno_(G賐QWtRO0ڹ + =Ypiۊ9A^]'>qxǎv$t4J\ (\3Fg$w |#̌,|\+u݇+ȓ@H p ;|~=:B1<_(vkBn2e]8!ԻYߴ2Hz7 5h|ar;Ͼu:}JrON.hKoO>0FRfY~zlRjɞ3T+,AH \]vٵvW~)d3قΦO0 2%57VVv )'˔rUXC.9Ӗ C g)lY g-k֘] $[]E/Ic{v^H6!gE =rXNsrE@ 6ėk ٬PA@$W~!G?kb?[YrF;xOQ\eB d\k+sOk/ID*LsK¤{SqK{3ON3$-g$0iiiikkõl|Mޙ5qu-P uAB XQ h-+_JV}-P-TEj\P JP@5 $3&! Xw}tr.3ə/u{D@`~_O;0 >pfPsbY3ӇL&HZK+,)1r^ Ez/6w^-0[t\FU~gM5 YPKܜɚNɊN'&ʧBa|/)i.@ N7Ʃw}4=]y |;-Lz e㬤j#N*OMnCi͘'EJAMeeb6:Ut"3M{`{\?R{V$hMV}wX&?J?~uٰU+-`<Zkmire~*,褄09?UFt(*@ :^@)߁:``$/|#aS:-L̙ͭZ.l l Uڇ9դXSY'6:2i:o+ؤlޅQԛ%"*'h'1S9lRzcc'R2W5%)8wC4:K@P Ly .6]CT@mq^f623jI˲&i; jkj)O69W-TѦ췫';C?'i`]}bՅ~K\(!zoly>+n9*bBG 9EAVXM|CNwsZjw*@ i@$ F_(U{뢸YO}dvL ^Rμ1\ͣ**8Þ?sBl Ųhw珑!EsXJ HiY r޶ťʞ,O kbA;ޑOZ,!p.W.*/H5k$%p' [L _o1I9% ,xmSq}yB U Ӯt9Aqw^۾}J>S~xJ׮vd*=,yX1 t>N5[kS ?+,Xu@ $Jz p7GFo J,k7%@:;"^ќh"-- ee%LԖ]۸cG"ų>Y]\Q?+0LI(z Ҋ:jmy6RFasfRO@ V#^~&Ir~|`COĸ=;mLwW>7'| >$b!Z +7wH[ّ03dsĪ< yhfpN#7%Q*EǧڏM.fL1P hU=Ђ5;0"rpߜiL`NsPo @ 4xϼnSՑw3v=Y4Kr*a-Uc'T s99X뿋Pv@ &O\1_:vʿQ=W=T,6'=#Etm%IJ9n9TW"Xq't)MF3zQcA])&i5;YBNOtm&^L}Q1p[TULUa<ߓ`:x6QET$XnEmy кQIJo$[:VY 797BEK]0*m=x_tA N>}  zQ tu!z޽ M vN; MNhLMME C_kUD"Bv*E؛)vs_7'u;Mbم{D> G3ۚ#!J[ʴ "BW ={jkk3 ɯ=zh2tLB8dܘq,֓{6_xJF͓ygJZeȢ)ۮ2vf3W`I U4oşՄiWVK+D./=I6@k7,ymY i8';k+ӎ-`L @KiyY!'*&9Q( kK#<0B>7 9JNO+p 4&i+,v DF{O5%#1M9w5eٳvVc Qt5{VsO_C)~ie ,:2~"XN<EiÖs/ ~o! nj?Xp>0z `7k_pv1.?p1kYԾIN\r)|vO@5"@'~ ra 61g0-XnᔻV;=O5B L{F.BQL=uL?j#luB'klM?΍?{]áŌcqvu>xgp?'/ҽV~=k =Ru)_̛`f9aQW00>d BGq>TƑj;ؔGGWGl1l !Ho.[|kWa_,(h @B{~O*0iIevslyo"C<`2e(5 2W8l9la}9q1N۾4W$ 5Hl qN2 ),[SzX J]k;7kn ظPƮ_ ?/(,.XVr_644Y:{NĪP痍1(86ړ{wwѽl`| xm t=)SVjGEGO7$uEw ?%iU ~@"yUI1-tMod8Pszƣÿ9[(VfzHaP^|nhݣٍC7cˎU灈w=GƄ] 1v^O|Tm@U&p&.0rwKmQ& ½gW@kg>ݱ}0L?k.q4,dw2}&:Z2a M[9EWS 2{ェ =lȈ f>] Q)#?))?u=~!&o ?[؂ܞ4 @"НGu:flj{k^Q IsFdj5c q胚tuQ4_Y:.ыGQ] /i/hv0 @ otiM֧Qrܔ[iz 6u4HPAzMG!Z_UwBuavQdoa@ #Śޗ|gf?aW|@ ^K݋ ~-J @U`= @o"@ T @ ` S@ P%F*@ x ,2L @@<` @- F[0E @UܿYS}HUkR&y;)*ܻ_m6J @ׂ@w`ѹ@cF?},5h.snk^>ɗD-r톆 @gL;Fl9qOp>Ԗþ9Ο33+.T\m~L+fGU[e w\juã!CmZGC9@ CF{ S9ӷ$7ܴظ2K-l+oJ,cwn>#Ɂ'm~Q+Y:k2U2hQGTչ+6'lʃ+HYyא\Ln\v}p&d-!ģ.i5 8P^%߰@6{pJ5d@ ^u7< W==.Ti>z߭VXsfI~ӻ}( ѩ>3g(mwm cÉs\S ֑ G oގʶ{DfEihI]tzScF~sΐIX%佅p/A'q]on}uv!Z;-k fAUߵ,jߤ '.^H c`@ zx #X1AZv=E~mџ%]Q8JsYSdžՂc~r?æxIݻ3>6!՛ 6-vpnXa bs8vI.5>-a`E AږWgp[H2!Hz ۣ#q& a@ x #X{}ؠ fөnc1Ec!ޗmǔMYl1)X0qqÌiE[a ]1_ۜ޾, _Ʉ৬Ih4wqDn|i֊ rIl->3 ]'v hv>%:4U{!]BH1_' jAh7i{!WgdalwaRp[U .@ x hݨo7ܽ)ü5cS;j!-"]=lJS &PVWEá hMY{/sA $ N]B44=Y(DԂMi6giT25 ]'PpӻZ o={jkk3 t֣G-2=~?2uJWwS kё`vWdTZ>L @ [#nA^;"ϸ{tދo+I$4=c/ @ x`U4~ͺkd~ǸowVe {@'ƥש܏J+HM$Ԝ$zܼҺ: SӋτШXѴ(#G#Pp֝iJ@5 `"2QVL1ki*rSy>Jgq4Kx(6/i߀5*\둮!Eai]'C^^e*}"V)Eo.Qfof7".圸lG(5Gki/ tE4:5]TyOPTҊ-}֟d@@05QX>&Ԡ^ɾO)&3p~Ym+&!;vD Ŵʚ;~Vή˫!YA)\qp%5?[W=RoX:*Pә|&w0i,D!KqT-䎽j=;֊%[ʟN5Ś ^%`J" "t/KVyK- `EI)LIj8%+u):_|[> i a)ZAէJjy;KfrFK˿ٟۘd>>#[D I,R#UC%7;{3 WFO*H KWwm@Bۼs~ oO6r(&E|["Dfvx?IJRG={4m^9)%W$:PTYQ)iPMu,! Vt 6yx4,90Md3p/`yD wdG$'F*E{G:o%+*,\J2 hpX\%plZ.hh!=sP%+Ū;lGfy} Fk排e@K"TXgIa$/RTݦn1:zP ǝ8xUESJQ 0X2̰a/b(G3 τlIa^IqÍ-B g3ºloh :AӢ0fx/,x[ Vrg0 l֟%-niSf&7!0BGLC-S5Q(F:s 6q_WhS$7qX&  }2ykpǐq9wg&HܖnhnVtПk !'d^#% A>D_9]+Q)%hs- H@ кQʩ )SGO2iB1ɤ!P(a0uf72tRc10q܌DB2rF @tUބ@ɉ=4 tfڒ=Xڳ_"j$i\oUWTW"44#8{}m ޙ<@F ??gϞ oq»t:FCǏKȔN[R#FXO~x{Ws|ΨT$5hLvNl4ԓ"{YAǣўJDA)E 輱zC8"> cW@ JJj J7k ƖG~!;O߈`xW#FN&x &.$@c9:ZY@ 88@ :Ok\Ts[ & @ 8BrPlB QyBHfTѰgKOHrZVa;@ ϐ@:Է'CpJ{E2)\Qp dOc}r7҂?*쪼=nqy؀FҢ*MsX jDtU-EHDP_y'q`@ Oڽ)#&3R?-%}iSg[[#PCݗ+Xy3رb!hJ{f%Bpy p"Kgn)K?\Q3Ґ ɵ ͐WKl8 y| @:w ͅ ܟ7mOL|2ƒ4 CvQ¢SVr0ѐn]Vƥh#R^ZS~b.w~ń<:w)2$˱<:8R8he6D H@ @@3m.ܳl嶄v^e=aDxîYiD)ç0Lru>#SƄp}g21%,:.3{ r圾B @*Uoy| 0_)F hJ0ɨ6:HGJcenrNZ䁎f\X: +}z-[tDZ^ 73*- v.| @P tM 0 $2yԛޤy.tSƏ]9d<Uij ஞo.sқq<©ou<ԆVL2wY={MEeAo2Qajb^ŤGwk߭o~Lƫ+NMLS~&A@ pgW$o$4zDo7CާZ] zD&7ꔍ?/ۏmу"D괏$Pp_Mݺؓk׍({C.c[df*vw73YeazqQ:uڭ| ,Kk,PtFwVx</@si4'߳gOmmm8]:NzEǏKȔ Op!zttt{I-`Nl#~SFni`5` u-`Oob,1 H6OA&Ba-'&~BT- i銺O~'# IWS!|bzf%?Rx t~8-u6l=LhϏԬbsľNErkHLfO--הC9O_o/w]jeq}*, 'FEwJQ{y؇ c{>{롃f&Ra’ӟZm|T &>hsWLtFn"l-籴Ġ\/ʓ9~ k툑%ũpI$zU(w%*]C"B<]WVHb"6UJ]gb!XTj-s"$KC3U6XDUd(}kH*IFS:雥ʇJ%P״P+~RYtN.pL^m]^ k\^ROhlx+.ak(+e}"R,OT pXR1%1&f&"Z`V_BU $UaH#,NMA_?:2hT)DTƇ-6АʚńF]AZNMaB&Ϭܤ0vKX(nFó55|7ZB 359#:7U|i,e!'?42SyJOlVb,/`yDW)$F_֌lmYCT!aq?@AMe#;SM&1ٶ"iZV^|B͈aEKt̝,zH[bo^3wo, eV#?&qb=Vc=?UNXv$~mBG~_.K=ÿO_<}J?][C$QysSFcգ}6r²c%GYY9su0Z~g8ڹGQE7:UhIr,V^Ħ3’8(#` .oswr9 IǔM 3̭|!{WT+H#~kUGsWNgBCH@$`$0,@b0e3QxJ65 /C>q15Dhk@#d]dX/ї(R#2 ؤ%4e/ %Ÿ3[~7v}= Zl={El=ڵ7:V܆;SHa'o}NL }OL6iuştlC}%M"D#&PC TKNò%*#i;. <#*eB*={e2viHxg`a5DYgpGTl9̈yI2CR^tةA9RGaϔfB#yX%p v yFK\%ܰDhTD^I/o[ 4C~ (U3 ƕjN1N~=a"!&V0W IDATF&nȗX?H ;P<; lY2 Fwb6ہc5[MDt.Zpţ(1glܛ}^"h6B3S# rU hL2U$#`!TpMf13 =rzFT0$SIKE}SS׸ f/p?pG8b_(?z`;*A!ZlvFDҠ3av>o'TUOIIyj``4 /`]B5ȼbѕW*6 E jɅb2zebm|ɏ٪nmmмRZ)\%͹wyU 707a?;ѡ( tPqÑ>J"We4`Q̠zHX[Z\ #>@פ_tqܶxqVG:L#DTuN#Q1s:67U4jӀrnhLj33%G뀘CrP3'$95ΓaoOgoV"C1DwC6XX_ۗxOUʸ̊Vۑ8B^2`;l)tG8G~O` X\,*`V숖J;D5a=PkmadhUܱssP8@4`ڲϫ"oYƊ( Ğ=3hTjWn03-(gyL&>0G#%5 VtFmUUdqnmu!D!yҰH)*jZU,*;.ϛϛ0] &}X M)em>(GMPnxגK !2a0-X'Tp@A>SҏE" h43{fsׇ΂*;{dZ|AYٹJahf-B*\Ngp"E*72}:aY!)0NNPώ:SL1O q-rhfŜ|e$>6FaE:SI'nuFN)EI(ÐN7sbm2JK# F{}]xO-MO7GY8?\VJZbrnO;ҰKO^!Fx2^`7Mi'+`Vv=%"a+b0'iTBl0cm?bHz4 {:O5+ lZ&n@yȅɤ DF00 vfQPiw*Oڤ4T(fK/l2Ȍ憨~I!`t:C21u0,zaca`@+uE6e;>db|Դ?ۭQ[ʀ]1@2~6A bWفʽ2g ݍ!/_ڗZ3r{\?7k"7qy9wk+:a&.8ޜ t֪ly.jM+O5Cm*ց6槏8"a%#CQ.\5pƴu8VqustpIk[q9,_e~rgD؞;Ʋw̿wý'&8)6@hi7>™'voZ_2 aȴe"=Ux{htZ]?쪥 芣YZU~NcTkJc\&&f& אd`} '*ȢӞܜڢ1e/5ԢήXK/>HFlUA&De+Svir`v]n ^?FS oo\E0WޡwT JnVpz%f>?Gc33tyNa#m}uV [Oou0ceɰ$ hYn]a~g BUyq33ص=LH;-u&,>K%" VzZ=t 屆հ4P5 E-|LkHG͹Z+M3kW{*#B i L>;k|Ʌr*eR*^a1#ApnʉU(M>ܚAz6b1PپmXڌztPwz$BGZPj! G+N_FhO>Pp'*sAMxjJ2nϮ̤J BaBE>Z>FthDWҹ xhOOQĘɦ֡5g4*=7jՂsx޷z%vlB4u1-)zt:|+ct&^I D F1z!m̀y~=ӓ?X+'W]=v0>US#c>RXe:uL6E-GCI(nz?!ÿa YLtHdFuVA1}ԭe*;Ns ɮUwlkybHt>%6ΠאU4 gPTOr t]*R$HE o-- b MC }b23WVGQmbTDl"QUkHzLBy[J3T0G6Q|:G.B68~&{'ט"s$i8%<a"$c5b7rŐ"]dA l&f&rB\oG-{dbR$z&cZqxa.Kb # SS 6I!Dltk#tX"}3XGR|SweU}}feaQ9Ȫ9ff}il9iI"<xpG vܺ~ulYhWcƔ:Eh# <$=겎%nD1tOcG|KL[I+(K~_It\FUV_Zծ^.!\ dzT|r2_C1T&nLrtת}5~1koUh` {G;B俰|wK-G E/qɔ41\/--bY|3慐"}bd 9-FKn/% ^.*_!IS/5]!IUh,oN$)@^;BЈ*׬?R6u+Hؗ Ď$UwK3B2J^>ƺ.LϓɷA)HMJ3! |cG,n}\'9>?]/;4)jt5ꇂsx9Iՙq)r/va7\VyG/uo/4)6(2&nzZ"] ^`'Yn]6͊jJψs^-ұXK@ Vt v*֨c=Cs3sRJ ٳ93+ IJ~C Y?tGДOcjH(et FpᄆT%e8_X,`׈anڍhԁG *~rx=d9:KvƖ}_s]nCK.'innAd%..)}k|_R Ɗl,zºjr]ŠkhTwVϏ1&O,\;$n1m!QjW+V (yExcB>7sv^YO$|՚2PJ7ܿW#d Z8@`FekfGx Nēc?ʐ :QY}5ۉ^;^C5gk1-iUkV4P Eef n$j[pEv ^wIe|a øg%q½~8vVb!X|SX`R&JhCYltYָxqLqډ=ZCʌ:s`}n&>dL *xA)""K`8(L}͐3iYiC9n->B*x4^Sh1yJwY"]+BV'3?BA6Us6w%v:Nƥ2\rM١vc _̛k gλCpZ{Eu%v54kj/3g+?!;bf(Uч#am BW9L*m9d.ut&7!Y!F+S"aHWZY$,ے?ckHր\G|,M .`!vBwبL{2!z=n /@Hq0,/V~JxHllyy]aʭy?d9̈I&x&XD& ,FObbx+ m^O2yFOIߑ'҇LUa9Q #q&24lo2 썗K-ݪBh,06ދ.]F&΃z>iL8D?:iORy' Cܳ{JL } ;N-3tSNail%p['-E85 _4Mʠl.u7'2 8XTVJΚ|ߨx 8ȵi,l#H4L}H žqgDw`rDF+ۣ9`Ni(HbIJyK'y)~|t’urR/C85'4Jp ;Utei籃h9U9+gcICJ%ɶ7R _NXSaqz D6RG]Yzkٖ؈._SK-z>Mpj|6Tũۗ,W,RrGňZdDڙ+Crw/%5Fnh\o7?ڌWs*fT+تFdoYm NOCM*z(,iE1wi׋FץAo(/^hV Do[Nm1?{ı?^&HP*(VQPx}Vj냶XUVP-O@*TE @*  IDATD &$VVw>9=;{v,Bllƾ\< YrcFViDUqBSj0wJ\J獬LY=PtM!jNH bNNW67l_"˺}Q޶V<گ,+CEꪴr_T)ݔzH* ϑuU!0D B˗26dNІAWBm$cdamIN &,GB1퍈r`WO zyk&_f,M!m$9ݟJG-2P?unX xM+M;_Mh5WVTBT>W7|n>+ < \,3 ( p/ZCXȖ&+T\.[r4)cYiT9I"cցAQWQgKMƸd74\%U6k"w,MNMN%"}0(YUd! _kú>@^`I,*Y ÄvSW1]"|h;*Fnx+CY6 ᱿b0pHG$kT6jRXq).،uoc`/{w~MPS|p-贿^~)7\KOx {ew+*JT{uQ0ڼZB!. ˩kQF ҁmIHjm{z/-@6p@Qg$ު&Z:@ IM禌GpQY9(J 0ċx]o8 ɀ`1|7nǿgFZ{1 Eis/CWz ~* k?IڏRE\Y_ 9#̀`e+H^c$|\aan(W}4j$9J"-mSőaU[a!hn#:xSadU߸-^2VȬ2݅Gr -K|GX6ؗ_lTDEq5Q3gG̈})iPĊP,x`>!*QĨ~7̪ۖ%o%}=@#z^[?7Sx*WL4sWJ'5u\D 2^[SApЎ3FTԂ-<.mdߔdMUr8'wNl򴱷w"S\snu,%DY~Q1Cy^(j6WϺ4.fÇAo񨓙ױ hu ,Ϲ)ۧŚLFfs8[#tF}N榦6絮?|~ seQF%vy#z]n^Q56]pH*54nY596ƫ&^E6w8$WƩ7 M5AzB?I桨˄`NgyHhi# .Oz|w'C3@ꑂ괲:p|!h{#9q\&ØJLD|PT@A[uZCs @3ӦM{%{2R* 3' UìR) I'6S/< d!Bc kA 92I&XSQJ;XHM$xfMCy| 7wcʲS!b ,Kd*R$8ˡU8ɳT.'DtX*.H (aUH@ L wx['sj}7yhX S€$?X$d4$ 53*J6">_HYR \+n1RKGV)a;W@<>T6 oٌASTa$QPu:ReQAsCkJ|X}uG"*B]U7T6 r.ʕ+]tڵ@ _H zAwLUAcǎ=~!x^k\FYj2 cd:NosDKی+aæP[Qf똲df;} SizX? }8^3$ dN};F&X Q8{@Rת؅IeCΜm!v2.KTE=w-q+6jtkIb]^A}cH1Ģ Sf|Iic &K}s_0{dtieƸx1Nilj1pڍ KL]w~$r988BsB;!b! vbb 38x{zӗCCCCCCCCqN0g/u90s6!!!!!!!!!9/]s spppppppX{mjj?+]YXCw3Ν8+xrm988888ډ@N0)zw~g+#Է7Ɇ!|Y*??l:8+Z+xRmLܾyhpnt:nILaP(ZZZـ*~-j^kF {A2vۮdC?ZtT]*ˉu4ǝ|>8ܣC?iS6)>6{C2m2xޞsvR0y*J(ȪJȮ$%l4+MH ٝ"#؝{nRbڜ8ԩSdw"=(UI#>VV(C.bVFZ,.K!KuU_>t]B'J؝9q4Wq~Ξo*\ߠGwBU)-4,JblL#I!Ku%4;u"N(7JfJ'u6$ǟ19ZVFZ[}t皹OLlUΫzZ7Jۓ$n8N,`Ye,J=;7<_ 8}e佄}(-=OS~Q?&& 4Y |6/ꇈ#IuȕjlAHU~O|kN{H1BJiA2L<53$VU_M#u{2!ɭQJ4^-Ѳ_u ;[IskZοP}Ȕ=VeО!KʜYkj+El]% o hs,Gn zLWZݞ}i!OCs:rC%+jyF%Ҳ<Ҍ}X` ɦ mWuB9,"u'r|Zrt_&P75jLf#H7HOrAp1M=&H5c덪h4CO40-Xr"3nPoI 64)֌@^("--nE|G71@:8ߌJD̊{5X9TI#̔}ry2(ꥬo!u]j)DyQAse)Yi|gL]G-ɝRU7jT\HZŬuJO-YMZԯY~S>̔͹S13 uK]{u|{uYYyocOV_{j ;iŊRs.9>en]ۦ<\: dwg4e86#Cy[ƞ15,|^o0:S;)qf~y>MCR.>BOSI{cS񴕁 KaM< 5&^\?م1#sfaG_ ˺35ZqjZ$oM5;{0u8gvIht)VwТYEs#\(KRyzR'U$L9=RokhWyZ"voFA1V<)X~pe~n6ݧ{*}N o fs֬{9q7M䑪&np_da|ag;sڷiԽM݆$Gx;f;aHy(k ol7bæpx7.Ȳ~S"D~~oA}]= C4MH0wkfGsFƈPjv FF^JB;ww ;6^+ ;f BYհ/-8yc]x< Qt^TЀa,ͭzI77 '2ʽ¯ڻ3=s\NSSlNp>ϟ6:jA&h 6H,BT^@VLs8{BS0XX[INDÜ<\-P04CɼEΎ?:ĻPRрmFtDBBW܅{ȫ^`TfEOu1V|e<(=v1m:BQكߤ/m,`z5XR ` aK zml\0BJsIgL`L^|V-lpQX3T`ȁ<5WmT.4bIF{#ɛ} {7 zJn&/ #҅Ҋ\&el~FSQûxW8bPmYp%m q8$ 3տaыQPx!OrpQWa2ӾW/Hd>*ȐS֤ 7KP3ȄCkM=_px?;=.;G?5DY.$x%K? lZ`}"NTAhDOIWf2=SQnpL(E9M=47cԆ;CHSco\SPXʑ { P$G2(.%R:$Aw,;g{'z,$O1NxC|YIW]^`Uكq&EA}pI{ُUk_V !ՇI{HϯrO=dyfC0->tLk銄iͺiX6~2w kfv!ÒLb ("o^=.1B6OCNOPNNN[nk"zH:O_ZKeN]ҧ^gQaPFQȖU%<Z]V.3771.%":덍Ҁכl+?\d>S:$-`zֶD]ft"6c:TRϞ)o DK̷&|+ט%%{{)}h9ɖGUY WPib7U$}֭_áB3{\ \e UvJ꓿"С| E%!_w+о˓k BZS*V4EYEѭV( \2T*\60~_[hWr;Y 7jO)y&]2BͮP(lKBlOsfVVΰt~J) O2tr|d(]k *BW`&T,5J*| 2QT$< k߫&H4x>jw墝o8#Gt!!}(/:rpբP3PUη RBY{ԅ$:(FA_o?} .ӳ{5GwZS]>\1~I#ʃwXq=k[20]$ttdo:|a/X-rGv^ʯkQ0Ŝdr wٱ3&;EKraiIѡ+§EI֑P8hFf,I1nP dﶷ$9: 1tGjȤ;c!}f9/v 1Q۽G6,gw{,[g%ߡL9; uh`y8h{u#U&Ӫ'zMŕl.95 [- mH%HO%sÃq RgRaIbL# )2jMZkB9Ȑeڦm?l6s)&^ie~ň3u9/9DҼґZ{^S»\ q-kհ.b;Mꍌy)=VoqSzh^sR)՛fCglhz)uRxMMTX+F̱b7?Ұt zP ̃KONwk"<xC&w"Ͱ< >^'߿oeeh"V 0{-l.6mcQ}N)TFf=5bt -8!r+KdFW%܊$ǧ+FBJ _{OJ| f[z  DC#wD. Z 2㓑 ڿ*},BPK5L 4Y(0-LX]fĶmSQa73{[uڟtX[mAaW.$OM2z\F,?&Jmt. Fj% GW0T8+۶Sk}rcm!5.<a1\mrtE  -s?SB::2B–9!n@f=4,D ]=FF[QJ>0EE0`aPDä]t A.]P/o_H˃K##x HB:v옎2!H,1E[n! ̙3?1$ fe;XgaWcS:Lolg ;k|33>uZ髨SKbPuvz'рBfF\KsB|z]J頰r2fhz W4"M=nvF]8epp40%wxdIdA}K]z l#b^? CwdhJ`Pс 7˸ m`ʚm֬mՅ<.n^[_>:۱ _yQt>st=` СOȨU"sm=EnGFjT].J`H^Z'W{~ x-0 @ںu'qLv-A \t~_MKo%XKAnCC@Zn{Cz#:齚&E>K ]h^s7qLE0]a ȑ{qp#7s\x yOwJxFtս[H 䍍MF&-{/Z m8MyBV}M;HN/dxrpppppppp=xZ<.Y`=qg~70Qq=iΏ5#)O2tt; :DJQz,<"7=aN8jNy=y֬Y>[ly TCDrW.SRFXcgy {R>U%4W=6_Jܻ}6haB6& ғ~NRFѲc&~RFuCNi_~Xp( c[APzjA_^Iccoίȯx!M}N4Y\Mi;*0WqbUx/dNCg5yi^ #২l}rXӤKO~"`}(.KTZ!ߔ]ВfҳdN]x*VMñWKԌ8\խlQez/Џr4;45̏4N!922 i,M?4]hGwCgģ.:MN8K$Yݥkfz-`3TOJ6vwZ5}@!wbBĪ~QIZsa d {vr#/jIY:%oVnɨ{ѡP6oy0LLGFJV|p+y[I#蝊o=D9}SBѰ)uf5Kvr(<=7k#T{)ֽӔdrZ:Ƚ=k'ܚզl؉œ(:PP>P)MW^Aa NM3H'0XEҖE7%V;ƈO=_vQޣFyo;xur䝻p/6V<)?[>:S3 nUֱ-OM glő{x"Qܻy&2W}PpO|(k6O7_@dY@Xȼ?~O[>+ݾM@ 8djM+'jZB*k6L=Ө/ќ7. e ev}u/Zā ZD. HP򨡨dKQo_TGڹW++',fCL&ԹZkPCoܺeVVAvQaF(Ş X\6R":r:^b$yW^d !M8vfaJ^yN}IRIޔ!LeyEzd]oTUE5Ep\G$a?_pZW%_SAkN(KAQ3lIbyӃ]>3&e&m 2O㔇\Z+4p,%+moiXHZZuՙ)ΦٜvEIA3=Ϗ.J5:3;g}US"^ ($R啓|^3Au!gj6! A`4MžǕ$L/N6ab:N3$f4Jyŏ+'e/O8qu1B Ks\ n:25jSNxEiEY^9L/ahDcfƌ4->H.k̒TM]UsSV-wZjOc8'd#dck_ft"ߝ>|yzPkEȞ;fI7KޞT0%o,;);}Mg*V#@:"FBz^|;j2n_u#n Ǒj2,B+t4:g&)Gk& /iScaе37R <Hq28Y4'~"d = n*ǩ)LnQAI)qibη3n4NɌKy{\&.{ұ&~y`%'2&DR3~ir`1. @"az_ݬzG#Ž37AK#vqn2sU75髼&?z桟#Ppq3=QB$믄="x}tPo51]Q^}Aûb k1iؖFCoi+\t?7kGHqv >iw;:{gB#Vr SHe-P<6軠3iFy+z`{SaH޹6{NӀ'nv B%ƌ} SL8r B.T#3b`A^ag;sڷߍQl'QR`E|gߌyA(ZI5K 2Į 6|  & 4 `ie{Vp0 Mj 9yȦŃzvHg5=69SΘl IQd*0&y ܓ歜0U{#ʺq^!Vcd~oq-95-1Z@M{ h-_y|7WYլ+SlPFzDM{=(%.[/'}y<֠(aA߀-`yſILo; 4ѥ#i_ڹÍ#<ܰ>u,D05,V 5Kx=viW2H)2ݬ<&mc*c/iLs8[" kUuMf~Ֆ@㻏hV`P;)0 c+ SMB}[L6\kJ \ ,E߭[}3 j/!LI~eazXY̍z΅nHf!k'Ñ(^v^}{Ito6IaG,{ |ް2;f;a6 FY[X  *bnĆM(Ѫ}2zpiv%t-"zq л~{{Tϒ<35>c+&=Quۏ>'v݋1x !EazKX\ϩoؿ]VSVZb*%뎈>pʊI)ܯ߳Plt C .b uVgoF./n?n]Of.GO܂C]Qa Kk*1?O~y)%lMp};~YH+AE7~6d[rB|fex݀׌ݟQsk&]F0|Ž/?Di IDAT%>hf?t)2dpKhRWW44| Q(WaiNIB+Vz'nV,zkcoQ(8:e~JXؼ<-N85@o8 71G"CAST.-$IppeDʊԖKOJiD؋ &!F6bM DPXPs>I,;+y/L#)FߔnjmrmU9E]IҔ]UٔW'XzILTAhDOLI'& AnPGYCc+8uxRe)p^r+j vEJ,GLE1Gl :^Ly.[;SabRiZdH^Ջ?uob'>Px<6+; 4炟K)*9 [&>fղ'PO{b<t c<ZnrCb6T4%@UʀKF6pYI:0Tdžg!͍-0|׏ Bhz?]sgrל* {#['cǜJ PyY*~߈~4}_|+R47dBW/óLx("4Xuae1__]ӻMGF4Aݟo +g_Fx<`ҧ@X0:_vnjXbaU Z'T(G t FSŏzfObpsoϞ^/U1D*Eȅ#] &B'a`(Zt߁fݪ_L|T=9m,h'oiۇtzrOLaٷo_No?qh"bd~:fGٓ##rQVܶįNeQɰ~ΌXUg T|9To$'VCi{N8oT&BoXgFvE>%"$P^8tBX6LѮI5iVķ)CT;2^8NǍo ߤYq9`HV OЧBe9%:Ɖ/3E1sd3zJ>nٞ>eRvZs1jRE7ymo5PSj_hRJ0a߱hƸ/#EhٳLRԝ5dPOf8r[BbP]跦/|aox|H0&"n]ÇwOM^KΎݳh57W+ݜo~HE(OVZEI`IhՂ9 ͙~/}FJT*h+AlmB0 LLdGf Gdt(PԻ;s*7`!Á<&++#}ȻxH(Z훱<9 9b<@"LyEwjb5~_@khWr; >ZKXPV<ζQD77x{oy)酜Jq~^dV^4 }/Af*o?w|y*3Zv_עxXdYuErVqv iuYIJvoA1(+>i$jvpW\V̥MLmh 6 #g:˥D^uND$u=!ɩE^a oo VzZ)vv~5큨%f=@'AJBW`&T, ?~lk)V+ZsmBf򭃽Q%f}jTeD;QV^ݚA +R /:0H B\]SZ;Y!!@)8v*P& wVL=^) '? z-ls#^>7oXX``KEax1 ٱtk֬+ۑN D(xfCҁmIHڀ*M$D"RJŋۆlnO'GhgcRKʮ2ym}}s(cm}{ y PQUy*8@f܎ CIOA^>3M.+~sr=jinqooG!Q jF )k͘ftOY_NǷ!'1eFĽsNaj͐(׫ɺ~,Crr+Ü3hPz3o?DF&kIaq~Fcu@gK[0oYjbalQ떌add;_[hٌYЊ0'^$zEs}מL/G9Gڿ boonܵAQ;W^=>FT6NFoC䃍wӆ "& N*n$HK 0y ˖,"[jL $pfh3wX{#1̌<@8/*U!(K 8JZUUjFL2hG+/zx9^R1rn&ZoW#vFYל^?+J7x%62YȀOB}l\&Z^mwE4d45&EʌYmd>l")dx6Bm:ݭ3_J8ӈON nt<(;YߌF(QEc D;ӘQW; )& Cǚ 8t8I11k Xvt764tՌ:k0 ~e)2R|1PNJw]w;=`d{9:vjW y6u^3iI'?.ET?IV(br3 /e]<3m4f7 :+]6u%b 72FA2Yɡ).;%a!ˑP!DȐM$\uXe4l6~VY a؅#,K !OD\FhR }g_X;ƂX*9کn7Q/aK5rP_+k q}#>Mfd(2d[0 <{Rծ IÊL$U4m,S.,YJSVe 4EChF!4R~>Po<lCQmdSRAϳ㯎]4ܖzj=UDaofFDP5{Ft&ISTR&.ck 7;,?;Y3<F]z(FoȆUv) \୨i>s?s4o iQY`zZ KWX1h,uqyPCfЃ} ςޢѴP$Ppe橏EJ7lkY]T6 *kEf @0$#3ݎxB„I V QenY,İ Nli0kX,"1yy}Yj5h6Fw"gi;2qO6EYʽM fN@g:2-m ypŻdQHrxj mU6rvepo$ZanV~3Ɋ4HRhlk51le9eO  HxV+"QP顱M<|H<#CiRDA<įK{;/w8{\K(PY 3(*Py %jB(5ʿf%tԎ1 K %(gf EmO>0;>󝴇wf fdsT}v|nC GC$UKӬ>g[P}hlnT ͯ>t6y.A ܳR3v[Vz,>fo3s}ׯ iM @@37\5g+g‚蜪\6ɣ;\+nv~}wڬ]$tQ5Ei9܌GS?RN ֳ=iۆN\:O3"Wiߞ @Q0ˇy>k^ l{i/W v?Uw/Y]7o:c7o}o*xɊ,[ Lv"叡 *EO/?)LI8גk֎YlWEI Fo-+u?ёJw<#sWخ o7Ub'+&-~ ?ep'_5"6gmr=3G>_ٻ)%<Hz%,k5y!@ `9={7UoI)Rk￐wwz z6=$3ѝQw>_ß<]©7 }qs5}avAGٱ'c;SxpjN+Rɇ}cOXv z\4D]j7ܓ֜֩i=Xܞ/-{:zs9W%=ױ~)SE.>ﭤc_L\@5qk܋~)ٵb @ `3Db ~.D/}BҰ^O.0V<47αΎ|-1۵S9I6KŬC׎w6bI \w^Rz#1GCgH+՟Zz}oXmKF8O$&$zSJֶǨJY9U\o?L'bUO1mǑ\7ݗ]48ep:HȕV =m_#ޜ7b5ůϚ9.`UD`RurkM覼bLf͋Ү$8l ޤn%W:vƐ&/%|yzT[y8Y{͵c'?a"SSfxn^{&l/41#jJQ'SmsYn̴/[Yiqm!I;bAFtwfg'gѳCe]-(ۺ,؆[;Tl NXm+Е- 6"ơqv-qLNa)3h3nOSe$n8 zuњ́M@̞7Y6L,,$"Bk,×&NnX g̹# j2u߱=llI_.ELM۟1eAϥhs񸞁Isz t\եc\eR}uF}L*,cO֞S&"Ury5xQK2'op]vm98dbZ+@/U2J]<:u|\;1"}uᎽGfQi/݄҂'N۟:d]"3x~ۨP()!.LYr6k*/g']I>Zq9s]ؘt)fyE/>{Bxe .Cblyq- x< $/jƕL1Ϲh7**'33je.߼7z8Ϣ~_+KW> euYOˍg]& dN9Aњ́UWUg=5D~G#CwAs(%"AY0s`DbInǭbAcÂKܗUp5laل ~gui40Y~}ggiOqc5XB$?nx '4X`,XJ&85Dܤ'n])_nyw)+]jFe[yQ ~w! 6'>KO)JZ wr{ d}GK:V9 hKYsWnv䄞]: JlB s Le(W~n>! 4L1g NE~}h"ƣ cOmn耾Nl3D{dء3azȔ1N5`K?JҵbٶcgQ;=s8!wy==>هݚs9vy5؅eP0y,g̫9p#x䌍* !"n"&Z$@u oYʷ6TE w}P߭T,B\(:^ڢGͨܕME'KhLs6s3 ^E.}t[u0HnrbB($g{u_~SBrv'bY>i4EN}LƂd[6b^}yx,{Y~h9#,̿ jAQѩҝ05Y{gL+4 >}o2E'IQz{v* :} Ӹ|Hj*H~/iҴ#"gTƆd Y_ v DZ/q+풽]9 p5Lyn*қ7OLz96]O1?8h45 ML6s#X^s,ӑr]Sik⬼kڏ-GHz&w~ępw~jW^pKّ}SojF|¸ ~IiZӧ  (".")+_ߛAPӌ,eТMJl!wׂ9eJ(xAWZug ^)S(J؊L򯰥,3l8~M k:'A ˼xBcZpXo_±2;UTp %0F$Bu'ٴiMx*mm\##9?Er/"4ĝ)gz1i2n}뷳s`eorb钒M+,H O 0=:?V(ˈ{?eMhP2"lˌ2yS\6vgvZW&5l E_c9^RcA`4\4i{~bS[rڹɲRv%<~$_x`ףܾO!bxzؔެXbYRUqx!7lŬWf/vEHꢟf#2TYT~E߶e9M_.koo  !"n"&Z" \藔%xgWwV:;>OU278aYg!b;>;쏟6],v!ߟ/E:եlUPlaS{!A1lK z^f r9<=6~dGũKe3fxlIK+ vpљӧ;Z뾻YAR?I^N#fӄ|Q4yPg ,n ]" fl&cԀV&Loz, Q-4kIܪSRՊ/Vgا9qc,zLe fCؼ读sQC[z(:~(-c*;Cwc~;094;"?ۻM +(rdf*,g:jny ҎC+=rEgJîvP~>n(Jr2:YHY=z{ Ƈoapeفvgdg #N":V8zbm=|* vpڞU:}$Cͤg5@@K8)H0Gi<: sδ"@vN-ctcbݍq&ҮJ;Ly;B=U+ӆ.0n=9(-?-x+Cb̄:ؗb(Z,;y006UaKߞ:(0P,(6}13Vu1T^OpBZ*@"z`}RXx@}7nܰJעn,ݓRog=:(́[ŘՂVco+;hY,_vX=k-cϐMt-ϠY%ܑ& p PKC4k/TB0 ݻ #ĸ1ߴ3*7o7 qԤ?|wqaӇ  @Tw}4[ZZZVVvʕK.:}xzzviۢ=( 1if,noSHO3irG;TBER' "G?L6Y~n(F MWS-HlE\t&rrihd>4s3݇BE/\?f9m/Ȯ!674a&7J<=whG+"}Yҫ!@ 7@o۶miVVV n߾}>}zM4?XM-zF ,3]5\j fh骺r[ݽfTyq8 u%@xQIQakBs ˖*`(1,-BKnr V&{oCLSV?Fͮvw`]k͉Н0⯧gV!@F4WKupNhRiѨvEpCl^ $Ÿu)Ď3.:cbŬS8YmH}KIa]3cHggtkt/7 6xYߢRƯFvO s45ѦM .i^5 Zōa4sE|#ٲG'3,:tQpv<6wKqyYh9{sގfo\=)PXn#=CA;0P]vO7ՄiC_fTm/T5S6]U;!#vtrUFyv\E~4n|ȭg5@@|$-T.0J',^lT1|@Xd+3wzqm8^# dFO]pʢ:1Eڏٯ0&:q b  p}ݬY̙9o޼[Ҍ0L4nu@ T>]:];cXFSb]0hd5×l|^oιș,hG{wU]|[H(,V!@-ЗG c֛"EhtO r8SJn)f_ôiPL,MS.O3݌8u_=th{66ͩ [cT3Ļ23M{^is-NO&CZ&@5̕H$'NLHHuzBp{0ehu Np;anMlg4*63z!>veww}Xڷ~S8eEEyj wȡs'aAnʲ+LloߡKgGnR &* IDATA(WTVW8;5+L,.qpJMW"t_.(JK54l[ISv@#G...?,~PܹsK,ٳ' vzt1z$<>@wD@$qtrn1cھ$Nnƃͦ ::K[R'gTJMLJz- @qF=;4HL"t^ @ U_I`KS'V;RsK2@_0thƸ֯ S1⯛#a cg+Y'_s88u@ոބ7n+4L F  p" ᑷ{0 1*sW/T+J*|"X u#w @.*m]F/MN?݈ +[f55o0m2}ߕ8 @EF}N @ `ݬ3L" @H?. @"Q-Fr @ KP=$vRK۴nn=ػ}hP @ P\詩~{NZh8o[vW~)m;F?蔵L^9fjD6mFt5-|.œ=ϊ_9e}̝A yQ`Mz\=#rIDi Fy y}Jjԁw,Ϻv~s*!*툛/n喛_>ma~acCs-J|I¶f[Uiϳ/wMҽȷ[;YS@ pO v"f+5QtK7([>ҽ +CY0Vu&MܗfǽlW½G1j6({?9J#?I?[%gɾ]Zҽo&TF:f]y3'|uW}X񊧽 g8id~ðtl-+g.|uЧKP#٢/V=v {ٳkQ?5Ll*';5^E1𥉲O_~ɞ{cZW[:MwˋMrsKgl3KXu ^ci}[u'0w>u̯gBLRζbNu}X @@E0wW}N}fl*_Gu]9iܳ/}ݡ`;ק-L w]UMN?/[ʜuzn9|ꙧILVx-󅯺{ug2dЧRնfvc Umus*~&T)ϧG8N:}VIX%=KuU ԒN"zT\8}ˋq[W|xΘ?ܼyi :&jpʎ~W5z^򊽣vL\>Ǜ9 8xg!=jO}dƾ9kr]l]=ZQ[<,.=ItX;ksZۿxq8 @6YDZ*?oLaƄ=}Ҹ6j.;2jjD{[Ul)=1g sE[4UEٸ;c Ua>'EL#cә=4eZ7` cyR|o>E7YO';@Ο8 dr=02b`O/8я-O|VQGRMԣO|]۫0SzHTZu< ?_Vo\Ğ~-ЭC WVtmڻbv(s⭩1}Mu?q ߊF_cOB幰]յ4d˘}~'3~>{᱃,x:%ccXeصcҭm?>xvt jgӡVu⬛Zkۺ_vܵOYM +\ݛ rh ;=#N4VTA{Uˊh5]ze5US.m]K6}L  @_hu=f,txV܈)դp_;k @q~P۪צ룯p)y, a~]UՊ?,lKO} Z4W oo;PG{+= ǸQS{e$߿zX/Wċp;]L#+.o, @*@լw:Dct뛞۫VbzЂFN׮"30Zj[/-ՊZk&JmRe'+7SԌq Lj&7M;kmk(~ SS&"u=OEIᎪU+U+@ D@1J?X>9mKnb+cZ DVܴG/~ih }퇺̉g6tW7b#OTn4ɤخh s[=شZ!4JN(C=([A$V @Z"@#6>%M @ WD<, @ B=M  @fO8 @Z&0mڴ(tz~IENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/scrn2.png000066400000000000000000003550631232362746500226360ustar00rootroot00000000000000PNG  IHDR3dhiCCPICC ProfilexZuTo~gNr8twwЍtw(4X X ҈6* R53{eޙy@gDD  1"::9 8ؾOhW5.?'1]1`}voD 6;C5C=D_.bO(ؖ{^OO^{#ّ1`| p=b vwDr{m׷0#߲0}-5b}b~'/D$& ?Kvv6Oبlz@@H9 ԁ0 'AQHi(@=@+h 0&3̂` MpX!nHd!H 2  P(J2<TBP Ac5-C0 SŒ0',Kʰl ®p!|kp< O "P( 2@9|Q$T(uu5zF}Dh:47Z6Dۡ=$t$,* İcD1#Ĕb0W0}q[7, ˈ*b Xl6[mŽb_cP8&NgsE2pqq7q7<ρ | ,~HGDaFAKGQCqb5wOP%X| I# ncgJ %7e"e)e#esoD"QIt q"Jʎ**4U'cej"5ڍ:zzQq)iyG rӪ:-mDG3ˠ{EMEAB@_N):*CC9M [\ZO332Ę̘BZ&~03k2{0g0cfBϢĒre+ k2k" ;[&[wv.v= bvi,GG7NN=Σ8縨ScQS;@JL=WVC_#CWLJѺݭttuuu1F9d"ْ\BcsԫЛ?ɀdcaece8lDmdkTf4m,`hljnfrǔ޴tL,Ҭ?hذ$[XNZq[[XmZX~GpO؞6h+rY[Y$!;F;KvG_9H:$8˸kێ N*N&c\\]Z]Qu+nnܕsݟܗõ#'gguoUO/ٷw p h D2*:&:\-4|1(6b3!re_TO4gt\xLL~XؚحN080'> .NHJNLN|T|3#%)EfjuAiiYiM33"2322<?%.#+'-g.<%9/)o&8 `и(hk\YşKJ)9QSzcb>nz '~:9ZVv3gϞ:G{.yO.^li8ԘӸy1KF46W]}y%}[+]W^&x-jo_xsf7ojl$u6U}viuWn7;{grohB_LNSX޹7d:ta{{=uG{=|hp{cSϧܧf>^xdiӍgQs^P(}zZpZޘqy뻄Yx6ay>|)oy/_Ɩ\c_ְ2j{kk w767[e?~6~7 s\o.........+@!Gra JVGmBbKh@f t@0d .r]8Tt̜,BbҤ*fVi%} aC#/ 6拖VZ{lmBms{M0q+rxz%z| h\ VIN!e?6́kqo֓hESRƧLoϘ\>-E@B"zR%L%GfKm8sI2r J*jS5:IugF6=}A"Assּ+_+n+jHx#fxg-N]z=J} ݼ8\6/~ب1>?81I}Lxӱg7׿(|3J5 [wwKsK}H k>F~t13 ×oK|=-io5uS[2X viar58Ph `pU+.1jց~Q m56Qv}\ܶ<6|~тiBGDD_I0IIYKdd_+*U(M%^i?ՙ'1240DCfC#_yV rumig >Ypt  K?Qy*)vXqLr ։aI)'SEdxgfQcG/Lv_Trģh'\NZIUN՚W;c~\  /74^oiͺy5ZZ[F{;& Ǐ!OCh惼#+o6ykjȓO'=~=Z͌۠wQqsS2?~,TXҗ_IVھkY7^l~z!v8v ?I`C pJ5ޏ|v#)\d2, - 6<C=c$V1l9L99nrx_(F OԉFiCCVRLRSeH rr;m !Jʋ*Tm0jE{54?i]>c+Jҫҏ1036ԙf[[b-g$۸jqww^vpvqsur3w'Aīۻѧ7/?2/9"X/D9T<#"|5Mxdtlo&.%IJLLz`YOlNƃ̺,7ss/Jm-/;Vzh1lߟh?_SN$VU-TOyT;zznWϝ=_b雷[-4|W䯒Yٵ8z}&gۭS_vqP݊ᾑOEGo=:>9uI3r\HXl!;5I_e §5vXD\E~"̵)b@D8^0:Dr! CYP%th&p.BѠtQ1lNE0L-fˉuVa_pA:_C!KEH(!,RZP6"j2)j,u83#k´'6c03|f`\`rae6`D&V 6 Zw8p ZaE>BBKOTPY )72eXl<9%-Ƥ&a顕]3QO3c!{Sob&GV;{H6!~:Y5CkW?(8T5&2`jLD⁠O QɇSv;gٖ9[y 1~X]jxt孕շjn\_w|JCE&W:ntVV5,`,n\y)ʗ^[͜񏣋K,_Wؾ~|X,A "0 dt!O؆ErAz CHO{p \߃Q)t-ba~`U^g;W)) ( VJ,%;.UiNLAQqɐb2j-m=&W7]ɣ7wj/DAbG%IKIKxIra 抚JZ*:jfZ:u=Ol{ M0YY^mf[ox'*87֞z|G9GCÎoFD{V\[Bdɔi2,3_Ρȭ(x\]L_rH"Jzq֙sh/5i7o_).1sdmBPOIπïu<8Q?UYiW?\z:'/8b EdX2eh`Z:"򋈱Gg`""?mOM)҃ykT=~`_0hh1ٍň7 VB~_t|0'!f?!_dG^ G;/@?rO$ƮnwtŮ_!5H[ƄFˡd&Z xhv V@uhuD ;ij1m2<^!^u#??'S]'{o[on E&}%yM¼%yH$U_5^x pHYs   IDATx\SWOvHX"CA@A"uYٺ::R:j}[mjj-QwՊ{ ( {C$79&! C,XhM=9ۗ_N{ &$@H $а u177NӧNl$@H $X {6wO$@H $P'&LpNl$@H $ЈZM#r]EH $@uBtVdfRkKD̨¢ZuH $@H!n?X(tЪH(h EqJz֭wRҹ8:صmٮi͠A+'>N{T[H $@>QWڶ~uතݪjT.ߎ[>[\i˘rNX@H $ݼy#̫~ y](Tk7nh؅o-@H $@÷lmPmmἺ]H@H $P[B"55`Akk#$@H Z:?_XrZ$l@H $Pj h5lBX@H 4H5y\l?XP@']{vIZ~v=Guot/n)=m5i(odU~<_ٖs}qBmêBQRH qJY֭FWX -۰Y^Z LX@H &Ps|p=EPSv!WrWo퇿u/^_ۋ?|lB|[k')Z|i;믱F/zo2Y E˿dMhq/њʿm{۳֮ת^ tf3Sre|m›!o\Bd[A1_.s#ұ9m_4ihOˠJ}r\ziOı=˽_<ѻ)MsKǞ Y^) AgIHփ?pͰ”'q a.o\H邵+{ݞ;s=;Pw}r84qΆi7Cޣ{:ˡOӷK6ozk+ौ>sBXPt7g,F(>ekɵ]:}S/sB6itws!q!Qqos1L 2a n&6qju1E;@wnL+ًqm:kkO Z^6+FttZir)`"mh%\Z]:[ zNG6t3dP칱-=owfP+''ŏ/s{#r[Dg`q|Ǥ/ޮf2AlRTZTO4crk7$@H k>N5S٠;w$.G hx#{Tk] ZX0aZE_rnBsk@.7`vqcr~҂ Iˆ6hh,UK]\!y?faYc8)hd"Mz֡I];Ϧ3%Y'~ _9Cr:X2q'q5._K$ ~?~+0.,a$v-cV+zךge)Isɇ[yi*>7S|li5g׽ebM(1|CH $@WA48kVR"t~5+`X6NfFOШJ-5Ȫ>Mqv!S<-5F"$9з{%)u(tF mW4BQ RF me3nKHxqn~޴V#niU Ftn)+1c#Peq3eBYO8yj&)|o 9*[Bl"2&;qF]pv˒q8,YA}WCK5/,=}KCH $ܴ:¥L-_YNhTe^> 6=N~ޯ'(c?t0S0 &2QCl/ A#fVEş̿o7QAt q}mڡ9jS{،*gjKJnQz8tI &z6MzG;h!ccHKp d4&1CCȜdKo?WxόO}\H,{o܋D$Zm$X{aXTnX㺔[pOt- 웂(_Լ;d0D`?2w @H $lJvaT3UeEi٭E >r,Ṿ-*nqB}M/Hw:*ۍgّHV.8;ؽ{[^dnMC e%>G?p>}_l:eU=7- uv!Sm'v^(60 6u\ j:ONQ},]-'_gy9W{LsኴdzZv'湽Pl ;{wJ@lr&UB`KZ!R&{uB74r,}.=lu}*V;;h׵{beil}]Z5+:xٗ򗀑yDt/,\k9-;k{%wn ]tԾz Y k1dǯ]s=ilvCH $j :(xp41e#P16)mwMgw5Nm{֖©Vm+O~I,) ܂GOGmQ}f".nC|cl"ʖD棏5”$;e?8:sP zrk6L3ڡelzm~K&ws9oU)`t $@H < $@H 4z@H $g]@H $=~ qH $@@ ݰ @H $hnܸ!U s$@H $lZzb/$@H $x 5^s$@H $lp?ٸa/$@H M@ƽ=@H $g}@H $;U\Dn$u5}^MN&m$dº=-kXS]ɣ#sw5?GG>qS]ǽ:NliaIªY3YWC[cS1@H AnRF3w@'in|7nۂ)ëgpo!w$4-pͺ9N. n ~p}WJEe,UMY>gq{ IW7ziK,>+8 ^OZKt=w!@H 4d5='%&0t' 笁.p~3WDrsۖA=9\},Jn{`gKZJ T$tkyq_MvW'޽u+1%][uсE I.lM4Yo mt IgOoqӼ=50OEE^bcƥV#_α ջh/ /lѷ%Ɩ0jGyBB&xA]`⪼w"?|P;f#&%@Tw/fzwД,lbV̅K7j"ܳ9)JK=<IU݂y9@iL ?14ϭG`s\:m~62ANՌϧ3l](±SI ܿkki^BRc^$M޽gDܧkPbBH $&PS\A`gԗu΍~H}nM}B/soD;ްl%a4;xEH⟓VZ`l)pt$ʼ_q^x[}ӁVßO8l0 k_|Z-Bh-_olrL@=o~5`_%8x L<䗯=r#vSwlj&I!nKT,FS;swՙ~] Ww7*cc?2V0sq7)(]Q!f^ytq~1R|h"lR;/eJ[d7,"4$mڮd"# ;[6ِޞ! IDATTӊ:R˖s-4(Y!/U.e'̴ T j:kP:85S_5m{tvg1CaJyV1<VF8iFK!Ct5$gq:x'olL<=Y+ ?1Z)WXBQ `MV.U%XQ3jרʌkv^+PnXH,)5LH $s$DQ <[0Qv+JK eanoFƞ ~iv]ڕAƼ2*yD]w'%_4*40S?"sƩ}s-Uy{Nl/mSAt*6OYgR2tb]ִ`Zߠ6%"P?Nܝ+ѹ}C=ఱT/V½I?JDbxNVQu{at8 TYz@ ySyBe;% X-g`x:ө$Sk:йtL@VoB$[Si!ge[ҤE䉱Bz~jBL_k1iݝat~~^U_J@H z'P~V޽ KZ[dv85y`G4OV$5\Ϸ&vf2Z6C=[0ZwaZЌS߄t̻|=ѐŚثg.VԾA/SD.[ 9kW=|gT[-vs/qȾt+ؽc~$Eu/_pjOoHuւsĨVkv0Py/tz=L_Pk6ǤLqNC}f+kf1Yԉ˛eO~lYWl2kɩ>y緲QY$ݦ}@N/|_(?Zعy)ͯ=@H $PjDlVl;y͸E?GJcs=+lR?o]3kŮ~[Z8vW/|xdJ،SI}7gSN_gϊ];c°Uw^qY"2!AȕS"oHmgE[8Mȉ9ؐ-%j{ ~gՌ 6ٷ!UΙΫKZBxV4kƾ7ZM t馛iT?ea ߿5ܺ.6<%5hc&o{g㭇E|AY3xM_'ʡЬIJd:FlFǁq/&v̘c atZuiVXj%"wƲKWbUV]hэY+:9lղK@H $PvATn-$|[ً`OdmMPR9Ϻi"!T./NOTka/$YsHr =mu %y|_P dYJu'O`LYdRBwR"!-)P $fiՎ-y ?;22)6쒤}PK9Q]ʄ][3H[r-֞B%8% BD}NY3v6:L3+4YfBcԻm3BRXwu3'ʝ E$) 4@KRs;L |Z8IVbrtv L0SINYR8OADfss|EuLZ-}b6-L<6es+Fg%EށCmFƲÅmfl"40hŔ{w,(8i32tS$ XUceD $@=PlhxEf2[#<0tRfCJ2tv S̸K=zma9zKYW(ȈdWthDǣ@:-9o]"lS]gGB}Bwşa9Ʃ:[e{w+.l)okf;`O"RxJt0>Dn۶ioآ,Ջ={d9i?3Y^شߜ+/_o½Ξ=0=A @HO@]PrCtb֛Xǥ,QGL<-$˛/"۱•75Vndӯ?1wkBJ /FL.!k?{T2jbUL*C9K+ëmnɔ*#M_?ѱ2g'1]0< Rə#7&U'[q:εj "HMA!9+g.P!sOtٳx B̹3GEdٚ>·]9x{g ^-T2ryۥO-畦YH*\`35Fn(%#fXr-+W>Çsտ4mN&S[:_`K'[ϧaŃ !Z{ "LāekG/DN^zP w"ξ>>e2F4 =U_dgϏw%[{\/W@}c{ogђ\m.y!d$)G"2xdr+Ht`OGнjh(ܿK}=WT~x@HU7nAmH$c]mWHᎶ/l'^{BS.-ڿtWUh#NCmm^z?)PJLUI^ Eb@7&pխ aRvbSH%wY%1b˛ٶo{/[KYcZ+?R〞֭5x9+& gq9B[:f2ĝRgsjUqJ6MȞH1X2eszzXnWaPA\q4n=38`0tFO*o.ll w~M)CM ;P/F ^]<{qH~9w;iK5{ҷR--ч 0^,` @KHq&8nDuAS rw`M0j-N߳ݠ:aFUou"$x>o@b3%2l89#HfWfldRְLkeξTp;EO,qarBՒN;/?d;eahocZءfwثJI[RIA >圂+M5n~t鈪LX2tdu^=LD g ]JSC9XOsłU6ew/UJ۷/Hj-(-W:=q; C`w;9hBwMv\HIb@ȭIK nd;D>t4*pO6BMIWO:67Z+r= 2]u?uC3 jܨ%]ދ|'c潷I]2tw$@ρj,61d\Vd2bB}L=SN as)ʜh@X*&ٰ-&u^񝘄?{]IK**oZ *za4-ā]oȕkZ_OJ_X. };&-σ1,RlG;_^mj1zU.o=M+(nm~(˫FOH1((hC}* ۡ=OW*;ekQ(2"B|n]y$fB  IF"WHaE'W*7}֎''ǬT!V|{L!oq%Su2@KdNb("8z>(RYHw+Mr*O^(as6θ'Jb!@H # !lU)B2Of;9q+,XTe$RՕ(őn8K!+HTQ@\T!QYii+ޏ|2LGM˦щ*EaQlՉ8c: i߽8i!-'M[\zz-ew%V bT?}wsYT #m|C~ǁ$+ Y9яAA7)sUfSF6V{*Bu_W4V <݅0jloO6=!1o4nM 7N?k~l·V/#/  \b-xDLKou1bK&﷘V{-ݱ +AV$^\r [fĒ%D>,HڂGtH{ڳ~<|hc5!Wx낳w{YvzɆʣ/^~]eMEnP/ێљƜ#mγm?[s&8BK 7︵m->C𹯎v˖^Yg'|r o&vϛ}!sܔ= ٷBnt\۫۽{/}Q* ΨS*D" 0Bȁ %jQfT9( ʫs`v~Ò:cLӚCr3`{3ʂbF"3>$\3m\^SMbm9uuH FB7ei2ߤZck ,Vw)r1JRC2heq)C2MZ>eSk?eZE B7jN!!1LUrYٯ3\P:AL*1DMY14LAئE!#W ܹsOy?pd/"TT WoBV׽BsMϑ0VWOkiAP&UfAj붱#f@H !-i*Z}r,D(D1K@=ѥҖOz|k0S7+u +&O1APo-TF{sCųW6̳[ÞH $@" YGҤm.:'ϓ6@H9;UsvC /;; $@H z%w^Oz#$@H F 7/9ܾVKKK'HD")b"ɉý[\\ ^RՖ/?.@H $ JT\Q7o- 2'y' @ ~0B-`?28$@HT`5ӊ]_^00p $6Pel$@H T XE #.3ƅ`4Q@H 4F`zQ: 3 ߨ( x\x~ $@ jD==ziӦt:;g@H^T|'zΝ;rlWUic#$@HHf^T6r?Cv|!ǾrJLL \&$$\~q33H $@Uj+*GU +P؅Q={/!gWh $)_2?@YF<҅2ΤFYl, @H $*%P+QQ)C,gC\Xg|Q@H FHiDE}Mk)Đ@H MT3(*j ΅;E\b>64 V5O_;ș/4;-AtLؠ‚!cL0'č|f>@H $\ P(+QAsTT<&Gwܢ3@z S&f]r8Wg IDATc/w϶޵l0lxǕ Vu'GvA{ݩOKˏs=\>idzG4"g/ɽgNJCCL TTl*((n\waoXVíܰFo@H G(*'ﳳ#pSO[`xGs6ĠKˢ{Ϟe՝j׾ paKU8ԙ] )è#KgLVu=,Z !=]Bpm|ƣ>"v o|A,k}fÐs;<6d/~bgLe$ z799~0===??D"qssg檺)$@HyQ۽ 322f8= 'nZҪׯ>Kq2kΠ!$,JܺxuG1pSD+o]K2%ψEF72wZ W=߲DY/7r% |Οټ՛(og' C=*Z@715t ޽{LG^g&f2 ܿۧO֭[~g!$@ϝ 5B& | ?~~~M6\SaKo _=lУg~krwۋ9q7oC$.ΎX&N ӻ|{ޞ"54d{i޺լX{H5 7?pDlBD,j&%6*oOY— @H $P92o?~ }!Wf 7oooT z`okF\E{'܃ZU>WLU `CKQf5)O#mπf}߂"ƽ@h鮝}/]v\N_.~ͬJ`j,"nfyȞČJ-db{Ħ!0&͛Fc@H <q! 0'-,,4ibggP[Z[@c6rtГK}H_h~Rٲ-!>>*GC.K_M&Yw7t (3vߐzp݇mϯCes=o{42Ok01Sfs |2-ل @0T 7Y[m,zv-hp6<#$@H`T ~K$+$x^AB%E6xə%u; +r)a3RЈfss͊"!dWâl< N~=m!WsGqaZ1*g>]^ڤpQ!y&fM-̚ȅf!+; 1c1_q`$@H8wFX*Y5,=Bt ĉ'@Y4fLk-/18˭H5ؚaz E8Fa/ 5g68P,K%2LN"bWqSdf&@H $\  _/ I:cRu_d2ܮ^x?qz"w13D& yD*|HDD(&ByRɅԓh $h@0 @ +@؀WH0FIlOu}QM,dp{t:7)Y/"|1+ -(pvH $!b..Q l 9kj`)` /4tgt|-}gC?$j'?̅4@H z# l0}0WU7m"!D=4BǦtf`u4 $@5`PI#J/ Z3 /jybB~<Ba(-o-"84=9-G`BH $@@CqdyrCiKlRH,fM%ns'-,lӦMӦM3;n&)o=||JuJeVRj!sk!0iWi=fgLuGxpr0 ~7P ps@H $P=ɉmg^DZ&8 ,$<3tRT"17O>箉L]hmRPs6j爕dÒ΍A+3d.Q׼BǃSiB_@@5& !$sW+"""b&mBkHn&IB "*}WI,I,5üRvv3]JkK#ڙ9Y6m׼})?9^t|~i"+3t?2swͤl-K, "Tot<l @óqJ?R\DH $@%0̝Hc+frWnr{L$JĖffVrskKkIsFs.2(]\XBfNtSaw@ڙ x뻇E |oʃ{7Y3^7{JI]Kؕ aF~ֻg[Z6Uwv hBoMں;OwC!-vE)Oc C~E?<˵ ?.l v~3txA_ꑀ~HnG'ED"&eq KiB5)=^?ڊ $x>ɋ]4qha& Bf&er iꓗWRR.Nd@+L&7[WMv44M\-[t.l?~ )a^.WydY "4tO'Dag$=C_IHMtN}W^"%jעۗ=LGMHXJ["P*IԌX9@ajЭv:X͆5(K"lkM&&{;q:e͍|bYNZښ[P׵1$بm۾{גSL&x"!tP@LD (f0R;l߶= | ^:Pͫs+ bQBxg_l5ZJNe6)Y퓕P7ؒp+$xݍKwsY媲6̉M#:.{>[z 7©!,$2 2F@%9@H $j"Si@Q7D_,+<OC%_;]'&_:Y|`F$ 5 9ͼ7?RC܍w`%ţL?g5M6=ax+ɫ,# v}р٫|]I7Oj߄ZMJtLKy3aCw1=W(j;3MN)!@ܺ~tǍL=_Qnp>: >z9ubccG<=:usp{8z)xؐdu1#:oTx#ZQEm]ٿg̵k{/sԩӗr`LRRbۯ{O  $BԄjA Fh maRW0ykfYGh!Z{/ʷ&a+ղ뫣ɖo|&yv˦m_~x da&_3Ӭ4  Yr|ɺՅsWДľ>z~!|8"wyy=6g`%8 mXLtԴ[3?/} m5h-SowhڱH`[#7)OhXMDL4 |1i$ ʙ_?8aͩB~q>޹T=}_:dkL;0J$}/z@HM L>k$/"ḠD" QCR9si_9qa^Q%V?!B\v_YT(5~3CN^$B9N)y* 2K{zrt6;(IJe]S+ *q9FYDۢiɩDb.z8SbAF*'qJƙX1mfp:oڐy,buO% -DF"@k~p`TjqnӼrv}3"1vhڅϑ41>\s@HG|a7~fb)|`x Ա1^]%*n' A|J#7}Tot/?\ % +8 4׏eThK/MڳWݣӬ=|KAFQ2ZZQFNC5jb_&\_p&7;٘QP1@PQrD|n/R|j&nKLSK-g (h"T4QDdma 0$;o̹3C~O=r׾ IicX O!3愲ELhf5dLk*~3>!cUJG+>SVp_k/9$@/.vUeصp(LJ$**t1PnY+(GdU;J%T PK0t-X!S3mlkPBBYaq+Ϥ~1R~ps \;tr3{8QTVArzFȷ}\<}\<+a $C1jZXet48h m9;\X7Wx QʮK(SKc 5HaZPZX9?YB)E\F]Vi]4ԩܟb mpWԱ1ǜYEW"'AϹ!?%mA<@Huۊ|_YV H:ԅ(IQ~X,1U!5 ‚`/ew T7gn_O]Jiuդ9 Sd&'Ͼ_5ݹsL֖{?1)w VbЕuyx@Hy$^߷U!Q\)Tn(?R<,(zS}K\ggg0 W_pOTqx{ x4X4&v3,7ǀ"W{gY>d8oUβ#I c߳}y֥SC+ZJ `-D xۏp3[4_!~ ESOUc遰;I!WFB.xU+IlᤝOl^Tք+Lԉ]hWa}] 5$[~_fkZM7KMVgw`]N]z`dB`/aGe_ 8K>ધ,6@H < ꩃ!1Ay;46z]^}!۳˳}ҵ}lF^ qZ姕/jӐ_rc%y𿟃oWn۩a'|5]N:z]\/L `z t: k`pVh:b5QkH}S@]"$@K!:0dY1n iw=_ܙY8O5ո=۳\#6Qk/<(zx+u#LN% ]G2Rb-ڃ@H x fv#O֯.s;sK}Po?:XTbWC˞FۣptNL]Sux#o'm^ScK8Z`0 kTVjʩ_kD5Ǘ} $xSҸ3IzWZeQ6вHlvOn\:qb֤h!kjdi^-|U:Ck;Vh`h-#ܥ+3O*ԫ-U`#3UƁOT a-VqU: ~S1&$@H 4-z`Mof%wjB&{td/|WdnU<:eVw\yz.o.prΘTtڏ'j]zT2)2QُNGq1QCфti~He k҂9` D@H %@ͱH .F?fޛZ+Rݚ:}C;/NHM\QK/I=?rway׭dҭ>~?'zO.^0[Ѝ|1ݦ{OKOnBPZ=nblt`yh\/j-G%.*G&H@g:a0!_j^ $@/ YI_V@VvzGČ8ǗZ꽃-v #}=v>_^H/:N_jRK_Ks0rto JRKGJi+K>Gw6cw( _$՛F0X)$qtbD0}7f-=k6q $Faʏ/me'[l|:qAc#~u6X@yQKh]C AyķiϰGf$.L#\Rdhq\3~@H @tpS-V2 Ү qzL;}utթBE ꮢ6f8O0õ B0G,4~$@/ɦ#o]a[5,տOv|pw2<zX $xxgs:S`.` x CP>8ᨐ@H <=)ȩ22Km671쟤>B33Sp/",_l'K@Ѥ}[̭"8\pX "xU\U`f_d<8w$@H 4Nm:yƥc%06PcQ՚~5[T:%Sz4̻u=an&U KcLb;gŨm"9hy0!$diI̛'@7O+Z;#> m|x $x 4]`76U0#fjJza6 XQ5<9FUh8if~NnBѳf/X#|5eLH/ iRJ/b *I'ݛCvN&$7j˪B$ / \:"XSr3WR[; x:ZINSkt:'c! Ƌ$@H 4MdOI{AIa Vmf_WH23VoC2$ﺭPjE{M1˫_i}cvRxN0Zh;Y&$@H 4)gmشf }OEôА"bݮ @gB.], ij)0 Dsl>ϵrRB>ɢt(N[-,w#w.910tQVYFrs N|y |`Ż}nm _HQawY O8;ʻ{)FCaăf\m[6Rl;9hw%ȨfxЮsmY4CfAnV [D͓ r20vT@&a5,I$@H g@S` 4) ov|G9<,TINիɗeP7]>[@{:Z:i}d1qK.#ƿ +Y-j$/@Hzk=(rzwNUoĺ6d=;RmJmxDޙSٌL38ꢷ"Kәwwhyo3#TS]/ڻg֍NR+0M1~;g$꯵w"tjFj-KI!$xB!R`[^mnzF>iK|ll_Rv|;x,x v~˨"ľl7=omo X~$C[:gCA%34DSL4-XzJ][Cۯ<A\[6?n=*/L{-w|m8#d8ųAI0wu@^KFK_ hn_> $!:XYnI$@SfFG'fĞ미\BxzPBd_ҭ]lFHݪ`"n罹N /'ry;{;cRek'M#E̓Rs?JU9UP*؀{apKK;ۂ x5EQ깋)jnl|r0`֨JՔ;!p֨ ɵ/@H!:XdQ)ly_"z˼e?kV_ r"B^եM5# I:5Y)Bg[NaݔH? ɹwʙKaw]g\=/Lrow1?(h%gg ?k"[ѷUHcmھmۅ4>l ^?abPQ>yA`wX_ؚqsx%qQ%c݁ Nz!΄ls'i}o;T a-5pU: ~S1&$@H 4-NFN@MۺJ v~M3 ' [R5*zu t ڗDVH%VSCKM<Y(U0{6#ԲGqJdJu[xKiuHU)hAaC^?bGmYcDJL)U`#nk#m%#֖D,%Ra?~,@H <,,,@މbxG$ bSXXX!I \Cl26Ab9]>hLVڤSϪR- RWp9Tj+#ur/'@HhurOd` FCr̵ :p8DE 1!$@Hi nZZ Nr|D0A s9@A U@H $E4-Olt2Ӏ=Z /=C,QڵsjH $_}EsO {8A`"x(`a tpH $OwBn Ղ!, p!|:MیiG^8I$@OڃtY' @ka'9Nѕ눆 anE<_  $G"EvRxZP-f/!4haWE~8)$@O@3>o~Οq ]kX0x1!4&$@H 4-i\=~-GevJ1c_4['ӣdc*!zm U7?zc~N/ti(f4Zjj  ǘ@H $дU6|0iv6 +2zf:HHU^bԉ<)]/?=C GMQFnYFiC;gnH,7;1;eZ. uCLWR P}bs8pDH El2O(%˾֧*ҫQ_q 4kY^M}}ˈ[k}S;mIP}¹}ْW x(Wڎ!i\t!܆Ϙ5QBң7n8I&uuwX\3(m \}X,+N _.h.3"" .e̞^ذ|W[w4+}DH"b,Q # $ЇGTWw8rypm UoG]b:OܸtDE z |!8j Q`@LT*{x֐y.ٔ̚}L{yKJeu[wppp=evLnsiӦ7.[jyVz}‡@JJqLrV/(Fa}oUL{-w|m`O=cG2YM%1M[~-$ݗN8g2K,($n7!J ݰ BcU ơt9.!t @hiN?D_]sHtI2voXt$eغ_UQN?p#{3m\y?{քabŝOH6W_uaOb.G/Wi%-Y#i%NEØokت/V.ǯYT퐕/V9U^\iW.҉6s3 ְӄǮ /0'\{-tRRFcRΠ2^SZmgW({؁)JOR"w>eYt*&4 9r?רh!-)X.2UɕaK3}ԅ-OTrBYx>}bp`#͵ϷEThJf>]߃b<{rfn`wJ೓[{Q лQ)ɹv3'dfE|eB 0BSPE$>paKe*x-,\',PqT[Cs;}mYџ%t窉965bQ* ?F-r!WXC0\avxŔ,M6wAy_-*.76:wyɁK;y2k=E`nWuyDCGz$bcC2}ԤXj8C[蟤T2cg: EK?[m5wjV]*k\ٚ11!Fo8mx$5ʃףOE+?>=`]v!$:DgWo&~"$F%VyXJ#_94 }nň][$ܳ⒨|y׾up !=vS|~ߦ}+aS;kD聵bb}JT&bꤸ}bA*cSnc+f9t-=+ u%&XZU,mӼsD9CwugoWr)@Wۿ3tNZȽڅX(D9-D{ObLPs~rZF@0<?R>[ޡ1 Ko},7^#'߆ ?FR"wz[ijel xc}o.uxBONح֌C^:Y>mG&,=Dlom;g\lz7?kFH>,N8=WƊ_d1B-mL*-Ž{連lRĂmRb"9׉{+2ih 4m+2"4$ $@-ݗ.}k:637 zV %R?6(l$B2}a\V> P/ ƭ6䜯' Mv]}7h- T'r Q~/kR t0@凕mx.mm;SD dP' 2"ZGNHɥذZC} FkvV_bmkc!Ъ"yW%9VdAwUfK*/43(hw6Vka:Si9H#x=}A^Dos'*"}ܕ`Wځnva+T9^(]sΨNs=ڇ4qSq.bVRL "- ~)I?W = w4e;5KӿG^,qk˘GH $PUTZc$D8\"E (`Em\k͞V2*Tcf6ٻ辝C,~ןƮ2[ϿptvN'] ǕC>*;1/ 8y/w'S OP ZG]~ ]ɻ?AO ,+t JvhtoSBMkKqs=>ꑝUv\7[)Uf}}[3l?,9?lMGV~:M/B'4+/zڂʩjl1|CH $P>էEMMrSa8> k?lwƂ51R'wP:&6F;R^?Q,_> z{ h$Krп_38 0=m'ЇBU֢ Ft'4-L +),EdqFw-?m{&jw'C*n\wWS_3XENEټl^"MAN3R+_쳂,Z8MXaLʱpG^.K+Цq]q{MU)w j-#qzegY>$r#}@8aLϧ68!YFJbwbQ'_`!aKPWTBnWn=6z]BXHoQQ}~^ -T -^vq@٧I!_%:Kպ:VcET͞?ibY)C:Zo֥Ch1bm,'(7K;ݝ(~Xd!k3\oy q,9$@d3Zji$d&hS;a(5wmmD"/(B???Ǐo5d~a JAsR <Je|a񘮤RARu@-icܟ59մ}Kav`$E X|̌Q 5meS}lUU?Е2e%5׶ W,TYx,-Эeښª2#j1,@O_D b!@& VŞȱN:0C`5xD"HsO=ViWYcicR)e\Y9];3z+2?Es,xU9WP^IpVWmaQuK!$ oV\6HNQi 䵄k u@H $n_bKGˁ` faѪZM9p֨!BK# $x~w0|S/CU~a㏽ppH $ :IP6M"E0_-(`- {aBH $@xt&LxXlb6L071&`L앥Ɏ=dN{h\c8cW]刐DYT.B{AH $_atPc7*ؑpwy>~dʻlH(R w|c±]=sk4iLj%}c#)Kbiz5pH53="1qtbD0}A`14[z!]m"@H $mV>n IUUT$R[I_V.ѽ؀|Nunzbi/b]Sųqxɚ)Rk}תEUUvUiEɠsZ,>a"AٝSAj[{JFLa}́QB:o[h;FQX2YQ""qSSq:4m4z5o|žt{e >W9<Y, Z\'_v@Hh ,sWv3죐 >@QrξHqZ̗w{7,B ^Eg+Wo%=z㆓dX_w5{I-oU~QBNU  w{K+b56Ы"CJo߳(GJUUZzM?4g5ö9u:{Ϟ?FT9S/a >%R5+dOwIѥcﺭVFu+pmK"":#d$ŻGu(.-SduX1 Qߜ9˵҉ÄY]ݿ'JG`g*($)v?{1"*f_ޓn"s[ݴ *ڦ]lݒJuȰIɀ)D= jVd(nQPѺܤFSZ@rcvYv3v⿕̀$WIx5Q\{`a?%Zl"40K`6ͅtpDRQ+ 'S2y^bdTݿVg:Bnndcj;uQU+C.r2SO_Rz(r~4P}Ub]fI֠2}]:1?$e]&$ \'Y' U#;G G`k@; 1 u07$@H 4Ks#`W%-K-ݸ/eGmӶ|wZ`Mqݗvbznn#9jpФAa;ڼO5dWuXy{|J"{J5T 33\n6C֞8x0:yzSYčA/ tlhR)5ŗ[9'q"E0Wq[GH $^4MagS>U/GAYWɭ(\xpɸYe8ǏkWg{/W0ݴǝjIFX8tq-|Pg`6twÒR+^.mRL.7>6ǵ.Cx359\c/d*S5uFɿ{+S!ɧo\&yj PNtd#;#> k0!$@Hi 4YT(C ai,aXN=} mj<&M"eIlM= *ͺPKDBpެъw}.'ԚIT90 >qdo['͛2`b^p'p?' 6լ( Z `:$oĖyg]`qC\tSt VuDCOpsXqH $@ ){BQ)JD *VQTʓJ愦kT[Il(JG'{2n;9W;hD VΝJafj@˚bרΨ_b JPҥyM{#&,Uf|[ EYLc$֢VԊXZR)~~~e\ @H $ b!@ &Fð),, &`<. .(Erf 3֖`$a F*VʱQ/@VԥK=7<X+9\5쨬}0 p(@H Gzަ(tmRh$J Kc(J,٭=1ke"tPNQ3-G<&0~pb%$@H<:NN( /wXsݿZ50wuh5Pa-}%thn?!@H &{_ $r XÀYXb*v2 C4jur-;)!$xP?/ |!8Z!jt-" bLH $hZ'@Z\ AjAka_0C"!$@h/<7\ˁj-G%.*G&Bā"$xn~n>P]H 8:1O"`<`BH $@@剭5Cl [ Zbǘ@H $дP_4-Ol8r̵ :pwaÆg 1!$@Hi nZZ 8>5s jˁ G j˜@H $д\=~-GUeVD}I{Mkτ9oRN=[ rfubṃn/|sS6`BX $x 4J_p셡t̅f0dD/Ih1>&N%)M)n^RӓMWeH r cMqWsTLq'pL#E< s "GYY@d?^GH $hvrN8f&WՍ)^nVu5&Yw5E)7s*UQ^AQ5+X9Q)U!=):7h7BQkf0ͣ DRHPN6lќk4E)7S2ZV£'CI-GPw q!|: =0J}bˊBJeb ;g[mRk3.7Ol<0@H @E}H\;^섓F )w˽&ϘAJZX#3 [//='FMsOZ %s߼֪ؐEA#;f8G%aqrMažWn Vmf_WH23e<}3W返O?YF8UZ+*p>n2ԮSi![c=km O3\?:BM`@H h\{?MbRzrznZBbs3v_3} l  y2!VC vTuP32\mĜݳݤSMi֨7|FJKT~zE f$v_;·r"/LH $_h俗mz={.& ݕ lv*OzFG6(,Lڞc<,eQ*fzt`"8V-jNB^̨/6Q!0uJWtSHUoYh SSݭk*}[9:rkX&)Vu&hr迠K72ݤBr4\F k i؃OI=?aC1 {jԀAHj)zQƉ[kޙJ'!lhb3rZĎN"#C[BG>wt]}Ey[1.Wa#C?_#aC{i|8iBuυoo";p:/'e(H $ednVDo1VS$74`Oc FDǧ'.^8h!g?x3U뷬_U{1yB`m]!c #$/8 P( >Y9lR|65Xu` 1iP&M"DC"A6BcG6wNDnj-O 05lԈf;t|_dݵ?6ܡ&􏼱DpG䝽#YHq-C7b\Y^^nٱCeAkDzmW ; :jv]7Z"x`?aOd*ξ pJ`=zz]iVznGҦWڣi5?i#"$hƋhHX $q9` ZB G,t͖SK}8ӳg_gOÈ]*0Iu aRqY˨-xKS˜~$d'>_; @_}E}N륧&ڹztճ Z;Q NMrN 51cNZx@H nD Nr|D0A s9@A URO{XLGL2Os9`((uFÒENѶu<^(8,@/2c5<Ésr0RSH4IA}`8N|j׶b֮C',:UL8)1@Hi4hc+-TL` Ya>8hij ׇ͔Xl24&VكVF7XW{;"Wv|jxbv)} M;GwTF<Ѹ1hUw~Q{ch3H $('?oWwBK8ejΩtm[aQRBDD Y?k`bj dZ zuWC{փ2'3__eME $^\\ׇ.!, 5p!|:M[E>էݖRo%[?NހjQC{6F}ME $@] 4溎˽<ak:FW#z>f4FH $Guf!xZP-aw- /LH $hRMY(,.ex|%X*6Ehiw 裱KNqT&̡S0j)S-WM.ʄZ9aGed`V3rH $IњUzϢ`VC1h7񨡟Qy0/Ҫ&}[DM~=%f.*ʨr Wz9Kʻlp@]سt_\>yMG1[9LJljM?{Us0CQ$HI 2_Hp L6T| EהW|b F+USɠ|"H5E#q*JJahJIܣ~@H <\`h`*_Io kր 2zV ib֭{W9*ܩZ茱tj R h<ޜQ~_I"sO\Ⴊ΄3ÖSAEU Ns3 X[UUzm S 'q!w=})?V('DW/ ПawmlZcwV~IO)-&U(ې@}cȑVg8.rJcAx7k^4wU"n [na|ڣ( F `X5 yPP1@H $z@t01xPsΖ𠠉/X\}|w:;Pz!f/:ŗ_mSerb,]g%K]z1hBWt<NmP./t$zkJEC@[2g*+^k'ChE%RFd~bu+M񂢥w;g(Yцe~v76=7;dnb3oʩ~ÀV~C^Exkoi8}8LLq߈ 8@ Y" 6v/I,OI:k`s#T4 AA9̋x5@H @tuvxQ fjw;:[ኔ 7yp!h V   0bB1բuU*i?{Ua4Wz5ne׼,[UO򄈸k%CWŭ;k}seMw=九a4e+Rr]7ymUhgYNBٙpjv{߅u5M/e%~]i^7驲>#3b/6р{MGS]$@H&-l76`)%~+NuUeS_y`JNBy\'t`+_W>Ry](AtVd J erK\ {ljࡵI]-!uEY w]]{FUyevlЇEev7_w;Uo+0߶r&GÔ93r/*8}UHsT?)m: :@{i| ``А@H $гѮ;egM=(m š3WtDUG X+$)>M7=8t)%fYed|F\4!- FiaMu3FKݺywbd-EPJ:Y`L G#P| !@w R[sws:W*<ңm-tY&PrZ7M V5plI8u%ٕn\n4 *wD XHԍ`$@H <$!S ѐ+X7QqfT:x/2q~Η (-VVizR1G9Um ?| [Q繭V9;} 9LB 5˶sJ m5VBϳ ;=D1Qmjw&۷\lًH͉XB${͜H1cCx*H $SGٳ&&&"H,; B@Te-g݋7I,Dt ԭaPRa4l\c&$7VL HC݇u'l4W?;Mhu/<n.?a !<$3@'o[;j $x tY³9MT#oP*H/*`@-DŽGg%og^ty}8wR-rbniŇOnUymFm?0{*ML}oᬺT5|ъA, d12ds5wf> IU-Vd-;)8"fݜ߾gI{3d9 D眍4/+HO.[~c Kȯיِ^PN9Z$רK~֏_D bRr );]|J,J4ye +O n=~ܩZ茱0˜G 5-Nw.7V<.$jC\[clg%܁[V/}-C/NϺrT`+- , "&EJY,>7xÊ^y[e}c~r<<TOL4"Wb}{bBf.2Zp!#—Qx $F6<eYu~(ҕNg W1Ү';D  $oEoXJ:#AZq#g:u7p[4\~"RS/RaÀ@r0a>(a ?NOʹ=p;n-֭igD795@@H Ç8 1y:pk9*gDs 4hi!$@V D?ʕPRVswrkP~JΥd ٯRh@wgn} `Kem= }XY;^td487C)=c]pfгfTݿujeBC;ΠW9Ak] '~uϗ'A_3mjt"^0E2.á qfSu?c_|PxVt`}%%NɡI^.;9:R^u,ݹP_k.bEv+n*ƒވW. IDATVCT0OMGac6#7Ӽm 2,Uam7nzH $@\B?ژɠw4t檷]_(dMԚ/A1{A{,~AkSև;d}V@<-,`C}q2Oť-_]&,toVg@4D_$ ыe/.]9n[(l˶}Tt7%¤PO~4z[r=q$tS+w Rb}yYd$R%2dهK|BC\ b/Rnˇa9>gz*je }/|X{"9?{A~3N}q8f̧3/jعf٢ 1"h]u]C ?CpNg~ 3Jf/?ؔPK9M16ކ=F(d xE:Wբ"$@]#V>D6 D- P]]gsbQ) "@NЂ1S}qɰ% +=Rl;P`|"Vyr菴ّauGVNfnY;]U[77328㧽4!Aa'G*yoFIB[ْ ~q%K7O |!V>IAR@$fq'm3k:;XԄgIj;K3ksa^bbFԑS+63f0'A~$B %x~B4nF|h(:(v vw'yy\MIClxߖ jk ?6SJd ^||Hf&TxW*e@H Ξ=kbb2N,; B@6mJR%''Gި= jW#: f O$`ێ꼽 t4$Cu"#UWvMeNwi[?l^:vx|dSAoMES!C3X c֫C&W62i4<fY#B. Bbbxj3B78һԸVV/ZiYoՙdγo䫭^S+}֦&]=+&:qwR󆳀 Ep3 ,!$iz]w7vmݩiz|yUIl%jqMBzĵ=*Ʋuy''Q2<%쨬OYB':f&nmTֆ84pc.|Ps^;4ꦷ9A1IIf=Љ+LZcA[h$@Hu0n-{W;=WӝhI-%ݑl4 4U(!{H7=[=%xkJЕB+] :a95|HKi3f) u!"s|xiqK6$GH $z@='cCBGO#w5Bc&4D/_&s{7WSX{jE&;2x6>h޴1~ӳvcHy'Ei&-ߗkXGs,"$@$:y` ,V)XBE!/RUJh\g:{X/%ӌ>;Ƈ{Z欏Ȃn/km8aQKubsYYIw h_ő@H $H~$Xig F  &Oi"֑2gyz̞mm];M?u$msFԥM3r#iZ?so@h[SzhH $#!:`E%+EӌJ ՠ_ЈPU#~ 3ҸGO'ټ̴4HY,D!Օh|X8gN抺^,\_̴Ę1i> X $x4f &]?8*ɺLZ!kg!X3 3\~|w6LR)x>d2{J!-XL@ZZJd$O#$@@׾~0$$`C7`0l#g4bFH~&z= #c66m@*s݈G<@H tEt6aQ&Da/eOB*ѐ@H $гP_,OU O@0gB4BBӅ|b";HFCH $@DE+wjDji;``-O<:UQ]Z}?KQOL!ahO#BP< |>/jYX $xU?$o?Tוgx;nz'Yd Ret^˯l0'`s42q B0^1,x0CLPdžg@H `u^ YYl-#Y9N[:^j14>YH%#ɮ3Ŗ] a ' fO020 $h@tp酯`;pNC?ioG`-=cg:\;-rcbO.L!ߐu3FtI޾D4^8 ˊ[ 5⢮]tm$HJ>mR0lQJTcbSnL_?~Nlr׉Q[I0asDOYz{ݧo]DFƇ+9&6uȆOph.lTb]x` 5TWC~&|><ǃ# <, nvbX $xztfE= 1ʥ/YArt󉋣ք;X}r7'e/Yy^]\;DoȐI׳\UU5ygkf|"VS\7g2upw# ؉Ι@VuVſF_2ܴ~ި}UldvZry: kRJI -IXx Ta#5!_BX ;ɩ5J恆 e ntl $MfP&zVF^x(7U\͇3%={2t*l!7O{s,C)\A;Nw"W}rt?ƈ/DyPH{4qv͗H?'BTL&V_|Y^VP.{&Owa((]--^.z2Łz[3e{7I֋P ̪YVb$Z^hH $Q݊tZ? !Ҍç@* rIJ߀>,3Thj/ ڪ7-}?{s9O6s]kܹ֠ff~7)=ēkjEԟD L}{I2eIfD i# h43fxAH $Kٳ&&&"H,; B@+QT`*xŃ1$!S!'(֭}TeƠ;1_چr!FaShhZ@BhH $Y١Z0_5(`5<9L̋x8pH $ NG+<ǘP)̈yDȃ]刈DC$$dAH $U! `Fh| `n -!$@Hg `^DDo]% `TD XH1@H $zꋞ޺JLR! ]X'&YHFCH $@@ܳ<[ J 90,!*@H %gy.|/B0^1,x0CLﴋ\@H $&m ƒ0])/S̃`08o@H :ϿY͓ajȏ& `n4xf $@=KupDo]$ k@a'9F<=A@,aa; 4$@H ('+Φ|2N}eYӼI\T 25_9aUNX5BKw3-UNLL|㌑@H <{`w }͝*t=V^IϬ&!t"[z؁hҢ"E)'o}(@>ԨQadBY9vTKPc v@H <z&{۷FOR}k$=EMeybfך>e&2I᪪AbiMjJJY['gib5}ڙ-w1aWB1}ׯgXVVVV*ia3#!UM4G\Ϯ@H $`H:$G |nˇa9>{zqwAw"e .j0H5N9vY:cAhݜcU!Xߴ3\Vtk&{k u Z2g)HåS@dG'<ߚ팚LfO3C7r1_" _1 tlt\vF% ˗GG/_4MT ce5u64p'=} $SZY)f "15H@kff"~s{ t6s}z<`?'PvUrR(*/'G Q )դNIh{{ẺRNroN@@M̘1{@ޚs7͟ƙ{o<w\?2٘ijӓ'?rj<鲇q^i}݋a ݣ$xj={D$bxCP(|>Ù ,99Ma#3ȯk*V$ѭn&1 c;4bߎE0wUc/͟A_$$##x gx&.,? idBkJ;#w}cۤ!?A>\﷗vt.",Q5LfX $@gYY>]kQb#; fr<$3@'jnNqYD*B:1S1255/3 bN[ x2@`8,12L^IRclrXA4]@H }f}!ZZ}XVZ3/@@{*L!pЂC_2pD#`A+:lᄐ:3J^R eD4^]^B{C_07bϞhF,63 Z5=b1}1 yF3B94Nͽ08_e0`9(?ۃPxn İ9$,MC'o#hn02z-萀ٳ#bLX8EM;AH @9O+5w0O_Kj;CR \AiWLsdl3_ԿLG۔-ϓz)RyDh=!$M掳oA@׀VNrjRy!*z>uA![ 1P+B` "(?y IDAT?77tZf<LpQe>Bb` F:Q3e8:@u e KE0v #.|FT!A\]N6ٹup|FYx%eeҹpQf.?X^6͙6l$?~ a\<}W%|-JH $":-2xa5`PeaQ(p;ɁcjN:vFl5z'z8bO.mwS{?fv\QHnl$'_w` bk49]B}(hSS]hL&shܟ5j+bDq5}z6d& x܎sqT=XSΚGJWb+wZq~ig]ũgk;Za $@0#''exJQY{А0C6 iTB /4'»Mnm2oXB´m ֹvhP;UO /.@dx ҵuZIJOT4>NؼQOb=pTY{FY_[O~|ǭ XOYva^ڎH14kz&٫Us:9ˉ]ȐeEɴ,)yVIt#O:ymΠZ#<@H $0P? % h44Ҩ!(dUjJ_d`Sz[hI;WrJ-?8 b+j뇔wZ:O 8g#VQO tSjP~G۱Z6 vi3li(^ LGTAMYĪv*m Ľ%tY0E՝rVˮvS>;C.-̾eUUd#Mv`\gMXR[,&{@OB˂XSs#Wch43f 6:`g6ٚ"jOU5-^]cԲEYM=*얄k, _R~`$2PjջtZflCMzrlBdOR(miGj*¦pm[N67ԷWkbӶBi$xj={D$JBP }T(>gɝXKYj̡NwpơEϒ=(+2 $_ꩠkVA{ç;s#쵔c )1۲F'qvW o'EkAUڢ`HPÃqjxr2%@4 EɋhT6i%eioX;ll ʸV 𕯭ֆ#iYw0m@ ]څ*ݻ!Y^mm656H<@Hc ~/:P q vw 5bCהXM;~A?5*xq2-ٝ:m;gB*1Zo3/f03qҷ7/K%#D}9i,@lZ-Nu\ALm;Jm^;-)4NjD8mA(kGj]SZ>rbԺ53vz 5))fm0l<ǘ jF#B*GD&iԏh $xHΎ{ѴJzt/䊼gT"}g͌ѐYxhGjhEȿGoZE+W;J_7؜_2nʳRy?+..m͙;rsGQӧ3ff" 5 Z@CH $@"5HooXQk^t﴾'VuVm6g'Z>ڭ{12 f"v6sOv#\IL=g;1|՚e9\]I*ϻ7gE+ѯ_ Z XMlL'dc4$@H ,G/7/#=ur9_*QzY4^/?Gt FG 4h?˫ i7AD,Ba7vS~7;2ݐctJBf>ʰ5a|K77}qr_~uMJ#ddH'zu8].,xI?Ά鐢pdK5i#:I{5ҋݳ.yd:RwN;?: :j!iu;C1  BHFCH $@x:Up|s `F$CWŭ;k}N_]{ =$w+*@Rk3YP⡗*qxU8 XKߌwgXGαEHq㟷 4Wk,7S [u 4y^T6G>_(Y%'~eIN@<$1P =0@H $z@ H@Mb7@z wVUU^>so_eDaANwS>tBR4 '3yWtd<Ć۰f{Z I}]w?CR9~ِnpN'M[HiUvX~YMaַ_Mqnc9Y*/ źw>Q^[vI8;hĸ;z7t2 ]b Ε}h/|3D[7k73srxeymѹ ]ӳw j(i`Oe>MD ~@[̩c X S' [NKgܷ兽M++2;Ԗ1K ,#$ƃ/WAQ3DaY ZTî& !EX$t(oO?۶|uژ{x+s[l(E ?U(#7"D(66my6Pr:*Ye N…==I$i +Av4Dl8le>x˩SfwfN/oc/llEPp爄i -k^zl=FgdC'$f'N-ˎM7˚(i{ž }'r߈=G .߈Cu=/<ȃ3R۔z,ߞZMr`{t/`H ''&G, B0FaShhZ0E@BſYcwS>]i=:3B^Í̭i7K<ŇnRVuTC&5t}3Ҥw &}anI;?Z}JpFy%ō5j $@_axP R `(q"Sm_f?7d 0ڞsׯCNhEz.b˞U4w6{_8qqږCҾBE,L2mܔ?mO5Wٕ㉄yy}ɸ.#ipE&}'7GgO)nRG}1#=,ͼ?֚go0  ./xH9|Q4>V|IȎB9/m+Hhk%vnL1WWbڳsMi/kHχQxsuMȹu?/~-}a;I:_6\Ld٫Ǔ)nuBi̩ḧ́=9ekge륁IG!N^y f8p7MJ셃ottsVJf2hH *g4<ƄG#D'[1rXKU~m#32Ҍ;mrc/b?|bHM=z3I޶z-h{!#pCd7r2 js m4%5u]kOxo>00|*xML|=ʽ%mKV(Ұ,ea+aQ -A*0yEHC8P?g h&hC~_8+q[)5TY>w3|4J.F-DݼyNUJ(p?=UVYd6_4ʆ!4*9dZxYѰ Z%)SM%a8\&G0¾*y[5:iAOIS%Q#4r|F%H $L5uKRA3tU Eo4* 2gg@Cca%JrLMAf%H7A @xR8l6ÁO(phmm ?VVVZ-,BX5¡ Cz&EٵUR+SN?%nݑ!dc*47wgaNvlV^vKخ#و:gE!:9nI̾"\hjq+Ϡe'|b!SC<8M>z){ Nzuճ\h[e-*.I5.:Ɗ 4T`x0. 78LI;> [  ņǧd$)`IS!6&n1s} }O sY6|Z]K̘jX,Y uol1 4k@t1S A]daB칟[-A @xLkƻhJ 2Lz6R:'4;[7ܶO5sU ӰލWowݸ72Vzb[b.Z}- 6]wmw2u ˟f˒W_?/\&"OI@@r/7pgYhW]Cߞ~4ү:Ɏ}rŬes &~f,[jF8jQLPqLVTp> Y2^(p:#J!xF #0oֵ#7j|6kYv]  lß}־֗Ǚ7M 2\g,+~P`* A @~YNܨ]C1̎ܶuʁT>=ׂ0jlM_ec0ʑ浭Jh .RUis/n.^9 W^Y1|3_95|1%qK)>Ar,[4I(-u~e4 K;5{V\M~JFH)'^t4sσ`5vp&Lq *-95G|.Pה^^ m<<4xrveosU[~b p_/W*b&}`R,ۊϜZ p 3.xK[7A\7<~ؗ\2+rv9^ 9>/X9w0m_ݽ_sR) h"twϙ6~ncm"UN k#C)BGzw2Tnjy(fӘ!5  (ZU;xCFJר1cA @_끸PQAj2DcrkB&"䭧Dɽ?.vh?evxkR$ 2eΉQs+굳fa Hj(/\ 52acu~ޕבOo27XԞwi[s<{<Ckڰ WQ"8Nn"AjF3PxC=2UWxr0>z \qM\4 4SF4E_?!oh nkԴiڵG+\cRA @ }AF)9T^Ӊ j? /ݲ>}lXS>xy)wI^c%͕xyc#xmm*+[PJ_PK+ ~:?A @ }Fo~{C]9 "Gn>[|G4##E}wHdj`WtGaWH:H&vp7lޜtݝN~SL7J a.V>2VwG-β)C|4dl^:$ .{+ϴ1g'ιL(տ tBWoZgߙwjW.cg֡W2utȼMQ~ΊI7}Of?aEb)-[h%Uj훓wt. |IaO@WqM4hxc:'bMq3OS='d;R,l:U&L([5 wѝk< , $A cQ4k7=[X\.8@j A%#% 9 ykgK1AjnU')KԊV|:51Q[AhAaӴ LcŶ5LI7=BVEE>i.gE=xC1LĞ$UFqP7 t(dj; ߵ4rc;X /AH>A @ ,?"lc'VG Fkek7'wyVB#Am *mQQnnn z0dA @ slb;8?lݏu7qT7ͺ8迪=;MuR:jnA @ =D==D ^e.mmmں <<`. dӴ85A @ ~{?7OŰel6 C Ā*ޮƓgBO4JKK !'4?QV K >1kZ M/b2֗eZpʑA @ 9}/B}514NjTz_"T< O]=O5ucQקQc?dQ?vT5kw/Wx+юQ={(@uciI>—OjDA @ :=8M>z){ Nzuճ8VYdFϪ+qtN⹸;v?j>Yl<7 SQ/ӬayΎ\+D[ :i*U,ۼii'L!ƶi- d -Z[}aa9 A @ O!;N*7ᕤ%OVvHn>κw|߻V8i{`t n̈dP*O|zP#/ߨ0Vz:)Lugk %GS2]>ջx:WO;YH^k&S L Eu+zn#z `y^n^> )n]W@ы׭n"=-1!o1&q[ygfQQ&FR#/ Pkar7O~3d?aIԫi񗚐uK&SU?cƆZ:np*xۗ~v7&m/VI/(e+|۪rjl𥕅o۾aB~I Aශ@%r=ƏEucBTѨ 6}u|ꖍ+G]d1R^@;XqZEc'yFT[B"eQrZ>b-. `QɻV.|hBܰ1KOyRyIT3z+fWmoz`o$`*Z3R#fّ۶N\9s>^lCgD5I [Ԑ$M=ei8Bb>Jġ ӊ&AV]DOtaGm掶G $/ψߓk7}MP\*.+ڟE.ttsZQÇ|uTR1>}S<Z,J..DW,3-Enǃ;#},%A @x\lP5L l^u__<cѤ(?<J.CE[y̘7nnӘ!"5 CP=n9zsg\7[*#}'Orb{'wm KȉvOE-P,1\Pdq[BՀ EM9x]n:s:o|၁8ug|J7ʉ  OH\j" A d"/~@}zC=YnIS^ȚNMTnTkC][/G%oMܻ5tvZI7lLX<·Դbmzo0vPWki..C/ +"t h%k}݇"?XBSTAr-jeRM>;or^n]'i^|R8YR\YXAYCH[Uk2;[VsZYcuO_Z`^l/A^^y+X%j+J*ۊ^ P)>:iMtL>44Ej:F*o69w]=V*yPO_zn(߱/EU і,h]B\'[\|EZ;3JhX^ 'aI2:u_QS{{"3!V0Jn)bqsxppYVI. k$kN3x*8~yE lWR 2c3 pLT$WdC!A @x2C쭪E{1e#Ϗ->3z;WVuystO׼}rO~cSO;5A;K jQUIM?J58O}wh]Q[ْ}qcy'n|;l/əi1XpM? a{5bc;|Fu%044 ;^1Η8qs1BضD-Ɂy6os8r+\:Yk?ҏ,͌[ GCgG9͸KCQ=IvT#v=fN>?G+tkh(i뢨yF[Wh@RR}>b(@9ݒ#gG'\8׼fiWՄ&ַ!:i 77j LGW7Z,y#ncz̈́|AB`@uᗮSv׿{A!Q(\.D(55֐a*gYd2X(R'@^*8r[jvp0G#}\Zn@BDx_o-YP1Qpߜwn7Xb1n3Q*.O:kB4"@:ha;8; 1aC:/~-p\a<\v /ǏJ[A9|B }># ˁ!ʋ8~0;R 9irP|͞7[Ў8-cH5L(ٽCTIiP>xCA @ <2t'O+?l8W>08RǒiؙԽϝNsЏG+iֺ`6ФX 5Yڇ:"o@,}!p"}&p@́G^̻B 'NI"|N yMܚsfs2Xvjsmkb D R&xLRމ{*mkar7hgofBR :roֵ#{_wWQ̺#;E nN=81rۿ;2vkƹ\E1q"=-4Z0=2l7$nv}2VF"R#A "Чnp˟nN˾8=ŕgA)`=>OmsCq&8N|cGa]+/lgAhFME{}M4HSt$ ోv}k^G A @ '>h mPrO?T# 4|H`u&>raحo"\h8MC{!]}^>sﰟzs;)V.9IM#4F++ҫߞ?\&OUpKJʋK)[yj24ǽC4x GF,ȷէ@&D}iJ ӘםhPWXsE >g-r{_-dt5PE\>fP{jzN3n HΠ }_w/9ԷoãsEh(/h% @ > l.8Ś5mzfFLhMLz"̶M[v4:&tj>XÈӀ޴`Þ3.QSQRdnO: 'wC` $TbG.YndGiR fʁ>ôm $pwrj-zm3\)LO\|^ca0u2CE53' YxURyGa.NਉC8?YRzsglu%\Dr'>HyB^;-97 lPS'@dԤ8z*yPO_ Y KQ_~e(ou@",pp;:"=YR\YXPAxSN꥕UtN/ty]C2ǂw;:/޷=Կ="էE#}>UYŝ=6tqA'T`m8͑3@RC|iG΢&;)HO/;clW4+GƮ @XzK(G=R=FiX|fc7_'рRMξ_8}Ĝۜ\H~RpjIvFV*Z{>=1*tsdG!\dƭ]l9ŸT& IǼ#pUiI'Б;W)I[ PC{.w&[>NACx+`X 2x⬩M/]t5$˰VPɓ:,OO(CF vJ>jJEւQX*[.ؒGPhYn P@JSv?) `2c"/ jIitXѕ& IEʰP4kۂIvȓSBQ0?Ӕ? / b奩vIEO97RE't)MY$ }gQSXvjo3L1oVJp;}QbsC 'z' J޵k δ V@9T8tsf"2L{=5qq}#48&)'ЁEtaqGT.sYDBCgyVP\CSU, )9!ǩ7H\$%/zұ 9uĖ 5dJ(8ao~mXK & u-Wߠߐ77է'r\!nkȠ[#L)gX 1ڕ9qMxۋD") T,}1^Sڥ!W_'Ruh~_yM! T-F͑4BT?ͩЛfaLח$,fNqN59!zx 銅{{9>] e֐e4lMӟ]I|Ooѐ|W.\X0p`7BlXeaCdI'cI֕P`ݵkLj\{Gx)Ol1sx?8;z8mB8YH2.yx,Oՙu&?mZ4|&,wQ'Aik,*4ýS*(|'_E*k sD | =QC6r/@pW{UT2icD.¶%pg{S埫 3.SJ&,w[Mzsaat_?$ /1Qyk9 #! %i3DG//~ $ZZ蝂 KwD xɹB90g-IϡCѷmhAuF 0\0!`w; "`B8uU~֭&'FUop#%]ÿma X"`G[m}UAs<`/gNj٪kg:Ó{,<,5sZb;);,\|+7ǺdLO83(x ļ^bZHg@Ѿa/)jx0!i^7?Ac JKc DtqR֒%A9lhnNlS1]7 )\>} &%iӪ]O~Wlk:U9n P@per!U+`|aNDx sGRL5QS &@4ypkk6/hNjf YA^#R^f!%>Q{3)p 7iZI G-OPt&npzo)JYW++d6i[^$Ĕ:F+UmهaXܤ9aMmW6n!~#h-Юݤv OKͣe0t(̀D}[`TD FKᑧt^NS覻(#ȻյhKnme`EaX άhooK A WR#)ƫ!26,j;a%ФtIpZnH 8Qz{]M4 z*}/ WiPXEβΗ^mquPKrv0S:A3JWLо"`鳀>[;GҋBO9;]Ce+gpsTgXM xt˦UI џe]aPM}MgKO2!a0t9|"+>럼)Rǎ@zĂY Cgsh!99YY9Y_ܖc yDp#Dwg q C>#///s{Bho9pgҋ8p~YCV+3y6ziiqqq/ K.^72&phgj奥 ʍsyP󏍙Iз6j}K`F}8GDN#9~ڼ9'cuRu1pwNp2|/Xk-FÞtّ݃Ol'\A1q[3Xkz#1T0-iDJkS\1m<@_oSCv©ԍkS78w(}{= &, c^%Z Yi+MjL0*}ĜKA/A10%?#kIhCVNdؽ+UƠPqůؐ|85qGUO#XA$8yh2FFD%&: Rc&¨جRyWQ >.=lKs<a ?Fzr۰v;A䆵 >_H۳Xt;>xNE/15fp/Tl\q?CR.]Çx_JjPj2? yX$X'o)e Н=h^ZK[ZЇ8%a§/Q3-TX7 ?'pb.Ŧ4&G Ξ_%t\$՚׏81~Ka/ѐ&WٜhjNzaO/6Ʌb? F`P9]H{HeA-Op1n@D^=I\Q};p!5zݗ.*>{6ZYUK`00{B]}ɋg]~s>RR zP߸vT~[4lD?ϠB=鯳*ayf%2 N@JIo5j ["ΤC X%Z7KӺ/JC8(/Ceӛ QwfGUmE&\ Aw5L\g됉-0:Г_`MjQPlՂAlmo?b,a* _ylU8_h}/ DCկo+R>z1l#5ļT5_0BD m:X{T~>9zO @'=^al8h_3RA /=[D.|>`w4}ËZGRcٿxR鍪Ism͑ ϔ M]:f QBJVלzT{4>)>?͇z'RM!TBohb,Ay)C6dy/U*߃ġJR>j2o{>bpBw-3D-[m肬a)*ܗVmN@pmEA2iu݃g#vF?duj+&0&<X6g>T#B#D _O l;%G3r H>uOv]i3F/^oĶh\odWe8gw݈ **>SO]=ō: FJ83b>Y2))T" 7j'Ld߭E*f#. 5`Z #?~٧ K^MԄ&[2Tf,[ĕZ'Ou@R'A @ >~=R&G~?\(G̮+6#mĹr `ӖVeUuA @0"`{V|T3I] C^L)5Ihiϸ#Ca\TSC3U8cIt&C&s'r ͆zli,@͵w)Ǜw$K،bؓ_t+pm-Z}Ox ;EwBf4Zל-jj48]?{[~U%e1>wș Ń[64qQ%<\W#6+> [:lGF?>X}4q' 7]ȫ I_qz[!i*=tСV;S=ڵyu_yql[B;[>NACtdM׏#L=;Az* {^n{ @)KTܼ0ha->vSC#m0a=)l{ lӿA FUV+Zje<@Q( rN((ZZwMd:A%?aǒ_bknB;2Ъ[ZdŶe3t2Ӄ#&hnmCBb ^eK[y_ΊHfta%Yl}fRHC/ |GAҳ75(YLZ0fi]ܢA钤B.EZ{m?Vp@@ dV|=!P؛%Q `xeąJE=DRPzJ!حcQQB]-||n sS{ ~S8W /وӭ!dmF8jG=Hk$|AmI]YVX3>"Tؤwi(+c4+TX;p-/>L9y7銇ueq׽MXÃH!mӱgz(+qf'}mbE&G ~)?1 up$NcŸ});Jx;8O-gչ|hLX`\x#56ISu5V-Zu8XEڰ `y$ \=N04hZkS H#ըbNNVVNWHޖQ@(b4Cّ8@!l)8Dƹ<􏍡` h%x` B<Fyz'7$0Xj/v1#6nް ė%La)A @#Hy{I^cM[g5S99i]ɒX Zs!N Ȕ|6[ ɬ˂uYtuhsYό0 !Q]W@eA\lu}6ޟ]2>H(00$K,60R6ބb́;\+xП9C,ENqR~iS2 wd A @ 0x I @0 pgmjLUUdhm[ln]v&! eNJZەJC:*ʇm zf} vƣ R6`mMp`8=|Y%OC]pŚwȑ\AӂcH_5rl ҨmXJgD~GpݝW ycA F'Z v%7un<w"8@dS3 ;w(i #pD{rLG~פu$QI喙yģȥ .rT FC 11bCiTZuJUB(8m*,wF.b6NkΡ׈Um~"^CӮTYYFh"$Hȏ'j11ARh3z6PikܭyffB)-ՄrV4 [hjVH;fz:9v nd}A+cņ{؎ O7#<I V32ش;Rq?H "\|tؕ~A ~Gy<3ixGc>/ey;'<~4f_}1ruݑ4r\c2h[{#N];ygs.9zуkQl6u \_5얦|X,AH <NŸO'̌ΏVҹyGð6kE߂$ JtlOW)/ވrИѪbf{m=\r>L,S烓;טzZgY@k² ےVWVY}LX_1.PmMR*pSGƄįY8Manqcj0}@?~a |#`QfݰB/|Xs=7;{ϟ2>ٙi>kΚYcX>N~$5ME\%$M7CaaK;&ݶ֔FGt3 @H'0~ܘ>Gg|9]HX^^[$O[<շKZ4Zi~JJ<3ͶVVRvX3wyMH,SjLZhb[;5%%ni6:^"ۆUz bCWk:KU]~C)t kD[r~G杝GmQ #$cKs`@Ix -ِ MV*p onĆ 탷n'| `.+.i8Nv̍&GBJE,ݛNCh!sO})G'^Rk %ZX+XJԢE֯m C@H $`$$'ȈHьV;E7?Y:8^~]ػEbNT88 !cKub:kwB.Yƴsrqbu}ܢQzoX6P\2z\4J1'^`]:(yb5K"{^E1;\TTCKzmR/w$BBIs>}!MҙoH $@@"(He?7MBSbX/"t}-x("^L dhTmz潠BQ KMt0 8dm}e҈ C&z_S,VkTM\G^#̘T-<^Bsh)2mQ^8t2ۍY1_NUr\7~dYuC)" 17~<Յi7nfF akҀ SD IDATy [JN[aCosoCE&1!$@H <]a}PЌoo>퍗@Y iDgvr6ouIS?{zcUxoN=ϼNܮ!"{G$@!QK[]U}ԗC$rh,?juTȔӡ}D9.k1|Jf/sԳUa&잳a[y\(4{Γf _/ëI@H $!Ѕ: F4xVچi;xM[4gtѻƫ7DSQ[w:zUuLh8.+b҇)>mAtXD5wWE ڐiNt90&Lji6 $PMLjR 7@H $$*LLX{תn\+9ӵrdǰ}z]gY] 7o@:9 SU[";W4p? ]]jV߭Ϲge@NM hVo޾&}J )j[,"֎'*gx^"!$@H =BK.ąmޟ.u."8Ǻa;W2BHŁc&0! kˮ WzbkԆ1?5a C!"hdYEӋ=挠4GЙ+@H $C!`V\nƒR!>_ B\F3i5j`2|ەV =F3*3G_1a" CQ]ܔIZuo65j8*ennLJP(WH\.1cV Kt3f/ $@Hk @H $EuyH $@]C]*tP x =pH $ Vܭ>4 $@H!@C %?$@O8?N $@O(OFH $Nq5ŅZB3vT_g5 yLJ],֢H $']p\Uc;"cӁƬ֯b'9s]My AM\j@mLYQ@e\ۅ.4B/ ۽4Ԇ!nnv*;Eoozi ))*VsV@w PS*kFml=77xnj+{<+mw4B|O@H <:AzkCtaEQﮚ"oģc)Q} ZvOOwg}qOK`#~5u Cu wlmcE{m\[H[quPwnLJ OdJ!¦З/fzmltZZ6ʺ: uLJ.'BfBŦR$@]H "S}.ϹgeTN8uG̑m+oFRu}^h{Ar%QqXn,?gp  LH{P  Zi3Lҍ0ʌKr6~9!IG+˝-_[ww#DZb>n覅"3;(zpO_H N-IwĭYxRUQ9ksDZGZz'0lIOIaqfN'_~&5z,KCMO7_E~_n @H@bHakk-v e5eزF.V~2ϫC*pԶ~1Ii7깝Bl͈)~~A &m8SDD.͐<[[`LH $ 3Z!8gjVFzvk0 a5''-U{37u"?p7VfGP-azHJZR/܅$&. ]--9ws{L{|>h{LH $:BN\7~7RT66tR %3FV^Fm 0an;AH H[MH !XJd-?T+zTrvv˕صF@H @Kak$gΰɆ"{tev0Td $}g:Cg599Q5zR(m7@H1":10q*H>\v $NEh%٧ROfzu je%7JmoG],O&$n6[ڪE-:Z_}eؕ&O-@H G@kö<ž7JCK>?Cc0C,QM9qen^T4tIަMl/F- /jӞ >N^ GӔ(Na-9MZÑ@H $XOMhղ29$Z͕HE|;S N-mΥV5E 7ȪU"Io"+*urK. + ~L<)1Rрi.{p2cidҤķuٕBs_1|LVZ8м9Q_ZQAY3ҺS $@N!>)9t6R8^$D8t-_sITT[vtQGuƎUShC|/;k/9FmeGQB@bWU4{^εCnJ>4J8qݿCHm_OWg˯ Ly<<=jXtګ9񿔏oBEG+:, +xòP՞ijJi^uVeXx!>u5 8 [44hc_v(Bu3PWw%dV9;4?7;<"$@H[T|DYvTi纭>0Zxa=˸ IQ KMtTPnP$"U ϤeF5 y۵~saȺV*jYKUsgu\ Q^CAy AM\j@mLYQiiTT$r{a3q_h*Jj ,]4jˈbMIAu|zHzkҀTؾo#LH $x tJܨ큳ŢbR%yQOgl{c}?\:`/?5Hq7ωl s. |q!@hF/Z4p C7CFl秝'߸-n[f;Z=éþ_!CFY 5W~_ߨGyE ')n/DZUS 2tjl+r鿧3r%R\2}'c!nDv>0W:č}iwI d1!$@HI'9LYxLZ5b]iDɺ\;xM[4gh "hTcK:kcYC{m)]yUq@PgZoX& %ĭcM\0@ͺOغ-;uݐמR6'nZpMN_0W_^E;fIcE@#/YV WhNp*uY~?/.3P#+/V(5u~?3J+`c}"O_x|t/mDՓ]04.- t}EH `V\ήIpQ+jjb |~Dm .ZS$'F^m}ov;^bR~kп {2MO͕fmΥ2CMkF%|Qk/U%VZt:0VYӁhtZZ4G]277B^!&rnjIZVäǏϘ1O;^74D7N?KOREΞnӾmfmzܖ̴϶rqFOFKnWŁ2?Ѧ:WyTlL Oj&|TBx6St/>ua{?9#CCqΞ3>XSۖ1`,s7ܦ4ԍڙx8ԢwR4 $n5岻jca-i.5*"5Y :/T;Hh_Xq'y3V;3[:sY[BDŽNQq3򚚆Q|wNOtAt[P_t?T˽]ee]'P)JB|[u=Ss7b۞2S_;'$-:n-Ž|}`@H}r~z|KI>6t۾OwP@pB[yEp`=4{B`{[Ɗ`;}!wy*: 9u0 8[yvt:iۗ<( YYAR4 tbݼ);'F'Bp-U.#v4 ysi;@Hso96 <"TE %>U05r?"ZE.OK 5k;Io>Oz\zzZa5h/Aik8ߺfȈ$z\]Wo}t@tF65re<́PWWu54ogmh<`?J2A‘Br#vCaI֫Cѳ9o)z`ʚ-9ё礅:Xiut#+! ̋hmѾ$-dKiTܺ>ϐxۇGOov?P[xqg-ƀ9kwn ͉ +գ&bhRƤ BwfIII[>dE&oǂ-iS܎M% $Qo\DGI`}$SRoX=A %&=<7D 7'.>UoM>xy'o<߅{t76 U%Eyig ꝴ2CLo3Mg&-v;d-,XJ<<\acT3-d^- ˙W*뫙[LPApa^=G#Kj?%^޿yujZ-ji]=#)9HEpe9;yljst W@GCut=Vc4FyKip2?)(2o jMȨnH%iFs1i2ߊgo0%7{vxnoWqEV glC%&s$ܹsm\lzKG{>#;5q'tJ]Ǭ0rƽ8,ʹO31R4TdFMJ s>XgVFgArS=l ,cHPf:ig+8O4D]gZ1|>o(7mWU;_ !i[B"^|?I 5 ZmnMQnVFFѷհ $mH 0GоԌܬӻ>60~ho#1|d{ m}ɩMoԣ4r\c2h%dHW/۵obm(9)&zQFTb7&8~zQyy1txc#1}+2e}> %K>ztק{%bUM4;~_Ǟ0xklN)~L[@=4EjsFeȑ^-.פ>n"$L`#LR$\3ozFZ7g|lsOع۪a"B@Ader!:g%3|ל5ưl;eHߥ/Oڵ-KH2j#GȔ)!kfyYeV Nbsx5 $gGƭNE<3con#SS C}f)3?NDR=-}?&zV:L:?%4Bu D-pwVܻmFw^ܖYi[#e/v/XAgeut⃭yIVLJxG_fyFU[u}}5KSGl@FZfX8HoЄlH[OWХVkOڈt= I?#$0+.W6-mmu٭*%k٭S.ogA٭ ~̌+G?~|ƌF^^TV }rtkvFƊn*+/CL;&acQeJ  rhweS46#7u:erER:Y6餼HllΆJ,ۮt߱@rq%ր@ovKefJV.pV>>`>C"lHٛJFcgѬ3v젓MH  $".vh7_sVd$nZ% s "X[#݆{AԲ94h5w(JAaVYѢm>ӁJ_Xy{԰Ws))ů6Ήc3Ŵ7NVk>ܔ}hisCp 2t% pZؐ .E'zoX68zO]rd)iٟz gmO"tb{ńD:PƷMC}ؕڒr/s@K#߱컔v r%9ߣbw=ty|Cݕ[Pm"h[NBH $E\犜G(]/̿שFo’؏VJ*/>Vһ+Js]n+,Q%ll7qI2eEF-O"no>ZQfQ!Udwҍ7e0K؞"tE”=g w0`/?=l$Z5wi|PN5JŅq?R>”KU Džm棥+Ew`۷~ &$ h"@H JKtBn[ .7~е*Khp"F/Z4p l}2ҷf;?vxaȐItRZj 6 n>G@H $*9( PO Z߻wOäߦde1'#~ɵoiIe@8$@H  7o/TV"t\¯Br|etOGcC@H t? JrF:uNr5\?n#j1d˱=XuXW&zq}%׀?)tI[Σcp5v*oVk#J$/|E3`vSH]BxF"GtM̮}6$4P9 u- 8iʲv: b $#Af`ƌ&a%5Pɬ` ;u)9pB"ۓpܿOӉW<=د,*|/Rª` ,&zfPaJY¡N"YΘ.R4s7M7ALϚ+ml>WVaj8XqcEo㓢OC"r evQĖm@H <)XadU1T3+.gVa0`FQU#-*h ~rpU]*e]FPP_@xn$Du4jY] fekkrIAS{'*+@,ϊ{5D$etp p{tev0@dEyxBE~:[`& XxAaSٛ|ƔxX4?zԴuH460G{_~jS`@H # ~muatq;'U>9JPWvdGO>xw/;R y*?rNwP{W}rΑHvkL8'eeʺ맢x|F_o<9KwQUGJ~Ɗఽv]qQt@ ϓ IDATH "`: KC!%P*c |kՆMOamٚ > 7 s;d>O͹MH޺UwtR߱#9CBBK?b O<;=u@H .#!s@%$ Q@a$7rB\DAAHj"N{5p@?ʇ7`xV\Kq}=e t&u 1-u׼ v0/O 7~MJgBTk\׿4m7k c_ <@H Nj\`PuX 갺;OE8"!j"@| PΏҭ{Ċ`#<$~ bp-fW|5I_M 6{roI9y{v΍L&?ݷQܛ^MM7-x4ȧϑZhԙ;tMmw,BH $Мۢ" Q* `Ajp/>457K@c7%N@á~QSSyP+%N?.Hػf`<4o7(g6삃]mM$@U~m'.`-\` C %Pn(d+Cr63_}z+ #{!pۚ=+{9@4'`2CZpbٿvG%s9WX7SZp4gXGH $h߮yw{rT qGHLz.d^p! Dž7ýC՜5yc}^<2Vl˗~_󪢗׭ݜ-zZr9fN&=bvόKi\$7zPf=@S8fZтb}/vEzmS+/_>~IE3|_xޕj0(XH ! 7oS! i 4p6+7vM&s Dpz (..N;y#oOMp)q=8uZtzzPÌv _I=} C}L3c}/v^=\Μk?\V]#M_=eY_R{.# ._XE ng&c4ОuPL~o'.L[oYa &±:ظ '@jj_v+x_{\1zF@$<^1C{z_ÚmR'=8ZOU2u/6flx{-p%uzԊĎ;W|sv W4(u`)MDP:"[ML 3hPMD&" ˎ?`7!VxԩM.l2(Cܤ<##44_hD >S91wުb;sLlVwό=17p5#<|^/8=Zjk4H%V<¯'s@5>WBbe08Y0B!_ЃUڝpO6@`cPw7<8cccY; g܆ P?ݻp .~W\PN!8 "6!Zļ^nz:V[Cut0PX"񅕽>WX s!U<p] pЃ tBzzo%f :]L"h ?<l8%T##^{/ !cXk+ꠘ bm/s{A~Sx<:Z{Lߦy@ 1Cs8{X }zуs:Qݽ^wUufUj 7* Ɨ?%pf=pMg/ Ib-d A 8XSA'@?!C$}{S>zV3g΄se\ 8 =x`aa!h_8`/`pBeBkbӼ_,4!"'Z1 q XKÅaB"= ,h5<>g|x>yuD Tuժb.\4Spa (bJadYF+\7a.C d f t#:yD;3n~o3|Ãx!L`U2d\\\-[/@jZ[[+Y&𬠇Xh˂kmŵ9h~ erC=x@#<|^/zpMrB%#1%όKhW  T3LwUCE. %l|0+u pM/Ƭ `uQf+w<…8ٟ9 "FBH kpV>|B!$h5 6żb_$ =-Oy~P] W72_/2A̹vRg0wQ7:-G>x<!@tX:cN)!nS+7V `*0mqC2t0D?8>]VO % :t0$m t?Dp!0Ćq!p&@ƐP6p&m!]d % fWaα3'BMm4 rT?1^ac@xUp|x>Cn^pJa0՜DPn^GDQka5d :x!w 0UJ& ::6䥦Zا?鎎5N=LOɮ0kSc-9qoad%_yn}.SgT % h{ l"'gO+[Z\|C~Ro5 fY`/p@H G\B3z#ٜ9:Rg0(EA1X~ fBL#^[1z`3'Nh~]%K]9qր'(͝WUDZQBF[Sݾ#;5yR]?⻗Up诺sd<;a!߶^s\E{^nyƑW ` K Q * ] 'qW6-TcWKWǐ~u;8JJJ`U5 - !p/@H nEkpk(Tb1pֳgO׾nb*X5` wpolЉA'/_f@H $@s'a8G$@H $` P;_N QRIY*%R0biPM7,YdL1H !k~iR2ci?~N紝;3>|繗뺾u~ <]SWE %@ P%@ PM@+*nt|J(J(JS# 3F>5>%@ P%@ PMObΏ$+Uk# ).kߧeF6moQ>%@ P%@ P̃\bDv̦K} K'.D- Ee[1=8D*80B/t +7yfrus--ǺUWe\;k1Z/ }uM~!f.q7&sjsSIb5)buaX+x@ n~5hVWAbw% T.S@9׸Zuuk]x:yJnUg^ێq;c;C5Չ~&mY>XMJIQ%@ P@%Ka^i6f* üVo/Ew'OL%e_ɎW$i͉90T,)_,aRIIU`qX|얜g1 z ?e5oMX똲ɤv;L7?uLF-yaMV}GC̲đ]LY"n2 Sp;xN3.fQE(J(Jj=eΝ{|x-`Vob#v/_Tar2T4a?߃ͅЮH*q2.Rae L$L70mu;fa*J n!nm3;3b2| 쳉uGdĘ6|"Q!ZykU7Ǵ2|_G 73jxX£g6ι8Ul - DXUY}2Xo{: !OF˟dx'o?uϧzWdٷ{'́"ݰZ_ch&Ր}$96ZbgM+ Hga)~|[mmϹ$Z-K\@m] D<W}܀2JHB'b-B]zi^2 Jub4o2 uQ1=CQĭAo.3@4r('%@ P%@ 4mƄSg1g`-mʵFl4=âىWi%0WƓOdn{ҷ{ N9J`;os΃|#rC1=MmVTH4<|gI2$"U:"\L?H,A%xl{i-Lr3K*-PUjacϝЯY 3d8s2ˠ9]98턬6\Tc8Kd62rg5+Uh29~ 2uLdX g%1* aa^D)5/ρV: {Pi/ e6(J9ˋ_X۾F|5Q""n `MNnUJ(Jhp>/=|ִul?4L -RsL[XNlȋUkЮ|q=(7e@*8X!)1'ǦF+Vr7_aAmv +pǙC,0\DjhSsv]wuؗ/ZEe_LY)t Ild$FΝ!e9Y*re4(Ͻ (\QVcm'y0`Mg'Sq ̓؍[xiF֯a+pJY&S۰I-aJ!>J^inKڎ2+5)22nwٮÐ>ROqxW :oN>a͕='g&u̇6.wẳfyaO_&yӡ-R7rNd5 MS(0Lcg}%$%jGLYHR+Dv\3ĭJ5`I P%@ PM}YÄ^'o9z,4 e)b\@N X0xo&;j,3cr]WFw% VÎo&}!YAHJszE~pB*cۺ8g ݳSzZ=nI`g6y弤ųJp(7oYs=<ܭ@uu2ZTt{&R9ٔeEA&0ƺ9Ql_o爈ѩү]71 AyScTK[9 0spT4Xx龻g!L\y_䃕֭&|Z6V-BXa˴T?ꐃ`t\< @R$yf+gmZBz^+UƊc;Yk=p::O7/pq[O(D6 #opUMTr7+ i3pZp6Un^µWSU7B uZQ ȍ&G_QF= Mn:{xBfM]5L#@U@ T>U1SPSWX蘩މ)J(J7񇛪ToJ(J(JQt7|߾Q}i'J(J(J MX}:%@ P%@ P%[#:.%@ P%@ P@%;V7RIENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/searchfield_leftcap.png000066400000000000000000000015271232362746500255470ustar00rootroot00000000000000PNG  IHDR-gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATxڌT]KbQz4?$m*m`-C`!00/0p@AM `eE%fnٌ=}:kV:5UlR|uu5JfYґ?2v\nN2 f24P(~X,###&&` X;^b%C]]]Ap]wpeeE=::ޅB!%fI>-//.qHzzz^ϯ"\o>Rbq @B 4Dio`>G}ȗIENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/searchfield_repeat.png000066400000000000000000000002361232362746500254050ustar00rootroot00000000000000PNG  IHDR5^KMgAMAOX2tEXtSoftwareAdobe ImageReadyqe<0IDATxb,//g```<~8#?bbZP,Xnݺ <~EIENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/searchfield_rightcap.png000066400000000000000000000010221232362746500257200ustar00rootroot00000000000000PNG  IHDR gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATx|j@'BWpP8dYw ы#Ѓ0$ r?ND"$U \4U +OAvq~ jvk@E4rt(PaJ6fyF_)3$עw@dbu:FqBi!`*2fPl-6K]k =֯]V'0W5/$IENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/title_background.png000066400000000000000000000002041232362746500251070ustar00rootroot00000000000000PNG  IHDRv6 pHYs B46IDATc8{?"CYQYb_^bX1mcK|;IENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/triangle_closed.png000066400000000000000000000002651232362746500247340ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<WIDATxb`+`JJJ _+bBb;@̏K $z|@H!ÂErݻ#6E (X~sIENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/triangle_left.png000066400000000000000000000003031232362746500244060ustar00rootroot00000000000000PNG  IHDR  pHYs  uIDATc`+`f @Y1?L`ddL@+rrrJ +`{di,E1ß?WMY{pv(IENDB`pyres-1.5/docs/source/_theme/ADCTheme/static/triangle_open.png000066400000000000000000000002771232362746500244270ustar00rootroot00000000000000PNG  IHDR tEXtSoftwareAdobe ImageReadyqe<aIDATxb`+`${D@ @쀦f1A| jCN &<)B6ƸO%%% sДȊ N6hvIENDB`pyres-1.5/docs/source/_theme/ADCTheme/theme.conf000066400000000000000000000001151232362746500215440ustar00rootroot00000000000000[theme] inherit = basic stylesheet = adctheme.css pygments_style = friendly pyres-1.5/docs/source/_theme/flask/000077500000000000000000000000001232362746500173245ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/flask/layout.html000066400000000000000000000013561232362746500215340ustar00rootroot00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {% block header %} {{ super() }} {% if pagename == 'index' %} {% endif %} {%- endblock %} pyres-1.5/docs/source/_theme/flask/relations.html000066400000000000000000000011161232362746500222110ustar00rootroot00000000000000

Related Topics

pyres-1.5/docs/source/_theme/flask/static/000077500000000000000000000000001232362746500206135ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/flask/static/flasky.css_t000066400000000000000000000144441232362746500231500ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 0 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } div.related { display: none; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 18px 10px; } div.sphinxsidebarwrapper p.logo { padding: 0 0 20px 0; margin: 0; text-align: center; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; background: #fdfdfd; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted #004B6B; } a.reference:hover { border-bottom: 1px solid #6D4100; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted #004B6B; } a.footnote-reference:hover { border-bottom: 1px solid #6D4100; } a:hover tt { background: #EEE; } pyres-1.5/docs/source/_theme/flask/static/small_flask.css000066400000000000000000000017201232362746500236150ustar00rootroot00000000000000/* * small_flask.css_t * ~~~~~~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: white; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: #aaa; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } pyres-1.5/docs/source/_theme/flask/theme.conf000066400000000000000000000002441232362746500212750ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px touch_icon = pyres-1.5/docs/source/_theme/nature/000077500000000000000000000000001232362746500175225ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/nature/layout.html000077500000000000000000000171401232362746500217330ustar00rootroot00000000000000{%- block doctype -%} {%- endblock %} {%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} {%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} {%- macro relbar() %} {%- endmacro %} {%- macro sidebar() %} {%- if not embedded %}{% if not theme_nosidebar|tobool %}
{%- block sidebarlogo %} {%- if logo %} {%- endif %} {%- endblock %} {%- block sidebartoc %} {%- if display_toc %}

{{ _('Table Of Contents') }}

{{ toc }} {%- endif %} {%- endblock %} {%- block sidebarrel %} {%- if prev %}

{{ _('Previous topic') }}

{{ prev.title }}

{%- endif %} {%- if next %}

{{ _('Next topic') }}

{{ next.title }}

{%- endif %} {%- endblock %} {%- block sidebarsourcelink %} {%- if show_source and has_source and sourcename %}

{{ _('This Page') }}

{%- endif %} {%- endblock %} {%- if customsidebar %} {% include customsidebar %} {%- endif %} {%- block sidebarsearch %} {%- if pagename != "search" %} {%- endif %} {%- endblock %}
{%- endif %}{% endif %} {%- endmacro %} {{ metatags }} {%- if not embedded %} {%- set titlesuffix = " — "|safe + docstitle|e %} {%- else %} {%- set titlesuffix = "" %} {%- endif %} {{ title|striptags }}{{ titlesuffix }} {%- if not embedded %} {%- for scriptfile in script_files %} {%- endfor %} {%- if use_opensearch %} {%- endif %} {%- if favicon %} {%- endif %} {%- endif %} {%- block linktags %} {%- if hasdoc('about') %} {%- endif %} {%- if hasdoc('genindex') %} {%- endif %} {%- if hasdoc('search') %} {%- endif %} {%- if hasdoc('copyright') %} {%- endif %} {%- if parents %} {%- endif %} {%- if next %} {%- endif %} {%- if prev %} {%- endif %} {%- endblock %} {%- block extrahead %} {% endblock %} {%- block header %}{% endblock %} {%- block relbar1 %}{{ relbar() }}{% endblock %} {%- block sidebar1 %} {# possible location for sidebar #} {% endblock %}
{%- block document %}
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
{%- endif %}{% endif %}
{% block body %} {% endblock %}
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
{%- endif %}{% endif %}
{%- endblock %} {%- block sidebar2 %}{{ sidebar() }}{% endblock %}
{%- block relbar2 %}{{ relbar() }}{% endblock %} {%- block footer %} {%- endblock %} pyres-1.5/docs/source/_theme/nature/static/000077500000000000000000000000001232362746500210115ustar00rootroot00000000000000pyres-1.5/docs/source/_theme/nature/static/nature.css_t000066400000000000000000000073171232362746500233540ustar00rootroot00000000000000/** * Sphinx stylesheet -- default theme * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: Arial, sans-serif; font-size: 100%; background-color: #111; color: #555; margin: 0; padding: 0; } hr{ border: 1px solid #B1B4B6; } div.document { background-color: #eee; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; font-size: 0.8em; } div.footer { color: #555; width: 100%; padding: 13px 0; text-align: center; font-size: 75%; } div.footer a { color: #444; text-decoration: underline; } div.related { background-color: #6BA81E; line-height: 32px; color: #fff; text-shadow: 0px 1px 0 #444; font-size: 0.80em; } div.related a { color: #E2F3CC; } div.sphinxsidebar { font-size: 0.75em; line-height: 1.5em; } div.sphinxsidebarwrapper{ padding: 20px 0; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: Arial, sans-serif; color: #222; font-size: 1.2em; font-weight: normal; margin: 0; padding: 5px 10px; background-color: #ddd; text-shadow: 1px 1px 0 white } div.sphinxsidebar h4{ font-size: 1.1em; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p { color: #888; padding: 5px 20px; } div.sphinxsidebar p.topless { } div.sphinxsidebar ul { margin: 10px 20px; padding: 0; color: #000; } div.sphinxsidebar a { color: #444; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: sans-serif; font-size: 1em; } div.sphinxsidebar input[type=text]{ margin-left: 20px; } /* -- body styles ----------------------------------------------------------- */ a { color: #005B81; text-decoration: none; } a:hover { color: #E32E00; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: Arial, sans-serif; background-color: #BED4EB; font-weight: normal; color: #212224; margin: 30px 0px 10px 0px; padding: 5px 0 5px 10px; text-shadow: 0px 1px 0 white } div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } div.body h2 { font-size: 150%; background-color: #C8D5E3; } div.body h3 { font-size: 120%; background-color: #D8DEE3; } div.body h4 { font-size: 110%; background-color: #D8DEE3; } div.body h5 { font-size: 100%; background-color: #D8DEE3; } div.body h6 { font-size: 100%; background-color: #D8DEE3; } a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; } a.headerlink:hover { background-color: #c60f0f; color: white; } div.body p, div.body dd, div.body li { text-align: justify; line-height: 1.5em; } div.admonition p.admonition-title + p { display: inline; } div.highlight{ background-color: white; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre { padding: 10px; background-color: White; color: #222; line-height: 1.2em; border: 1px solid #C6C9CB; font-size: 1.2em; margin: 1.5em 0 1.5em 0; -webkit-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 1px 1px 1px #d8d8d8; } tt { background-color: #ecf0f3; color: #222; padding: 1px 2px; font-size: 1.2em; font-family: monospace; }pyres-1.5/docs/source/_theme/nature/static/pygments.css000066400000000000000000000052351232362746500233760ustar00rootroot00000000000000.c { color: #999988; font-style: italic } /* Comment */ .k { font-weight: bold } /* Keyword */ .o { font-weight: bold } /* Operator */ .cm { color: #999988; font-style: italic } /* Comment.Multiline */ .cp { color: #999999; font-weight: bold } /* Comment.preproc */ .c1 { color: #999988; font-style: italic } /* Comment.Single */ .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .ge { font-style: italic } /* Generic.Emph */ .gr { color: #aa0000 } /* Generic.Error */ .gh { color: #999999 } /* Generic.Heading */ .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .go { color: #111 } /* Generic.Output */ .gp { color: #555555 } /* Generic.Prompt */ .gs { font-weight: bold } /* Generic.Strong */ .gu { color: #aaaaaa } /* Generic.Subheading */ .gt { color: #aa0000 } /* Generic.Traceback */ .kc { font-weight: bold } /* Keyword.Constant */ .kd { font-weight: bold } /* Keyword.Declaration */ .kp { font-weight: bold } /* Keyword.Pseudo */ .kr { font-weight: bold } /* Keyword.Reserved */ .kt { color: #445588; font-weight: bold } /* Keyword.Type */ .m { color: #009999 } /* Literal.Number */ .s { color: #bb8844 } /* Literal.String */ .na { color: #008080 } /* Name.Attribute */ .nb { color: #999999 } /* Name.Builtin */ .nc { color: #445588; font-weight: bold } /* Name.Class */ .no { color: #ff99ff } /* Name.Constant */ .ni { color: #800080 } /* Name.Entity */ .ne { color: #990000; font-weight: bold } /* Name.Exception */ .nf { color: #990000; font-weight: bold } /* Name.Function */ .nn { color: #555555 } /* Name.Namespace */ .nt { color: #000080 } /* Name.Tag */ .nv { color: purple } /* Name.Variable */ .ow { font-weight: bold } /* Operator.Word */ .mf { color: #009999 } /* Literal.Number.Float */ .mh { color: #009999 } /* Literal.Number.Hex */ .mi { color: #009999 } /* Literal.Number.Integer */ .mo { color: #009999 } /* Literal.Number.Oct */ .sb { color: #bb8844 } /* Literal.String.Backtick */ .sc { color: #bb8844 } /* Literal.String.Char */ .sd { color: #bb8844 } /* Literal.String.Doc */ .s2 { color: #bb8844 } /* Literal.String.Double */ .se { color: #bb8844 } /* Literal.String.Escape */ .sh { color: #bb8844 } /* Literal.String.Heredoc */ .si { color: #bb8844 } /* Literal.String.Interpol */ .sx { color: #bb8844 } /* Literal.String.Other */ .sr { color: #808000 } /* Literal.String.Regex */ .s1 { color: #bb8844 } /* Literal.String.Single */ .ss { color: #bb8844 } /* Literal.String.Symbol */ .bp { color: #999999 } /* Name.Builtin.Pseudo */ .vc { color: #ff99ff } /* Name.Variable.Class */ .vg { color: #ff99ff } /* Name.Variable.Global */ .vi { color: #ff99ff } /* Name.Variable.Instance */ .il { color: #009999 } /* Literal.Number.Integer.Long */pyres-1.5/docs/source/_theme/nature/theme.conf000066400000000000000000000001221232362746500214660ustar00rootroot00000000000000[theme] inherit = basic stylesheet = nature.css pygments_style = tango [options] pyres-1.5/docs/source/class.rst000066400000000000000000000006411232362746500166230ustar00rootroot00000000000000.. module:: pyres ResQ Classes ========================================== .. autoclass:: pyres.ResQ :members: Job Classes ================= .. autoclass:: pyres.job.Job :members: Worker Classes ================= .. autoclass:: pyres.worker.Worker :members: Failure Classes ================= .. autoclass:: pyres.failure.base.BaseBackend :members: .. autoclass:: pyres.failure.RedisBackend :members: pyres-1.5/docs/source/conf.py000066400000000000000000000144461232362746500162730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # PyRes documentation build configuration file, created by # sphinx-quickstart on Wed Jan 6 16:25:18 2010. # # 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 # 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.append(os.path.abspath(os.path.dirname(__file__+'/../../../'))) # -- General configuration ----------------------------------------------------- # 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.todo', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'pyres' copyright = u'2012, Matt George' # 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 = '1.3' # The full version, including alpha/beta/rc tags. release = '1.3' # 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 documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # 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 # 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. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. sys.path.append(os.path.abspath('_theme')) html_theme_path = ['_theme'] html_theme = 'flask' # 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_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # 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 = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = 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, 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 = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'pyresdoc' # -- 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', 'pyres.tex', u'pyres Documentation', u'Matt George', '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 # 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_use_modindex = True pyres-1.5/docs/source/example.rst000066400000000000000000000031551232362746500171540ustar00rootroot00000000000000Example ========= Let's take a real wold example of a blog where comments need to be checked for spam. When the comment is saved in the database, we create a job in the queue with that comment data. Let's take a django model in this case. .. code-block:: python :linenos: class Comment(models.Model): name = Model.CharField() email = Model.EmailField() body = Model.TextField() spam = Model.BooleanField() queue = "Spam" @staticmethod def perform(comment_id): comment = Comment.objects.get(pk=comment_id) params = {"comment_author_email": comment.user.email, "comment_content": comment.body, "comment_author_name": comment.user.name, "request_ip": comment.author_ip} x = urllib.urlopen("http://apikey.rest.akismet.com/1.1/comment-check", params) if x == "true": comment.spam = True else: comment.spam = False comment.save() You can convert your existing class to be compatible with pyres. All you need to do is add a :attr:`queue` attribute and define a :meth:`perform` method on the class. To insert a job into the queue you need to do something like this:: >>> from pyres import ResQ >>> r = ResQ() >>> r.enqueue(Comment, 23) # Passing the comment id 23 This puts a job into the queue **Spam**. Now we need to fire off our workers. In the **scripts** folder there is an executable:: $ ./pyres_worker Spam Just pass a comma separated list of queues the worker should poll. pyres-1.5/docs/source/failures.rst000066400000000000000000000010041232362746500173220ustar00rootroot00000000000000Failures =============== Pyres provides a ``BaseBackend`` for handling failed jobs. You can subclass this backend to store failed jobs in any system you like. Currently, the only provided backend is a ``RedisBackend`` which will store your failed jobs into a special *failed* queue for later processing or reenqueueing. Here's a simple example:: >>> from pyres import failure >>> from pyres.job import Job >>> from pyres import ResQ >>> r = ResQ() >>> job = Job.reserve('basic', r) >>> job.fail('problem') pyres-1.5/docs/source/horde.rst000066400000000000000000000013411232362746500166150ustar00rootroot00000000000000Prefork Manager =============== Sometimes the fork for every job method of processing can be a bit too slow and take up too many resources. Pyres provides an alternative to the pyres_worker through the pyres_manager script and the horde module. The pyres_manager script is very similar to the pyres_worker. However, instead of forking a child for every job, the manager takes advantage of the multiprocessing module in python 2.6 (backported to 2.5 as well) and forks off a pool of children at startup time. These children then query the redis queue and perform the necessary work. It is the managers job to manage the pool via signals or a command queue on the redis server. ex:: pyres_manager --pool=5 queue_one,queue_two pyres-1.5/docs/source/index.rst000066400000000000000000000007321232362746500166260ustar00rootroot00000000000000.. pyres documentation master file, created by sphinx-quickstart on Wed Jan 6 15:11:19 2010. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to pyres's documentation! ================================= Contents: .. toctree:: :maxdepth: 2 intro install example class tests failures horde Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pyres-1.5/docs/source/install.rst000066400000000000000000000020751232362746500171670ustar00rootroot00000000000000Installation =============== Pyres is most easily installed using pip and can be found on PyPI as pyres_. Using ``pip install pyres`` will install the required versions of the above packages/modules. Those requirements are currently: :: simplejson==2.0.9 itty==0.6.4 redis==1.34.1 pystache==0.2.0 If you'd rather install from the git repository, that's easy too:: $ git clone git://github.com/binarydud/pyres.git $ cd pyres $ python setup.py build $ python setup.py install Of course, you'll need to install the Redis server as well. Below is a simple example, but please read `Redis's own documentation`_ for more details. :: $ wget http://redis.googlecode.com/files/redis-1.2.2.tar.gz $ tar -xvf redis-1.2.2.tar.gz $ cd redis-1.2.2 $ make $ ./redis-server This will install and start a Redis server with the default config running on port 6379. This default config is good enough for you to run the pyres tests. .. _pyres: http://pypi.python.org/pypi/pyres/ .. _Redis's own documentation: http://code.google.com/p/redis/wiki/index?tm=6 pyres-1.5/docs/source/intentions.rst000066400000000000000000000000151232362746500177030ustar00rootroot00000000000000comming soon pyres-1.5/docs/source/intro.rst000066400000000000000000000012251232362746500166500ustar00rootroot00000000000000Introduction ============ Pyres is a resque_ clone built in python. Resque is used by Github as their message queue. Both use Redis_ as the queue backend and provide a web-based monitoring application. Read_ the blog post from github about how they use resque in production. :synopsis: Put jobs (which can be any kind of class) on a queue and process them while watching the progress via your browser. Read our :doc:`example implementation ` of how pyres can be used to spam check comments. .. _resque: http://github.com/defunkt/resque#readme .. _Read: http://github.com/blog/542-introducing-resque .. _Redis: http://code.google.com/p/redis/ pyres-1.5/docs/source/tests.rst000066400000000000000000000014411232362746500166570ustar00rootroot00000000000000Tests ======= Pyres comes with a test suite which connects to a local Redis server and creates a couple of *queues* and *jobs*. Make sure you have nose_ installed:: $ pip install nose Also make sure your Redis server is running:: $ cd path_to_redis_installation $ ./redis-server [PATH_TO_YOUR_REDIS_CONFIG] If you don't give the ``./redis-server`` command the config path, it will use a default config, which should run the tests just fine. Now, we're ready to run the tests. From the pyres install directory:: $ nosetests ............................................ ---------------------------------------------------------------------- Ran 44 tests in 0.901s OK Add **-v** flag if you want verbose output. .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/pyres-1.5/pyres/000077500000000000000000000000001232362746500136755ustar00rootroot00000000000000pyres-1.5/pyres/__init__.py000066400000000000000000000314621232362746500160140ustar00rootroot00000000000000__version__ = '1.5' from redis import Redis from pyres.compat import string_types import pyres.json_parser as json import os import time, datetime import sys import logging logger = logging.getLogger(__name__) def special_log_file(filename): if filename in ("stderr", "stdout"): return True if filename.startswith("syslog"): return True return False def get_logging_handler(filename, procname, namespace=None): if namespace: message_format = namespace + ': %(message)s' else: message_format = '%(message)s' format = '%(asctime)s %(process)5d %(levelname)-8s ' + message_format if not filename: filename = "stderr" if filename == "stderr": handler = logging.StreamHandler(sys.stderr) elif filename == "stdout": handler = logging.StreamHandler(sys.stdout) elif filename.startswith("syslog"): # "syslog:local0" from logging.handlers import SysLogHandler facility_name = filename[7:] or 'user' facility = SysLogHandler.facility_names[facility_name] if os.path.exists("/dev/log"): syslog_path = "/dev/log" elif os.path.exists("/var/run/syslog"): syslog_path = "/var/run/syslog" else: raise Exception("Unable to figure out the syslog socket path") handler = SysLogHandler(syslog_path, facility) format = procname + "[%(process)d]: " + message_format else: try: from logging.handlers import WatchedFileHandler handler = WatchedFileHandler(filename) except: from logging.handlers import RotatingFileHandler handler = RotatingFileHandler(filename,maxBytes=52428800, backupCount=7) handler.setFormatter(logging.Formatter(format, '%Y-%m-%d %H:%M:%S')) return handler def setup_logging(procname, log_level=logging.INFO, filename=None): if log_level == logging.NOTSET: return main_package = __name__.split('.', 1)[0] if '.' in __name__ else __name__ logger = logging.getLogger(main_package) logger.setLevel(log_level) handler = get_logging_handler(filename, procname) logger.addHandler(handler) def setup_pidfile(path): if not path: return dirname = os.path.dirname(path) if dirname and not os.path.exists(dirname): os.makedirs(dirname) with open(path, 'w') as f: f.write(str(os.getpid())) def my_import(name): """Helper function for walking import calls when searching for classes by string names. """ mod = __import__(name) components = name.split('.') for comp in components[1:]: mod = getattr(mod, comp) return mod def safe_str_to_class(s): """Helper function to map string class names to module classes.""" lst = s.split(".") klass = lst[-1] mod_list = lst[:-1] module = ".".join(mod_list) mod = my_import(module) if hasattr(mod, klass): return getattr(mod, klass) else: raise ImportError('') def str_to_class(s): """Alternate helper function to map string class names to module classes.""" lst = s.split(".") klass = lst[-1] mod_list = lst[:-1] module = ".".join(mod_list) try: mod = __import__(module) if hasattr(mod, klass): return getattr(mod, klass) else: return None except ImportError: return None class ResQ(object): """The ResQ class defines the Redis server object to which we will enqueue jobs into various queues. The ``__init__`` takes these keyword arguments: ``server`` -- IP address and port of the Redis server to which you want to connect, and optional Redis DB number. Default is `localhost:6379`. ``password`` -- The password, if required, of your Redis server. Default is "None". Example usage:: >>> from pyres import * >>> r = ResQ(server="192.168.1.10:6379", password="some_pwd") # Assuming redis is running on default port with no password **r** is a resque object on which we can enqueue tasks.:: >>>> r.enqueue(SomeClass, args) SomeClass can be any python class with a *perform* method and a *queue* attribute on it. """ def __init__(self, server="localhost:6379", password=None): self.password = password self.redis = server self._watched_queues = set() def push(self, queue, item): self.watch_queue(queue) self.redis.rpush("resque:queue:%s" % queue, ResQ.encode(item)) def pop(self, queues, timeout=10): if isinstance(queues, string_types): queues = [queues] ret = self.redis.blpop(["resque:queue:%s" % q for q in queues], timeout=timeout) if ret: key, ret = ret return key[13:].decode(), ResQ.decode(ret) # trim "resque:queue:" else: return None, None def size(self, queue): return int(self.redis.llen("resque:queue:%s" % queue)) def watch_queue(self, queue): if queue in self._watched_queues: return else: if self.redis.sadd('resque:queues',str(queue)): self._watched_queues.add(queue) def peek(self, queue, start=0, count=1): return self.list_range('resque:queue:%s' % queue, start, count) def list_range(self, key, start, count): items = self.redis.lrange(key, start,start+count-1) or [] ret_list = [] for i in items: ret_list.append(ResQ.decode(i)) return ret_list def _get_redis(self): return self._redis def _set_redis(self, server): if isinstance(server, string_types): self.dsn = server address, _, db = server.partition('/') host, port = address.split(':') self._redis = Redis(host=host, port=int(port), db=int(db or 0), password=self.password) self.host = host self.port = int(port) elif isinstance(server, Redis): if hasattr(server, "host"): self.host = server.host self.port = server.port else: connection = server.connection_pool.get_connection('_') self.host = connection.host self.port = connection.port self.dsn = '%s:%s' % (self.host, self.port) self._redis = server else: raise Exception("I don't know what to do with %s" % str(server)) redis = property(_get_redis, _set_redis) def enqueue(self, klass, *args): """Enqueue a job into a specific queue. Make sure the class you are passing has **queue** attribute and a **perform** method on it. """ queue = getattr(klass,'queue', None) if queue: class_name = '%s.%s' % (klass.__module__, klass.__name__) self.enqueue_from_string(class_name, queue, *args) else: logger.warning("unable to enqueue job with class %s" % str(klass)) def enqueue_from_string(self, klass_as_string, queue, *args, **kwargs): payload = {'class':klass_as_string, 'args':args, 'enqueue_timestamp': time.time()} if 'first_attempt' in kwargs: payload['first_attempt'] = kwargs['first_attempt'] self.push(queue, payload) logger.info("enqueued '%s' job on queue %s" % (klass_as_string, queue)) if args: logger.debug("job arguments: %s" % str(args)) else: logger.debug("no arguments passed in.") def queues(self): return [sm.decode() for sm in self.redis.smembers("resque:queues")] or [] def workers(self): return [w.decode() for w in self.redis.smembers("resque:workers")] or [] def info(self): """Returns a dictionary of the current status of the pending jobs, processed, no. of queues, no. of workers, no. of failed jobs. """ pending = 0 for q in self.queues(): pending += self.size(q) return { 'pending' : pending, 'processed' : Stat('processed',self).get(), 'queues' : len(self.queues()), 'workers' : len(self.workers()), #'working' : len(self.working()), 'failed' : Stat('failed',self).get(), 'servers' : ['%s:%s' % (self.host, self.port)] } def keys(self): return [key.decode().replace('resque:','') for key in self.redis.keys('resque:*')] def reserve(self, queues): from pyres.job import Job return Job.reserve(queues, self) def __str__(self): return "PyRes Client connected to %s" % self.dsn def working(self): from pyres.worker import Worker return Worker.working(self) def remove_queue(self, queue): if queue in self._watched_queues: self._watched_queues.remove(queue) self.redis.srem('resque:queues',queue) del self.redis['resque:queue:%s' % queue] def close(self): """Close the underlying redis connection. """ self.redis.connection_pool.get_connection('_').disconnect() def enqueue_at(self, datetime, klass, *args, **kwargs): class_name = '%s.%s' % (klass.__module__, klass.__name__) self.enqueue_at_from_string(datetime, class_name, klass.queue, *args, **kwargs) def enqueue_at_from_string(self, datetime, klass_as_string, queue, *args, **kwargs): logger.info("scheduled '%s' job on queue %s for execution at %s" % (klass_as_string, queue, datetime)) if args: logger.debug("job arguments are: %s" % str(args)) payload = {'class': klass_as_string, 'queue': queue, 'args': args} if 'first_attempt' in kwargs: payload['first_attempt'] = kwargs['first_attempt'] self.delayed_push(datetime, payload) def delayed_push(self, datetime, item): key = int(time.mktime(datetime.timetuple())) self.redis.rpush('resque:delayed:%s' % key, ResQ.encode(item)) self.redis.zadd('resque:delayed_queue_schedule', key, key) def delayed_queue_peek(self, start, count): return [int(item) for item in self.redis.zrange( 'resque:delayed_queue_schedule', start, start+count) or []] def delayed_timestamp_peek(self, timestamp, start, count): return self.list_range('resque:delayed:%s' % timestamp, start, count) def delayed_queue_schedule_size(self): size = 0 length = self.redis.zcard('resque:delayed_queue_schedule') for i in self.redis.zrange('resque:delayed_queue_schedule',0,length): size += self.delayed_timestamp_size(i.decode()) return size def delayed_timestamp_size(self, timestamp): #key = int(time.mktime(timestamp.timetuple())) return self.redis.llen("resque:delayed:%s" % timestamp) def next_delayed_timestamp(self): key = int(time.mktime(ResQ._current_time().timetuple())) array = self.redis.zrangebyscore('resque:delayed_queue_schedule', '-inf', key, start=0, num=1) timestamp = None if array: timestamp = array[0] if timestamp: return timestamp.decode() def next_item_for_timestamp(self, timestamp): #key = int(time.mktime(timestamp.timetuple())) key = "resque:delayed:%s" % timestamp ret = self.redis.lpop(key) item = None if ret: item = ResQ.decode(ret) if self.redis.llen(key) == 0: self.redis.delete(key) self.redis.zrem('resque:delayed_queue_schedule', timestamp) return item @classmethod def encode(cls, item): return json.dumps(item) @classmethod def decode(cls, item): if not isinstance(item, string_types): item = item.decode() ret = json.loads(item) return ret @classmethod def _enqueue(cls, klass, *args): queue = getattr(klass,'queue', None) _self = cls() if queue: class_name = '%s.%s' % (klass.__module__, klass.__name__) _self.push(queue, {'class':class_name,'args':args, 'enqueue_timestamp': time.time()}) @staticmethod def _current_time(): return datetime.datetime.now() class Stat(object): """A Stat class which shows the current status of the queue. """ def __init__(self, name, resq): self.name = name self.key = "resque:stat:%s" % self.name self.resq = resq def get(self): val = self.resq.redis.get(self.key) if val: return int(val) return 0 def incr(self, ammount=1): self.resq.redis.incr(self.key, ammount) def decr(self, ammount=1): self.resq.redis.decr(self.key, ammount) def clear(self): self.resq.redis.delete(self.key) pyres-1.5/pyres/compat.py000066400000000000000000000011001232362746500155220ustar00rootroot00000000000000import sys import types try: import cPickle as pickle except ImportError: # pragma: no cover import pickle # True if we are running on Python 3. PY3 = sys.version_info[0] == 3 if PY3: # pragma: no cover string_types = str, integer_types = int, class_types = type, text_type = str binary_type = bytes long = int import subprocess as commands else: string_types = basestring, integer_types = (int, long) class_types = (type, types.ClassType) text_type = unicode binary_type = str long = long import commands pyres-1.5/pyres/exceptions.py000066400000000000000000000002351232362746500164300ustar00rootroot00000000000000class NoQueueError(Exception): pass class JobError(RuntimeError): pass class TimeoutError(JobError): pass class CrashError(JobError): passpyres-1.5/pyres/extensions.py000066400000000000000000000072771232362746500164630ustar00rootroot00000000000000import os import datetime import time import signal try: import multiprocessing except: import sys sys.exit("multiprocessing was not available") from pyres import ResQ from pyres.exceptions import NoQueueError from pyres.worker import Worker class JuniorWorker(Worker): def work(self, interval=5): self.startup() while True: if self._shutdown: break job = self.reserve() if job: print "got: %s" % job self.child = os.fork() if self.child: print 'Forked %s at %s' % (self.child, datetime.datetime.now()) os.waitpid(self.child, 0) else: print 'Processing %s since %s' % (job._queue, datetime.datetime.now()) self.process(job) os._exit(0) self.child = None else: break self.unregister_worker() class Manager(object): def __init__(self, queues, host, max_children=10): self.queues = queues self._host = host self.max_children = max_children self._shutdown = False self.children = [] self.resq = ResQ(host) self.validate_queues() self.reports = {} def __str__(self): hostname = os.uname()[1] pid = os.getpid() return 'Manager:%s:%s:%s' % (hostname, pid, ','.join(self.queues)) def validate_queues(self): if not self.queues: raise NoQueueError("Please give each worker at least one queue.") def check_rising(self, queue, size): if queue in self.reports: new_time = time.time() old_size = self.reports[queue][0] old_time = self.reports[queue][1] if new_time > old_time + 5 and size > old_size + 20: return True else: self.reports[queue] = (size, time.time()) return False def work(self): self.startup() while True: if self._shutdown: break #check to see if stuff is still going for queue in self.queues: #check queue size size = self.resq.size(queue) if self.check_rising(queue, size): if len(self.children) < self.max_children: self.start_child(queue) def startup(self): self.register_manager() self.register_signals() def register_manager(self): self.resq.redis.sadd('managers', str(self)) def unregister_manager(self): self.resq.redis.srem('managers', str(self)) def register_signals(self): signal.signal(signal.SIGTERM, self.shutdown_all) signal.signal(signal.SIGINT, self.shutdown_all) signal.signal(signal.SIGQUIT, self.schedule_shutdown) signal.signal(signal.SIGUSR1, self.kill_children) def shutdown_all(self, signum, frame): self.schedule_shutdown(signum, frame) self.kill_children(signum, frame) def schedule_shutdown(self, signum, frame): self._shutdown = True def kill_children(self): for child in self.children: child.terminate() def start_child(self, queue): p = multiprocessing.Process(target=JuniorWorker.run, args=([queue], self._host)) self.children.append(p) p.start() return True @classmethod def run(cls, queues=(), host="localhost:6379"): manager = cls(queues, host) manager.work() pyres-1.5/pyres/failure/000077500000000000000000000000001232362746500153245ustar00rootroot00000000000000pyres-1.5/pyres/failure/__init__.py000066400000000000000000000012551232362746500174400ustar00rootroot00000000000000from pyres.failure.redis import RedisBackend backend = RedisBackend def create(*args, **kwargs): return backend(*args, **kwargs) def count(resq): return backend.count(resq) def all(resq, start, count): return backend.all(resq, start, count) def clear(resq): return backend.clear(resq) def requeue(resq, failure_object): queue = failure_object._queue payload = failure_object._payload return resq.push(queue, payload) def retry(resq, queue, payload): job = resq.decode(payload) resq.push(queue, job['payload']) return delete(resq, payload) def delete(resq, payload): return resq.redis.lrem(name='resque:failed', num=1, value=payload) pyres-1.5/pyres/failure/base.py000066400000000000000000000023371232362746500166150ustar00rootroot00000000000000import sys import traceback class BaseBackend(object): """Provides a base class that custom backends can subclass. Also provides basic traceback and message parsing. The ``__init__`` takes these keyword arguments: ``exp`` -- The exception generated by your failure. ``queue`` -- The queue in which the ``Job`` was enqueued when it failed. ``payload`` -- The payload that was passed to the ``Job``. ``worker`` -- The worker that was processing the ``Job`` when it failed. """ def __init__(self, exp, queue, payload, worker=None): excc = sys.exc_info()[0] self._exception = excc try: self._traceback = traceback.format_exc() except AttributeError: self._traceback = None self._worker = worker self._queue = queue self._payload = payload def _parse_traceback(self, trace): """Return the given traceback string formatted for a notification.""" if not trace: return [] return trace.split('\n') def _parse_message(self, exc): """Return a message for a notification from the given exception.""" return '%s: %s' % (exc.__class__.__name__, str(exc)) pyres-1.5/pyres/failure/mail.py000066400000000000000000000054551232362746500166310ustar00rootroot00000000000000import smtplib from textwrap import dedent from email.mime.text import MIMEText from base import BaseBackend class MailBackend(BaseBackend): """Extends ``BaseBackend`` to provide support for emailing failures. Intended to be used with the MultipleBackend: from pyres import failure from pyres.failure.mail import MailBackend from pyres.failure.multiple import MultipleBackend from pyres.failure.redis import RedisBackend class EmailFailure(MailBackend): subject = 'Pyres Failure on {queue}' from_user = 'My Email User ' recipients = ['Me '] smtp_host = 'mail.mydomain.tld' smtp_port = 25 smtp_tls = True smtp_user = 'mailuser' smtp_password = 'm41lp455w0rd' failure.backend = MultipleBackend failure.backend.classes = [RedisBackend, EmailFailure] Additional notes: - The following tokens are available in subject: queue, worker, exception - Override the create_message method to provide an alternate body. It should return one of the message types from email.mime.* """ subject = 'Pyres Failure on {queue}' recipients = [] from_user = None smtp_host = None smtp_port = 25 smtp_tls = False smtp_user = None smtp_password = None def save(self, resq=None): if not self.recipients or not self.smtp_host or not self.from_user: return message = self.create_message() subject = self.format_subject() message['Subject'] = subject message['From'] = self.from_user message['To'] = ", ".join(self.recipients) self.send_message(message) def format_subject(self): return self.subject.format(queue=self._queue, worker=self._worker, exception=self._exception) def create_message(self): """Returns a message body to send in this email. Should be from email.mime.*""" body = dedent("""\ Received exception {exception} on {queue} from worker {worker}: {traceback} Payload: {payload} """).format(exception=self._exception, traceback=self._traceback, queue=self._queue, payload=self._payload, worker=self._worker) return MIMEText(body) def send_message(self, message): smtp = smtplib.SMTP(self.smtp_host, self.smtp_port) try: smtp.ehlo() if self.smtp_tls: smtp.starttls() if self.smtp_user: smtp.login(self.smtp_user, self.smtp_password) smtp.sendmail(self.from_user, self.recipients, message.as_string()) finally: smtp.close() pyres-1.5/pyres/failure/multiple.py000066400000000000000000000026201232362746500175310ustar00rootroot00000000000000from pyres.failure.base import BaseBackend from pyres.failure.redis import RedisBackend class MultipleBackend(BaseBackend): """Extends ``BaseBackend`` to provide support for delegating calls to multiple backends. Queries are delegated to the first backend in the list. Defaults to only the RedisBackend. To use: from pyres import failure from pyres.failure.base import BaseBackend from pyres.failure.multiple import MultipleBackend from pyres.failure.redis import RedisBackend class CustomBackend(BaseBackend): def save(self, resq): print('Custom backend') failure.backend = MultipleBackend failure.backend.classes = [RedisBackend, CustomBackend] """ classes = [] def __init__(self, *args): if not self.classes: self.classes = [RedisBackend] self.backends = [klass(*args) for klass in self.classes] BaseBackend.__init__(self, *args) @classmethod def count(cls, resq): first = MultipleBackend.classes[0] return first.count(resq) @classmethod def all(cls, resq, start=0, count=1): first = MultipleBackend.classes[0] return first.all(resq, start, count) @classmethod def clear(cls, resq): first = MultipleBackend.classes[0] return first.clear(resq) def save(self, resq=None): map(lambda x: x.save(resq), self.backends) pyres-1.5/pyres/failure/redis.py000066400000000000000000000026521232362746500170110ustar00rootroot00000000000000import datetime, time from base64 import b64encode from .base import BaseBackend from pyres import ResQ class RedisBackend(BaseBackend): """Extends the ``BaseBackend`` to provide a Redis backend for failed jobs.""" def save(self, resq=None): """Saves the failed Job into a "failed" Redis queue preserving all its original enqueud info.""" if not resq: resq = ResQ() data = { 'failed_at' : datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'), 'payload' : self._payload, 'exception' : self._exception.__class__.__name__, 'error' : self._parse_message(self._exception), 'backtrace' : self._parse_traceback(self._traceback), 'queue' : self._queue } if self._worker: data['worker'] = self._worker data = ResQ.encode(data) resq.redis.rpush('resque:failed', data) @classmethod def count(cls, resq): return int(resq.redis.llen('resque:failed')) @classmethod def all(cls, resq, start=0, count=1): items = resq.redis.lrange('resque:failed', start, count) or [] ret_list = [] for i in items: failure = ResQ.decode(i) failure['redis_value'] = b64encode(i) ret_list.append(failure) return ret_list @classmethod def clear(cls, resq): return resq.redis.delete('resque:failed') pyres-1.5/pyres/horde.py000066400000000000000000000351261232362746500153570ustar00rootroot00000000000000import sys try: import multiprocessing except: sys.exit("multiprocessing was not available") import time, os, signal import datetime import logging import logging.handlers from pyres import ResQ, Stat, get_logging_handler, special_log_file from pyres.exceptions import NoQueueError try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict from pyres.job import Job from pyres.compat import string_types import pyres.json_parser as json try: from setproctitle import setproctitle except: def setproctitle(name): pass def setup_logging(procname, namespace='', log_level=logging.INFO, log_file=None): logger = multiprocessing.get_logger() #logger = multiprocessing.log_to_stderr() logger.setLevel(log_level) handler = get_logging_handler(log_file, procname, namespace) logger.addHandler(handler) return logger class Minion(multiprocessing.Process): def __init__(self, queues, server, password, log_level=logging.INFO, log_path=None, interval=5, concat_logs=False, max_jobs=0): multiprocessing.Process.__init__(self, name='Minion') #format = '%(asctime)s %(levelname)s %(filename)s-%(lineno)d: %(message)s' #logHandler = logging.StreamHandler() #logHandler.setFormatter(logging.Formatter(format)) #self.logger = multiprocessing.get_logger() #self.logger.addHandler(logHandler) #self.logger.setLevel(logging.DEBUG) self.queues = queues self._shutdown = False self.hostname = os.uname()[1] self.server = server self.password = password self.interval = interval self.log_level = log_level self.log_path = log_path self.log_file = None self.concat_logs = concat_logs self.max_jobs = max_jobs def prune_dead_workers(self): pass def schedule_shutdown(self, signum, frame): self._shutdown = True def register_signal_handlers(self): signal.signal(signal.SIGTERM, self.schedule_shutdown) signal.signal(signal.SIGINT, self.schedule_shutdown) signal.signal(signal.SIGQUIT, self.schedule_shutdown) def register_minion(self): self.resq.redis.sadd('resque:minions',str(self)) self.started = datetime.datetime.now() def startup(self): self.register_signal_handlers() self.prune_dead_workers() self.register_minion() def __str__(self): return '%s:%s:%s' % (self.hostname, self.pid, ','.join(self.queues)) def reserve(self): self.logger.debug('checking queues: %s' % self.queues) job = Job.reserve(self.queues, self.resq, self.__str__()) if job: self.logger.info('Found job on %s' % job._queue) return job def process(self, job): if not job: return try: self.working_on(job) job.perform() except Exception as e: exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() self.logger.error("%s failed: %s" % (job, e)) job.fail(exceptionTraceback) self.failed() else: self.logger.debug("Hells yeah") self.logger.info('completed job: %s' % job) finally: self.done_working() def working_on(self, job): setproctitle('pyres_minion:%s: working on job: %s' % (os.getppid(), job._payload)) self.logger.debug('marking as working on') data = { 'queue': job._queue, 'run_at': int(time.mktime(datetime.datetime.now().timetuple())), 'payload': job._payload } data = json.dumps(data) self.resq.redis["resque:minion:%s" % str(self)] = data self.logger.debug("minion:%s" % str(self)) #self.logger.debug(self.resq.redis["resque:minion:%s" % str(self)]) def failed(self): Stat("failed", self.resq).incr() def processed(self): total_processed = Stat("processed", self.resq) total_processed.incr() def done_working(self): self.logger.debug('done working') self.processed() self.resq.redis.delete("resque:minion:%s" % str(self)) def unregister_minion(self): self.resq.redis.srem('resque:minions',str(self)) self.started = None def work(self, interval=5): self.startup() cur_job = 0 while True: setproctitle('pyres_minion:%s: waiting for job on: %s' % (os.getppid(),self.queues)) self.logger.info('waiting on job') if self._shutdown: self.logger.info('shutdown scheduled') break self.logger.debug('max_jobs: %d cur_jobs: %d' % (self.max_jobs, cur_job)) if (self.max_jobs > 0 and self.max_jobs < cur_job): self.logger.debug('max_jobs reached on %s: %d' % (self.pid, cur_job)) self.logger.debug('minion sleeping for: %d secs' % interval) time.sleep(interval) cur_job = 0 job = self.reserve() if job: self.process(job) cur_job = cur_job + 1 else: cur_job = 0 self.logger.debug('minion sleeping for: %d secs' % interval) time.sleep(interval) self.unregister_minion() def clear_logger(self): for handler in self.logger.handlers: self.logger.removeHandler(handler) def run(self): setproctitle('pyres_minion:%s: Starting' % (os.getppid(),)) if self.log_path: if special_log_file(self.log_path): self.log_file = self.log_path elif self.concat_logs: self.log_file = os.path.join(self.log_path, 'minion.log') else: self.log_file = os.path.join(self.log_path, 'minion-%s.log' % self.pid) namespace = 'minion:%s' % self.pid self.logger = setup_logging('minion', namespace, self.log_level, self.log_file) #self.clear_logger() if isinstance(self.server,string_types): self.resq = ResQ(server=self.server, password=self.password) elif isinstance(self.server, ResQ): self.resq = self.server else: raise Exception("Bad server argument") self.work(self.interval) #while True: # job = self.q.get() # print 'pid: %s is running %s ' % (self.pid,job) class Khan(object): _command_map = { 'ADD': 'add_minion', 'REMOVE': '_remove_minion', 'SHUTDOWN': '_schedule_shutdown' } def __init__(self, pool_size=5, queues=[], server='localhost:6379', password=None, logging_level=logging.INFO, log_file=None, minions_interval=5, concat_minions_logs=False, max_jobs=0): #super(Khan,self).__init__(queues=queues,server=server,password=password) self._shutdown = False self.pool_size = int(pool_size) self.queues = queues self.server = server self.password = password self.pid = os.getpid() self.validate_queues() self._workers = OrderedDict() self.server = server self.password = password self.logging_level = logging_level self.log_file = log_file self.minions_interval = minions_interval self.concat_minions_logs = concat_minions_logs self.max_jobs = max_jobs #self._workers = list() def setup_resq(self): if hasattr(self,'logger'): self.logger.info('Connecting to redis server - %s' % self.server) if isinstance(self.server,string_types): self.resq = ResQ(server=self.server, password=self.password) elif isinstance(self.server, ResQ): self.resq = self.server else: raise Exception("Bad server argument") def validate_queues(self): "Checks if a worker is given atleast one queue to work on." if not self.queues: raise NoQueueError("Please give each worker at least one queue.") def startup(self): self.register_signal_handlers() def register_signal_handlers(self): signal.signal(signal.SIGTERM, self.schedule_shutdown) signal.signal(signal.SIGINT, self.schedule_shutdown) signal.signal(signal.SIGQUIT, self.schedule_shutdown) signal.signal(signal.SIGUSR1, self.kill_child) signal.signal(signal.SIGUSR2, self.add_child) if hasattr(signal, 'SIGINFO'): signal.signal(signal.SIGINFO, self.current_state) def current_state(self): tmap = {} main_thread = None import traceback from cStringIO import StringIO # get a map of threads by their ID so we can print their names # during the traceback dump for t in threading.enumerate(): if getattr(t, "ident", None): tmap[t.ident] = t else: main_thread = t out = StringIO() sep = "=" * 49 + "\n" for tid, frame in sys._current_frames().iteritems(): thread = tmap.get(tid, main_thread) if not thread: # skip old junk (left-overs from a fork) continue out.write("%s\n" % (thread.getName(), )) out.write(sep) traceback.print_stack(frame, file=out) out.write(sep) out.write("LOCAL VARIABLES\n") out.write(sep) pprint(frame.f_locals, stream=out) out.write("\n\n") self.logger.info(out.getvalue()) def _schedule_shutdown(self): self.schedule_shutdown(None, None) def schedule_shutdown(self, signum, frame): self.logger.info('Khan Shutdown scheduled') self._shutdown = True def kill_child(self, signum, frame): self._remove_minion() def add_child(self, signum, frame): self.add_minion() def register_khan(self): if not hasattr(self, 'resq'): self.setup_resq() self.resq.redis.sadd('resque:khans',str(self)) self.started = datetime.datetime.now() def _check_commands(self): if not self._shutdown: self.logger.debug('Checking commands') command = self.resq.redis.lpop('resque:khan:%s' % str(self)) self.logger.debug('COMMAND FOUND: %s ' % command) if command: self.process_command(command) self._check_commands() def process_command(self, command): self.logger.info('Processing Command') #available commands, shutdown, add 1, remove 1 command_item = self._command_map.get(command, None) if command_item: fn = getattr(self, command_item) if fn: fn() def add_minion(self): self._add_minion() self.resq.redis.srem('resque:khans',str(self)) self.pool_size += 1 self.resq.redis.sadd('resque:khans',str(self)) def _add_minion(self): if hasattr(self,'logger'): self.logger.info('Adding minion') if self.log_file: if special_log_file(self.log_file): log_path = self.log_file else: log_path = os.path.dirname(self.log_file) else: log_path = None m = Minion(self.queues, self.server, self.password, interval=self.minions_interval, log_level=self.logging_level, log_path=log_path, concat_logs=self.concat_minions_logs, max_jobs=self.max_jobs) m.start() self._workers[m.pid] = m if hasattr(self,'logger'): self.logger.info('minion added at: %s' % m.pid) return m def _shutdown_minions(self): """ send the SIGNINT signal to each worker in the pool. """ setproctitle('pyres_manager: Waiting on children to shutdown.') for minion in self._workers.values(): minion.terminate() minion.join() def _remove_minion(self, pid=None): #if pid: # m = self._workers.pop(pid) pid, m = self._workers.popitem(False) m.terminate() self.resq.redis.srem('resque:khans',str(self)) self.pool_size -= 1 self.resq.redis.sadd('resque:khans',str(self)) return m def unregister_khan(self): if hasattr(self,'logger'): self.logger.debug('unregistering khan') self.resq.redis.srem('resque:khans',str(self)) self.started = None def setup_minions(self): for i in range(self.pool_size): self._add_minion() def _setup_logging(self): self.logger = setup_logging('khan', 'khan', self.logging_level, self.log_file) def work(self, interval=2): setproctitle('pyres_manager: Starting') self.startup() self.setup_minions() self._setup_logging() self.logger.info('Running as pid: %s' % self.pid) self.logger.info('Added %s child processes' % self.pool_size) self.logger.info('Setting up pyres connection') self.setup_resq() self.register_khan() setproctitle('pyres_manager: running %s' % self.queues) while True: self._check_commands() if self._shutdown: #send signals to each child self._shutdown_minions() break #get job else: self.logger.debug('manager sleeping for: %d secs' % interval) time.sleep(interval) self.unregister_khan() def __str__(self): hostname = os.uname()[1] return '%s:%s:%s' % (hostname, self.pid, self.pool_size) @classmethod def run(cls, pool_size=5, queues=[], server='localhost:6379', password=None, interval=2, logging_level=logging.INFO, log_file=None, minions_interval=5, concat_minions_logs=False, max_jobs=0): worker = cls(pool_size=pool_size, queues=queues, server=server, password=password, logging_level=logging_level, log_file=log_file, minions_interval=minions_interval, concat_minions_logs=concat_minions_logs, max_jobs=max_jobs) worker.work(interval=interval) #if __name__ == "__main__": # k = Khan() # k.run() if __name__ == "__main__": from optparse import OptionParser parser = OptionParser(usage="%prog [options] queue list") parser.add_option("-s", dest="server", default="localhost:6379") (options,args) = parser.parse_args() if len(args) < 1: parser.print_help() parser.error("Please give the horde at least one queue.") Khan.run(pool_size=2, queues=args, server=options.server) #khan.run() #Worker.run(queues, options.server) pyres-1.5/pyres/job.py000066400000000000000000000124131232362746500150220ustar00rootroot00000000000000import logging import time from datetime import timedelta from pyres import ResQ, safe_str_to_class from pyres import failure from pyres.failure.redis import RedisBackend from pyres.compat import string_types class Job(object): """Every job on the ResQ is an instance of the *Job* class. The ``__init__`` takes these keyword arguments: ``queue`` -- A string defining the queue to which this Job will be added. ``payload`` -- A dictionary which contains the string name of a class which extends this Job and a list of args which will be passed to that class. ``resq`` -- An instance of the ResQ class. ``worker`` -- The name of a specific worker if you'd like this Job to be done by that worker. Default is "None". """ safe_str_to_class = staticmethod(safe_str_to_class) def __init__(self, queue, payload, resq, worker=None): self._queue = queue self._payload = payload self.resq = resq self._worker = worker self.enqueue_timestamp = self._payload.get("enqueue_timestamp") # Set the default back end, jobs can override when we import them # inside perform(). failure.backend = RedisBackend def __str__(self): return "(Job{%s} | %s | %s)" % ( self._queue, self._payload['class'], repr(self._payload['args'])) def perform(self): """This method converts payload into args and calls the ``perform`` method on the payload class. Before calling ``perform``, a ``before_perform`` class method is called, if it exists. It takes a dictionary as an argument; currently the only things stored on the dictionary are the args passed into ``perform`` and a timestamp of when the job was enqueued. Similarly, an ``after_perform`` class method is called after ``perform`` is finished. The metadata dictionary contains the same data, plus a timestamp of when the job was performed, a ``failed`` boolean value, and if it did fail, a ``retried`` boolean value. This method is called after retry, and is called regardless of whether an exception is ultimately thrown by the perform method. """ payload_class_str = self._payload["class"] payload_class = self.safe_str_to_class(payload_class_str) payload_class.resq = self.resq args = self._payload.get("args") metadata = dict(args=args) if self.enqueue_timestamp: metadata["enqueue_timestamp"] = self.enqueue_timestamp before_perform = getattr(payload_class, "before_perform", None) metadata["failed"] = False metadata["perform_timestamp"] = time.time() check_after = True try: if before_perform: payload_class.before_perform(metadata) return payload_class.perform(*args) except Exception as e: check_after = False metadata["failed"] = True metadata["exception"] = e if not self.retry(payload_class, args): metadata["retried"] = False raise else: metadata["retried"] = True logging.exception("Retry scheduled after error in %s", self._payload) finally: after_perform = getattr(payload_class, "after_perform", None) if after_perform and check_after: payload_class.after_perform(metadata) delattr(payload_class,'resq') def fail(self, exception): """This method provides a way to fail a job and will use whatever failure backend you've provided. The default is the ``RedisBackend``. """ fail = failure.create(exception, self._queue, self._payload, self._worker) fail.save(self.resq) return fail def retry(self, payload_class, args): """This method provides a way to retry a job after a failure. If the jobclass defined by the payload containes a ``retry_every`` attribute then pyres will attempt to retry the job until successful or until timeout defined by ``retry_timeout`` on the payload class. """ retry_every = getattr(payload_class, 'retry_every', None) retry_timeout = getattr(payload_class, 'retry_timeout', 0) if retry_every: now = ResQ._current_time() first_attempt = self._payload.get("first_attempt", now) retry_until = first_attempt + timedelta(seconds=retry_timeout) retry_at = now + timedelta(seconds=retry_every) if retry_at < retry_until: self.resq.enqueue_at(retry_at, payload_class, *args, **{'first_attempt':first_attempt}) return True return False @classmethod def reserve(cls, queues, res, worker=None, timeout=10): """Reserve a job on one of the queues. This marks this job so that other workers will not pick it up. """ if isinstance(queues, string_types): queues = [queues] queue, payload = res.pop(queues, timeout=timeout) if payload: return cls(queue, payload, res, worker) pyres-1.5/pyres/json_parser.py000066400000000000000000000025631232362746500166020ustar00rootroot00000000000000from datetime import datetime from pyres.compat import string_types try: #import simplejson as json import json except ImportError: import simplejson as json DATE_FORMAT = '%Y-%m-%dT%H:%M:%S' DATE_PREFIX = '@D:' class CustomJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime): return o.strftime(DATE_PREFIX + DATE_FORMAT) return json.JSONEncoder.default(self, o) class CustomJSONDecoder(json.JSONDecoder): def decode(self, json_string): decoded = json.loads(json_string) return self.convert(decoded) def convert(self, value): if isinstance(value, string_types) and value.startswith(DATE_PREFIX): try: return datetime.strptime(value[len(DATE_PREFIX):], DATE_FORMAT) except ValueError: return value elif isinstance(value, dict): for k, v in value.items(): new = self.convert(v) if new != v: value[k] = new elif isinstance(value, list): for k, v in enumerate(value): new = self.convert(v) if new != v: value[k] = new return value def dumps(values): return json.dumps(values, cls=CustomJSONEncoder) def loads(string): return json.loads(string, cls=CustomJSONDecoder) pyres-1.5/pyres/scheduler.py000066400000000000000000000055301232362746500162300ustar00rootroot00000000000000import signal import time import logging from pyres import ResQ, __version__ from pyres.compat import string_types logger = logging.getLogger(__name__) class Scheduler(object): def __init__(self, server="localhost:6379", password=None): """ >>> from pyres.scheduler import Scheduler >>> scheduler = Scheduler('localhost:6379') """ self._shutdown = False if isinstance(server, string_types): self.resq = ResQ(server=server, password=password) elif isinstance(server, ResQ): self.resq = server else: raise Exception("Bad server argument") def register_signal_handlers(self): logger.info('registering signals') signal.signal(signal.SIGTERM, self.schedule_shutdown) signal.signal(signal.SIGINT, self.schedule_shutdown) signal.signal(signal.SIGQUIT, self.schedule_shutdown) def schedule_shutdown(self, signal, frame): logger.info('shutting down started') self._shutdown = True def __call__(self): _setproctitle("Starting") logger.info('starting up') self.register_signal_handlers() #self.load_schedule() logger.info('looking for delayed items') while True: if self._shutdown: break self.handle_delayed_items() _setproctitle("Waiting") logger.debug('sleeping') time.sleep(5) logger.info('shutting down complete') def next_timestamp(self): while True: timestamp = self.resq.next_delayed_timestamp() if timestamp: yield timestamp else: break def next_item(self, timestamp): while True: item = self.resq.next_item_for_timestamp(timestamp) if item: yield item else: break def handle_delayed_items(self): for timestamp in self.next_timestamp(): _setproctitle('Handling timestamp %s' % timestamp) logger.debug('handling timestamp: %s' % timestamp) for item in self.next_item(timestamp): logger.debug('queueing item %s' % item) klass = item['class'] queue = item['queue'] args = item['args'] kwargs = {} if 'first_attempt' in item: kwargs['first_attempt'] = item['first_attempt'] self.resq.enqueue_from_string(klass, queue, *args, **kwargs) @classmethod def run(cls, server, password=None): sched = cls(server=server, password=password) sched() try: from setproctitle import setproctitle except ImportError: def setproctitle(name): pass def _setproctitle(msg): setproctitle("pyres_scheduler-%s: %s" % (__version__, msg)) pyres-1.5/pyres/scripts.py000066400000000000000000000133521232362746500157420ustar00rootroot00000000000000import logging from optparse import OptionParser from pyres.horde import Khan from pyres import setup_logging, setup_pidfile from pyres.scheduler import Scheduler from pyres.worker import Worker def pyres_manager(): usage = "usage: %prog [options] arg1" parser = OptionParser(usage=usage) #parser.add_option("-q", dest="queue_list") parser.add_option("--host", dest="host", default="localhost") parser.add_option("--port", dest="port",type="int", default=6379) parser.add_option("--password", dest="password", default=None) parser.add_option("-i", '--interval', dest='manager_interval', default=None, help='the default time interval to sleep between runs - manager') parser.add_option("--minions_interval", dest='minions_interval', default=None, help='the default time interval to sleep between runs - minions') parser.add_option('-l', '--log-level', dest='log_level', default='info', help='log level. Valid values are "debug", "info", "warning", "error", "critical", in decreasing order of verbosity. Defaults to "info" if parameter not specified.') parser.add_option("--pool", type="int", dest="pool_size", default=1, help="Number of minions to spawn under the manager.") parser.add_option("-j", "--process_max_jobs", dest="max_jobs", type=int, default=0, help='how many jobs should be processed on worker run.') parser.add_option('-f', dest='logfile', help='If present, a logfile will be used. "stderr", "stdout", and "syslog" are all special values.') parser.add_option('-p', dest='pidfile', help='If present, a pidfile will be used.') parser.add_option("--concat_minions_logs", action="store_true", dest="concat_minions_logs", help='Concat all minions logs on same file.') (options,args) = parser.parse_args() if len(args) != 1: parser.print_help() parser.error("Argument must be a comma seperated list of queues") log_level = getattr(logging, options.log_level.upper(), 'INFO') #logging.basicConfig(level=log_level, format="%(asctime)s: %(levelname)s: %(message)s") concat_minions_logs = options.concat_minions_logs setup_pidfile(options.pidfile) manager_interval = options.manager_interval if manager_interval is not None: manager_interval = float(manager_interval) minions_interval = options.minions_interval if minions_interval is not None: minions_interval = float(minions_interval) queues = args[0].split(',') server = '%s:%s' % (options.host,options.port) password = options.password Khan.run(pool_size=options.pool_size, queues=queues, server=server, password=password, interval=manager_interval, logging_level=log_level, log_file=options.logfile, minions_interval=minions_interval, concat_minions_logs=concat_minions_logs, max_jobs=options.max_jobs) def pyres_scheduler(): usage = "usage: %prog [options] arg1" parser = OptionParser(usage=usage) #parser.add_option("-q", dest="queue_list") parser.add_option("--host", dest="host", default="localhost") parser.add_option("--port", dest="port",type="int", default=6379) parser.add_option("--password", dest="password", default=None) parser.add_option('-l', '--log-level', dest='log_level', default='info', help='log level. Valid values are "debug", "info", "warning", "error", "critical", in decreasing order of verbosity. Defaults to "info" if parameter not specified.') parser.add_option('-f', dest='logfile', help='If present, a logfile will be used. "stderr", "stdout", and "syslog" are all special values.') parser.add_option('-p', dest='pidfile', help='If present, a pidfile will be used.') (options,args) = parser.parse_args() log_level = getattr(logging, options.log_level.upper(),'INFO') #logging.basicConfig(level=log_level, format="%(module)s: %(asctime)s: %(levelname)s: %(message)s") setup_logging(procname="pyres_scheduler", log_level=log_level, filename=options.logfile) setup_pidfile(options.pidfile) server = '%s:%s' % (options.host, options.port) password = options.password Scheduler.run(server, password) def pyres_worker(): usage = "usage: %prog [options] arg1" parser = OptionParser(usage=usage) parser.add_option("--host", dest="host", default="localhost") parser.add_option("--port", dest="port",type="int", default=6379) parser.add_option("--password", dest="password", default=None) parser.add_option("-i", '--interval', dest='interval', default=None, help='the default time interval to sleep between runs') parser.add_option('-l', '--log-level', dest='log_level', default='info', help='log level. Valid values are "debug", "info", "warning", "error", "critical", in decreasing order of verbosity. Defaults to "info" if parameter not specified.') parser.add_option('-f', dest='logfile', help='If present, a logfile will be used. "stderr", "stdout", and "syslog" are all special values.') parser.add_option('-p', dest='pidfile', help='If present, a pidfile will be used.') parser.add_option("-t", '--timeout', dest='timeout', default=None, help='the timeout in seconds for this worker') (options,args) = parser.parse_args() if len(args) != 1: parser.print_help() parser.error("Argument must be a comma seperated list of queues") log_level = getattr(logging, options.log_level.upper(), 'INFO') setup_logging(procname="pyres_worker", log_level=log_level, filename=options.logfile) setup_pidfile(options.pidfile) interval = options.interval if interval is not None: interval = int(interval) timeout = options.timeout and int(options.timeout) queues = args[0].split(',') server = '%s:%s' % (options.host,options.port) password = options.password Worker.run(queues, server, password, interval, timeout=timeout) pyres-1.5/pyres/worker.py000066400000000000000000000330001232362746500155540ustar00rootroot00000000000000import logging import signal import datetime, time import os, sys from pyres import json_parser as json from pyres.compat import commands import random from pyres.exceptions import NoQueueError, JobError, TimeoutError, CrashError from pyres.job import Job from pyres import ResQ, Stat, __version__ from pyres.compat import string_types logger = logging.getLogger(__name__) class Worker(object): """Defines a worker. The ``pyres_worker`` script instantiates this Worker class and passes a comma-separated list of queues to listen on.:: >>> from pyres.worker import Worker >>> Worker.run([queue1, queue2], server="localhost:6379/0") """ job_class = Job def __init__(self, queues=(), server="localhost:6379", password=None, timeout=None): self.queues = queues self.validate_queues() self._shutdown = False self.child = None self.pid = os.getpid() self.hostname = os.uname()[1] self.timeout = timeout if isinstance(server, string_types): self.resq = ResQ(server=server, password=password) elif isinstance(server, ResQ): self.resq = server else: raise Exception("Bad server argument") def validate_queues(self): """Checks if a worker is given at least one queue to work on.""" if not self.queues: raise NoQueueError("Please give each worker at least one queue.") def register_worker(self): self.resq.redis.sadd('resque:workers', str(self)) #self.resq._redis.add("worker:#{self}:started", Time.now.to_s) self.started = datetime.datetime.now() def _set_started(self, dt): if dt: key = int(time.mktime(dt.timetuple())) self.resq.redis.set("resque:worker:%s:started" % self, key) else: self.resq.redis.delete("resque:worker:%s:started" % self) def _get_started(self): datestring = self.resq.redis.get("resque:worker:%s:started" % self) #ds = None #if datestring: # ds = datetime.datetime.strptime(datestring, '%Y-%m-%d %H:%M:%S') return datestring started = property(_get_started, _set_started) def unregister_worker(self): self.resq.redis.srem('resque:workers', str(self)) self.started = None Stat("processed:%s" % self, self.resq).clear() Stat("failed:%s" % self, self.resq).clear() def prune_dead_workers(self): all_workers = Worker.all(self.resq) known_workers = Worker.worker_pids() for worker in all_workers: host, pid, queues = worker.id.split(':') if host != self.hostname: continue if pid in known_workers: continue logger.warning("pruning dead worker: %s" % worker) worker.unregister_worker() def startup(self): self.register_signal_handlers() self.prune_dead_workers() self.register_worker() def register_signal_handlers(self): signal.signal(signal.SIGTERM, self.shutdown_all) signal.signal(signal.SIGINT, self.shutdown_all) signal.signal(signal.SIGQUIT, self.schedule_shutdown) signal.signal(signal.SIGUSR1, self.kill_child) def shutdown_all(self, signum, frame): self.schedule_shutdown(signum, frame) self.kill_child(signum, frame) def schedule_shutdown(self, signum, frame): self._shutdown = True def kill_child(self, signum, frame): if self.child: logger.info("Killing child at %s" % self.child) os.kill(self.child, signal.SIGKILL) def __str__(self): if getattr(self,'id', None): return self.id return '%s:%s:%s' % (self.hostname, self.pid, ','.join(self.queues)) def _setproctitle(self, msg): setproctitle("pyres_worker-%s [%s]: %s" % (__version__, ','.join(self.queues), msg)) def work(self, interval=5): """Invoked by ``run`` method. ``work`` listens on a list of queues and sleeps for ``interval`` time. ``interval`` -- Number of seconds the worker will wait until processing the next job. Default is "5". Whenever a worker finds a job on the queue it first calls ``reserve`` on that job to make sure another worker won't run it, then *forks* itself to work on that job. """ self._setproctitle("Starting") logger.info("starting") self.startup() while True: if self._shutdown: logger.info('shutdown scheduled') break self.register_worker() job = self.reserve(interval) if job: self.fork_worker(job) else: if interval == 0: break #procline @paused ? "Paused" : "Waiting for #{@queues.join(',')}" self._setproctitle("Waiting") #time.sleep(interval) self.unregister_worker() def fork_worker(self, job): """Invoked by ``work`` method. ``fork_worker`` does the actual forking to create the child process that will process the job. It's also responsible for monitoring the child process and handling hangs and crashes. Finally, the ``process`` method actually processes the job by eventually calling the Job instance's ``perform`` method. """ logger.debug('picked up job') logger.debug('job details: %s' % job) self.before_fork(job) self.child = os.fork() if self.child: self._setproctitle("Forked %s at %s" % (self.child, datetime.datetime.now())) logger.info('Forked %s at %s' % (self.child, datetime.datetime.now())) try: start = datetime.datetime.now() # waits for the result or times out while True: pid, status = os.waitpid(self.child, os.WNOHANG) if pid != 0: if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0: break if os.WIFSTOPPED(status): logger.warning("Process stopped by signal %d" % os.WSTOPSIG(status)) else: if os.WIFSIGNALED(status): raise CrashError("Unexpected exit by signal %d" % os.WTERMSIG(status)) raise CrashError("Unexpected exit status %d" % os.WEXITSTATUS(status)) time.sleep(0.5) now = datetime.datetime.now() if self.timeout and ((now - start).seconds > self.timeout): os.kill(self.child, signal.SIGKILL) os.waitpid(-1, os.WNOHANG) raise TimeoutError("Timed out after %d seconds" % self.timeout) except OSError as ose: import errno if ose.errno != errno.EINTR: raise ose except JobError: self._handle_job_exception(job) finally: # If the child process' job called os._exit manually we need to # finish the clean up here. if self.job(): self.done_working(job) logger.debug('done waiting') else: self._setproctitle("Processing %s since %s" % (job, datetime.datetime.now())) logger.info('Processing %s since %s' % (job, datetime.datetime.now())) self.after_fork(job) # re-seed the Python PRNG after forking, otherwise # all job process will share the same sequence of # random numbers random.seed() self.process(job) os._exit(0) self.child = None def before_fork(self, job): """ hook for making changes immediately before forking to process a job """ pass def after_fork(self, job): """ hook for making changes immediately after forking to process a job """ pass def before_process(self, job): return job def process(self, job=None): if not job: job = self.reserve() job_failed = False try: try: self.working_on(job) job = self.before_process(job) return job.perform() except Exception: job_failed = True self._handle_job_exception(job) except SystemExit as e: if e.code != 0: job_failed = True self._handle_job_exception(job) if not job_failed: logger.debug('completed job') logger.debug('job details: %s' % job) finally: self.done_working(job) def _handle_job_exception(self, job): exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() logger.exception("%s failed: %s" % (job, exceptionValue)) job.fail(exceptionTraceback) self.failed() def reserve(self, timeout=10): logger.debug('checking queues %s' % self.queues) job = self.job_class.reserve(self.queues, self.resq, self.__str__(), timeout=timeout) if job: logger.info('Found job on %s: %s' % (job._queue, job)) return job def working_on(self, job): logger.debug('marking as working on') data = { 'queue': job._queue, 'run_at': str(int(time.mktime(datetime.datetime.now().timetuple()))), 'payload': job._payload } data = json.dumps(data) self.resq.redis["resque:worker:%s" % str(self)] = data logger.debug("worker:%s" % str(self)) logger.debug(self.resq.redis["resque:worker:%s" % str(self)]) def done_working(self, job): logger.debug('done working on %s', job) self.processed() self.resq.redis.delete("resque:worker:%s" % str(self)) def processed(self): total_processed = Stat("processed", self.resq) worker_processed = Stat("processed:%s" % str(self), self.resq) total_processed.incr() worker_processed.incr() def get_processed(self): return Stat("processed:%s" % str(self), self.resq).get() def failed(self): Stat("failed", self.resq).incr() Stat("failed:%s" % self, self.resq).incr() def get_failed(self): return Stat("failed:%s" % self, self.resq).get() def job(self): data = self.resq.redis.get("resque:worker:%s" % self) if data: return ResQ.decode(data) return {} def processing(self): return self.job() def state(self): if self.resq.redis.exists('resque:worker:%s' % self): return 'working' return 'idle' @classmethod def worker_pids(cls): """Returns an array of all pids (as strings) of the workers on this machine. Used when pruning dead workers.""" cmd = "ps -A -o pid,command | grep pyres_worker | grep -v grep" output = commands.getoutput(cmd) if output: return map(lambda l: l.strip().split(' ')[0], output.split("\n")) else: return [] @classmethod def run(cls, queues, server="localhost:6379", password=None, interval=None, timeout=None): worker = cls(queues=queues, server=server, password=password, timeout=timeout) if interval is not None: worker.work(interval) else: worker.work() @classmethod def all(cls, host="localhost:6379"): if isinstance(host,string_types): resq = ResQ(host) elif isinstance(host, ResQ): resq = host return [Worker.find(w,resq) for w in resq.workers() or []] @classmethod def working(cls, host): if isinstance(host, string_types): resq = ResQ(host) elif isinstance(host, ResQ): resq = host total = [] for key in Worker.all(host): total.append('resque:worker:%s' % key) names = [] for key in total: value = resq.redis.get(key) if value: w = Worker.find(key[14:], resq) #resque:worker: names.append(w) return names @classmethod def find(cls, worker_id, resq): if Worker.exists(worker_id, resq): queues = worker_id.split(':')[-1].split(',') worker = cls(queues,resq) worker.id = worker_id return worker else: return None @classmethod def exists(cls, worker_id, resq): return resq.redis.sismember('resque:workers', worker_id) try: from setproctitle import setproctitle except ImportError: def setproctitle(name): pass if __name__ == "__main__": from optparse import OptionParser parser = OptionParser() parser.add_option("-q", dest="queue_list") parser.add_option("-s", dest="server", default="localhost:6379") (options,args) = parser.parse_args() if not options.queue_list: parser.print_help() parser.error("Please give each worker at least one queue.") queues = options.queue_list.split(',') Worker.run(queues, options.server) pyres-1.5/requirements-test.txt000066400000000000000000000000141232362746500167670ustar00rootroot00000000000000nose==1.1.2 pyres-1.5/requirements.txt000066400000000000000000000000551232362746500160170ustar00rootroot00000000000000simplejson>3.0 redis>2.4.12 setproctitle>1.0 pyres-1.5/roadmap.md000066400000000000000000000003461232362746500145030ustar00rootroot00000000000000pyres todo and roadmap 1.3 === * resweb moved into own package 2.0 === * move from duck typed class to a decorated function for jobs * add better hooks, like retools 2.1 === * add namespace support * cleanup workers/extensions pyres-1.5/setup.py000066400000000000000000000035231232362746500142500ustar00rootroot00000000000000import sys from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand requires=[ item for item in open("requirements.txt").read().split("\n") if item] if sys.version_info[0:2] == (2,6): requires.append('ordereddict') class PyTest(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): #import here, cause outside the eggs aren't loaded import pytest result = pytest.main(self.test_args) sys.exit(result) version='1.5' setup( name='pyres', version=version, description='Python resque clone', author='Matt George', author_email='mgeorge@gmail.com', maintainer='Matt George', license='MIT', url='http://github.com/binarydud/pyres', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), download_url='http://pypi.python.org/packages/source/p/pyres/pyres-%s.tar.gz' % version, include_package_data=True, package_data={'': ['requirements.txt']}, entry_points = """\ [console_scripts] pyres_manager=pyres.scripts:pyres_manager pyres_scheduler=pyres.scripts:pyres_scheduler pyres_worker=pyres.scripts:pyres_worker """, tests_require=requires + ['pytest',], cmdclass={'test': PyTest}, install_requires=requires, classifiers = [ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python'], ) pyres-1.5/tests/000077500000000000000000000000001232362746500136755ustar00rootroot00000000000000pyres-1.5/tests/__init__.py000066400000000000000000000052271232362746500160140ustar00rootroot00000000000000import unittest import os from pyres import ResQ, str_to_class class Basic(object): queue = 'basic' @staticmethod def perform(name): s = "name:%s" % name return s class BasicMulti(object): queue = 'basic' @staticmethod def perform(name, age): print('name: %s, age: %s' % (name, age)) class ReturnAllArgsJob(object): queue = 'basic' @staticmethod def perform(*args): return args class RetryOnExceptionJob(object): queue = 'basic' retry_every = 5 retry_timeout = 15 @staticmethod def perform(fail_until): if ResQ._current_time() < fail_until: raise Exception("Don't blame me! I'm supposed to fail!") else: return True class TimeoutJob(object): queue = 'basic' @staticmethod def perform(wait_for): import time time.sleep(wait_for) return "Done Sleeping" class CrashJob(object): queue = 'basic' @staticmethod def perform(): # Dangerous, this will cause a hard crash of the python process import ctypes ctypes.string_at(1) return "Never got here" class PrematureExitJob(object): queue = 'basic' @staticmethod def perform(exit_code): import sys sys.exit(exit_code) return "Never got here" class PrematureHardExitJob(object): queue = 'basic' @staticmethod def perform(exit_code): os._exit(exit_code) return "Never got here" class TestProcess(object): queue = 'high' @staticmethod def perform(): import time time.sleep(.5) return 'Done Sleeping' class ErrorObject(object): queue = 'basic' @staticmethod def perform(): raise Exception("Could not finish job") class LongObject(object): queue = 'long_runnning' @staticmethod def perform(sleep_time): import time time.sleep(sleep_time) print('Done Sleeping') def test_str_to_class(): ret = str_to_class('tests.Basic') assert ret assert ret == Basic assert str_to_class('hello.World') == None class ImportTest(unittest.TestCase): def test_safe_str_to_class(self): from pyres import safe_str_to_class assert safe_str_to_class('tests.Basic') == Basic self.assertRaises(ImportError, safe_str_to_class, 'test.Mine') self.assertRaises(ImportError, safe_str_to_class, 'tests.World') class PyResTests(unittest.TestCase): def setUp(self): self.resq = ResQ() self.redis = self.resq.redis self.redis.flushall() def tearDown(self): self.redis.flushall() del self.redis del self.resq pyres-1.5/tests/test_failure.py000066400000000000000000000041661232362746500167440ustar00rootroot00000000000000from tests import PyResTests, Basic from pyres import failure from pyres.job import Job class FailureTests(PyResTests): def setUp(self): PyResTests.setUp(self) self.queue_name = 'basic' self.job_class = Basic def test_count(self): self.resq.enqueue(self.job_class,"test1") job = Job.reserve(self.queue_name,self.resq) job.fail("problem") assert failure.count(self.resq) == 1 assert self.redis.llen('resque:failed') == 1 def test_create(self): self.resq.enqueue(self.job_class,"test1") job = Job.reserve(self.queue_name,self.resq) e = Exception('test') fail = failure.create(e, self.queue_name, job._payload) assert isinstance(fail._payload, dict) fail.save(self.resq) assert failure.count(self.resq) == 1 assert self.redis.llen('resque:failed') == 1 def test_all(self): self.resq.enqueue(self.job_class,"test1") job = Job.reserve(self.queue_name,self.resq) e = Exception('problem') job.fail(e) assert len(failure.all(self.resq, 0, 20)) == 1 def test_clear(self): self.resq.enqueue(self.job_class,"test1") job = Job.reserve(self.queue_name,self.resq) e = Exception('problem') job.fail(e) assert self.redis.llen('resque:failed') == 1 failure.clear(self.resq) assert self.redis.llen('resque:failed') == 0 def test_requeue(self): self.resq.enqueue(self.job_class,"test1") job = Job.reserve(self.queue_name,self.resq) e = Exception('problem') fail_object = job.fail(e) assert self.resq.size(self.queue_name) == 0 failure.requeue(self.resq, fail_object) assert self.resq.size(self.queue_name) == 1 job = Job.reserve(self.queue_name,self.resq) assert job._queue == self.queue_name mod_with_class = '{module}.{klass}'.format( module=self.job_class.__module__, klass=self.job_class.__name__) self.assertEqual(job._payload, {'class':mod_with_class,'args':['test1'],'enqueue_timestamp': job.enqueue_timestamp}) pyres-1.5/tests/test_failure_multi.py000066400000000000000000000013171232362746500201510ustar00rootroot00000000000000from tests import Basic from tests.test_failure import FailureTests from pyres import failure from pyres.failure.base import BaseBackend from pyres.failure.multiple import MultipleBackend from pyres.failure.redis import RedisBackend # Inner class for the failure backend class TestBackend(BaseBackend): def save(self, resq): resq.redis.set('testbackend:called', 1) failure.backend = MultipleBackend failure.backend.classes = [RedisBackend, TestBackend] class BasicMultiBackend(Basic): queue = 'basicmultibackend' class MultipleFailureTests(FailureTests): def setUp(self): FailureTests.setUp(self) self.job_class = BasicMultiBackend self.queue_name = 'basicmultibackend' pyres-1.5/tests/test_horde.py000066400000000000000000000042211232362746500164060ustar00rootroot00000000000000from tests import PyResTests, Basic, TestProcess from pyres import horde import os class KhanTests(PyResTests): def test_khan_init(self): from pyres.exceptions import NoQueueError self.assertRaises(NoQueueError, horde.Khan, 2, []) self.assertRaises(ValueError, horde.Khan, 'test', ['test']) def test_register_khan(self): khan = horde.Khan(pool_size=1, queues=['basic']) khan.register_khan() name = "%s:%s:1" % (os.uname()[1],os.getpid()) assert self.redis.sismember('resque:khans',name) def test_unregister_khan(self): khan = horde.Khan(pool_size=1, queues=['basic']) khan.register_khan() name = "%s:%s:1" % (os.uname()[1],os.getpid()) assert self.redis.sismember('resque:khans',name) assert self.redis.scard('resque:khans') == 1 khan.unregister_khan() assert not self.redis.sismember('resque:khans', name) assert self.redis.scard('resque:khans') == 0 def test_setup_minions(self): khan = horde.Khan(pool_size=1, queues=['basic']) khan.setup_minions() assert len(khan._workers) == 1 khan._shutdown_minions() def test_setup_resq(self): khan = horde.Khan(pool_size=1, queues=['basic']) assert not hasattr(khan, 'resq') khan.setup_resq() assert hasattr(khan, 'resq') def test_add_minion(self): khan = horde.Khan(pool_size=1, queues=['basic']) khan.setup_minions() khan.register_khan() name = "%s:%s:1" % (os.uname()[1],os.getpid()) assert self.redis.sismember('resque:khans',name) khan.add_minion() assert len(khan._workers) == 2 assert not self.redis.sismember('resque:khans',name) name = '%s:%s:2' % (os.uname()[1], os.getpid()) assert khan.pool_size == 2 assert self.redis.sismember('resque:khans',name) khan._shutdown_minions() def test_remove_minion(self): khan = horde.Khan(pool_size=1, queues=['basic']) khan.setup_minions() khan.register_khan() assert khan.pool_size == 1 khan._remove_minion() assert khan.pool_size == 0 pyres-1.5/tests/test_jobs.py000066400000000000000000000023101232362746500162370ustar00rootroot00000000000000from datetime import datetime from tests import PyResTests, Basic, TestProcess, ReturnAllArgsJob from pyres.job import Job class JobTests(PyResTests): def test_reserve(self): self.resq.enqueue(Basic,"test1") job = Job.reserve('basic', self.resq) assert job._queue == 'basic' assert job._payload self.assertEqual(job._payload, {'class':'tests.Basic','args':['test1'],'enqueue_timestamp':job.enqueue_timestamp}) def test_perform(self): self.resq.enqueue(Basic,"test1") job = Job.reserve('basic',self.resq) self.resq.enqueue(TestProcess) job2 = Job.reserve('high', self.resq) assert job.perform() == "name:test1" assert job2.perform() def test_fail(self): self.resq.enqueue(Basic,"test1") job = Job.reserve('basic',self.resq) assert self.redis.llen('resque:failed') == 0 job.fail("problem") assert self.redis.llen('resque:failed') == 1 def test_date_arg_type(self): dt = datetime.now().replace(microsecond=0) self.resq.enqueue(ReturnAllArgsJob, dt) job = Job.reserve('basic',self.resq) result = job.perform() assert result[0] == dt pyres-1.5/tests/test_json.py000066400000000000000000000014271232362746500162630ustar00rootroot00000000000000from datetime import datetime from tests import PyResTests import pyres.json_parser as json class JSONTests(PyResTests): def test_encode_decode_date(self): dt = datetime(1972, 1, 22); encoded = json.dumps({'dt': dt}) decoded = json.loads(encoded) assert decoded['dt'] == dt def test_dates_in_lists(self): dates = [datetime.now() for i in range(50)] decoded = json.loads(json.dumps(dates)) for value in dates: assert isinstance(value, datetime) def test_dates_in_dict(self): dates = dict((i, datetime.now()) for i in range(50)) decoded = json.loads(json.dumps(dates)) for i, value in dates.items(): assert isinstance(i, int) assert isinstance(value, datetime) pyres-1.5/tests/test_resq.py000066400000000000000000000122361232362746500162640ustar00rootroot00000000000000from tests import PyResTests, Basic, TestProcess from pyres import ResQ from pyres.worker import Worker from pyres.job import Job import os class ResQTests(PyResTests): def test_enqueue(self): self.resq.enqueue(Basic,"test1") self.resq.enqueue(Basic,"test2", "moretest2args") ResQ._enqueue(Basic, "test3") assert self.redis.llen("resque:queue:basic") == 3 assert self.redis.sismember('resque:queues','basic') def test_push(self): self.resq.push('pushq','content-newqueue') self.resq.push('pushq','content2-newqueue') assert self.redis.llen('resque:queue:pushq') == 2 assert self.redis.lindex('resque:queue:pushq', 0).decode() == ResQ.encode('content-newqueue') assert self.redis.lindex('resque:queue:pushq', 1).decode() == ResQ.encode('content2-newqueue') def test_pop(self): self.resq.push('pushq','content-newqueue') self.resq.push('pushq','content2-newqueue') assert self.redis.llen('resque:queue:pushq') == 2 assert self.resq.pop('pushq') == ('pushq', 'content-newqueue') assert self.redis.llen('resque:queue:pushq') == 1 assert self.resq.pop(['pushq']) == ('pushq', 'content2-newqueue') assert self.redis.llen('resque:queue:pushq') == 0 def test_pop_two_queues(self): self.resq.push('pushq1', 'content-q1-1') self.resq.push('pushq1', 'content-q1-2') self.resq.push('pushq2', 'content-q2-1') assert self.redis.llen('resque:queue:pushq1') == 2 assert self.redis.llen('resque:queue:pushq2') == 1 assert self.resq.pop(['pushq1', 'pushq2']) == ('pushq1', 'content-q1-1') assert self.redis.llen('resque:queue:pushq1') == 1 assert self.redis.llen('resque:queue:pushq2') == 1 assert self.resq.pop(['pushq2', 'pushq1']) == ('pushq2', 'content-q2-1') assert self.redis.llen('resque:queue:pushq1') == 1 assert self.redis.llen('resque:queue:pushq2') == 0 assert self.resq.pop(['pushq2', 'pushq1']) == ('pushq1', 'content-q1-2') assert self.redis.llen('resque:queue:pushq1') == 0 assert self.redis.llen('resque:queue:pushq2') == 0 assert self.resq.pop(['pushq1', 'pushq2'], timeout=1) == (None, None) def test_peek(self): self.resq.enqueue(Basic,"test1") self.resq.enqueue(Basic,"test2") assert len(self.resq.peek('basic',0,20)) == 2 def test_size(self): self.resq.enqueue(Basic,"test1") self.resq.enqueue(Basic,"test2") assert self.resq.size('basic') == 2 assert self.resq.size('noq') == 0 def test_redis_property(self): from redis import Redis rq = ResQ(server="localhost:6379") red = Redis() #rq2 = ResQ(server=red) self.assertRaises(Exception, rq.redis,[Basic]) def test_info(self): self.resq.enqueue(Basic,"test1") self.resq.enqueue(TestProcess) info = self.resq.info() assert info['queues'] == 2 assert info['servers'] == ['localhost:6379'] assert info['workers'] == 0 worker = Worker(['basic']) worker.register_worker() info = self.resq.info() assert info['workers'] == 1 def test_workers(self): worker = Worker(['basic']) worker.register_worker() name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert len(self.resq.workers()) == 1 #assert Worker.find(name, self.resq) in self.resq.workers() def test_enqueue_from_string(self): self.resq.enqueue_from_string('tests.Basic','basic','test1') name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert self.redis.llen("resque:queue:basic") == 1 job = Job.reserve('basic', self.resq) worker = Worker(['basic']) worker.process(job) assert not self.redis.get('resque:worker:%s' % worker) assert not self.redis.get("resque:stat:failed") assert not self.redis.get("resque:stat:failed:%s" % name) def test_remove_queue(self): self.resq.enqueue_from_string('tests.Basic','basic','test1') assert 'basic' in self.resq._watched_queues assert self.redis.sismember('resque:queues','basic') assert self.redis.llen('resque:queue:basic') == 1 self.resq.remove_queue('basic') assert 'basic' not in self.resq._watched_queues assert not self.redis.sismember('resque:queues','basic') assert not self.redis.exists('resque:queue:basic') def test_keys(self): self.resq.enqueue_from_string('tests.Basic','basic','test1') assert 'queue:basic' in self.resq.keys() assert 'queues' in self.resq.keys() def test_queues(self): assert self.resq.queues() == [] self.resq.enqueue_from_string('tests.Basic','basic','test1') assert len(self.resq.queues()) == 1 self.resq.enqueue_from_string('tests.Basic','basic','test1') assert len(self.resq.queues()) == 1 self.resq.enqueue_from_string('tests.Basic','basic2','test1') assert len(self.resq.queues()) == 2 assert 'test' not in self.resq.queues() assert 'basic' in self.resq.queues() def test_close(self): self.resq.close() pyres-1.5/tests/test_schedule.py000066400000000000000000000062731232362746500171120ustar00rootroot00000000000000from tests import PyResTests, Basic, TestProcess, ErrorObject from pyres import ResQ from pyres.job import Job from pyres.scheduler import Scheduler import os import datetime import time class ScheduleTests(PyResTests): def test_enqueue_at(self): d = datetime.datetime.now() + datetime.timedelta(days=1) d2 = d + datetime.timedelta(days=1) key = int(time.mktime(d.timetuple())) key2 = int(time.mktime(d2.timetuple())) self.resq.enqueue_at(d, Basic,"test1") self.resq.enqueue_at(d, Basic,"test2") assert self.redis.llen("resque:delayed:%s" % key) == 2 assert len(self.redis.zrange('resque:delayed_queue_schedule',0,20)) == 1 self.resq.enqueue_at(d2, Basic,"test1") assert self.redis.llen("resque:delayed:%s" % key2) == 1 assert len(self.redis.zrange('resque:delayed_queue_schedule',0,20)) == 2 def test_delayed_queue_schedule_size(self): d = datetime.datetime.now() + datetime.timedelta(days=1) d2 = d + datetime.timedelta(days=1) d3 = d key = int(time.mktime(d.timetuple())) key2 = int(time.mktime(d2.timetuple())) self.resq.enqueue_at(d, Basic,"test1") self.resq.enqueue_at(d2, Basic,"test1") self.resq.enqueue_at(d3, Basic,"test1") assert self.resq.delayed_queue_schedule_size() == 3 def test_delayed_timestamp_size(self): d = datetime.datetime.now() + datetime.timedelta(days=1) d2 = d + datetime.timedelta(days=1) key = int(time.mktime(d.timetuple())) key2 = int(time.mktime(d2.timetuple())) self.resq.enqueue_at(d, Basic,"test1") assert self.resq.delayed_timestamp_size(key) == 1 self.resq.enqueue_at(d, Basic,"test1") assert self.resq.delayed_timestamp_size(key) == 2 def test_next_delayed_timestamp(self): d = datetime.datetime.now() + datetime.timedelta(days=-1) d2 = d + datetime.timedelta(days=-2) key = int(time.mktime(d.timetuple())) key2 = int(time.mktime(d2.timetuple())) self.resq.enqueue_at(d, Basic,"test1") self.resq.enqueue_at(d2, Basic,"test1") item = self.resq.next_delayed_timestamp() assert int(item) == key2 def test_next_item_for_timestamp(self): d = datetime.datetime.now() + datetime.timedelta(days=-1) d2 = d + datetime.timedelta(days=-2) #key = int(time.mktime(d.timetuple())) #key2 = int(time.mktime(d2.timetuple())) self.resq.enqueue_at(d, Basic,"test1") self.resq.enqueue_at(d2, Basic,"test1") timestamp = self.resq.next_delayed_timestamp() item = self.resq.next_item_for_timestamp(timestamp) assert isinstance(item, dict) assert self.redis.zcard('resque:delayed_queue_schedule') == 1 def test_scheduler_init(self): scheduler = Scheduler(self.resq) assert not scheduler._shutdown scheduler = Scheduler('localhost:6379') assert not scheduler._shutdown self.assertRaises(Exception, Scheduler, Basic) def test_schedule_shutdown(self): scheduler = Scheduler(self.resq) scheduler.schedule_shutdown(19,'') assert scheduler._shutdown pyres-1.5/tests/test_stats.py000066400000000000000000000023241232362746500164450ustar00rootroot00000000000000from tests import PyResTests from pyres import Stat class StatTests(PyResTests): def test_incr(self): stat_obj = Stat('test_stat', self.resq) stat_obj.incr() assert self.redis.get('resque:stat:test_stat') == b'1' stat_obj.incr() assert self.redis.get('resque:stat:test_stat') == b'2' stat_obj.incr(2) assert self.redis.get('resque:stat:test_stat') == b'4' def test_decr(self): stat_obj = Stat('test_stat', self.resq) stat_obj.incr() stat_obj.incr() assert self.redis.get('resque:stat:test_stat') == b'2' stat_obj.decr() assert self.redis.get('resque:stat:test_stat') == b'1' stat_obj.incr() stat_obj.decr(2) assert self.redis.get('resque:stat:test_stat') == b'0' def test_get(self): stat_obj = Stat('test_stat', self.resq) stat_obj.incr() stat_obj.incr() assert stat_obj.get() == 2 def test_clear(self): stat_obj = Stat('test_stat', self.resq) stat_obj.incr() stat_obj.incr() assert self.redis.exists('resque:stat:test_stat') stat_obj.clear() assert not self.redis.exists('resque:stat:test_stat') pyres-1.5/tests/test_worker.py000066400000000000000000000305671232362746500166320ustar00rootroot00000000000000from tests import PyResTests, Basic, TestProcess, ErrorObject, RetryOnExceptionJob, TimeoutJob, CrashJob, PrematureExitJob, PrematureHardExitJob from pyres import ResQ from pyres.job import Job from pyres.scheduler import Scheduler from pyres.worker import Worker import os import time import datetime class WorkerTests(PyResTests): def test_worker_init(self): from pyres.exceptions import NoQueueError self.assertRaises(NoQueueError, Worker,[]) self.assertRaises(Exception, Worker,['test'],TestProcess()) def test_startup(self): worker = Worker(['basic']) worker.startup() name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert self.redis.sismember('resque:workers',name) import signal assert signal.getsignal(signal.SIGTERM) == worker.shutdown_all assert signal.getsignal(signal.SIGINT) == worker.shutdown_all assert signal.getsignal(signal.SIGQUIT) == worker.schedule_shutdown assert signal.getsignal(signal.SIGUSR1) == worker.kill_child def test_register(self): worker = Worker(['basic']) worker.register_worker() name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert self.redis.sismember('resque:workers',name) def test_unregister(self): worker = Worker(['basic']) worker.register_worker() name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert self.redis.sismember('resque:workers',name) worker.unregister_worker() assert name not in self.redis.smembers('resque:workers') def test_working_on(self): name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') self.resq.enqueue(Basic,"test1") job = Job.reserve('basic', self.resq) worker = Worker(['basic']) worker.working_on(job) assert self.redis.exists("resque:worker:%s" % name) def test_processed(self): name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') worker = Worker(['basic']) worker.processed() assert self.redis.exists("resque:stat:processed") assert self.redis.exists("resque:stat:processed:%s" % name) assert self.redis.get("resque:stat:processed").decode() == str(1) assert self.redis.get("resque:stat:processed:%s" % name).decode() == str(1) assert worker.get_processed() == 1 worker.processed() assert self.redis.get("resque:stat:processed").decode() == str(2) assert self.redis.get("resque:stat:processed:%s" % name).decode() == str(2) assert worker.get_processed() == 2 def test_failed(self): name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') worker = Worker(['basic']) worker.failed() assert self.redis.exists("resque:stat:failed") assert self.redis.exists("resque:stat:failed:%s" % name) assert self.redis.get("resque:stat:failed").decode() == str(1) assert self.redis.get("resque:stat:failed:%s" % name).decode() == str(1) assert worker.get_failed() == 1 worker.failed() assert self.redis.get("resque:stat:failed").decode() == str(2) assert self.redis.get("resque:stat:failed:%s" % name).decode() == str(2) assert worker.get_failed() == 2 def test_process(self): name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') self.resq.enqueue(Basic,"test1") job = Job.reserve('basic', self.resq) worker = Worker(['basic']) worker.process(job) assert not self.redis.get('resque:worker:%s' % worker) assert not self.redis.get("resque:stat:failed") assert not self.redis.get("resque:stat:failed:%s" % name) self.resq.enqueue(Basic,"test1") worker.process() assert not self.redis.get('resque:worker:%s' % worker) assert not self.redis.get("resque:stat:failed") assert not self.redis.get("resque:stat:failed:%s" % name) def test_signals(self): worker = Worker(['basic']) worker.startup() import inspect, signal frame = inspect.currentframe() worker.schedule_shutdown(frame, signal.SIGQUIT) assert worker._shutdown del worker worker = Worker(['high']) #self.resq.enqueue(TestSleep) #worker.work() #assert worker.child assert not worker.kill_child(frame, signal.SIGUSR1) def test_job_failure(self): self.resq.enqueue(ErrorObject) worker = Worker(['basic']) worker.process() name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert not self.redis.get('resque:worker:%s' % worker) assert self.redis.get("resque:stat:failed").decode() == str(1) assert self.redis.get("resque:stat:failed:%s" % name).decode() == str(1) def test_get_job(self): worker = Worker(['basic']) self.resq.enqueue(Basic,"test1") job = Job.reserve('basic', self.resq) worker.working_on(job) name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert worker.job() == ResQ.decode(self.redis.get('resque:worker:%s' % name)) assert worker.processing() == ResQ.decode(self.redis.get('resque:worker:%s' % name)) worker.done_working(job) w2 = Worker(['basic']) print(w2.job()) assert w2.job() == {} def test_working(self): worker = Worker(['basic']) self.resq.enqueue_from_string('tests.Basic','basic','test1') worker.register_worker() job = Job.reserve('basic', self.resq) worker.working_on(job) name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') workers = Worker.working(self.resq) assert len(workers) == 1 assert str(worker) == str(workers[0]) assert worker != workers[0] def test_started(self): import datetime worker = Worker(['basic']) dt = datetime.datetime.now() worker.started = dt name = "%s:%s:%s" % (os.uname()[1],os.getpid(),'basic') assert self.redis.get('resque:worker:%s:started' % name).decode() == str(int(time.mktime(dt.timetuple()))) assert worker.started.decode() == str(int(time.mktime(dt.timetuple()))) worker.started = None assert not self.redis.exists('resque:worker:%s:started' % name) def test_state(self): worker = Worker(['basic']) assert worker.state() == 'idle' self.resq.enqueue_from_string('tests.Basic','basic','test1') worker.register_worker() job = Job.reserve('basic', self.resq) worker.working_on(job) assert worker.state() == 'working' worker.done_working(job) assert worker.state() == 'idle' def test_prune_dead_workers(self): worker = Worker(['basic']) # we haven't registered this worker, so the assertion below holds assert self.redis.scard('resque:workers') == 0 self.redis.sadd('resque:workers',"%s:%s:%s" % (os.uname()[1],'1','basic')) self.redis.sadd('resque:workers',"%s:%s:%s" % (os.uname()[1],'2','basic')) self.redis.sadd('resque:workers',"%s:%s:%s" % (os.uname()[1],'3','basic')) assert self.redis.scard('resque:workers') == 3 worker.prune_dead_workers() assert self.redis.scard('resque:workers') == 0 self.redis.sadd('resque:workers',"%s:%s:%s" % ('host-that-does-not-exist','1','basic')) self.redis.sadd('resque:workers',"%s:%s:%s" % ('host-that-does-not-exist','2','basic')) self.redis.sadd('resque:workers',"%s:%s:%s" % ('host-that-does-not-exist','3','basic')) worker.prune_dead_workers() # the assertion below should hold, because the workers we registered above are on a # different host, and thus should not be pruned by this process assert self.redis.scard('resque:workers') == 3 def test_retry_on_exception(self): now = datetime.datetime.now() self.set_current_time(now) worker = Worker(['basic']) scheduler = Scheduler() # queue up a job that will fail for 30 seconds self.resq.enqueue(RetryOnExceptionJob, now + datetime.timedelta(seconds=30)) worker.process() assert worker.get_failed() == 0 # check it retries the first time self.set_current_time(now + datetime.timedelta(seconds=5)) scheduler.handle_delayed_items() assert None == worker.process() assert worker.get_failed() == 0 # check it runs fine when it's stopped crashing self.set_current_time(now + datetime.timedelta(seconds=60)) scheduler.handle_delayed_items() assert True == worker.process() assert worker.get_failed() == 0 def test_kills_stale_workers_after_timeout(self): timeout = 1 worker = Worker(['basic'], timeout=timeout) self.resq.enqueue(TimeoutJob, timeout + 1) assert worker.get_failed() == 0 worker.fork_worker(worker.reserve()) assert worker.get_failed() == 1 def test_detect_crashed_workers_as_failures(self): worker = Worker(['basic']) self.resq.enqueue(CrashJob) assert worker.job() == {} assert worker.get_failed() == 0 worker.fork_worker(worker.reserve()) assert worker.job() == {} assert worker.get_failed() == 1 def test_detect_non_0_sys_exit_as_failure(self): worker = Worker(['basic']) self.resq.enqueue(PrematureExitJob, 9) assert worker.job() == {} assert worker.get_failed() == 0 worker.fork_worker(worker.reserve()) assert worker.job() == {} assert worker.get_failed() == 1 def test_detect_code_0_sys_exit_as_success(self): worker = Worker(['basic']) self.resq.enqueue(PrematureExitJob, 0) assert worker.job() == {} assert worker.get_failed() == 0 worker.fork_worker(worker.reserve()) assert worker.job() == {} assert worker.get_failed() == 0 def test_detect_non_0_os_exit_as_failure(self): worker = Worker(['basic']) self.resq.enqueue(PrematureHardExitJob, 9) assert worker.job() == {} assert worker.get_failed() == 0 worker.fork_worker(worker.reserve()) assert worker.job() == {} assert worker.get_failed() == 1 def test_detect_code_0_os_exit_as_success(self): worker = Worker(['basic']) self.resq.enqueue(PrematureHardExitJob, 0) assert worker.job() == {} assert worker.get_failed() == 0 worker.fork_worker(worker.reserve()) assert worker.job() == {} assert worker.get_failed() == 0 def test_retries_give_up_eventually(self): now = datetime.datetime.now() self.set_current_time(now) worker = Worker(['basic']) scheduler = Scheduler() # queue up a job that will fail for 60 seconds self.resq.enqueue(RetryOnExceptionJob, now + datetime.timedelta(seconds=60)) worker.process() assert worker.get_failed() == 0 # check it retries the first time self.set_current_time(now + datetime.timedelta(seconds=5)) scheduler.handle_delayed_items() assert None == worker.process() assert worker.get_failed() == 0 # check it fails when we've been trying too long self.set_current_time(now + datetime.timedelta(seconds=20)) scheduler.handle_delayed_items() assert None == worker.process() assert worker.get_failed() == 1 def test_worker_pids(self): # spawn worker processes and get pids pids = [] pids.append(self.spawn_worker(['basic'])) pids.append(self.spawn_worker(['basic'])) time.sleep(1) worker_pids = Worker.worker_pids() # send kill signal to workers and wait for them to exit import signal for pid in pids: os.kill(pid, signal.SIGQUIT) os.waitpid(pid, 0) # ensure worker_pids() returned the correct pids for pid in pids: assert str(pid) in worker_pids # ensure the workers are no longer returned by worker_pids() worker_pids = Worker.worker_pids() for pid in pids: assert str(pid) not in worker_pids def spawn_worker(self, queues): pid = os.fork() if not pid: Worker.run(queues, interval=1) os._exit(0) else: return pid def set_current_time(self, time): ResQ._current_time = staticmethod(lambda: time) pyres-1.5/tox.ini000066400000000000000000000001431232362746500140440ustar00rootroot00000000000000[tox] envlist = py27, py33 [testenv] commands = py.test deps = pytest nose nosexcover