Frozen-Flask-0.11/0000755000175000017500000000000012156420704014231 5ustar simonsimon00000000000000Frozen-Flask-0.11/setup.cfg0000664000175000017500000000024412156420704016054 0ustar simonsimon00000000000000[build_sphinx] source-dir = docs build-dir = docs/_build [upload_sphinx] upload-dir = docs/_build/html [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 Frozen-Flask-0.11/README0000664000175000017500000000035612153050341015110 0ustar simonsimon00000000000000Frozen-Flask ------------ Freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. See documentation: http://packages.python.org/Frozen-Flask Frozen-Flask-0.11/Frozen_Flask.egg-info/0000755000175000017500000000000012156420704020306 5ustar simonsimon00000000000000Frozen-Flask-0.11/Frozen_Flask.egg-info/top_level.txt0000664000175000017500000000001512156420704023036 0ustar simonsimon00000000000000flask_frozen Frozen-Flask-0.11/Frozen_Flask.egg-info/requires.txt0000664000175000017500000000001412156420704022703 0ustar simonsimon00000000000000Flask >= 0.7Frozen-Flask-0.11/Frozen_Flask.egg-info/PKG-INFO0000664000175000017500000000211112156420704021400 0ustar simonsimon00000000000000Metadata-Version: 1.1 Name: Frozen-Flask Version: 0.11 Summary: Freezes a Flask application into a set of static files. Home-page: https://github.com/SimonSapin/Frozen-Flask Author: Simon Sapin Author-email: simon.sapin@exyr.org License: BSD Description: Frozen-Flask ------------ Freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. Links ````` * `documentation `_ * `development version `_ Platform: any Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Frozen-Flask-0.11/Frozen_Flask.egg-info/dependency_links.txt0000664000175000017500000000000112156420704024356 0ustar simonsimon00000000000000 Frozen-Flask-0.11/Frozen_Flask.egg-info/SOURCES.txt0000664000175000017500000000222412156420704022174 0ustar simonsimon00000000000000LICENSE MANIFEST.in README setup.cfg setup.py Frozen_Flask.egg-info/PKG-INFO Frozen_Flask.egg-info/SOURCES.txt Frozen_Flask.egg-info/dependency_links.txt Frozen_Flask.egg-info/not-zip-safe Frozen_Flask.egg-info/requires.txt Frozen_Flask.egg-info/top_level.txt artwork/LICENSE artwork/frozen-flask.png artwork/frozen-flask.svg artwork/icon.png artwork/icon.svg docs/conf.py docs/index.rst docs/_templates/sidebarintro.html docs/_themes/.git docs/_themes/.gitignore docs/_themes/LICENSE docs/_themes/README docs/_themes/flask_theme_support.py docs/_themes/flask_theme_support.pyc docs/_themes/flask/layout.html docs/_themes/flask/relations.html docs/_themes/flask/theme.conf docs/_themes/flask/static/flasky.css_t docs/_themes/flask/static/small_flask.css docs/_themes/flask_small/layout.html docs/_themes/flask_small/theme.conf docs/_themes/flask_small/static/flasky.css_t flask_frozen/__init__.py flask_frozen/tests.py flask_frozen/test_app/__init__.py flask_frozen/test_app/admin/__init__.py flask_frozen/test_app/admin/admin_static/style.css flask_frozen/test_app/admin/templates/admin.html flask_frozen/test_app/static/favicon.ico flask_frozen/test_app/static/style.cssFrozen-Flask-0.11/Frozen_Flask.egg-info/not-zip-safe0000664000175000017500000000000112153050356022535 0ustar simonsimon00000000000000 Frozen-Flask-0.11/PKG-INFO0000644000175000017500000000211112156420704015321 0ustar simonsimon00000000000000Metadata-Version: 1.1 Name: Frozen-Flask Version: 0.11 Summary: Freezes a Flask application into a set of static files. Home-page: https://github.com/SimonSapin/Frozen-Flask Author: Simon Sapin Author-email: simon.sapin@exyr.org License: BSD Description: Frozen-Flask ------------ Freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. Links ````` * `documentation `_ * `development version `_ Platform: any Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Frozen-Flask-0.11/setup.py0000664000175000017500000000305112156413275015751 0ustar simonsimon00000000000000""" Frozen-Flask ------------ Freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. Links ````` * `documentation `_ * `development version `_ """ import re import os.path from setuptools import setup, find_packages with open(os.path.join(os.path.dirname(__file__), 'flask_frozen', '__init__.py')) as init_py: VERSION = re.search("VERSION = '([^']+)'", init_py.read()).group(1) setup( name='Frozen-Flask', version=VERSION, url='https://github.com/SimonSapin/Frozen-Flask', license='BSD', author='Simon Sapin', author_email='simon.sapin@exyr.org', description='Freezes a Flask application into a set of static files.', long_description=__doc__, packages=find_packages(), # static files for the test app package_data={'': ['static/*', 'admin_static/*', 'templates/*']}, test_suite='flask_frozen.tests', zip_safe=False, platforms='any', install_requires=[ 'Flask >= 0.7', ], classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) Frozen-Flask-0.11/LICENSE0000664000175000017500000000300612153050341015230 0ustar simonsimon00000000000000Copyright (c) 2010-2012 by Simon Sapin and contributors. See AUTHORS for more details. Some rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Frozen-Flask-0.11/MANIFEST.in0000664000175000017500000000023012153050341015755 0ustar simonsimon00000000000000include LICENSE artwork/* recursive-include docs * prune docs/_build prune docs/_themes/.git recursive-include flask_frozen/test_app *.css *.ico *.html Frozen-Flask-0.11/artwork/0000755000175000017500000000000012156420704015722 5ustar simonsimon00000000000000Frozen-Flask-0.11/artwork/icon.svg0000664000175000017500000013243712153050341017400 0ustar simonsimon00000000000000 image/svg+xml Layer 1 Frozen-Flask-0.11/artwork/frozen-flask.png0000664000175000017500000006301012153050341021024 0ustar simonsimon00000000000000‰PNG  IHDR€œďFęsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î< IDATxœěw˜TŐůÇ?ďŮN‘ş¤Š(ˆb‰˝@ÔŘŊĹްĹhl?c –h”XEŁ˘hě]T˘b#*Š(*X@ę˛Ëť´…-çýýqł3ťÓ8Ÿç™‡áνçœ;swć{ß*ŞŠ'=ˆHPTUŞjƒííTuőz\žÇăńx<â`ęˆH?œđ[ŞŞßEź6řřXźź¨Ş•ëzÇăńx<ŕ`JˆHW °DU—Űú‹Uu…ˆ <…}ďójŕ÷@.p˛úŔăńx<Ď:Ć Ŕ$‘ U­žď÷Šę×Áöف ŕ `k`g ĐxŘ˜ź Şs×ůIx<ÇăŮ$ń0 DD€=p˘ďh ¸'ôڇb˙ZŁp08¸řŤŞŽm­u{<Çăń˜ő˝€ )‘ہ2`°7pp0°°W<â@UgŠęŞz°%NDž/"űŠHf뜁Çăńx<ˇĆ…ˆôŽNʼnżˇk€%Ŕľ@1pŁŞ.ˆrŹŕÄá§-Y÷‚lá.ŔĹŔ+ŞúQ:ĎĂăńx<źlÉîžÁšy'VUUDÇĹńÝŚŞľ1Ć8ČśTŐK˜{8°-p—ŞVĽv&Çăńx< xWc DdGŕz`ĐGU׊Č^@_`ŽˆŒv.ˆ%ţöSŐE¤G"óŤęł"2¸LDžT՗’;Çăńx<žĆřŔDd3š œ¤Ş×ýDä źĚVՓTui C†DöEU—óW‰Čm"R’čÇăńx<‘x `€ˆd§݀Šj…ˆt‘Ń8îăŔíŔTŕHU­ŽsčP÷Ýç“Y›Şž'"“KDd1đˆŻčńx<'YźDäD` NXý""™‹÷ŕ6ŕHŕ<`”ŞÎOpřA,áÍĚ_üČ&EŤ ¨Şk€[Ed0p§ˆ<¨Şß'¸Çăńx<žMŰ,"=Eä.`–Ş^ˆżwp–ťűK€gTő´$ĸŹŢ`eŒ5ě‹Ë"^Ť)xIsƒŠę4ŕr`?ůS`šôx<Ç㉛MÖ("zWŠęŮ g훋b]€cStˇVűËŁŹĄC0˙!"R,ʼnť–°ŔŁ8ayŤˆ<§ŞŸ¤°FÇăńx<››\)ţ<ŻŞŸˆHGŕ˙pŘŻRՙižoνűąŞŢńÚš¸VqcsUľ2ąťőŔa¸Vső$öx<Çă‰Ĺ&ĺbýÎÄož,"§/Uő˜t‹ż€€qĺd"Ůxř'p]"â Č@Ž> Ƹ:¨Mčńx<Ç“MBŠHAĐÂmĄŞŢ„Křx菪oˇâô?ó|ĺľÁ8qř˘ŞţĚ઺JU:âD`ڈÜ""›%ť`Çăńx<7}  ˆ ŽWŐUŰľ/đ‡D-nIŇ—d˛DDvTŐŠÁşşŰť¨ęy1Ö.Ŕp TU?lnUý!p /ţ""o¨ęÓy2Çăńx6|6ÚŔ@`]ź­ŞďŠH.VnšŞţ¸×qĐ_Uď‘{UuTŘúć=c“‘›pîęTuHsîÔ[á WßŚŞŤ›?Ęăńx<ĎŚÂFi‘aŔÎŔhU]lÎUŐçÖÇr€‡‚çánŮĺŔZč$Ú˙ýD&TŐ˙‰H!Đx -"ĎúLaÇăńx<°‘YE¤3Îę÷žŞžłž× "™ŞZ<\UO {mU~|_`^hŒ$ć Tââ {w¨ęÚdĆňx<Çłq°ŃXEdÎĹű×xKĄcÚáâżłÖśŠVŐ:1€âܲáŻ5+ţ‚}f§8˙ű"R„Ťkřp[ DżLe\Çăńx<.E°ˆ\ PŐ+ýq%`¸ÂsT+.ącPLzr+ÎU]¤Şďżż şˆl47Çăńxâgƒv‹H;ŕFŕ…Dăی1Ç;5¸d‰Éą.ĆĹYŚŞ‹ÓˇJÇăńx<ë›6-Eä2`ŚŞž–158A Nř},ž´Ö>aŒů.Ţ/˛VŢ9ÖÚ#ÖšNž˘Ş #ç‘QŞzoxW Ů \¨ŞIťŰ=Çăń´-Údœ—ˆd‹Č­ŔťŞúšˆœ&"[Śkü ţ/$ěfK:ŕAcĚMŔž@)Îę÷YŘá5ĆüÍł]hƒŞN†VÁHBYŔ˝ÓuëUý—síú^‹Çăńx<žôŃć`Ďv;.Acžˆ\ŒŢ‘ŠŽoŒŮx‚÷ďÖ¸řšßťŻâ’<î´ÖöNڇ ‘ë7<Ő3ĆÓ@Uרęx\6mřůtÄĹôKuýë‚Pˆˆd+€Eä˜őť*Çăńx<é˘M @鍳6]ŁŞóAŔŮŔ2ŕJ ޞ+Ń0ĆloŒ™ŒqR°ŮâúăŽnĆĹž‰+sY°Og`ˇ(Cfł1c5Ƙ(ŽŇ"œˆ‚ @ŠČ]ŔűÁ‡ď€ăĚxöx<ÇÓĆik˝`ËźÝADڍęű"Ň8xCUk˘X OĆ5Ó řvŕŔŕyîÜ? qE™dcrE¸Ň0áÜĹ3‚ˀ-€opBňtŕcĚĂŔ#ÖÚP]žb6("ťŕ’`^6mᫀˇpnîW×Ďę<Çăń¤‹ś&ŰŠę<)N‘,ŕ_Ş:śšƒTuЈź\/"ŞęŒ(ťÝk}ÖŘ+ŘŤóĹ*ŕe\Ť¸IÖÚ+#w2‰çYkëƒ˙ç›××cŽÂŐ e-÷jî<ŇAŕr^döď,ŕ"œë}oÜű슪o‹Čxčńx<ĎO›q‹Čf¸,\Tľ&}ݎˆH‹ÂIUgăÜśż‘ĂĂ_3Ü܊łî‹s߆SŠ‹ ‰qťďŰgcr"çłÖΰÖ. Ć߸.|Jࡸ>ť%ŔÚ`űҖÎ#YDdKšč™léU]˘ŞsqĺovŽŔY;§ťT‰H—´,Řăńx<ĎzŁ-Y qB p‰5đ˙€Dä› É"&ęjÚÜ#"űŠČKŔ߃"ŇDŮýgŕ>ŕmkíˇĆ˜3k­­¤Á cL!Ž‹Ča87é.@.Î-|p 0JD–¨ę˙‡ýŘü[8"˛p0x ŐÂÍ"˛5°ŽDÎj`Ÿ°:€/âDśĎ öx<gŚ­ ŔĽ"28QDţÄý]%"§ˆČhຖ,\Şúˆ|*"żĹeƒ+ćüޤËë¸äŒŸŹľß™ÁÇŕzüFZ1Ćtś ťŕDßl .ćon°űŔ›Ć˜óbU=„†ĚओX‘L\ńéý€KS鉌™+üź¨V­áŠŔY=ÇăńlŔ´%˜GƒŤô\œőŠ+ náÇEdGŕ^ů3ŽxóÚX…•Uu­1ćw8A7 'Ö:ââú^Äőű=ŔóÎeŰ|`Œ98çž-BnĎe8ůpđ.1˘oäüAlŕ|Š;ݤ ˆ´Ă%ŻüˆKŠ9+ŮńÂĆ-ŽúăqĺpžSŐŻ î˝Yl†Kjńx<ÇłӖ`G Sđź3Î Ő(öNU§.ᛁ;qú¤FUçÝq>žćżľÖÖÇczż˛­ľ?şGóp‚'DΊ8¸ĂZűfřƘ7˙#64Xg7ł_TDd‹`ŒmpBt‚Ş>’č8cÂY<łpű =.Sz'`ż`żŽŔُ¸Č§ƒçMâ!=ÇăńlX´%Ř—1 ŽHsxŤś_QŐ šř#đ °XDSŐףŒů?`Žxs6pż1ćpkí|œ%ŻkŒéŒž!žÄ‰žYÖÚçŁ-ŘÓgšĚoćź2,€Şşś™ýt>ÉÇeâfâ,~éj#÷ƒŞŢĚłŽÝ@ŕnŕXU­‘Ýp5ď 2€'ËŇ´Çăńx<뉶$ŤţÁóľ8—cÔőBčo"˛N”|."W˙VŐpň2N(†bŮs1łqâŕĚ(S”Xkc–Ÿ1Ćđ ŽźL÷XűáÜËQëFCDrpBt0'ŐŘžÔ™ŇçľŔUý l %8áyAÇUUą8K­Çăńx<ž ˜ś$Á%fLƉŤFEEäâ`ŸŤŐńˆdŕŹlw‘˙Šj¨VÝǸąđŹU!vAćƒqmßĘŹľ-YŰś ÖrÎz‹ œ9.+aHÄFvI )Ŕ ޓ÷pÉ#?ŻeUľRUŠH\‚ÉLůLUďÁš ßMçš<ÇăńŹ{ÚL@œ0+PՇTľ*(ëÎÝŔvŔ?BTľ>¨XŁŞ×Ĺ"2FDň‚DŒWqnÚH*ƒ˙ƒËč}0ÖÚÖÚ:cL“lŕ ĐxކŘĹhÔŃŕÚ^/ˆH?y çŻöVŐQaâo/ŕ`U­‘ÝDäv\rËaŔ¸DpI9 ×ýx<ÇăI'mMţęJVp8„秉ČUŃzÓŞęƒŔ#Ŕă"˛ .ă7’eŔĂÁó!Ŕ߀S€?‡ísŚ1Śg3ë f­}˜°ú…Q˜Ă:č é."OâZšm­ŞˇŠę˛ŕő"ň0N ž!"ÁżžJUď ę˙ĺăcŔ•€ŠXç'âńx<'­´%ĐUDB"pX´‚NĂqľřމ|]Dňp‚ě4ŕbU %R„ÓWĐœŐńĐŕůš°}ď-ŢXĹZűq°Š.ˆ ŒĆϸ,ÚuŠˆüםä)\äŢŔöaݟsľ?ŽsëvnVŐG"’MňPĂn¸ŘLÇăńx<0mI .롃ˆôöŒľŁŞNPŐýUľIvŽŞV[ƒTő<ˇIľ0÷㸞ŔÍcŕ[ŕDjţŠľś>('˘€ĆÄᄊEŻ3\ŚŞßŕ _{}Ëŕ˙×KwűŠ 'á°+^z<ÇÓ*c2ŁžZeŽu1Iœ łƒpmՒBU?v‘?¨ężE$2›ˇ¸'xţ=p­ľö¸âӁëpą}MJŃD!Ÿ™ŔA'“Ç>‰PŐꠍ^Ž÷ńpU=EUżvé‰s랊ŞÓ[.ÜܕVěgěńx<ž cŒcŽ4Ć|`Œšf}ŻgC"xďNÂi’šĆ˜& &Ň>gkOŔŸăHŚÜä źŽ'ŕÚՎǵœÍjíšŰR˜p؛ôtœ8WČřA™ŽË Žä&cĚŔ+8ËŘŔ^Öڟ˜çYܚ?IuÁń""ýŮ!A&"—â˛w#÷Ý—ÜQŽŞ×'8՚–wńx<ĎŚ†1& řpN¸„ÓçAŤ_×ëÚ0Ćd#€Ťqak‘thí5´% 0—4X'ƒd†sEäO¸?pîßđx˝bDg_ŕmÂŹ…Ć˜ĚȚ€Ć˜­€šÖÚľĆƒ+`­ÔLJˆH!°ZUW„mŰ—Ĺü“ŞÚ°ýNĆuńЈ1:ËUuB’˨{Y–Çăńx<­„1Ś׃˝˘´ĆZwcĆÎŔuČĘ ţ =2qĺËćĆŞ‡żłp•3bUš¨ˇÖzBÁű~*.?VMbpş¤UiK0€Š(b€Şz‡1Ś ÎBö,N,…3^ÇY 7ž°Ö1ĚHc̡ÖÚÉaŰÁuš&ç#`˙tŹ9Xw/` `ŞŞŽ‘öŔq¸;ŠWTuyŘž‚ť˜Î â ĄŞ•‘Ű$|L/=gÝ1‰˚1Ćâ<3Ő1ţ54v‘"/‡–,wŇP-#|ţŽŔ:wĹ"˛ú†ÇńŽ’IKlr0ܘ˛ŮXU­ˆŹ‘mDä.ŕďŻďœmŒa­ý)lř qpî/š 7!D¤+.óřU}ODzŠČŐ83úŞş ĘaçODi"VvłÇăńxZ—hęZ58ĂCkW—Xc{>Ž$XKTˇźË&IŹ’q‘´şëź­%d.ĚLҤ~UľNUg¨j?ŕyž°’(¸;Ą{€Ďpîß>@–1f1f.¨ľ'đ˛1Ś}pĚK@Ś1f[œ%pKRř°D$/pS_< |%"îžTŐKى?9W/ń¨d玃˜É-ÇăiUŽŽ$˘-ę:˘,ÚFkí,\’ÇŰ-ďc˙˘`­ŽŤ=üA ťśşÇ­-ZÁőĎ]ÎÁUő3cĚÇ8Ëß+Ŕ%a/ĎvĆŕŠ'ď ěŠ3•ďëɛCa­kŒ9řho­mŒégŒék­ČşDä8`$pƒŞ~.";ˇă,~×6sÜopŚäJ\§Ö˘8˜ŻiţL6vşwďţzűöíÓBUŤ.\8ÎZűŚľÖßm{<A ŢmƘŻáŽVŐZŮ P{ ŞĆüă‘=7qq‹'ŤjUbť€ńń%Ń`Ť[ŰZ  Y­ LlĽyîvÂľh[ńڑ¸$ŒîÁkĹ"ň1đˆˆ”47¨ľö Ż1&Oƒ€’ńw,N\ţWUOÍ!"qÂđŕ÷4˙> ‰H<ÝJ’!üœ˝lŁôîÝ;kłÍ6Đňžg罖wIkí4uEśh Ž]‰‹ĄÄť€ăăÇŰ7)¸w™€M%ŤUDrDdżhŻYkżĹY÷ŁéSČŞö&0çÖý.ţAŮ˝…Š?Ĺ Č}€Ş:?XOš gÁ;\U'Űw‘~č‰Ş~|ü÷–°—>Ŕ%ĄœŘÂş’%\Ľ­4'ƍˇ¤˘˘â?ë{§Ő‰§3U˛ÜHƒqe‚!%Ń̎ĆÇO1śoR1€Kq'œAęwţ-"bÄÇ=„‹×;$b{°­ľö/ŕzó‘*U=g |NUŸ0¨4˘Ş˙m‘.€ŞęŸƒ˙w‘ qI%ŞęŹ8ĎéA\ľő'€áާ9@ŕ`\œă$Â4\„Ůž \ę̞=›˛˛¸nŹ[¤ŽŽŽîÝť3uęÔľÖÚĎÓ2¨ÇăiËÄ*͒2ÖڼƘ˙ĂUĹH¤@´ś˘žnl|´Ś¨o–ś$—Đ`LĽ¤Ęí¸ÂÎŞz^´}Źľ1Âe/EúŢď2ĆěÜj­ýÉs>p݈L>TŐ3DdgU˝2lN‘íqV˛á"r+Ž’úĎAÝe"’ ŒÂeqý 'Ţö‘Ľa˝v›c2đ…ŞŽ‘9¸Î%óq°Ľ‚œ #"ăj6ťžäšţúëŤŢ|óÍŠC‹ŠŠŢĚĘĘJ*qŁŞŞę ľk×2lذ̜œœuŇkÚăńlô܇ó‚Ĺk˜í7;Ţzw›:ą,Ľ­ţţmT0^Çă˛j_na÷'q˝Ë‚°íY¸$‘‘Ć˜gp.×ŕ`…ˆt DŘgŔŞZŻŞjŒůDU Dä"ňb g‰ §áęőÝŤŞw„­÷-ŕ’ †ď!UmÎ4´- we?ŕn˝ˆ(ĐODĆŕŢżŔËŞšp6X}qep|GŠŻŻŻŤ¨¨¸*##ăE‹`­MęŽ~‹-ś˜°xńâŽß|óÍv$ţeíńx óx<ž4ŕ`j´úű×f~ԃlŮ œ.™ĆJŕÔ8ĹÖÚZ\ÜÜ渤X\Ëžă0č#"ŔĹĄUőmUýŘ\U?ĂYюùƒcúůUu™ŞŢŠs '"7ŠHďˆÝ‹ƒç+q.äČq–Şę8U=×U¤.ŤÉpASp)"Ό0O|ÔÖ֞œĘńeeeSVŻ^ýKmm­Yž|yŹ,2ÇăY_x˜­ŽĎڒœĽ­ ąÓ˘c˘Ş_ľźWîţ‡łn+uÎŕ6k­5ĆôĹŒř—”ń°™ˆ 2uë7Tu•ˆÔsTőŻŔuŞÚbFTPđďKx˜ˆěźŁŞ‹4dh­ ĚJ*"ƒK`řXUŔ'qżaˆH7œČťˇŁAüzÚ3ęëëC…[ç­×•x<OSźLMǰWÖd溘ĚZť×äVŕš(ťěJC#îUÁúŔő> Č J˝ü("ÀŔ"ň{U \."׊ę š1ŢuŠj­Ş>ŻŞăqľᏣżŚčÓ¸řHůKźăÇÁn¸’ßŕ„Ś€mYŔ\ą$qĂäńx<­Œ€ŠąÉ ŔJ\‡ŒÖՄAůŒ7q.ÖHWpWŕ\cLGkí¸ň1!†ßcžÇľŃ9¨VŐľŞz0đ"p.𔈌^Mf}ŞúľŞNÇ}VĄ’6+qÁ˙Ž‘+#O’:œ?'|;…´öŹ‚ś„_ZkŁ•`đx<ž˜c˛1;cZŤ]Ľ€ŠąI%€łjlŽëґvD$ŘXQď\çKp˛öÚi@Š1ć^\Ľôƒq1pÖĘcD¤NUĎ>‘|œ@;YU#"}Ddfę´) ŔJ‚„p"QDö^‘jUýGJŠž-"×㬜〝SĎÓ:Xk6Ć$ÝţMDňÚˇon~~ţoWŹX1}ɒ%wÇĘZíŐŤ×I@żˆ×J—-[6iůňĺ_˙kŽuŞĂ[ňóóÓž}űßXkĂă\řŠŹŹlňÚľk§ߍę’VZ˖YYYű”””ě§ŞýHń [Uż›7oŢŤŔGé^łˆädddœÔ­[ˇírssˇşŃx˝ ,[ťvíOK—.Q[[;AUIó6ëŘąăŮ]ťv˛bŊ/–.]úŞFí.$"˝˛łł÷-**:÷›ÂŞęŒE‹}\[[ű‰ŞŽoŃFĘ5Ŕu¸xň>­0ž€ŠąÉ ŔI@?U­K码(ť ÷†Ţ™k­­1Ü<Šëł.;ă’3ŽŔ5źţ/N>Šë^r#"űXk‘Ž8a8cX L6ĆL˛Öž’Âi`)a@U?‘ßϊČUÚS8Däl äRž œěXžĆt­ŔĄó IDATîÜ9ˇ{÷””$tgdd,[ľjŐ㕕•Yk—†˝ô|˛k)**zýźóÎŰëŕƒÎţţűďżňĘ+GˆČ ČDŞnÝşÝťwďżwÜqšĂ† ëŘŤWŻFă,^ź˜ŠS§5iҤĺď˝÷Ţ"9ZUg$ťŽXtčĐᤂ‚‚1;ě°ťě˛KűwÜąý€h׎]Łýć͛ˇçôéÓOž2eJՔ)SꋊŠŚ—••ÄŇŚ…Ž]ťąőÖ[˙űüóĎßlçwÎęŃŁ-4ôiUeÌ{ź÷Ţ{'>ýôÓľššš'­Ył&-}žóňňö/))yřüóĎ/űäöěŮłŃ:gϞ˝×´iÓN}ꊧŞJJJ^(--˝XUŤŁNęiŽÁż­ĺ Ü(`Đš+GU×´öT­<žűƒj+`k`BÇë\+f<¸ĽýEäzšFDfˆˆĆx,‘Şŕů"ňw`ވü.ƘÇVŔEd@ çňjŘómp––hűew;Ś0ןqYĎkqíŕ_ß×ƆřčÝťw…ډ%K–čÝwß]S\\üy:Ö´ŰnťíĘÂç5jT%p@Ř>ŠŠŠŢ9rdĺŞUŤâZç—_~Š[o˝ő’‚‚‚sÓąÎ`]ňóó_?üđ×.Y˛$ѡN_y啺’’’EyyyǤkM›ožů× ,Hx-ńđË/żhIIÉWŠŽčXTTôň!‡RQZZšĐęęęt̘1ŐĹĹĹóÝҰ–˘˝öÚŤŃ".šä’eŔ~ÁëŇľk×?őíۡüů矯‹w÷ß˙šÂÂÂ_p}××űß|k?D¤s3żMš ŒÓ#츏ҰŽ1QÖ37Ýç3ޜnŒšUDž‘i"˛PD>1Ć<†kś“ć9w5ĆÜ/"mPœßňŕwýCyÎsp8Đ.Á÷.7ĆçyA3ű""‡‹ČžâZĘö‘n"’•Đš­ď :ʛ˝Ř>Ĺ1˛qĺY&„˙ ĹńAd‰ČÇ"2JD*#> +"Kb|PU"r‘ˆ4ůŕEä(ŮMDv‘Y"’Ôšá,—Ąç€o[Ř˙p  Á9LŘc\âG?`äúž.6ÄG:`ˆ}÷ݡLDúŚş6`‡#F,űšçžÓ˘˘˘[ƒ×ˇ,**šČqˆľk×ęŔˀmҰÎm <ýôÓ5‰Ž#œĹ‹ë¸Ź¸¸xlŞkRU ÖŐ%üÖÄMQQтTÖô*((řá™gžY›Ę:ć͛§ƒ Şč֭ۨ×3ô‚ .hô÷0nÜ8Űž}űˀ­ żžě˛ËŞÖŹY“đ?úč#›ŸŸ˙Đ!•5n4 ŔaÇ=™†uE€óŇqÎ8KŘ"ňʈÔ4sţĄÇĎŔ!i˜wĎ@ôľ4_äc&Žujźď],őoNDÎhaţjY&"KE¤BœŃŞ\DĘDäŤÍ$ö–Ŕť¸’,Ɏ3 xכđUďąAmŔsp˝˙ {yŞ~ĄŞáb˙‹sĹ>ˆŤĎˇ'đˆ1ćĆ˜Nacž,˛Ö~†s×}`ŒŮ'‰Sű(ôD]‰—ŹćvVŐ׀\ÉI`Ž?ŕŢ3 tžöžL|šžÖ 33SpiRe‹Aƒ…'1`Ŕ˛łł“——ˇďŢ˝?~ë­ˇúsĚ1‰œÍSO=UPXXřœˆ$||éQXX8qâĉ%'œpBł×{KtďޝˇŢzŤó~űíw|qqń­ŠŒýů_|‘ę01 ú'…ˆ|ňꍯn9|řđěTÖŃłgOŚL™ŇmȐ!7ŢŃň1ŮbŕŔmŘb .ęßż˙ÇoźńĆvwÜqÇf99‰_Ú{íľ—\}őŐ˝ťvíú§Öˇ1ţcLŽ1Ś›1ڎ1f[c̎ƘýŒ17˙ ŰľľZIŚě‘Ó+Ű;¸šşń|ô‘çE¤_˻Ɯ÷"ň1Žn˘ô§!O b˝3iHF..t­ .Öˇ;îÚ(Ŕľý]hÇśx9đđ1Îő˜"˛M0Ć4ŕ0u]6ĆZű1ć]\?á3Š¸7ł§ˆ”ż¨kůvO0ߑŔ]Ŕ¸@ÚŔľĆ˜€ŃÖÚŐÖÚĐŘÓŔEŔŰƘ㭾‰dżń˙zÉĐfîUuŽHBI{§‹Č,\ů››p˝‹ŸL` O+QUUŌ3V[kÓ_—•››ŰčďßCEEĹ xf„ ] š´lŮ2Ţyç~üńÇ5Ćşté’Ó§O9řŕƒíˇýöŰsŇI'őž˙ţűO$‰ëGD:LzîšçJśŰŽéwiUUďźósćĚŠ]łfMŁżŢ˝{çnżýölż}ÓűČÇ{ŹăĄ‡znqqńüŇŇŇ{]WˆŠŠŠ÷‡zřá‡^uüńÇo6sćĚÚDÇŮvŰmsŽ<ňČFۗ-[†ˆ4÷ßě˜Ż<účŁEťîşk“żýďż˙ž)SŚP^^Ţä}ËÍÍÍěÝťwć°aĂČÎnЍ999<ű쳝GŒqfIIÉ҅ ޜÄҲŒ1Žˇœœ.ťě˛âkŻ˝6+++úďúĉYşt)?ţřăšĚĚĚě˜=ö؃üüFZ‡3Ď<3űoű۸Ÿ”‘Ź.]ş\ÚĄC‡cŹľůŞšÉČČXe­ž`Á‚k´q˛akóƒ1f)ŽBD{\é°x˜Űň.I‘ĘMLoyHD~ńŇBU˝ —´9ĐłżŞŽŒ2WžˆÜ’ÄÜCEdlÄć×Uő\Ýŕ/€Í€mŒ1§Ťępšž×;űĽBÔ÷ĎZű‘1ŚŘ—”zIŹ}C‡ŕ>ăqë0ôB[€spÉOáÚÂ҈tNŕ+9?YᎾöîŕ.éBœZ>×Řgš*ü[űVŕ9œhÚ;Řv50Ôs¨ľś*÷ c̅Ŕż€Œ1gYkgMÚ´ Ëd\|^łE€5dCwmć~ŚkDaęŕőƒq×D\äOpÝUҚ”ł)óîťďę´iÓâΒ-,,Ě,++Ť/--];nܸŐÖÚtÖ{lÂąÇŰqěŘąŮá ÄK/˝Tá…–×ÔÔ{ď˝wŁ/6k-×_}ő§Ÿ~šó駟~ZUUő .+„vëÖ­_^^ށ›ožůŕńăÇwéÓ§ĎŻ/fddđňË/wÚu×]o‘˙ŞjRmôŞŤŤż™4iŇŞŻżţşnć̙ă2iă"++ëŔ^˝z=yňä&&Ż^xĄn͚5ă’YW~~ţM'Ÿ|ňŔC9¤ŃRmm-—_~ůň{îš§Ł1ĆvęÔiђ%K.8<7??—+ޏâř§Ÿ~:×]wýőc ăƍëÔż˙‹EäU]–ĚúÂŮi§Ři§˘*ż/żü’SN9eńœ9s–öë×oËožůff‡ú´oßţáĚĚĚţóŸ˙ďšçžżz°:tč@—.]˛7hJÁů%%%/žvÚiż=돳Úmžůć“˜ŁŹ˛˛’Ď?˙|Đȑ#÷‘Tľ4•ő$@.î7!QZŤËSRPD†‰Č“4.uP§ŞĂT5$ŹŚOŠČ8y %j%z=žČpad¨ęŞz{ÄŽU¸ßŢwDäŻ"ňa™ÔƘtTΈyáYkWăŒBocśĄŠĐ­^ǝ^ľÖފ:Pşcâô­œe)+lŰV@ N¤œçű‡âŹn1?öÉŹSD‘űqĽ^V´ŕwŻ—†ŕĐ%"ňVŕ{Ÿ*"㎐†Ă'D¤(‰÷đ`Ϗ٠x''Ĺőî†+÷˘až@đŸ°ă3‚mf}\7Ú#Z ŕÉ'Ÿ\!"7‰ČȖƘGÚľkWŸ››űCˇnÝJ”ÓdŔ‰wŢygmřÚbĹ´Ykőꍯ^QTTô>Đ9Úx›mśŮ¨ŃŁGŻ?ná…ګWŻO“Y_IIÉühk9çœsŞDD;tčPœŢÜ™™™ű–””,úöŰo›Œ3f̘šŹŹŹSSx˙zdffŽ*..žYTTTžČą;w>f‡v¨XślY“u͚5K{öěšŘ!‰5IQQŃŇÚÚFŤ.Z´H T‘ŸŸe—.]žíӧϋ͝[aaáO/žřb“¸Ëťďžť6//ď˘$Övî<Đä|#Yąb…ž{ĹĹĹ_ßWÇćääXcĚ=zô˜Œ5ŕŔ\yě!C˝][Ä:óˇß~űőc'øqăę ţžĘz"Ň| `˛ŁÓ°Žh1€ĽIź˙íEdA´uâ:jE=Îs[Œc&8˙ÁcX\ܖŽ+‘oΛ’Ŕ{+đŇ8ŽÝ#ĐáÇ} "ŰĹ3÷:ľŠČŔpbâTŕ-UýQD–ŕŹhÍś×ßś'ŽKĹl T›oľś3đAK>xFD.ÄĹźB؝AŠł˜íˆ¤™Ŕ鸘ş[pqżłÖΰ֎7Ć|Œ+-sp„1ć/Ŕ=Aßx˜ě—ČɨęŔyâⲆŕâ.ĆeW…˜‹ë‚"Ŕ‚°ăëE¤ Ř^Dz•Şúq"kđđşľö-ídŒYS]]}Zǎ_ČÉÉéÜőľM=FŐŐŐ >|ŮÔŠSÇ-Z´č ží"Yž|ůë~řáu„Ý…S__Ÿ°EBD:lťíśMž›nźńƕăǏ˙Ř}ŐŞU† Ć×ÖÖ~ "ż=äCޟ}úÔef6^ÎA´ř§Ÿ~^]]=)//oPFFĆ+VʈY—TUˆČoÎ:ëŹĎúőëˇU¸+}ŕŔ™ÝşuœÄÚZäŐW_­5jTEUUŐu•••‚óđÔÔԈˆ”-Y˛$ôyĎř駟š\‡%%%4”ŕJ–­vÜqÇ´”ßŘe—]LNNΠtŒ'Ăpnžv1qľ]÷Š8ŽľĘ$3îP˘[1'ŕ QąÖŢ-"WDyi \,{źě94Đbx‡Ş–‰Čq"2÷^o'"ŮŞZ“Ŕܑ4űţcĹYřB–ÂrŕrkíăńNĐęI ş˝HDŚăL–]pâ¨‡Şžąűs¸`Ĺ÷Zś?. ńSUýžńĐQ\Œ`BXk-0BU{G¨ę}Á`ä!¸˘š˙ß 'L÷ÇŐü'oŒůŐÝa­c­˝(Ćš—>3ĆěM¨+űŤéŢÓÁsş1fĎ8Ž­WŐwUőĎŞzŞë¸/Řĺŕ%\"̂ˆcUU§ŠęëŔ˘ŕłMHˆzâfyűö헯XąâŤŇŇҤ:ɤ‚ Řu×]—üď˙űă‚ .Ž%ţ/\¸°Ńkm(ą*Q <¸‘ľÖňŕƒV­Xąâ@œ§â褪ߗ––^9nܸFn @Ô¤äŚ B:–gggϏg˙îÝťŸ7`Ŕ€1ďż˙~ń÷ŐW_ą˙ţű—/^źxcL-.¤%Qú 8°ŃűVYYÉŇĽKUWWOXłfÍôŹŹŹ…K–,iśwşŞVVTTœ7f̘Ęđí}úô!33sËXÇ% 8蠃–ž}öŮćΝť]Hṵ֖̈ŻYłfZ°6­­­]švmăhŸ’’’lÜďB*Ô×ÔÔÄíĘoŽššD$(ďXkgXkż´Ö~d­}ŰZű’ľvźľvŹľökíŢ8/P8mIž‹ëuÎLUŃÂďüBœA(’ÜD&‘]#6eűĆsl ENĹľč|”BBb-'Ö Ć˜ËpÝĹB_$_;%"ţ  ˆd‹Čy8Ń×8(RŐ3Tő-’ź ŞĹŁ`Ť¸:7ŰÓTőýhcÄŕ;Â2_ÁZ[|Ě‘qJd˸áˇŢY8Q7 —$ň"Q.FkíZkísÖÚÁłç1Œ1Í1-eđ}`ŒŮ˜łä% şS€Âŕ­?0☟Uőn .pl›Ěܞ˜,ĎÎΞ‰ťł[§–ÖĎ?˙œ=÷Üłtć̙‡UTT<ÚŇţŞş˛ŹŹŹ~Ŋ†ďî3f‘‘‘L†á–ƒn¤f͚EFFĆLuą°“322VŞjy<ƒŐÖÖ~üß˙ţˇŃJŤŘ-‰ľýŠľvŠŞVZknißÂÂÂËwÚi§Ń'N쒗×8TéÓO?ĺŕƒ.---ÝGUżWFbeô‘šĽďŔĂť˘0gÎ233ĂžŤŠŠů‰řúG˙˛xńâFžWŻ^ÔŐŐőHbmM°Ö2f̘ľťě˛ËüI“&XZZzxäwżŞ–gffVănvż mĎĘƚ=gNăK+;;;ƒř˛D›ăÇĎ>űŒćďuâă˝÷ŢŤYšreŞ ZƒŤp B´¨ŞŐŞz„¸¸ş'Tő(U¤-Ĝ7§Ń<‡-‡"iâe‘ą"Ň;žƒUő9kmďúúúsT5™›ßFSGnÚ÷=ŒŤőŇoďűXkcţVǢU\Ŕ"2'¸ĆŞjäÝFsÇ]H W­¸r&Uš\‹¤_H­ŐXko5ĆˆČĽ8S{ČďíiAp…}pč3ŔcĚŔ#ÖÚŻ#÷WŐ cĚÁ8k].đóúOĆĹ6iůĽŞ*"˙Ž6m ´ŘVNU?‘€3Äľ¤űg Ö"O|,_ž|ů—Ŕ4kmŤ´3‹Ćřńăk˙ô§?Í.--ÝOc´ęŠĆÚľk˙v÷Ýw9rd^uu5GqDůüůó/Mb ™YYY,Yuuu${žƒŽQŽ‹EM4ŤN*ĽVŚTWWožrĺĘéÍíT\\|ănťívá /źĐ9Ň=;iŇ$=ţřă”——ďŁAë5kíTMŽűFÇöíŰ7š řŽ ?÷ďTő'âkˇYźďż’••…ľ62Ř>)>˙üsnžůćo/^źŻ6ÓÉCUżÂ݄‡Żš&rmé@U—–””<1lذSĎ9çœ.Ą.$™™™Ä;_uu5Ÿ|ňIÍíˇß^śtéŇ;ӞȹÖVcžĂ…(A뢪“p]Á%Ú”P[J™ĺ§Ť‡ˆĚĚČČxĐZ{‹Žť¤žFďŸ1Ś;đ.÷!ÄcŔYIzZŇ+ƒŘźÓpEŠOkaß,\rFeđ˙ý€?Şjßhűk Ů˝°Iĺ"—`œIŔĽ"2ƒÎĚŰ܅ž XŽsąŽ5Ć||d­ý܇j­­0ĆlTXk0Ć솳Ĺ ×)Ƙ8qü!đA°_¸řř?kíűqœO˝ˆěŹ ŮTἂ‹cčKŘ]wsÇDdGŕ>šYUăryb˛˛žžţťu%ţT•ŤŽşjĺ¸qă>---–¨ŠŹŹ|oňäÉŤnźńĆŹÚÚÚĚĚĚĚkƒďtóžľ6ᐎV`jEEEoš‰1***şkčĐĄ#Ǐß92›t„ öÔSO[^^ž—Şţę?ˇÖ~Ôd ô1ËZ;ISËěO‹ĹHUÉËËűĽ9ńPWW÷Fäu¤Şőőő­Órzá…—fee˝6eʔcĘËËĎëÔŠSiuuuAAAAd)ŽX,[ž|ů•••iD[Ĺ6ÄĂ4Ŕ6cL‘hE$şHŹľŸÇ0ęä¨ę…"rVFFĆ?­ľˇŞjkeO‡řu!ƘA¸ßćp}ôĽľvd*¤MŠČnŔŞÚ˘Ĺ(ŕźŔ}â>œPj‹Ř q˘@UŸ×ó÷¸÷đ1\íÂÓq–žpڇm;.x,5Ć ŽŻ7Ć ĹŤ.5Ć|‹…źŰZ{Ł1&çÖœ<0Ɣá‚zĂ/ú1×ŕ,%58ąřm°ß=ÁóWDdŞž`ŒŠڇ'ž¨ę|ySD ů‰ oUăŢ,"“UőŠDŽ÷4bT™’dřá‡xôŃG*++ű]’Ü9ß}÷]V§NćUTTô­­­ý,í‹t|V_ݍęJů~ф˘˘˘ą‡vŘqcǎíůŁňâ‹/֟{îšłËËË÷Œň’VwUUŐ P炂jjjşţŚĽăşvíşYŠ7Îé˘IżkU­w!Ú­Cmmí˙fffîÖž}ű/rss˜3gÎa­6á:ĆZ;Éó0˜8bi“d];"˛­ˆD éHHŞę+Ƙˇˆ]:WU/‘ł322°ÖŢ͛–&rŒ1ǏŇÔëą­1ڧľ6iCK&Ŕ ƒŽ-)):6ż  oVFeĺ‹ç”•-zyę´i×Ǒ`ˆ XUŸŔÜ]‚T吅) č/"[ŠËVM7Ľą\Ÿq°—ÝŠ1(Q՗p?ĐçăĘĐź+ȸ%pNtŢ+ťŒ€+Ăr 038Úóđ.Áä2\–ńáŔu¸lˇ;qŮŔ5Á˙űŕ*}ď‹ë ÎBYGClĚŃŔ‚@ŕ äcśľÖţś–'q_ I˛ îz/‘SEä!ŕŠ–b8Íł ÉĽ‘äŕ´ÁyÉNšĎ^{uÓM7>꘣ĂăŮś˙`Ҥí˙ďškNÂŐbЉˆěü¤Ş‰ŢEü+x„ *lj¨ëh0M§“oq璬lG˜Ä ÂĽŞúƒˆÜłĚĺá˛qśŔ‰ŤsqⰒ–łÓЁŰp”ŻŕĘ´ôÄeD×â’3Jq™‹8a7 °Ô[ƒť(Â[ŕŁ p™ÖE4X€7‘v@×Ŕ ¸5.#+iTőąŕ‡ú1­ŞmĺG{ƒŔZťŘ ÜčíÚľŤ2Ć,fhŠy[`Z+Ž7QÄ_FaaáKgœqĆĐŃŁGG˛ĺĄ‡Z{íľ×~W^^žo,÷`:ăg;uęÄQG•ŽáČËËkŐrDńĐÚŔÖÚwŞŤŤ÷X˝zu2eÄÚ4A•‹Öü,Ó.OÜÎŔPq;öŚůxŕ„ďTužˆ ‘Çi9y4WU/,‚÷[kŻLă÷^ZÎ]8Óó¸ľvr2dnŢłç Ł=ŚÉ űB˙ţ[÷4hPöôéÓŁŚ˛‹ČQŔgáń+ń˘Ş‹Ä5'>XU'ŽĂŔ‡"r<îây6 dœ|…ű0?LâŘJœœ‹ťŁ Ŕ5Ŕ“AÜÝűƘUíA\ŠöN¸zzŰá„× œ@;Ž ]ˆžď t˘Á ýî˘ß#lÁY%”ÁWŠ€#qâŕ/Ŕ Áól\"JJ¨ęŒŕóźEDvUŐ{RÓÓvÉËË[h­]†ťžZ4dŘĽÉ.,,|ăâ‹/ŢýŞŤŽŠ a̘1ŐˇÜrË´ňňňýZŠ{kŤ,Y˛‰fľ\§´f `/Y˛d ߑžřI‹‘öŔH9,ÁIRI O.‘kq^ťćČRՋEä9)Ěł™.VHCR&đ”1fkm—ćçYł~yjüxęëŢ'k-ożő6ß~÷]i3âďˇŔ˛dÄ_WŁDd+€`ʁŔY8Áó\ŕŞL™ ›1Ąš@a,ŁZ`Y0N..îďמÖÚ2kíh\—ŽrŕFœ ěŽűÉÂŐA|›ćă#ÝÇG˝’Xˇó~ƒűC˜ œŒ+ÇóźŞ~i­}ĂZ[/Žçâa˙_¤(ްŐ5Şz)0ODţ$ y6BDä‡úúúeńşŢ˜‘ź‚‚‚÷Żžúę=ىż›nşiő­ˇŢ:šźź|Ȇ*ţćϟˆ$\n"ÝŹ+ `˝š6bqŢŔHIŠHIFFĆ-"2_DîĹľ; ‰ż ŕyU˝HU‡Şęš1†Iú.AwŠjU˝ˆ'o[y7]şgpşçUHôěýބő÷M„ĚĎŚ|>ŕĎW\1ćž{ď=´¨¨¸Gff‹J-œż`ţŰ?Ϟ}A´ƒDds ŸŞ>œĚ¤!TľBDNţ)"WŠę/A|ĎAŔý¸X´ŠČ%ŞšL=ąđ5çŇB‡‘fXJCQǟq*<XŠŞM*öIoŒ1Űâ.ŢÍqÂ1 óWƒSő?ŕÄY%ŤÖžs˙Ž ŽŰ—ć‹×*°řxx+°Ć`ŒÉj­ľu"’‰ŠĎˆČž¸:…Ăƒ˙wÇšœÓŠŞž$"_cDäŞężT72/^üżúúúj`˝'iŹ+D¤CAAÁ¤ŃŁGo{ć™g6ÉDźňĘ+W>účŁď—••Ž›ŞxYąbœzNIMM _ýőšŻżţ:ŤŚŚŚ5b´BU×I `Ŕ>†9)’íœkŒš[DFŞjxř’^PŐżăš@„‡JL2ĆÜHS YʉşÚŁ7‹ČÝ8ƒŐŸi>ĆżťˆŒŽHqęŕ$kí Ą Ƙ3pů‘ďípcĚYÖڇ™ 3pĽ\.x™@řĆ: (ßr˛ŞŢšČDąPŐ*9ř{P:d^`i;7Č&ý'pŽˆÜMl%@'âSđŃ€Çâ`\˛ÖÚo s!W˘7.Ľ§,Šü IDAT;wŃNĂĹÚ'ţB-|şáLĐíqŽě*\mĂŮa˙ÎJ­ľQ`ŹľáV‡ăq WˆČ`ŕ œĹ/TŕzI:ăÂ â+Î ćŢQUmy<ë‡UŤVMÇš[%¤­!" >źëŽťú1˘IrÄ\°âůçŸŁŹŹŹĽN­ÂĎ?˙ĚÉ'Ÿ<;'''˛ëR܈ˆŽYłćĐŞŞŞšŔŢƘ´%Š$Ëş˛$• çI\ŠHGyEU‡Fź4MUUŐ抮Gű=NŰEÄěŢ*"c17Şęٸxüh."űŞj*ąŁo†‹?kí{Ƙ=ńăƘ­ľq˙}F çÍ:gKŞşZD.‘NŞ:=Řţ79˜LęÁŞK ëSšÄą!B0‡8`$AđmH´­S‚Tů=q $ŸÂŃ_ăz4‡ďˇ/îÎ7í)îę:¸ÜńŢ ÜŞžfŕĆ—¸ŃŠÜ@ÔŹ]ťśŽ°ď§N:a­MÖ­Ňľ[ˇnMJRXkSşÉ‘üüüüţőŻő;ꨣš`>í´ÓŞ&L˜đܢE‹ÎLež¨'˘.ŠˆĐŠS§of͚•tŚ @çΝŻQŐbUݧžž>%oL:X—ŔVNfژIHŠHy‡Ś^Ž/TőwqXaىą´_$ŞZœ/"÷‰ČÄÎŇLŒĆqňIŒíWŕ<Š}"śçOcv‹0řÄ$Ą:="’tSŐ´×ëSUŤŞŻáúču Űţ/œß˙ŘÇŻVőű%ÜXC×ěőĹ "y¸‹ő"œKywœ¸>_UçűäŕLď@ӞŒiEUßÇ]ĚljČČ6Rs̓Şšt™0/Z´¨ŃßVqq1˝E¤8ŃÁşvíú‡#<˛Křśşş:ęëë“vNJHIAAÁ§ăƍŰ"RüŐ××3|řđŞ &ü{Š?€Y3gÎlôÝźÍ6ŰP[[ť{áŸ4UUUďŃp#œîr ŁŞuëĐčIŽD-€ Jˆ“ŞŢ§ >%("íů~QŐo­ľiŒŚAčW*Dý=´ÖŽÎ zŻáí€âh#{‚f¸ W—.­ˆČŻŮŹŞş\UĂ-n¨Ť/¸RDRý2­¤Ą‘|"„ Ŕ3iFÉ4l_/1‡O–Ţ5¸D”OB׀+˙ż˝3˛źŢđý|!Dʖ°ˆ,"(Bľjľ.TŠâ†kńW°­˘u­ťĽ­­Zľ‹Š´ZľÖĽÚŠ(­k]ęRÜPD¤(¨@AJLXڅdÎď÷: 3É$3“ É{_×\!ß|ó~g&!sćźç<đ•™M6łŹ??3Űlfw˙&HN}yZ/ËćϟżÝ˙Ӈ~¸K=fHŞŤ˙fşuëvÁ°aĂƝuÖYŰź9,]ş”źźźFUˇ%őéÖ­Ű{ÓŚMë;jÔ¨mÖݲe 'žxbůŒ3&—––6Ć/ćż˙ţűŰěJpýő×5Č$>F"‘čdwŤŞ63RţÝo4´˜LŻč—H7|DŇb9S‹”1ł‡$=™ŕŽÂT/äx"g"‘Čk8˝D|7œl{z+)'€ŃŞ\XţĚ4őV÷Âęŕ’.Kă:ë­q–rą ŕ.„şy4ž§°I “żq¸~Á3ĂĂpFńśSyŔ•á㊚(DĚŹÔĚf#$"i°”şŮ˛§e`f ć͛Wšyóś]‡~¸|đÁžĹĹĹIŞk I틊ŠîÝsĎ=ů /lçÁ;uęԊ5kÖ<ŘĐŘ$ľ-**zyúôé˝;ě°mţ~VVVrĚ1ÇŹ5kÖ-eee?ičÚébf%K—nŸ›Mœ8ąŕ”SN9Ś{÷îmě~řwóŮ Y3°7kâŔćD}z˛Í‰śA$Mbb w§KrwށD OĎ n׍¤'•ŘU$)‰,X#‘ČěŢ+ÉńúTKŽ'šďŕÁúüÎbw&ÎŽ-¤äëifoJÚ éŕ–Fl55jëڜĺS4›_Ť$Ę)ĽOžîÄŠŚ˙ř̜›ItŞťÖĚJĚ,ökO'œÝhÂ7—…q NˆŰţç5ÝhohOó§şşzň9çœ3éÉ'ŸÜĆFí¸ăŽkóöŰoď>nܸ{őęUYPP°”¸J|uuőn˝{÷îrĺ•Wvźä’KÚdžřř㏹뮻ÖlŢź9ѧöú8rôčŃĹßřĆ7śYtÓŚMŒ=şjÖŹYľÝťw?Ş˙ţG5tᚚšĹ%%%oÔÖÖžŰ؞ŘÚÚÚço˝őÖ3Ż˝öÚmś|§L™ŇqĈc&Mš4˛wďŢŤóó󿒔ę˙!ŤŽŽ^ źffŠúáf•ÚÚÚÖZŹk[ąÜľ%%J´œVm˝ŠfVÁzëí ,Ťëń’$úpÁÉŔóő]?$ú‰˘¤Ç%Ô€ţĎDqâc÷Lrź_’ă€Ű ‚ŕ\Ż`˘jă9ŔFÂ!ßD4$ěXĎN:¤ô)ŔĚfKÚÜ(éF3Ť ˜’p¸ .Ňé]Œ&{ëpYˇą$€¸ âu8W’łKÇ-´W’´7p†™ý4Ác÷ŢlŞ@ă cÜĆJŇnŔȰW1Ę`v8ŽďiŹ\šň7=zôŘóŇK/ýÎäɓˇéŰíׯožůf73ăË/żěSQńż~gIôěٓގú.]ş”ŃŁG—•••ŮČ^ćnC‡Ýn˜ŹŚŚ†{。÷ˇěۍX—e˖ńţűďO¸ďžű6śoßţԊŠŠ7—––ţŕŽ;î>|řđáÇ{ě֊ˆ$&L˜Đv„ ]7lŘĐuĺʕ{VW§Ţ łtéRfΜ9áؘŸŸ˙ؖ-[2ęWÜP"‘Hkí<ŤŽűŽÄőo炃’‡ŤTĽÂ`ťN’~-édŠ$(éVTÍě{AŽÓqě“Żť#‰ŠIŹĘČ1’Ţ“tşŐcM+Š¤Óâ—“úôx˛ŘţA䅲r ‰D"ŸApNÉ#Qô úߏD"ŰéwŚ”JJtábhˇ¤™}&éAœtĚU8ťŹ3$ýŰ̒Ž@›Ycú˙˘~(ĹíŮoŢAŚĂž…KToĆ9ĄÜ[ŇŔ$’oÁ§T™mJĚů-nӄ–ëŰʁ4‡mŞfNŐŚM›śó7 ˘˘‚ šM˝bŊó§NÚ˝WŻ^G_sÍ5Ű 1H˘oßž)ݎzőjFŽšrůňĺÇ×÷G˝ň śŰV),,¤°0ՖŸÄ <˜Ł>şÝЧžÚî˜cŽš §űŮ ĚŹFҨńăÇϞ>}zďƒ>xťX;vě˜4AŽ+śQŁFľ3fLťN8áfŕˆ†VU]]˝…[Ęp§QęŐŐ՛***ś™x.//Ż&˃kš$|ŁYÇ)÷AĐ ×3÷EşSî ŒŤO’ť/ ‚`09‰ÔYq6ł—%%Şœ÷‘ôy^^Ţ#‘HdPÁ>fś/°o¨[[{ciÁ~I΋—™.iN^^Ţ´H$ň(đŚĹ9I:8ÜQs¸ÖĚNOĽ zýţ8ÉÝ€_Apg$I:x‰D^‚ŕ’ďĐ Ě ‚ŕ&œ Ĺ'ĄIĘ=€ßĹőfe‹›‹›Ů`2đk Ď̞Ž’t­œ_`ś(ĂUłŃ ™1$ĺËYŘá,ĺz˜ŮfV)Š‹¤?żNąÄ†ô„mö˜Ůj3{:ě!| 8YŇ%–îôc:ÔÔÔl7eZRRR‹ëÎ5óŢyçm’äšsçZyyyłń<53+++;íÎ;ď|sěŘąkËËď@öĘ+ŻDFŒńŐňĺËǚY:f7­[ˇ.ŤbÎ¤ŚŚŚ1Î?€ëÓ.++;x̘1‹ŚOŸžŃXHUUU[SI˜÷ţűďo3É9wîÜ-+VŹH&uQ'6l˜;kÖŹmް?ůä“j`acÖkŽAÁ7ƒ ˜VAđb? ‚`:“NlĹAÜAÝUÇ6Ŕí@IĎAp}Éś<Crůöfvž¤ť%=jfWă ]qƒÓëxl,ŻF"‘‹“Ü—Hgx'3;GŇ+’Ş‚ ( ‚`N ‚ X+éâ’r3ťĚĚ^­+ˆ zAp.!Ťk°çr`i˃ ˜Á÷ƒ ŘÎc<‰ü璌θ\i.°6|ˇ`fuŢp?Ŕë;/đlřľc#;ř-.  Üý> ą~—P‘Í×$qţxˇenŠynš{§zÖx"×Ď#Í× 'đŕGŔÉ@Д×ďÓ§Ď33gδ(›7oś˘˘˘eš~]˘ż ˝zőZUYYš5žłÎ:k-pXŽcKtëĐĄĂ˙őěٳ슧žŞąP^^ncǎ][TTôîCPşŻŰîC‡]UQQѐ0ēO>éŐŤ×_2k§=z|pá…Ž_ťvmFb{â‰'ĐŽoßž+kjţ÷ă=zôJŕ€F>ˇŢx`it­ŠŠ +..^ÝÔ˙Çł} ˇ!-Í[Ľ¤:˙Ö7"Ž=%U42žÍ’ž×$-Iq5AüŘ=|ěN’žKrî—ŔšőüN•ôIŻól`L Ż] i~×ůY’uó$˝Đ€uJ>ń¤ČD3ł´„DëšĆłfv‚¤CműŠÔTżîţ3‹H†{ĂżŮęď lčľĆă>mŒ0łfríL"i€ĹôlJÚ —~h)8pHzŇĚNĎ^”M‡¤qͰO˜Ů MqÍ žŃ˝{÷żîˇß~ĹĹĹy/žřbMmmí­eeew4ĹőëŁ[ˇnWuëÖíš1cĆ̙3§röěŮóJKKś&ś+KI݊‹‹ö/..Ž <82dH‡:lÝĆ63žüňË͟~úiŐâŋľaÆ-×­_żţáLĹQTTtyŰśmŻ?ôĐC5hĐ ö……… ŢÁˆÇĚX¸páĆ÷Ţ{Żş¤¤dEYYŮH3[‘îş’TXXx~űöí>jÔ¨ ¨¨¨m׎] ňňňRV€cŰ0sćĚ-ĽĽĽ%%%%ß2ł÷?÷čŃcRϞ=/=ţřă fΜY9wîÜwĘĘĘNśúބ’PTTôçĄC‡Ž:đŔŰ=ýôÓŐŤV­şqŐŞU“łVs%‚‡q˜•¸ö–r\ĽŞ×ÓŢ7%Űçűď7 ő 01™KT#ăŽëًb¸ƒő¸mřő8Éľv8âč­ 0+‰—lmš‰ŕŤ$]‚ëae3đ’™ýxŢâ†%ľ ‚ŕn3;Wđ˜mngč1‹Űž­ăúà 8ÓĚă&t{=؞ǎX,0łťÍ,Ľa“Pžĺs Ü]†{Í*q&‰žnî‰D"Ď$YťnčĺĐwGĽß6„qߗJxPcfűšŕO™ŮŠ’~hfSšĆžŔxŕú0 쎍^f, ”tƒ™Ý,ékf6§ţGäš)ŕëqŽ €3ÍlAřŸŹĐępúô´™ŇDĄf9äËpÍҗšY“lA°ÎňďÍşzs¤Ýýq×[ěĘƒœŮz?œ~Ź\BÔ{1°Ä˛Ô§ţ}ưݖL#0Üĺ٧ƒš ä&$‡âŢP;“ÜÂ*ŤąÉižî‹{üw:żo’„›Ší‡ó†M;aö¤N(ń˛3.i٘žC9ó‰A@°ĚĚRjŸ ß÷j3őŢ/×VV„KŰâúЗ7vý0 ěˆë‰­Şęë‘lŕúĂpS͛pIßúH$˛]œJxpŻ™eÍŰSŇ_Ělʤ_šŮuiŹ3ˇEűăč§J9kšt<„ăŻq5pî ˝™˝›Šľ3ÜÄě‹Ŕ^Ŕ=Ŕćz÷ĂľX}TĎă˙nf'5A¨YGR}ł “ž_âŒĹ#âńx<ĎM*Ű=qŸÖ˛I4óMkxÜlČă8‰…Ç2–ü…Tâ>őŹÎUăŹĺšŠ;qúDŁÍěB ËęUő%!-Ú-Ź wÁ%f6÷z\“ëŘ<ÇăijRyƒßŰŁĄF*É×CtÄ9íé]3ű˜ dK‰żč`fŔęćlr$u–t٤9¸đP3{!ě‹üđçđ5Şoa´  Ü"ęóýŔLIÉŚÂ<Çăi‘Ôů/§Ť/wŇ ‹”™~͈|‹9oŰérŽ!™Ś‚˙YÓ C1×G8ásŒ¤‹$%? ű gâĞcf§™ŮjIg§—4 ‡§ĹôţĹąJҘč7fö°LR‹Řęöx<'ęŤđăŚN€­7ŁŽŮŽĚX2efsç%]›Š5C*rţş=i‚PŇ>8{´Ł€‡Ěl;ĂOIÇ㒿ĺŔ03{TR[Iż"fö“6ŹîÎ˙ŹqZ a5{ą¤bŽý 8TŮՏôx<§ŮP_Ř7vĽ?‰Ĺ3EJcÚŠNéΔôÆ<.Ôéäîh`_ęĺl4a­úMnÎ3łkÍlSÜšÝ$݂Űöžif_ĘŮă=ˆ,<ˇ}8• 8Í‡™}Ěöcň˙—ƒp<ÇăiręK p[žQöŔ9ad‹ŒŰř˜Ůë@•¤Q xŘŮÄXÇÄQ‰ŰŽ&b™Öź8NR?Ü@Ëgfvœ™-ˆ;o´¤gœ÷čţć<MŇa8˙ʼnfö‘¤ŢaBŰÍRˇĂk‹Ó9jŠtPŒ…™ý‘Ăx<Çăi2Z â+P&Łöja%Ofv?p˜¤A)>t8L$ÚX†ÓÇʤĆŕ(œ~YG\5ę<3›wNž¤ÉŔs8Ăk-ô@–tÎđú3Ű é2ŕ3{ޜnŞäÓ˛ŔľŔqÇzć"Çăńxšš6őÜ_Ěćö/d8 Ľ?ö—4řpW(äźŠ¨¤#Ý€×Ľfś<ɲ•ŔÎfśEŇ 2Ô''é œMŰ2ŕ˛PŇ&ţœ"\bz(ÎńGfVޗLŢ5ły’:w඀ëô%L-;\…Ȍec˘=ÇăiiÔWÜD$€’˘qd40gü>—Üý¸)t„ˆ?ďuÜĹůŔ´:–ŹÄUţŔ%XiU%ő”ô ppŁ™J’ü |ˆ›Âifcc’żÎŔЏdož¤ţ¸DńćF&ŕžcKN{łýďóWšÄăńx<žŚŚž°”mˇ€łUŒZ)5Ř_2E~ë‰LSüIföŞ™ÝPGő\íUź—46ŠĐfďCŕS`p¨KNOIĎŕH†›Ůk1÷ÍlŞ™EV“ŤÓ´:ۂó›lŠ ţwŹE˝x<ÇO* `l0[˛ QqéŒWÁmă´ńÎÇÉśü ˜ú‰6”|ÂĐĚŞęňŇM†¤’ţ†ëßťÖĚ6Ɲ“/éG8˙Ţ|`ˆ™Ýkh1l ŤœŃcgoŚčôQUŔiŽŃ, ]b–Ĺś„SÓËr•Çăńxáú[€ŸâŚg˙ŹŽ•TЀev%FŽFR‡:΍ž#Içâd]^6ł“ă“?I$] ,nnÇٸ˝w^×đšÔ&vžJf,ÜŇB+€Ŕ>qUÓB\…ŐW=ÇÓ*¨7QˆÝrjÂíłLÓdӗćxWɛ„v82:ˆ"Ї¤‹ęX˘ ŰęΕôľd'‡Ň3ށăú÷I÷Hz §çw5p+0ČĚŠMđ$í)ięé“4łwȌ@ő4śMţ[ĄFâCq‡˙ÜÜôŃx<Ç“Z)Zt­÷ʆÓäňfV&ˇżÄšz´Hş*>Ů żß§!Ľ™h¨DŇi¸jăx3űKhCËŕÜ@Ŕ3ť'ŽĎoIůf6+Á‰ž[ÚÓťföYŘ;Ůb´î3ŔW1ÇŽJŸťÇăńx<­‚őô™YMŒdK&ɸHޘŮFIO#g$}7ůü"đIӀ€_ŕ&q#q=ƒgŻâ^ËŰ`kŻß0ŕ3Ű:9-)çĽ<'ŕü \ŻßäC y8]ŔŔífV™éç^’úŐąÉҎLŘ[ŮËĚފ9śîçţíœćńx<OhL2— m¸lş‹Ô‹™­ćIş×Cˇř>đ0Ž2x-đ<0ƒ¸¤9tŕ8śşpT™ŮŰqÉ_ŕm` nËqÎąă–ÉßI8Ąç%föó$C€}šňşŮ"ô?`fďĆŰç˙{NKŤtz<ÇSŞoGQR{3ŤˆůžS§fw$ NÁ%~_gűáôâÜö÷oŁĚ X÷ ŕ~œ¤ÎÝŔ=fś&Áyá,ŕޞNeť7Hú9Îęn™™=“‹2E8ŕÓ'V1ěË|ۓéńx<Ok!•-ŕÎlŤXH#ePš;Ą‹Ć§Ŕ8 ?pŻ™=Vvţېj\ŘxNďď ŕĎfV•ŕźAŔŕS3ť8ýg’6yá­%TĆňⒿĄŔKŔM>ůóx<Ok%•°¤‚˜ÄĽTRçD~ş-p;đAI]€KŞĹM ĎLŇâŘƜ¤n¸aŽűĚlmxŹ#0W=|¸ žš>ît`wŕŸfv[֟\ę—ôďđ `TZHR{ŕzœFâůföXNóx<'‡¤˛œěgfÄëhf9ÜhJÂęßŔy8˙Ř*\ä۸>žŽŔ;föž¤]€‹€bŕA3ű0n­NŔIŔž8MŗBٖfE8ű°*‘XuÖčƒëˇŹŽšmćó31ÍNn˙.N7łçÓ]×ăńx<ž™z@Iű™Ůě˜ďóăô[%á鎸$đŕ[Ŕ}8ęđř@œŚ^0ĂĚV…ď†Ť.šYu?…„HÚ×˙—1áďpú<\et°×J°— ďŠÓXě†sJéŒKçরŁI኎•‡Űv?ˇľŢř;p•™ľTqkÇăńxR&ŐpßxoYIAkŸž”Ôř&Ž_pw\ŸŕœXóœ—ň›föď˜Çt.ĆUżŢĎśJŽ‘t2pđđz*ŐăĐńd`hxŰŘ —,~ ĚÇ%Ő큝qIöÎáí@\" 0 gť÷zŸ’Çăńx<;4Š&€ťĂÍ썘cm›KĹjG Źö‚KL^ˆ­¨śTÂí×c€5ą-iŽš+.Ůî÷ľnűxn‚űm\ľľ4Ń:Çăń´fRJ$]lfwÇ|ߨiíUŔT+…ÝÍěó\ÇŇTHÚ'†ýnŽäl<Çăń$Ś! ŕ!@e\/ŕnfÖâüb=éZŽő5łšŽĹăńx<Ďö¤œHúĄ™M‰ůž°oŹĂBkFR  5'ŒöĂ˝Y˙č]Üł}AAÁ9˝űô>;??§ŇҲ§Ö•—ßťŹdůęl_Űăńx<ž™†&€ßÄUv9ś7°"´Skľ„Á›‰sNt°ÖĚćeűZŒqÝ^Cöşéü‰űA~~> ćĎçńǏĚř׿ţţݎß:%Ű1x<ÇłŁŇ @ŇŔd3űOĚąýZĂPC2ÂÄřÜ4đ`3[šăšI§sR‘Y‘Ô§  `ß6A›AyyęˇëŽ]´i“Yľjő˘-ľ[ŐÔÔ|VUUőn]bă'Ž>žâŠżMo—ŸŸżÝ}p~䕗_î˛pÉZ¤XšÇăńx<钊H<“€{$‹Ń‰›&ÓZiĂ˙9fśwKČejÎ~—Ší]8é{)đ\ŹĺZ‚óŠťtérÍ Î:óôÓť ˛}űöĽ˙€ţbŊ,^´˜%K–°`Á|;üĐĂJ˙gńŁ˙-)ůeź˙ôşuë6/Z´¨ÝŕÁƒˇšÎ† řâ‹/jiĄv…Çăńd‚Waë@ČŠfö٘cťă;3) ź# éQ3;;×qdIˇá¤Un1ł9őœŰř)pOlE8žn]şüúƒšâü‰č[GMŰśmSŠĽśś–ˇfźĹď7ĽćŁ>žáó…Ÿß˝o§öíűîűľ}_ë׿_ßýäuěԑůŸÎ,[öeŮGÍ9sĺęU~Ĺăńx<ž$4*t<ÎůâňhŐ/クŠ5iJšjfgä:ŽL éœ‹Ćžf6żŽó~ü ŮЋ¤vĂöŢgÖř ă÷žôňËpĹÂĆńПţÄxŕÍoż}DÜ5Ú´iÓćĐ6yy*ŤŞ^ŠńŹöx<Ç“„Ćl`fĎIŠż §ƒ#fśJŇŕvI?5łu™ ľYSë2Č,``I˘;%uŽÎŽKh9//ďťGyÄޗ]qyÚA?že˖^XXxŕşuëޏý‚ßHűÇăń´"‚tlf/ŕ—^EĹWNźčŒĹváEąG˙żÉ؂Çăń´RŇJĚě%ŕqŕ­P3[ĘĄ ‘4%ŹľTŠqŢż-IC$] éŻŔRŕëŔQf6)•>ĎĺĽ_͝ú׿f,ž§ŚMcŃâ…/glAÇăńxZ)i'€fö đ#ŕIW†“Ą˜Ů]ŔCŔS’ŽÍÄľš!=Ů@Ż;úěaIDATI¤’†K:VŇ÷$ýTŇý’^”TŠŰ>XŒ2ł“˘ńW[[;ůŮgŸăő×^O;Ţ9ÍáŃGŮRž~ýoÓ^Ěăńx<žVNه@.&wÝqĂ!łĂăůŔůŔ7ßšŮ{ťhŽ ĺoú›ŮmšŽ%’voťá~FyŔ\żßrŕż1_ç™Ů–4Ż}vß>ťÝ 'LúÉ$ş÷čѠǗ——óŤŰnçŠiOŽýláÂŁÍěĂtâńx<Ç“ápë˘ŇIŔŕuŕ3[ď\LžŮĚ>ÉřśI—›Íě\Ç‹¤<`pNÖe#đOŕ=`1Ž—oQ]bËŒeH›ź6öŰ}÷ŻďąÇ@ 6ŒăFOqq]ťvĽ°sg$ą~ýzV­ZEYYŻźôÎú/~Qťč?˙ů{uuőyMŤÇăńx<­Ź$€°5Ůť'*|7Î=¤$źŻ8h ůóx<ÇÓšU‹™-3łŰÍě 7đ$0X éÎp˜$×t  @Im%ý ×ď÷pŹ™ípösÇăńxšÍj 8 ŕáŔ 8ý¸×ÇÍŹÉ1$uŽ6łk˛xCp˝‘ĹŔ83{.[×ňx<ÇÓ:ŘáŔx$í  D€O×Ě,҄׿ČĚ~—á5󀓁ˁpśzżđVhÇăńx2A›\.ĄŸđ'’şGIÚ Ź5łůMBĆô %ß.Ćő@>ěef‹2u Çăńx<žž˜ŒpŤ¸PlȖĤŽéZŘI\ŒvĆőú]afoe DÇăńx<žmhą `Î}ăkŔ0œ&ß.¸ä0úľXʌť-ÁyöžŸ‰Ş˘Çăńx<Oşü?Ť|灐Ôő(IENDŽB`‚Frozen-Flask-0.11/artwork/LICENSE0000664000175000017500000000147512153050341016731 0ustar simonsimon00000000000000Copyright (c) 2010, 2011 by Armin Ronacher and Glwadys Fayolle. Some rights reserved. This logo or a modified version may be used by anyone to refer to the Flask project, but does not indicate endorsement by the project. Redistribution and use in source (the SVG file) and binary forms (rendered PNG files etc.) of the image, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice and this list of conditions. * The names of the contributors to the Flask software (see AUTHORS) may not be used to endorse or promote products derived from this software without specific prior written permission. Note: we would appreciate that you make the image a link to http://flask.pocoo.org/ if you use it on a web page. Frozen-Flask-0.11/artwork/frozen-flask.svg0000664000175000017500000020735712153050341021055 0ustar simonsimon00000000000000 image/svg+xml Frozen-Flask-0.11/artwork/icon.png0000664000175000017500000002025212153050341017354 0ustar simonsimon00000000000000‰PNG  IHDRx‚m ­ IDATx^íÝyŹ,Y]đÄ%*ť¨ĹwŮD˛ő€&fÜîC߈1QYŢjT„M„(pQţPٞ‚K˜q‰DŔ}cip‰KEE‘ń÷šÔďĺźzľœęŽ~ˇďĺä—Ű]uęÔďüžçˇÖŠžW_uĽj \}ŞgwerW]ř”/‚+_řÔHŕö1“‡˝7衃ŢÓĚĚńŰ˝ăÔĚ´˜Č‡‚jĚ÷Aˇý| Dŕ˘×˝/čYA?wš€>Í/Í¤ŠŠh>űţČ ›‚ν9č`ďďôčÓôi>i–Ż‹Ď€óŹ˜ÖŽ@Ż ;=/č>ÍůĂbœ8ĚO#Ŕ%¨DŢtŽĽĽc@1ëËđGĹ_ŚűŠcíăůÓ0PžôAˇzCó}żŤŃŸÜ 权h8™ň Đ6îň^rěӂôÖ —ýX#F€č  îu•bgćiô/ĽŠŻźôxşd€÷Ü /úë -MI>$>đŁĎ÷ńqŽĎ]™tK 64ö„áv×ő¤|ßÉ÷˝:č§ńH…ä¸"âo ˘mO×eOlúX,SÍŽkŇôďmjuŇfŽ™Hi΋`č+ƒYŚYżRĄȗ‰¨7É¢ŃîšwŒ“0ŕ€ŔœŢŘ獀ńĆ o ĺů |,€6čŰj.čƒ<íU´},GpiFi,Í9 :h4G„<ŐĚ>#ŽůŢ ş żŹ„]'E˛ Ň×ĐŹÁPż-×Rýĺű 0šŚ3…Td~ß$쌌ńĽqíOńĹ4šÝŒ/ňŮ ˇ‰Č‡-ćFnŰđ6uŃŐŁWŃsÎŕE•éŮÍ€ý” @;ćťśmŞŘ? úü YŮđń[A_ä3mŻ5ă,Î:čÉAǚRíŔ4Tő)Íq ŸŽmŞŠ}k…Pž|WŔ"dQ9k‘Ś|Š6Y|UąŇĹôń3űń}Ď âă2ú ~;żSËěeA_´j Ě/]Â ?˝‰OM˙ÍҘšŐD÷łÍq–b B¤!´XĺŃŢś&xLP‹čđ– ĎZˇ:˙w|LĂÓś °Ćż6ˆ{Ů墽hÇ pćŹxP´ĐP‚§ösŰ1 ś9˙7qńg•%Kźýsl(¨˛ӌń@›š)݃›yʝwڎ `ŤŘ9+-%ĐŻRʘâ眎ű ´XŹjsôŠ ŠQ?rgš÷ۃ>m#ŠÍ†Ď;-Ŕ´”ĐĘĘŃ&ĽÂ 2íí Č77ü” 45??Î{ž,vKÚ ¤ofúŹíD›/'Ŕ&ľWE€ŕ§\X%Ŕdŕ5sÚ&˛ç´ióĺXĹ,{ŹWŰß5ë-…X{ż6Ŕľ×mŰĎĺÚ9űš3×ěŮ|ó.Î"­jĆŕćÓ"ŰőŘUJœCŔ đcm;™i€ćδ­|óŽÎGhŔÝ$*6Ń×Č ţ‰ř#ŇÝE›Ë4s'›Cşć”ą1ż=čÇ7P’ŁqwpW”ź 0&ÉL_S07 (˛Ú§G–qÍj[טvWِPńĘ\wiňŽ5xĘŚôÍÝ-ƒńĆ&ÓTš‘űŽ)ʧp7Ň÷­ĄĹS řGĂŔ‚ŻóAiËŰXőd[kŕ9‚ÂEP5”Zo 9`ÚÜ0ľÔë‚jßčăţhkN€Ë¤nŐl>ŘcČ!3¸Œó÷ ˛]ç¤hré ĺÇ/"ˇÎmżSÎMpůä$A•í(ömęSDÝçł4š˛ĹĹ2“,UáÓťpŇ/<(ˆu“÷Ëľ‚źśÓŢSv$‰)[áL1`ůÝĚírÓy¸™mk=ňšˆ1ŢŸ5 7ýWѐ@ēďî™|J-›F3ß6íĽ/^6÷qÍžkpîWœYŹřĺvlVöݛă‰n ŔšńĚůt¨v/rŰoˇńè*Đ{ÇĚsŁ•€˘™}ép‹Ž]Űž9ÎŮ6sô0#Úĺ˜‹ăÎ6ątd¨šŘšiâÚřĚ5–ř\o-Ŕ§ŠeĹÇ1ƒ:V›÷ZŽ+Í%f_ŃgžĐuœˇj ×//ĚŕƒՐŔ:Ço2Ńőď6ăîRƒÉ]Á_1•ޏišľ;1ŮćßěÖœ%ŕvhšAmšĎĘ#XEŠ{°Jyľ> s^~›Ž@aČ5‹‰Ś0Ëř#Ř5‰G÷–?œˆűüc|ţć óš`@XĐŹ“=fŔÝśeDćWŚH‚E éąĺM6ŘĘAĽS7š2eëĘCCČŻÂLpčÍqŒś:ĆdÓt“RŰ% uPžóć˘éÇä*”4ڏśÖt q¨ŻZmóZ)SŔŞ4ń‹ř7Ż9š1ÍßBuv)xÇ[m3Ú`ű… ¤-4@0™Bů™gOhćcŃÚÄń3qüńŮ uÜj˝3ś‰^}LčBk žŹnů°ż‹fŇ5ähqmĐ,n÷łHÜÓbŹą4cˇJK' …"ńˇĺؔíO‚ČI”}ŃsóZ€Ď6“'Ŕ>€1›ţÁęLhëžYÇ"xz€őŚčš˘Ż‰eáĆŚVœmű긎‰Ď”­ëǐéuNŮÇăţIA^łůFřc {lL€=oä#ý$gÝ1ˆe°cŢ­r_[qYO2ÝŕoŠ m§ C'ó´ é÷şrPĚŔ7P˜üš wÄw ˜i‘ňUíČ:ŽK‰1hŞ”m¨uF—Í›ř]<[PôŘÄó›žNŤHÉ0çjaYŇÖrţdęmŒ ­Vƒ—qÓűUA đŘĘǜń×AgśJA<0€[XçâďšäHt;Tđh˘jWgĺŚC’—Źę˘ĎT€;kœ4ň"›RnäJqXHZ\šk1 Œ6ř㪯ęŤé˛˙ˇ j?ĂĹ0†ş˘íˇŮąŃĘŃ!Rœ7!ŸPSÍß2Žbß~ŽŠ÷ą:Çń,K{(ƒ‚L9ߔŁ{ńůäéÍH?<#ƒqŢŤ6ů4Gř¸fɀ“h,XŸÓŸo 402ÇŽyęă¸Ĺž5ŔĚň[ƒ×¤?̌| Ý-@>:čB|f ňűšřp”Ú*M=ëC cůćŔ4‰-;Ó:óĄ ŢOţ ótlôAúŤ Ą`rçW /y]ň6Eƒ•ő7Đ]ƒúŠ5sWŞ4… UŞ›\äĆ,Ćçâ3stĐ ˜ŃĽIš wś'ÔqsËúülž6f jć5Ľ9xŁR3ď x9óĹc.¨´¤ă&HŰ`×óéîĎWÓČ#€ËV”+K ‚-Áˆ'AƀymôăďW ţÁ/ĆÄ?—\GˇY•ű׺Ëđ‘Ô؍—Ĺźö`Ě§ŻšuŚC-€>ˆöÝ}ž ŤňL‰zMźŒŔË[,˛ZĄŽ ÝyŚ—öĺâވžëuTÇőgIĆ4ś}ďlĹŻƒśŐŕdÖäŸÍďx6>.´8&`[ég'íu˙ž&#(}Œ•‰ż,qăŔx}§͉’ń3TZƒŹFukŒĄšŒąUjíl\SŞc,ϰUţA )"ă‡dń#Ŕe^˝Ů@‚‘/l Z^"&*:ÓŹ”Čü]TÂŤe°ŁŔđĎoZ|Š<‡Am-œű͉’Ľ2@ÜK€1›9­'"~řÄŢ,™Ł +Źă؋Ⳅš]5§™×kƒNRŕOŒžO‹žéĐ:˜ÁmßN¤^ŕD–ďXßΖ]GčËB{ 0ŹËŻĆŇŘ@~"řBkʔ´ű‚Ţô—A4évAL¸ŕ„ůf l"đtŠŻöÝ^%‹¤­ýĺ-†>ÓVÂ* - °ĐÚU§rZÍÂŹ‚Ś<'ŻĺKżňö^ŒYÂRÇ~tôK9ËćÉŃš,`ţoЗFŸŰÄšëâł* Ë“¨l̢ô8ë qěsmsm>¸ç÷L ۋfÁr)ҸÜRS{ß)ýfóÁňfłÇ "jˇčÔ2L Ě`î~Nya€šŒďŠă6üC|ölô÷‚jL/`h{-ĎLz–Sł‡żöÓ›ŽšškĂ'Ořke•ýŕEŔó%E‘Ú<˜F(J$ŔťĘiŸúźňG[ÓŇ ‚hŽjĎg5 ĺ¢iÂ(-`ez’ˇňý č˙‚hüظŽ<ů4ľŢg5vncWmgEŃgmŐžY-ŔČJ &¨šÉOÓچí1“}Q Í01—"cŠÍż‰˛kvvzX‘ľlA]f€°)˘ŘĆƟ%_‹ř".ř°FF¨Ćź+Aރv!ŤeŒ› 渐Kę›\c/§őEžŘľ˝F7”âĆ=dĹö ­ śLF t䡏đ˘N΅¨Ĺö5ž+ˆy/MYî|´HĘÔGÄě{MÄ+ź2ÉôáÍg §śnĎzXľĹޞdÜ`ÄqœĽÁ>-aîđucůĐł—?Ĺd1dt,˛\׸ĚńőA48K††\ńÍyM>˘óXŔľÂśčXźř ďo㰌ż„?ŚG‘3GVdíóHł`-ŽdV€{ă(Î×fćŤ/ŕąęýގ|ř’Í2WqLŤžp PAĚT{Á™|6ßýőA˙Ô4 ,áâ‹˙ĚŒ5ŒTŤ¸0i°—A‹ćsěřŘŮĚ?úŻú:Çő##éßŢ \•ęŒů8•§÷]˘Ĺ9Éh~úîBî势żǘĘĎ *}ŸÜš–đ‹ÉCE|żĹÔ“ŻvNd?´EŻĺŚDŸi­ŕU->îglţ9ÍžżŹ‚ 9íŔů€[SÓŢ.K SŐ-ś˛-¤úź X?ô_A45_'Ąa´¸ÉĐ|°˜‚Ua:A]ĽQ 3ýÎhÓ'Xů€d6€1ds—Hq&¨¨čˆy@{SĄ,bT@3•“ d&—ЁĽÁ× Ęz¨^Ţ/Wśƒ/÷˘IŤF>Ž{C&||W„mŢ)łŽó~ůnŒfŃ` Ť ‰—ÍDʉďę3-f†oj\}7ôD*ÎŃzÂ旙Xňł@%0&HÎ ÚtJ*cLăČ8C+@HÉŚŒŸž˘Ę"ÎQ2;*?6čő,Ě÷’VEťO´Ş:+&]ƒĎpŒ 6ž֎ [ʞ|açśAYžĚœWjCđ›X%ě ńv.ˆKŹď"xůúń\‚9}îŽĺ…¸‚5şdŤNvž°”DÔćU š\Mpfq˝/@&Äę@˙At~ů<Ő8&™ŕVb›Ŕ‡<‚ÖAm€ńň ‘ůXşTÎíĽę1eĎÜfě<¨˙“_Ż;˜đ/ƀż$*ÜEŁ V'sVŔ!)ÎăküqÇĆ=Ou€›÷d-ƒÔžÍIÁd›ĆD'xƾɠýĂ(*y÷íwZ+ň6Wr> űťÔţL7l_<Ä˙€­L%ÂÚFךËŔŸkţśťŃ:O{°Ŕ0]^VópÖą]GԊůjBcÎ ł6Đęşwŕžâ†€5꺗…,&(‹+$×A@ÇŻE™ŮČľńYîýˆ>Lxƒ( úGQs4ĚKQDćgƒĆL$_ Ü3šăcˆ‰fw&S&!4s]IgRDęü˛üxYÁCß-kv=+•ÁY×x€ś ń#źůĆ|(—çĺ0đďto˛XýÝďďcl ŔĆŔ˜EP›6Č §Ť¸Đ7Žëh"S+?öý¨ĹÜĹtç8~§r€Ć{2ĹLӐ3Aš˜Ň8ž{Č![h]ü•oOĎ÷!Ë/Óäu3˜ĹN-`˜ĐR˛V!ó×+>ŠščĂň0ăś\ۛO׌!}™˙jfŐ5ˊc4S‚š *.ťĐšżęeécâőoAŒGt&?Ó đAʇ´€V—÷ÎÉܘ>Bœ ršłłś}ÔČÄÍyź5<~_Đ?Yŕ”LNn^ĺžęKĆŠŘämQĄ˙$ŕšŇřY •üjĆ$LăŮóö–&3_ŞWv„xE#i¸(VpxĚ[U.ĎJ÷ ×MY€‹čż.˜C|Žqeš¸jćŐŤěkĄÓäÁ̈́ľ*s&2TTŻÝq¤IA4bŠŔĆ&ěÉQŽéňŢ Ł؊Oß[ŮÖևĹXGć.EFŢcźtĺNŮ"č ŚČŠ/;ŻzĺŐ!mŮô!‡ÁÔŤŕE ô– ŃsŹ ‡!‚ü„ Ś }fĎŘë ¤(č˘m޲œhškŒęýq+P(Œ ymńíI–č›ŸăłËČś†UŃršűłćš9_]YĹEü-šIăJM˝&žëϲ΢Ádvüę¸]˝N˝š13IprÚžű9émö ůĄA_ôşĺSrPAg|*ß|cĐ5ńýl]Kš–Mŕ" iƒLx4ř ˆ[’F]ňč2ďŐń—é ~ŽuĘľŇ"ăˆY,şl"~óÁ×]ƒ•­Vƒ ÎDťń+ƒÖ 2çL̰9„ţrÖŐȤűN+¸Öo^Ů<˙ďÜmྋ⼨™vó|óšm]NĐÜ !ş6_./r;d1MY(%űĆ; CäçgŘ`C?hBV•(uJ[Fçű1:0 ”Đ$č.A/ €ża ŕŻŃh~ď( h靁t.¨Ě§‡Žťđ3Šq/yʚôŚMúećƂ|‚Ő;Ţ 9˙j#Ěö€S˘Á.f†ŢÂFhďÓÜŤ?X¸ômĹžÁ󳫞ů弼4ÍçŽú\V .`–Ac)ؐ,i1?p™ţĺ?=`7ŃŇdqüŇŚfMÔiUnŇ`ÂŁ!Ţz°›Cű@8•śY‘Č/ J3źˆĎV‹żZf"^žńö1śĎů_aVńQDţ˘źiŒŻß&ć˝ädŕš\ťwÓ]űńٿĀ‚+ÁÓ.Úź ĆD˝Ş]ęËRşŰ Äřgué\¸ƒs0ř]1Ç2—n@L­U¨IMŒ]ýäć˝­đůř´˜VA žđh<–€RqÖaqŸ;ËŤUeă>ÍתX­›Qsđ< iâI ęĹŽMƒIPÎʕUŽNëi‘GkJƒ4ů /ĽšÂsĆ42Ńţډ>ˆĎĎŹą]÷Š1ƊP¤űĆ8w‰c,EŰ /ă˜tuÔęő,<çÓlwŃĎďEüf0wðśÚ``‹ĂŽB`iy9Žqo˛ĺ‡˙¤9vp8Îd:&‚WőŞŇnü5ŮÍ­RŠ7 sLŁű’`ü-šyJ-ń=”Ž 8dŠ’0!eşă] cűÎŘ XS&‰yĽĚÚ Y9vꊃü’íü¨F šĘݨ̯ĹpÄ2­Y§ŚĺňăƒZć敉őó5cĆxxňŕDqĆÂLWPĆ)mÚ;ôXÍ´1eä§Řńž ZŢ>W3‡ž>í‚}íXm€?2.ä§4n&Ĺrź,$řKŠ܏϶öÜkÚX2Đ< ąĐČÖ˘ăß‚˜h.’|ł ™ĽâŹZÉËGľ×ýĆśę­f­ŮSAŔ'‰ ÇB˙rn]Ÿ—qPUŚ6Ę,Ç(Ë‚Ź‹<3+Šć8î2KXÄg¡8ü%ěĂ ÂgÖőĽą@Éb‰˙Ł_gk4S_ăą0C2óv„ĹIîŔŐ˙AAĹ÷Ýć’ăcçĆT—L™¨H“Ysn]}×K;Ř'ýÂc¸V[•ű…Ť¸ďś96ŕi¸–‹ Á.MsŚZÉOůˇ}ňŁ@dZZ)Šď;p•UUÝFŞŽIÔěÚd¨\EŽabŽ7ŘĎĆ87őIzŕx‚°Š>L—ČւTěŻ2cÜłďňÉ?%h(žŔ_YŃ4kŕ1§MřĹ܌`×W˜‹@_2ׇ5lS΁ܠ\qsźŠ.śúiązŻČz5&€™Ď›~Ę<šöäŠÖAL=˜oaŚĺt^ţË2U™ëMîb˜ŮÂĚŕ™Lú˝ZĄd?×“ďŞ ŔSůÎţ€j9ňäJˇ(˘šŐąĘ\c“ů0˘+r›|‚4ÖŻë| °W7™Ŕ“0šIÇŚďl;›lĺćx[ŰŚ Đ °(Z[őĽHSĆÝu_sśCFŃBşç×Űď9/&֛037Ŕ] 3=ćfW€ KAlAďŠ×r\|˜‹Ă/Ë#5J HŁSŤ7âgwĚ´xVźIĽjĘÄäÄîŁí;Ŕ´—ÖÚ@Ą ݲc2›€đ[e(ť¸ d%AžŚ ů§€WÓwŮkt+KÍ`;ě\ÖćďšYFÝ4VósŤśK€ť@ĆźUëAő&ŘŘdœm5ÖůĎűĄ5syŚ;@g˘U ĎÝ8ť8yd:3…JÓĄΠňccLľr@ď3Ŕ퀒 śŞŕuAۖ~/ŹË°ŇÜ2˛e˛Űeşmm>eđŚ9őśg}\¸ƒš…ţ‚‰]DÓ%Ŕţeö.ćąÉ˜řaÚ€H9ßóemX¸Ů-ÎĺÖŕĘśeÍ>áŔ­źU.+úí*ęQœE9=gä,ŠŽ/OYYÇ03šn˙ÔÁţŰ}`BôؔţVŰ Xq-ż/8:Ô÷6đ庴֓.كEĄv/›˜-jnó{œ'ČĚő”jM睪b °ą=—Î pžK•Ĺ ϝ×•ňeŃâš8”9Ż Â8b„šSŋ0>n€13őy˛THqžË¤XŠR>š €ó˝+ăŤĘŠw™cď žŇצ֚/`ië"PË/Ç 0 ĐŹ` [Ő5AF{Ďv{r™ ˛>;hÝ;űi' 0€eŠťĚą9í˛lî{SkÝ­k§ę4.&ö>N€ą Trżu—fJlÇ!Ôű ­ţŘĂď'oۀ 1ĹCćŘŢ)ćř\PYjĚ ˛Oăˇĺą÷úăc%°>Űźţ3k÷ RĄZH$ÎW<†úŽ –e‘Żz(pÔeŽÁ$tŐŃĎöUmłŰeŒĎ˝¸ ˛WXždGĆÔĆçŃđ ŔÚ5ßÚń2}Qž!¨/:fnî¤ŐîXÍnciű Á9qž5ŕUńů;ƒŚŚËf°ŐŇäce‘Č[ťkbç$łí!AiŽł˙">ˆ/v%ÍsŸĆkFŠL!ÍPé™ň6ÓFŔş_ŸŸĽŮrÚłAśäŇîcđ¤Œ_Mö÷>Aů‹:csqž °1˜Ç!2ҧď&? |&[@ŕś?6Z×0zšúě›çźÓwׯÜú™~,‚ä‘}…Ŕâˆicőhc‰zilWĺ÷ľřu}VAç€Űř¤ĺŠý]’Ë…ďąu‘+űí(BUŐĘÇl^Š.7ŰŇcCţRŞOľŤíŰő͔ćL3öšž`11ŮîOű÷ś€ /ý¤pä¸|+íc>Ó$˝k7%Íśú´°*P÷Ző‹9/~Ęňă€gĄ.mPhއćšAŰ䳨 4zŘ|ďHęÔˇPf˜ćźCœ .g TAoă÷r?”1{@ł¨ÜoŮÜďě;‰'ďj´ř~ń÷]AżŢha iž2áşš0ý§‡ó4śk‘čŁö­đˇYHóŞäÄŃN2Ŕ9U`<&č˂Ědžüšcžy7äÎeIDAT$Ĺń X´ż;ĎěŐbĐçġÓp@ĎÂĐ(wžŐŮ9yłžBűü€÷\€ăŻS3ąKŤiďťƒŚ”A÷|ŞÝěV >‘`ě‚é+ďBŞ{4ć€÷Œ]°ň˙ T´uIENDŽB`‚Frozen-Flask-0.11/flask_frozen/0000755000175000017500000000000012156420704016714 5ustar simonsimon00000000000000Frozen-Flask-0.11/flask_frozen/test_app/0000755000175000017500000000000012156420704020533 5ustar simonsimon00000000000000Frozen-Flask-0.11/flask_frozen/test_app/__init__.py0000664000175000017500000000450312156413264022653 0ustar simonsimon00000000000000# coding: utf8 """ flask_frozen.test_app ~~~~~~~~~~~~~~~~~~~~~ Test application Frozen-Flask :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ import os.path from functools import partial from flask import Flask, url_for from flask_frozen import Freezer from .admin import admin_blueprint FAVICON = os.path.join(os.path.dirname(__file__), 'static', 'favicon.ico') def create_app(defer_init_app=False, freezer_kwargs=None): app = Flask(__name__) app.register_blueprint(admin_blueprint, url_prefix='/admin') if not freezer_kwargs: freezer_kwargs = {} if defer_init_app: freezer = Freezer(**freezer_kwargs) else: freezer = Freezer(app, **freezer_kwargs) @app.route('/') def index(): return ('Main index ' + url_for('product', product_id='5', revision='b12ef20')) @app.route('/page//') def page(name): url_for('product', product_id='3') # Pretend we’re adding a link url_for('product', product_id='4') # Another link return u'Hello\xa0World! ' + name @app.route('/where_am_i/') def where_am_i(): return (url_for('where_am_i') + ' ' + url_for('where_am_i', _external=True)) @app.route('/robots.txt') def robots_txt(): content = 'User-agent: *\nDisallow: /' return app.response_class(content, mimetype='text/plain') for asset in ("favicon.ico",): url = "/" + asset name = asset.replace(".", "_") app.add_url_rule(url, name, partial(app.send_static_file, filename=asset)) @app.route('/product_/') def product(product_id): return 'Product num %i' % product_id @app.route('/add/', methods=['POST']) def add_something(product_id): return 'This view should be ignored as it does not accept GET.' @freezer.register_generator def product(): # endpoint, values yield 'product', {'product_id': 0} yield 'page', {'name': 'foo'} # Just a `values` dict. The endpoint defaults to the name of the # generator function, just like with Flask views yield {'product_id': 1} # single string: url yield '/product_2/' if defer_init_app: freezer.init_app(app) return app, freezer Frozen-Flask-0.11/flask_frozen/test_app/admin/0000755000175000017500000000000012156420704021623 5ustar simonsimon00000000000000Frozen-Flask-0.11/flask_frozen/test_app/admin/admin_static/0000755000175000017500000000000012156420704024262 5ustar simonsimon00000000000000Frozen-Flask-0.11/flask_frozen/test_app/admin/admin_static/style.css0000664000175000017500000000002012153050341026117 0ustar simonsimon00000000000000/* Admin CSS */ Frozen-Flask-0.11/flask_frozen/test_app/admin/__init__.py0000664000175000017500000000073112153050341023730 0ustar simonsimon00000000000000""" flask_frozen.test_app.admin ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test application Frozen-Flask :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ from flask import Blueprint, render_template admin_blueprint = Blueprint('admin', __name__, static_folder='admin_static', static_url_path='/css', template_folder='templates') @admin_blueprint.route('/') def index(): return render_template('admin.html') Frozen-Flask-0.11/flask_frozen/test_app/admin/templates/0000755000175000017500000000000012156420704023621 5ustar simonsimon00000000000000Frozen-Flask-0.11/flask_frozen/test_app/admin/templates/admin.html0000664000175000017500000000033212153327003025572 0ustar simonsimon00000000000000Admin index Unicode test URL parsing test Frozen-Flask-0.11/flask_frozen/test_app/static/0000755000175000017500000000000012156420704022022 5ustar simonsimon00000000000000Frozen-Flask-0.11/flask_frozen/test_app/static/favicon.ico0000664000175000017500000004057612153050341024152 0ustar simonsimon00000000000000;@ (=&@@0N=(;€ ED ?OCCbjYF&"5SX' CZD ,+&INX[!*rk57, .83A+=(]:!$5ko;"-EU "07 ..(OwZ2 !P\(&F;%/iX*<L(" ?4a7 J€V>'J8uORQ'ˆ‚:-&.,%.'a /C7"Vs]H22. q`2Jo€W538K@G|o< "<"- Au?*:9) ="D@an/.8,'bGEa<>J-3 8”b<M**- =œ?J#$$9 :A$6ƒŠ  FŽW-3$+Pc#%/1Ta#$ T˜— 7eŠ—Œ›śśÝďθ§…cE=`[rG #H2\_"&xŸ˘Q"My”˘Î˙˙˙˙˙ÖĘţ˙˙˙˙˙˙äÍŽ>*3)&e 0 %~œÁ¸ĄšÝ˙˙ôŃćđĹšŁłÓ˙˙ÚČĆĆžšł+>M  FW&?ŠĆí÷÷˙˙˙ßi3d€gs…Wwîź6 !)XuR $>4Żď˙ýý˙úÜb+KpZn™2lsk#m1/;iŃ˙˙ńIJLJzv@ 7>uÜŠ]8j4%;- S°ăŇáëŠ[p—Œ?0&D´Ęt,Y!9D5 ZłôëźÔϒU mj"M2 "W›ƒ0](OF#5¨˙˙ý÷ň։:6kU;+F-$6 >jŁ–t–X!m/- „Ö˙˙˙˙ĐśWj~*A6G..d#*iZ(Z“Ur3`Ń˙˙˙˙ţŽT<i€J  I?O174DA…J+r7(02ľü˙˙ń˙˙Ÿ7݊G2':@i9 23V¨R4yA#hCpë˙ׁqŞśM_22??4,'GžY8M/B0Ą[0~O.D'ł˙ř¤]DC"\q Xg@*č}-L!nN9Šh]Ń˙̟Îk <F0<A(‚é¨D74“0ŚĺüĄŒš›: 1> :;-. TŤJ,% '’ˆZŘűşqiA\I`PG#5@=.BD,*|*Ş÷˙ąx` /7HF))%'(+,ŠniŇ˙űŒ>pHFGSC 2+6 )/|R ;‰ă˙řŻ9?n6;@1' 0M;QR-3a9X +B˙˙˙é~94" <YC/% &7 ,a; H4 2/‹˙˙˙÷şW=6!& 0GLD0:7: "a{ !''()¸ŹząźÓ¨O+(! &GWE:-YZF Fm 3&<ÇoMz?Pˆ ™v[SK1#34 Mh4<Z(#/>…ŔéĐc>)>/*Fgc~Żb%0@0PIG9+$ 1Ť˙ţÜĘł@ .›ޜ@. "& U–A' <:it ;Á˙˙˙˙÷ÝĐƚ@;;#B";nP-eY@Ĺ˙˙˙˙˙˙Ń_:-5 T> ''  Ľ˙˙˙ö­q"#U0'%E8;I)9P×˙˙˘Eh;'!Qg><D$<0Ú°:'GJF* :<2$jţv &4$.. %U&# ":0!8( 2+>'ky AA!??3+ HGrg:G <Y< ,?BJ* lJNz&CQ(4E@H@( "s<#BT)?2<AE_N /c%5'8 >SSI--J4( Ju^ZQ 2 )1-VŽ“N ,XN *I_)1WRkX00&<[9 ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙ŕ˙˙˙˙˙Ÿ˙ŕ˙˙˙ß˙˙˙ŕ˙˙˙˙˙ű˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙ţ˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙ż˙˙˙ŕ˙˙˙ż˙˙˙ŕ˙˙˙Ÿ˙ż˙ŕ˙˙˙ŸŔ˙ŕ˙˙˙žŕ˙˙˙€ŕ˙˙˙{g˙ŕ˙˙˙˙÷˙ŕ˙˙˙˙˙óŕ˙˙ţŸ˙óŕ˙˙ü˙˙ńŕ˙˙ř˙˙ĺŕ˙˙đ˙˙ýŕ˙˙đ˙˙ýŕ˙˙ŕ˙˙ýŕ˙˙á;˙żýŕ˙˙Ă˙˙?˙ŕű˙Á˙˙˙ŕů˙€˙˙ß˙ŕů˙Ž˙˙˙˙ŕű˙˙˙˙˙ŕű˙˙˙˙˙ŕ˙ţ˙˙˙˙ŕ˙ţ˙˙ţ˙ŕ˙ţ˙˙˙˙ŕ˙ţA˙˙˙˙ŕ˙ţř˙˙˙˙ŕ˙ţý˙˙˙ŕ˙˙ń˙˙ýŕ˙˙€˙˙˙ŕ˙˙Ŕ?˙˙˙ŕ˙˙Ŕ˙˙˙˙ŕ˙˙á˙˙˙˙ŕ˙˙ç˙˙˙˙ŕ˙˙ď˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙Ď˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ˙˙˙˙˙˙˙ŕ(@€˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Frozen-Flask-0.11/flask_frozen/test_app/static/style.css0000664000175000017500000000001712153050341023665 0ustar simonsimon00000000000000/* Main CSS */ Frozen-Flask-0.11/flask_frozen/__init__.py0000664000175000017500000005022512156420075021034 0ustar simonsimon00000000000000""" flask_frozen ~~~~~~~~~~~~ Frozen-Flask freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ __all__ = ['Freezer', 'walk_directory', 'relative_url_for'] VERSION = '0.11' import os.path import mimetypes import warnings import collections import posixpath from fnmatch import fnmatch from unicodedata import normalize from threading import Lock from contextlib import contextmanager from collections import Mapping from posixpath import relpath as posix_relpath try: from urllib import unquote from urlparse import urlsplit except ImportError: # Python 3 from urllib.parse import urlsplit, unquote from werkzeug.exceptions import HTTPException from flask import (Flask, Blueprint, url_for, request, send_from_directory, redirect) try: unicode except NameError: # Python 3 unicode = str basestring = str class MissingURLGeneratorWarning(Warning): pass class MimetypeMismatchWarning(Warning): pass class Freezer(object): """ :param app: your application or None if you use :meth:`init_app` :type app: Flask instance :param with_static_files: Whether to automatically generate URLs for static files. :type with_static_files: boolean :param with_no_argument_rules: Whether to automatically generate URLs for URL rules that take no arguments. :type with_no_argument_rules: boolean :param log_url_for: Whether to log calls your app makes to :func:`~flask.url_for` and generate URLs from that. .. versionadded:: 0.6 :type log_url_for: boolean """ def __init__(self, app=None, with_static_files=True, with_no_argument_rules=True, log_url_for=True): self.url_generators = [] self.log_url_for = log_url_for if with_static_files: self.register_generator(self.static_files_urls) if with_no_argument_rules: self.register_generator(self.no_argument_rules_urls) self.init_app(app) def init_app(self, app): """ Allow to register an app after the Freezer initialization. :param app: your Flask application """ self.app = app if app: self.url_for_logger = UrlForLogger(app) app.config.setdefault('FREEZER_DESTINATION', 'build') app.config.setdefault('FREEZER_DESTINATION_IGNORE', []) app.config.setdefault('FREEZER_BASE_URL', 'http://localhost/') app.config.setdefault('FREEZER_REMOVE_EXTRA_FILES', True) app.config.setdefault('FREEZER_DEFAULT_MIMETYPE', 'application/octet-stream') app.config.setdefault('FREEZER_IGNORE_MIMETYPE_WARNINGS', False) app.config.setdefault('FREEZER_RELATIVE_URLS', False) def register_generator(self, function): """Register a function as an URL generator. The function should return an iterable of URL paths or ``(endpoint, values)`` tuples to be used as ``url_for(endpoint, **values)``. :Returns: the function, so that it can be used as a decorator """ self.url_generators.append(function) # Allow use as a decorator return function @property def root(self): """ Absolute path to the directory Frozen-Flask writes to, ie. resolved value for the ``FREEZER_DESTINATION`` configuration_. """ # unicode() will raise if the path is not ASCII or already unicode. return os.path.join( unicode(self.app.root_path), unicode(self.app.config['FREEZER_DESTINATION']) ) def freeze(self): """Clean the destination and build all URLs from generators.""" remove_extra = self.app.config['FREEZER_REMOVE_EXTRA_FILES'] if not os.path.isdir(self.root): os.makedirs(self.root) if remove_extra: ignore = self.app.config['FREEZER_DESTINATION_IGNORE'] previous_files = set( # See https://github.com/SimonSapin/Frozen-Flask/issues/5 normalize('NFC', os.path.join(self.root, *name.split('/'))) for name in walk_directory(self.root, ignore=ignore)) seen_urls = set() seen_endpoints = set() built_files = set() for url, endpoint in self._generate_all_urls(): seen_endpoints.add(endpoint) if url in seen_urls: # Don't build the same URL more than once continue seen_urls.add(url) new_filename = self._build_one(url) built_files.add(normalize('NFC', new_filename)) self._check_endpoints(seen_endpoints) if remove_extra: # Remove files from the previous build that are not here anymore. for extra_file in previous_files - built_files: os.remove(extra_file) parent = os.path.dirname(extra_file) if not os.listdir(parent): # The directory is now empty, remove it. os.removedirs(parent) return seen_urls def all_urls(self): """ Run all generators and yield URLs relative to the app root. May be useful for testing URL generators. .. note:: This does not generate any page, so URLs that are normally generated from :func:`~flask.url_for` calls will not be included here. """ for url, _endpoint in self._generate_all_urls(): yield url def _script_name(self): """ Return the path part of FREEZER_BASE_URL, without trailing slash. """ base_url = self.app.config['FREEZER_BASE_URL'] return urlsplit(base_url).path.rstrip('/') def _generate_all_urls(self): """ Run all generators and yield (url, enpoint) tuples. """ script_name = self._script_name() url_encoding = self.app.url_map.charset url_generators = list(self.url_generators) url_generators += [self.url_for_logger.iter_calls] # A request context is required to use url_for with self.app.test_request_context(base_url=script_name): for generator in url_generators: for generated in generator(): if isinstance(generated, basestring): url = generated endpoint = None else: if isinstance(generated, Mapping): values = generated # The endpoint defaults to the name of the # generator function, just like with Flask views. endpoint = generator.__name__ else: # Assume a tuple. endpoint, values = generated url = url_for(endpoint, **values) assert url.startswith(script_name), ( 'url_for returned an URL %r not starting with ' 'script_name %r. Bug in Werkzeug?' % (url, script_name) ) url = url[len(script_name):] # flask.url_for "quotes" URLs, eg. a space becomes %20 url = unquote(url) parsed_url = urlsplit(url) if parsed_url.scheme or parsed_url.netloc: raise ValueError('External URLs not supported: ' + url) # Remove any query string and fragment: url = parsed_url.path if not isinstance(url, unicode): url = url.decode(url_encoding) yield url, endpoint def _check_endpoints(self, seen_endpoints): """ Warn if some of the app's enpoints are not in seen_endpoints. """ get_endpoints = set( rule.endpoint for rule in self.app.url_map.iter_rules() if 'GET' in rule.methods) not_generated_endpoints = get_endpoints - seen_endpoints if self.static_files_urls in self.url_generators: # Special case: do not warn when there is no static file not_generated_endpoints -= set(self._static_rules_endpoints()) if not_generated_endpoints: warnings.warn( 'Nothing frozen for endpoints %s. Did you forget an URL ' 'generator?' % ', '.join( unicode(e) for e in not_generated_endpoints), MissingURLGeneratorWarning, stacklevel=3) def _build_one(self, url): """Get the given ``url`` from the app and write the matching file. """ client = self.app.test_client() base_url = self.app.config['FREEZER_BASE_URL'] with conditional_context(self.url_for_logger, self.log_url_for): with conditional_context(patch_url_for(self.app), self.app.config['FREEZER_RELATIVE_URLS']): response = client.get(url, follow_redirects=True, base_url=base_url) # The client follows redirects by itself # Any other status code is probably an error if not(response.status_code == 200): raise ValueError('Unexpected status %r on URL %s' \ % (response.status, url)) destination_path = self.urlpath_to_filepath(url) filename = os.path.join(self.root, *destination_path.split('/')) if not self.app.config['FREEZER_IGNORE_MIMETYPE_WARNINGS']: # Most web servers guess the mime type of static files by their # filename. Check that this guess is consistent with the actual # Content-Type header we got from the app. basename = os.path.basename(filename) guessed_type, guessed_encoding = mimetypes.guess_type(basename) if not guessed_type: # Used by most server when they can not determine the type guessed_type = self.app.config['FREEZER_DEFAULT_MIMETYPE'] if not guessed_type == response.mimetype: warnings.warn( 'Filename extension of %r (type %s) does not match Content-' 'Type: %s' % (basename, guessed_type, response.content_type), MimetypeMismatchWarning, stacklevel=3) # Create directories as needed dirname = os.path.dirname(filename) if not os.path.isdir(dirname): os.makedirs(dirname) # Write the file, but only if its content has changed content = response.data if os.path.isfile(filename): with open(filename, 'rb') as fd: previous_content = fd.read() else: previous_content = None if content != previous_content: # Do not overwrite when content hasn't changed to help rsync # by keeping the modification date. with open(filename, 'wb') as fd: fd.write(content) response.close() return filename def urlpath_to_filepath(self, path): """ Convert a URL path like /admin/ to a file path like admin/index.html """ if path.endswith('/'): path += 'index.html' # Remove the initial slash that should always be there assert path.startswith('/') return path[1:] def serve(self, **options): """Run an HTTP server on the result of the build. :param options: passed to :meth:`flask.Flask.run`. """ app = self.make_static_app() script_name = self._script_name() app.wsgi_app = script_name_middleware(app.wsgi_app, script_name) app.run(**options) def run(self, **options): """Same as :meth:`serve` but calls :meth:`freeze` before serving.""" self.freeze() self.serve(**options) def make_static_app(self): """Return a Flask application serving the build destination.""" root = os.path.join( self.app.root_path, self.app.config['FREEZER_DESTINATION'] ) def dispatch_request(): filename = self.urlpath_to_filepath(request.path) # Override the default mimeype from settings guessed_type, guessed_encoding = mimetypes.guess_type(filename) if not guessed_type: guessed_type = self.app.config['FREEZER_DEFAULT_MIMETYPE'] return send_from_directory(root, filename, mimetype=guessed_type) app = Flask(__name__) # Do not use the URL map app.dispatch_request = dispatch_request return app def _static_rules_endpoints(self): """ Yield the 'static' URL rules for the app and all blueprints. """ send_static_file = unwrap_method(Flask.send_static_file) # Assumption about a Flask internal detail: # Flask and Blueprint inherit the same method. # This will break loudly if the assumption isn't valid anymore in # a future version of Flask assert unwrap_method(Blueprint.send_static_file) is send_static_file for rule in self.app.url_map.iter_rules(): view = self.app.view_functions[rule.endpoint] if unwrap_method(view) is send_static_file: yield rule.endpoint def static_files_urls(self): """ URL generator for static files for app and all registered blueprints. """ for endpoint in self._static_rules_endpoints(): view = self.app.view_functions[endpoint] app_or_blueprint = method_self(view) root = app_or_blueprint.static_folder if root is None or not os.path.isdir(root): # No 'static' directory for this app/blueprint. continue for filename in walk_directory(root): yield endpoint, {'filename': filename} def no_argument_rules_urls(self): """URL generator for URL rules that take no arguments.""" for rule in self.app.url_map.iter_rules(): if not rule.arguments and 'GET' in rule.methods: yield rule.endpoint, {} def walk_directory(root, ignore=()): """ Recursively walk the `root` directory and yield slash-separated paths relative to the root. Used to implement the URL generator for static files. :param ignore: A list of :mod:`fnmatch` patterns. As in ``.gitignore`` files, patterns that contains a slash are matched against the whole path, others against individual slash-separated parts. """ path_ignore = [n.strip('/') for n in ignore if '/' in n] basename_ignore = [n for n in ignore if '/' not in n] def walk(directory, path_so_far): for name in sorted(os.listdir(directory)): if any(fnmatch(name, pattern) for pattern in basename_ignore): continue path = path_so_far + '/' + name if path_so_far else name if any(fnmatch(path, pattern) for pattern in path_ignore): continue full_name = os.path.join(directory, name) if os.path.isdir(full_name): for file_path in walk(full_name, path): yield file_path elif os.path.isfile(full_name): yield path return walk(root, '') @contextmanager def patch_url_for(app): """Patches ``url_for`` in Jinja globals to use :func:`relative_url_for`. This is a context manager, to be used in a ``with`` statement. """ previous_url_for = app.jinja_env.globals['url_for'] app.jinja_env.globals['url_for'] = relative_url_for try: yield finally: app.jinja_env.globals['url_for'] = previous_url_for def relative_url_for(endpoint, **values): """ Like :func:`~flask.url_for`, but returns relative URLs if possible. Absolute URLs (with ``_external=True`` or to a different subdomain) are unchanged, but eg. ``/foo/bar`` becomes ``../bar``, depending on the current request context's path. (This, of course, requires a Flask :ref:`request context `.) URLs that would otherwise end with ``/`` get ``index.html`` appended, as Frozen-Flask does in filenames. Because of this behavior, this function should only be used with Frozen-Flask, not when running the application in :meth:`app.run() ` or another WSGI sever. If the ``FREEZER_RELATIVE_URLS`` `configuration`_ is True, Frozen-Flask will automatically patch the application's Jinja environment so that ``url_for`` in templates is this function. """ url = url_for(endpoint, **values) # absolute URLs in http://... (with subdomains or _external=True) if not url.startswith('/'): return url url, fragment_sep, fragment = url.partition('#') url, query_sep, query = url.partition('?') if url.endswith('/'): url += 'index.html' url += query_sep + query + fragment_sep + fragment request_path = request.path if not request_path.endswith('/'): request_path = posixpath.dirname(request_path) return posix_relpath(url, request_path) def unwrap_method(method): """Return the function object for the given method object.""" try: # Python 2 return method.im_func except AttributeError: try: # Python 3 return method.__func__ except AttributeError: # Not a method. return method def method_self(method): """Return the instance a bound method is attached to.""" try: # Python 2 return method.im_self except AttributeError: # Python 3 return method.__self__ @contextmanager def conditional_context(context, condition): """Wrap a context manager but only enter/exit it if condition is true.""" if condition: with context: yield else: yield class UrlForLogger(object): """ Log all calls to url_for() for this app made inside the with block. Use this object as a context manager in a with block to enable logging. """ def __init__(self, app): self.app = app self.logged_calls = collections.deque() self._enabled = False self._lock = Lock() def logger(endpoint, values): # Make a copy of values as other @app.url_defaults functions are # meant to mutate this dict. if self._enabled: self.logged_calls.append((endpoint, values.copy())) # Do not use app.url_defaults() as we want to insert at the front # of the list to get unmodifies values. self.app.url_default_functions.setdefault(None, []).insert(0, logger) def __enter__(self): self._lock.acquire() self._enabled = True def __exit__(self, exc_type, exc_value, traceback): self._enabled = False self._lock.release() def iter_calls(self): """ Return an iterable of (endpoint, values_dict) tuples, one for each call that was made while the logger was enabled. """ # "Iterate" on the call deque while it is still being appended to. while self.logged_calls: yield self.logged_calls.popleft() def script_name_middleware(application, script_name): """ Wrap a WSGI application in a middleware that moves ``script_name`` from the environ's PATH_INFO to SCRIPT_NAME if it is there, and redirect to ``script_name`` otherwise. """ def new_application(environ, start_response): path_info = environ['PATH_INFO'] if path_info.startswith(script_name): environ['SCRIPT_NAME'] += script_name environ['PATH_INFO'] = path_info[len(script_name):] next = application else: next = redirect(script_name + '/') return next(environ, start_response) return new_application Frozen-Flask-0.11/flask_frozen/tests.py0000664000175000017500000003444112156415721020443 0ustar simonsimon00000000000000# coding: utf8 """ flask_frozen.tests ~~~~~~~~~~~~~~~~~~ Automated test suite for Frozen-Flask :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ import unittest import tempfile import shutil import os.path import warnings import hashlib from contextlib import contextmanager from unicodedata import normalize from warnings import catch_warnings from flask_frozen import (Freezer, walk_directory, MissingURLGeneratorWarning, MimetypeMismatchWarning) from flask_frozen import test_app try: unicode except NameError: # Python 3 unicode = str @contextmanager def temp_directory(): """This context manager gives the path to a new temporary directory that is deleted (with all it's content) at the end of the with block. """ directory = tempfile.mkdtemp() try: yield directory finally: shutil.rmtree(directory) def read_file(filename): with open(filename, 'rb') as fd: content = fd.read() if len(content) < 200: return content else: return hashlib.md5(content).hexdigest() def read_all(directory): return set( (filename, read_file(os.path.join(directory, *filename.split('/')))) for filename in walk_directory(directory)) class TestTempDirectory(unittest.TestCase): def test_removed(self): with temp_directory() as temp: assert os.path.isdir(temp) # should be removed now assert not os.path.exists(temp) def test_exception(self): try: with temp_directory() as temp: assert os.path.isdir(temp) 1 / 0 except ZeroDivisionError: pass else: assert False, 'Exception did not propagate' assert not os.path.exists(temp) def test_writing(self): with temp_directory() as temp: filename = os.path.join(temp, 'foo') with open(filename, 'w') as fd: fd.write('foo') assert os.path.isfile(filename) assert not os.path.exists(temp) assert not os.path.exists(filename) class TestWalkDirectory(unittest.TestCase): def test_walk_directory(self): self.assertEqual( set(f for f in walk_directory(os.path.dirname(test_app.__file__)) if not f.endswith(('.pyc', '.pyo'))), set(['__init__.py', 'static/style.css', 'static/favicon.ico', 'admin/__init__.py', 'admin/admin_static/style.css', 'admin/templates/admin.html']) ) class TestFreezer(unittest.TestCase): # URL -> expected bytes content of the generated file expected_output = { u'/': b'Main index /product_5/?revision=b12ef20', u'/admin/': b'Admin index\n' b'Unicode test\n' b'' b'URL parsing test', u'/robots.txt': b'User-agent: *\nDisallow: /', u'/favicon.ico': read_file(test_app.FAVICON), u'/product_0/': b'Product num 0', u'/product_1/': b'Product num 1', u'/product_2/': b'Product num 2', u'/product_3/': b'Product num 3', u'/product_4/': b'Product num 4', u'/product_5/': b'Product num 5', u'/static/favicon.ico': read_file(test_app.FAVICON), u'/static/style.css': b'/* Main CSS */\n', u'/admin/css/style.css': b'/* Admin CSS */\n', u'/where_am_i/': b'/where_am_i/ http://localhost/where_am_i/', u'/page/foo/': u'Hello\xa0World! foo'.encode('utf8'), u'/page/I løvĂŤ Unicode/': u'Hello\xa0World! I løvĂŤ Unicode'.encode('utf8'), u'/page/octothorp/': u'Hello\xa0World! octothorp'.encode('utf8'), } # URL -> path to the generated file, relative to the build destination root filenames = { u'/': u'index.html', u'/admin/': u'admin/index.html', u'/robots.txt': u'robots.txt', u'/favicon.ico': u'favicon.ico', u'/product_0/': u'product_0/index.html', u'/product_1/': u'product_1/index.html', u'/product_2/': u'product_2/index.html', u'/product_3/': u'product_3/index.html', u'/product_4/': u'product_4/index.html', u'/product_5/': u'product_5/index.html', u'/static/style.css': u'static/style.css', u'/static/favicon.ico': u'static/favicon.ico', u'/admin/css/style.css': u'admin/css/style.css', u'/where_am_i/': u'where_am_i/index.html', u'/page/foo/': u'page/foo/index.html', u'/page/I løvĂŤ Unicode/': u'page/I løvĂŤ Unicode/index.html', u'/page/octothorp/': u'page/octothorp/index.html', } assert set(expected_output.keys()) == set(filenames.keys()) generated_by_url_for = [u'/product_3/', u'/product_4/', u'/product_5/', u'/page/I løvĂŤ Unicode/', u'/page/octothorp/'] defer_init_app = True freezer_kwargs = None maxDiff = None def do_extra_config(self, app, freezer): pass # To be overriden @contextmanager def make_app(self): with temp_directory() as temp: app, freezer = test_app.create_app(self.defer_init_app, self.freezer_kwargs) app.config['FREEZER_DESTINATION'] = temp app.debug = True self.do_extra_config(app, freezer) yield temp, app, freezer @contextmanager def built_app(self): with self.make_app() as (temp, app, freezer): urls = freezer.freeze() yield temp, app, freezer, urls def assertFilenamesEqual(self, set1, set2): # Fix for https://github.com/SimonSapin/Frozen-Flask/issues/5 set1 = sorted(normalize('NFC', name) for name in set1) set2 = sorted(normalize('NFC', name) for name in set2) self.assertEqual(set1, set2) def test_without_app(self): freezer = Freezer() self.assertRaises(Exception, freezer.freeze) def test_all_urls_method(self): app, freezer = test_app.create_app(freezer_kwargs=self.freezer_kwargs) expected = sorted(self.expected_output) # url_for() calls are not logged when just calling .all_urls() for url in self.generated_by_url_for: if url in expected: expected.remove(url) # Do not use set() here: also test that URLs are not duplicated. self.assertEqual(sorted(freezer.all_urls()), expected) def test_built_urls(self): with self.built_app() as (temp, app, freezer, urls): self.assertEqual(set(urls), set(self.expected_output)) # Make sure it was not accidently used as a destination default = os.path.join(os.path.dirname(__file__), 'build') self.assertTrue(not os.path.exists(default)) def test_contents(self): with self.built_app() as (temp, app, freezer, urls): for url, filename in self.filenames.items(): filename = os.path.join(freezer.root, *filename.split('/')) content = read_file(filename) self.assertEqual(content, self.expected_output[url]) def test_nothing_else_matters(self): self._extra_files(removed=True) def test_something_else_matters(self): self._extra_files(remove_extra=False, removed=False) def test_ignore_pattern(self): self._extra_files(ignore=['extraa'], removed=True) # Not a match self._extra_files(ignore=['extr*'], removed=False) # Match def _extra_files(self, removed, remove_extra=True, ignore=()): with self.built_app() as (temp, app, freezer, urls): app.config['FREEZER_REMOVE_EXTRA_FILES'] = remove_extra app.config['FREEZER_DESTINATION_IGNORE'] = ignore dest = unicode(app.config['FREEZER_DESTINATION']) expected_files = set(self.filenames.values()) # No other files self.assertFilenamesEqual(walk_directory(dest), expected_files) # create an empty file os.mkdir(os.path.join(dest, 'extra')) open(os.path.join(dest, 'extra', 'extra.txt'), 'wb').close() # Verify that files in destination persist. freezer.freeze() exists = os.path.exists(os.path.join(dest, 'extra')) if removed: self.assertTrue(not exists) else: self.assertTrue(exists) expected_files.add(u'extra/extra.txt') self.assertFilenamesEqual(walk_directory(dest), expected_files) def test_transitivity(self): with self.built_app() as (temp, app, freezer, urls): with temp_directory() as temp2: # Run the freezer on it's own output app2 = freezer.make_static_app() app2.config['FREEZER_DESTINATION'] = temp2 app2.debug = True freezer2 = Freezer(app2) freezer2.register_generator(self.filenames.keys) freezer2.freeze() destination = app.config['FREEZER_DESTINATION'] self.assertEqual(read_all(destination), read_all(temp2)) def test_error_on_external_url(self): for url in ['http://example.com/foo', '//example.com/foo', 'file:///foo']: with self.make_app() as (temp, app, freezer): @freezer.register_generator def external_url(): yield url try: freezer.freeze() except ValueError as e: assert 'External URLs not supported' in e.args[0] else: assert False, 'Expected ValueError' def test_warn_on_missing_generator(self): with self.make_app() as (temp, app, freezer): # Add a new endpoint without URL generator @app.route('/extra/') def external_url(some_argument): return some_argument with catch_warnings(record=True) as logged_warnings: warnings.simplefilter("always") freezer.freeze() self.assertEqual(len(logged_warnings), 1) self.assertEqual(logged_warnings[0].category, MissingURLGeneratorWarning) def test_wrong_default_mimetype(self): with self.make_app() as (temp, app, freezer): @app.route(u'/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'image/png'} with catch_warnings(record=True) as logged_warnings: warnings.simplefilter("always") freezer.freeze() self.assertEqual(len(logged_warnings), 1) self.assertEqual(logged_warnings[0].category, MimetypeMismatchWarning) def test_default_mimetype(self): with self.make_app() as (temp, app, freezer): @app.route(u'/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'application/octet-stream'} freezer.freeze() def test_unknown_extension(self): with self.make_app() as (temp, app, freezer): @app.route(u'/unkown-extension.fuu') def no_extension(): return '42', 200, {'Content-Type': 'application/octet-stream'} freezer.freeze() def test_configured_default_mimetype(self): with self.make_app() as (temp, app, freezer): app.config['FREEZER_DEFAULT_MIMETYPE'] = 'image/png' @app.route(u'/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'image/png'} freezer.freeze() def test_wrong_configured_mimetype(self): with self.make_app() as (temp, app, freezer): app.config['FREEZER_DEFAULT_MIMETYPE'] = 'image/png' @app.route(u'/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'application/octet-stream'} with catch_warnings(record=True) as logged_warnings: warnings.simplefilter("always") freezer.freeze() self.assertEqual(len(logged_warnings), 1) self.assertEqual(logged_warnings[0].category, MimetypeMismatchWarning) class TestInitApp(TestFreezer): defer_init_app = True class TestBaseURL(TestFreezer): expected_output = TestFreezer.expected_output.copy() expected_output['/'] = b'Main index /myapp/product_5/?revision=b12ef20' expected_output['/where_am_i/'] = \ b'/myapp/where_am_i/ http://example/myapp/where_am_i/' expected_output['/admin/'] = ( b'Admin index\n' b'Unicode test\n' b'' b'URL parsing test') def do_extra_config(self, app, freezer): app.config['FREEZER_BASE_URL'] = 'http://example/myapp/' class TestNonexsistentDestination(TestFreezer): def do_extra_config(self, app, freezer): # frozen/htdocs does not exsist in the newly created temp directory, # the Freezer has to create it. app.config['FREEZER_DESTINATION'] = os.path.join( app.config['FREEZER_DESTINATION'], 'frozen', 'htdocs') class TestWithoutUrlForLog(TestFreezer): freezer_kwargs = dict(log_url_for=False) expected_output = TestFreezer.expected_output.copy() filenames = TestFreezer.filenames.copy() for url in TestFreezer.generated_by_url_for: del expected_output[url] del filenames[url] class TestRelativeUrlFor(TestFreezer): def do_extra_config(self, app, freezer): app.config['FREEZER_RELATIVE_URLS'] = True expected_output = TestFreezer.expected_output.copy() expected_output['/admin/'] = ( b'Admin index\n' b'' b'Unicode test\n' b'' b'URL parsing test') # with_no_argument_rules=False and with_static_files=False are # not tested as they produces (expected!) warnings if __name__ == '__main__': unittest.main() Frozen-Flask-0.11/docs/0000755000175000017500000000000012156420704015161 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/conf.py0000664000175000017500000001666412156413250016475 0ustar simonsimon00000000000000# -*- coding: utf-8 -*- # # Frozen-Flask documentation build configuration file, created by # sphinx-quickstart on Fri Dec 24 15:20:25 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.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath('_themes')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] intersphinx_mapping = {'flask': ('http://flask.pocoo.org/docs/', None)} # 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-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Frozen-Flask' copyright = u'2010-2012, Simon Sapin' # 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 full version, including alpha/beta/rc tags. #release = '0.1dev' import re with open(os.path.join(os.path.dirname(__file__), '..', 'flask_frozen', '__init__.py')) as init_py: release = re.search("VERSION = '([^']+)'", init_py.read()).group(1) # The short X.Y version. version = release.rstrip('dev') # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. #pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. 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 = {} html_theme_options = { 'index_logo': 'artwork/frozen-flask.png', 'index_logo_height': '156px', # 'github_fork': 'SimonSapin/Frozen-Flask' } # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = 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 = {'**': ['sidebarintro.html', 'localtoc.html']} # 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_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Frozen-Flask-doc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Frozen-Flask.tex', u'Frozen-Flask Documentation', u'Simon Sapin', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'frozen-flask', u'Frozen-Flask Documentation', [u'Simon Sapin'], 1) ] Frozen-Flask-0.11/docs/_themes/0000755000175000017500000000000012156420704016605 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/_themes/README0000644000175000017500000000210512156420226017462 0ustar simonsimon00000000000000Flask Sphinx Styles =================== This repository contains sphinx styles for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' The following themes exist: - 'flask' - the standard flask documentation theme for large projects - 'flask_small' - small one-page theme. Intended to be used by very small addon libraries for flask. The following options exist for the flask_small theme: [options] index_logo = '' filename of a picture in _static to be used as replacement for the h1 in the index.rst file. index_logo_height = 120px height of the index logo github_fork = '' repository name on github for the "fork me" badge Frozen-Flask-0.11/docs/_themes/flask_small/0000755000175000017500000000000012156420704021075 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/_themes/flask_small/theme.conf0000644000175000017500000000027012156420226023044 0ustar simonsimon00000000000000[theme] inherit = basic stylesheet = flasky.css nosidebar = true pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px github_fork = '' Frozen-Flask-0.11/docs/_themes/flask_small/layout.html0000644000175000017500000000125312156420226023300 0ustar simonsimon00000000000000{% extends "basic/layout.html" %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {% block footer %} {% if pagename == 'index' %}
{% endif %} {% endblock %} {# do not display relbars #} {% block relbar1 %}{% endblock %} {% block relbar2 %} {% if theme_github_fork %} Fork me on GitHub {% endif %} {% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} Frozen-Flask-0.11/docs/_themes/flask_small/static/0000755000175000017500000000000012156420704022364 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/_themes/flask_small/static/flasky.css_t0000644000175000017500000001100112156420226024702 0ustar simonsimon00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: white; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 40px auto 0 auto; width: 700px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { text-align: right; color: #888; padding: 10px; font-size: 14px; width: 650px; margin: 0 auto 40px auto; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } 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 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: white; 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 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; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } 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.85em; } 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; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { padding: 0; margin: 15px -30px; padding: 8px; line-height: 1.3em; padding: 7px 30px; background: #eee; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } dl pre { margin-left: -60px; padding-left: 60px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; } a:hover tt { background: #EEE; } Frozen-Flask-0.11/docs/_themes/LICENSE0000644000175000017500000000337512156420226017621 0ustar simonsimon00000000000000Copyright (c) 2010 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. We kindly ask you to only use these themes in an unmodified manner just for Flask and Flask-related products, not for unrelated projects. If you like the visual style and want to use it for your own projects, please consider making some larger changes to the themes (such as changing font faces, sizes, colors or margins). THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Frozen-Flask-0.11/docs/_themes/flask_theme_support.pyc0000644000175000017500000000460412156420332023401 0ustar simonsimon00000000000000ó – şQc@s|ddlmZddlmZmZmZmZmZmZm Z m Z m Z m Z m Z mZdefd„ƒYZdS(i˙˙˙˙(tStyle( tKeywordtNametCommenttStringtErrortNumbertOperatortGenerict Whitespacet PunctuationtOthertLiteralt FlaskyStylecBsZeZdZdZi<de6de6de6de6dej6de 6de j 6de j 6de j 6de j 6de j6de j6de6dej6d e6de6d ej6d ej6d ejj 6dej6dej 6d ej6dej6dej6dej6dej6dej6dej 6dej6dej6dej6dejj6dejj6dejj 6de!6de"6de"j#6de$6de$j%6de$j&6de$j'6de$j(6de$j)6de$j*6de$j+6de$j6de$j,6de$j-6de$j.6de/6de/j06de/j16de/j6de/j26de/j36d e/j46de/j56d e/j66de/j76de/j86Z9RS(s#f8f8f8tsunderline #f8f8f8s#a40000 border:#ef2929s#000000sitalic #8f5902tnoitalics bold #004461s#582800s bold #000000s#c4a000s#004461s#3465a4s#888s#ce5c00s bold #cc0000s#f57900s#990000s#4e9a06s#a40000sitalic #000000s#ef2929s bold #000080s#00A000s#745334s bold #800080s bold #a40000(:t__name__t __module__tbackground_colort default_styleR RR RtPreprocRtConstantt Declarationt NamespacetPseudotReservedtTypeRtWordR Rt AttributetBuiltintClasst DecoratortEntityt ExceptiontFunctiontPropertytLabeltTagtVariabletGlobaltInstanceRR tDateRtBackticktChartDoctDoubletEscapetHeredoctInterpoltRegextSingletSymbolRtDeletedtEmphtHeadingtInsertedtOutputtPrompttStrongt Subheadingt Tracebacktstyles(((sE/home/simon/projects/Frozen-Flask/docs/_themes/flask_theme_support.pyR s~                                               N(tpygments.styleRtpygments.tokenRRRRRRRRR R R R R (((sE/home/simon/projects/Frozen-Flask/docs/_themes/flask_theme_support.pytsRFrozen-Flask-0.11/docs/_themes/.gitignore0000644000175000017500000000002612156420226020572 0ustar simonsimon00000000000000*.pyc *.pyo .DS_Store Frozen-Flask-0.11/docs/_themes/flask_theme_support.py0000644000175000017500000001141312156420226023234 0ustar simonsimon00000000000000# flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic, Whitespace, Punctuation, Other, Literal class FlaskyStyle(Style): background_color = "#f8f8f8" default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } Frozen-Flask-0.11/docs/_themes/flask/0000755000175000017500000000000012156420704017705 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/_themes/flask/theme.conf0000644000175000017500000000024412156420226021655 0ustar simonsimon00000000000000[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px touch_icon = Frozen-Flask-0.11/docs/_themes/flask/layout.html0000644000175000017500000000135612156420226022114 0ustar simonsimon00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {%- block footer %} {% if pagename == 'index' %}
{% endif %} {%- endblock %} Frozen-Flask-0.11/docs/_themes/flask/relations.html0000644000175000017500000000111612156420226022571 0ustar simonsimon00000000000000

Related Topics

Frozen-Flask-0.11/docs/_themes/flask/static/0000755000175000017500000000000012156420704021174 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/_themes/flask/static/small_flask.css0000644000175000017500000000172012156420226024175 0ustar simonsimon00000000000000/* * 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; } Frozen-Flask-0.11/docs/_themes/flask/static/flasky.css_t0000644000175000017500000001444412156420226023530 0ustar simonsimon00000000000000/* * 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; } Frozen-Flask-0.11/docs/_themes/.git0000644000175000017500000000005012156420226017363 0ustar simonsimon00000000000000gitdir: ../../.git/modules/docs/_themes Frozen-Flask-0.11/docs/_templates/0000755000175000017500000000000012156420704017316 5ustar simonsimon00000000000000Frozen-Flask-0.11/docs/_templates/sidebarintro.html0000664000175000017500000000153112153050341022664 0ustar simonsimon00000000000000

Useful Links

Fork me on GitHub Frozen-Flask-0.11/docs/index.rst0000664000175000017500000003672212156420117017034 0ustar simonsimon00000000000000Frozen-Flask ============ .. module:: flask_frozen Frozen-Flask freezes a `Flask`_ application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. .. _Flask: http://flask.pocoo.org/ **Note:** This project used to be called Flask-Static. Installation ------------ Install the extension with one of the following commands:: $ easy_install Frozen-Flask or alternatively if you have pip installed:: $ pip install Frozen-Flask or you can get the `source code from github `_. Context ------- This documentation assumes that you already have a working `Flask`_ application. You can run it and test it with the development server:: from myapplication import app app.run(debug=True) Frozen-Flask is only about deployment: instead of installing Python, a WGSI server and Flask on your server, you can use Frozen-Flask to *freeze* your application and only have static HTML files on your server. Getting started --------------- Create a :class:`Freezer` instance with your ``app`` object and call its :meth:`~Freezer.freeze` method. Put that in a ``freeze.py`` script (or call it whatever you like):: from flask_frozen import Freezer from myapplication import app freezer = Freezer(app) if __name__ == '__main__': freezer.freeze() This will create a ``build`` directory next to your application’s ``static`` and ``templates`` directories, with your application’s content frozen into static files. .. note:: Frozen-Flask considers it “owns” its build directory. By default, it will **silently overwrite** files in that directory, and **remove** those it did not create. The `configuration`_ allows you to change the destination directory, or control what files are removed if at all. This build will be most likely be partial since Frozen-Flask can only guess so much about your application. Finding URLs ------------ Frozen-Flask works by simulating requests at the WSGI level and writing the responses to aptly named files. So it needs to find out which URLs exist in your application. The following URLs can be found automatically: * Static files handled by Flask for your application or any of its `blueprints `_. * Views with no variable parts in the URL, if they accept the ``GET`` method. * *New in version 0.6:* Results of calls to :func:`flask.url_for` made by your application in the request for another URL. In other words, if you use :func:`~flask.url_for` to create links in your application, these links will be “followed”. This means that if your application has an index page at the URL ``/`` (without parameters) and every other page can be found from there by recursively following links built with :func:`~flask.url_for`, then Frozen-Flask can discover all URLs automatically and you’re done. Otherwise, you may need to write URL generators. URL generators -------------- Let’s say that your application looks like this:: @app.route('/') def products_list(): return render_template('index.html', products=models.Product.all()) @app.route('/product_/') def product_details(): product = models.Product.get_or_404(id=product_id) return render_template('product.html', product=product) If, for some reason, some products pages are not linked from another page (or these links are not built by :func:`~flask.url_for`), Frozen-Flask will not find them. To tell Frozen-Flask about them, write an URL generator and put it after creating your :class:`Freezer` instance and before calling :meth:`~Freezer.freeze`:: @freezer.register_generator def product_details(): for product in models.Product.all(): yield {'product_id': product.id} Frozen-Flask will find the URL by calling ``url_for(endpoint, **values)`` where ``endpoint`` is the name of the generator function and ``values`` is each dict yielded by the function. You can specify a different endpoint by yielding a ``(endpoint, values)`` tuple instead of just ``values``, or you can by-pass ``url_for`` and simply yield URLs as strings. Also, generator functions do not have to be `Python generators `_ using ``yield``, they can be any callable and return any iterable object. All of these are thus equivalent:: @freezer.register_generator def product_details(): # endpoint defaults to the function name # `values` dicts yield {'product_id': '1'} yield {'product_id': '2'} @freezer.register_generator def product_url_generator(): # Some other function name # `(endpoint, values)` tuples yield 'product_details', {'product_id': '1'} yield 'product_details', {'product_id': '2'} @freezer.register_generator def product_url_generator(): # URLs as strings yield '/product_1/' yield '/product_2/' @freezer.register_generator def product_url_generator(): # Return a list. (Any iterable type will do.) return [ '/product_1/', # Mixing forms works too. ('product_details', {'product_id': '2'}), ] Generating the same URL more than once is okay, Frozen-Flask will build it only once. Having different functions with the same name is generally a bad practice, but still work here as they are only used by their decorators. In practice you will probably have a module for you views and another one for the freezer and URL generators, so having the same name is not a problem. Testing URL generators ---------------------- The idea behind Frozen-Flask is that you can `use Flask directly <#context>`_ to develop and test your application. However, it is also useful to test your *URL generators* and see that nothing is missing, before deploying to a production server. You can open the newly generated static HTML files in a web browser, but links probably won’t work. The ``FREEZER_RELATIVE_URLS`` `configuration`_ can fix this, but adds a visible ``index.html`` to the links. Alternatively, use the :meth:`~Freezer.run` method to start an HTTP server on the build result, so you can check that everything is fine before uploading:: if __name__ == '__main__': freezer.run(debug=True) :meth:`Freezer.run` will freeze you application before serving and when the reloader kicks in. But the reloader only watches Python files, not templates or static files. Because of that, you probably want to use :meth:`Freezer.run` only for testing the URL generators. For everything else use the usual :meth:`app.run() `. `Flask-Script `_ may come in handy here. Configuration ------------- Frozen-Flask can be configured using Flask’s `configuration system `_. The following configuration values are accepted: ``FREEZER_BASE_URL`` Full URL you application is supposed to be installed at. This affects the output of :func:`flask.url_for` for absolute URLs (with ``_external=True``) or if your application is not at the root of its domain name. Defaults to ``'http://localhost/'``. ``FREEZER_RELATIVE_URLS`` If set to ``True``, Frozen-Flask will patch the Jinja environment so that ``url_for()`` returns relative URLs. Defaults to ``False``. Python code is not affected unless you use :func:`relative_url_for` explicitly. This enable the frozen site to be browsed without a web server (opening the files directly in a browser) but appends a visible ``index.html`` to URLs that would otherwise end with ``/``. .. versionadded:: 0.10 ``FREEZER_DEFAULT_MIMETYPE`` The MIME type that is assumed when it can not be determined from the filename extension. If you’re using the Apache web server, this should match the ``DefaultType`` value of Apache’s configuration. Defaults to ``application/octet-stream``. .. versionadded:: 0.7 ``FREEZER_IGNORE_MIMETYPE_WARNINGS`` If set to ``True``, Frozen-Flask won't show warnings if the MIME type returned from the server doesn't match the MIME type derived from the filename extension. Defaults to ``False``. .. versionadded:: 0.8 ``FREEZER_DESTINATION`` Path to the directory where to put the generated static site. If relative, interpreted as relative to the application root, next to the ``static`` and ``templates`` directories. Defaults to ``build``. ``FREEZER_REMOVE_EXTRA_FILES`` If set to ``True`` (the default), Frozen-Flask will remove files in the destination directory that were not built during the current freeze. This is intended to clean up files generated by a previous call to :meth:`Freezer.freeze` that are no longer needed. Setting this to ``False`` is equivalent to setting ``FREEZER_DESTINATION_IGNORE`` to ``['*']``. .. versionadded:: 0.5 ``FREEZER_DESTINATION_IGNORE`` A list (defaults empty) of :mod:`fnmatch` patterns. Files or directories in the destination that match any of the patterns are not removed, even if ``FREEZER_REMOVE_EXTRA_FILES`` is true. As in ``.gitignore`` files, patterns apply to the whole path if they contain a slash ``/``, to each slash-separated part otherwise. For example, this could be set to ``['.git*']`` if the destination is a git repository. .. versionadded:: 0.10 .. _mime-types: Filenames and MIME types ------------------------ For each generated URL, Frozen-Flask simulates a request and save the content in a file in the ``FREEZER_DESTINATION`` directory. The filename is built from the URL. URLs with a trailing slash are interpreted as a directory name and the content is saved in ``index.html``. Query strings are removed from URLs to build filenames. For example, ``/lorem/?page=ipsum`` is saved to ``lorem/index.html``. URLs that are only different by their query strings are considered the same, and they should return the same response. Otherwise, the behavior is undefined. Additionally, the extension checks that the filename has an extension that match the MIME type given in the ``Content-Type`` HTTP response header. In case of mismatch, the Content-Type that a static web server will send will probably not be the one you expect, so Frozen-Flask issues a warning. For example, the following views are both wrong:: @app.route('/lipsum') def lipsum(): return '

Lorem ipsum, ...

' @app.route('/style.css') def compressed_css(): return '/* ... */' as the default ``Content-Type`` in Flask is ``text/html; charset=utf-8``, but the MIME types guessed by the Frozen-Flask as well as most web servers from the filenames are ``application/octet-stream`` and ``text/css``. This can be fixed by adding a trailing slash to the URL or serving with the right ``Content-Type``:: # Saved as `lipsum/index.html` matches the 'text/html' MIME type. @app.route('/lipsum/') def lipsum(): return '

Lorem ipsum, ...

' @app.route('/style.css') def compressed_css(): return '/* ... */', 200, {'Content-Type': 'text/css; charset=utf-8'} Alternatively, these warnings can be disabled entirely in the configuration_. Character encodings ------------------- Flask uses Unicode everywhere internally, and defaults to UTF-8 for I/O. It will send the right ``Content-Type`` header with both a MIME type and and encoding (eg. ``text/html; charset=utf-8``). Frozen-Flask will try to `preserve MIME types <#mime-types>`_ through file extensions, but it can not preserve the encoding meta-data. You may need to add the right ```` tag to your HTML. (You should anyway). Flask also defaults to UTF-8 for URLs, so your web server will get URL-encoded UTF-8 HTTP requests. It’s up to you to make sure that it converts these to the native filesystem encoding. Frozen-Flask always writes Unicode filenames. .. _api: API reference ------------- .. autoclass:: Freezer :members: init_app, root, register_generator, all_urls, freeze, serve, run .. autofunction:: walk_directory .. autofunction:: relative_url_for Changelog --------- Version 0.11 ~~~~~~~~~~~~ Released on 2013-06-13. * Add Python 3.3 support (requires Flask >= 0.10 and Werkzeug >= 0.9) * Drop Python 2.5 support * Fix `#30 `_: :func:`relative_url_for` with a query string or URL fragment. Version 0.10 ~~~~~~~~~~~~ Released on 2013-03-11. * Add the ``FREEZER_DESTINATION_IGNORE`` configuration (Thanks to Jim Gray and Christopher Roach.) * Add the ``FREEZER_RELATIVE_URLS`` configuration * Add the :func:`relative_url_for` function. Version 0.9 ~~~~~~~~~~~ Released on 2012-02-13. Add :meth:`Freezer.run`. Version 0.8 ~~~~~~~~~~~ Released on 2012-01-17. * Remove query strings from URLs to build a file names. (Should we add configuration to disable this?) * Raise a warning instead of an exception for `MIME type mismatches <#mime-types>`_, and give the option to disable them entirely in the configuration. Version 0.7 ~~~~~~~~~~~ Released on 2011-10-20. * **Backward incompatible change:** Moved the ``flaskext.frozen`` package to ``flask_frozen``. You should change your imports either to that or to ``flask.ext.frozen`` if you’re using Flask 0.8 or more recent. See `Flask’s documentation `_ for details. * Added FREEZER_DEFAULT_MIMETYPE * Switch to tox for testing in multiple Python versions Version 0.6.1 ~~~~~~~~~~~~~ Released on 2011-07-29. Re-release of 0.6 with the artwork included. Version 0.6 ~~~~~~~~~~~ Released on 2011-07-29. * Thanks to Glwadys Fayolle for the new logo! * **Frozen-Flask now requires Flask 0.7 or later**. Please use previous version of Frozen-Flask if you need previous versions of Flask. * Support for Flask Blueprints * Added the :obj:`log_url_for` parameter to :class:`Freezer`. This makes some URL generators unnecessary since more URLs are discovered automatically. * Bug fixes. Version 0.5 ~~~~~~~~~~~ Released on 2011-07-24. * You can now construct a Freezer and add URL generators without an app, and register the app later with :meth:`Freezer.init_app`. * The ``FREEZER_DESTINATION`` directory is created if it does not exist. * New configuration: ``FREEZER_REMOVE_EXTRA_FILES`` * Warn if an URL generator seems to be missing. (ie. if no URL was generated for a given endpoint.) * Write Unicode filenames instead of UTF-8. Non-ASCII filenames are often undefined territory anyway. * Bug fixes. Version 0.4 ~~~~~~~~~~~ Released on 2011-06-02. * Bugfix: correctly unquote URLs to build filenames. Spaces and non-ASCII characters should be %-encoded in URLs but not in frozen filenames. (Web servers do the decoding.) * Add a documentation section about character encodings. Version 0.3 ~~~~~~~~~~~ Released on 2011-05-28. * URL generators can omit the endpoint and just yield ``values`` dictionaries. In that case, the name of the generator function is used as the endpoint, just like with Flask views. * :meth:`Freezer.all_urls` and :func:`walk_directory` are now part of the public API. Version 0.2 ~~~~~~~~~~~ Released on 2011-02-21. Renamed the project from Flask-Static to Frozen-Flask. While we’re at breaking API compatibility, :func:`flaskext.static.StaticBuilder.build` is now :func:`flaskext.frozen.Freezer.freeze` and the prefix for configuration keys is ``FREEZER_`` instead of ``STATIC_BUILDER_``. Other names were left unchanged. Version 0.1 ~~~~~~~~~~~ Released on 2011-02-06. First properly tagged release.