pax_global_header00006660000000000000000000000064147740457560014535gustar00rootroot0000000000000052 comment=d22107a827e9195de4cb3d6f17ec933860a8f797 hishel-0.1.2/000077500000000000000000000000001477404575600130115ustar00rootroot00000000000000hishel-0.1.2/.github/000077500000000000000000000000001477404575600143515ustar00rootroot00000000000000hishel-0.1.2/.github/dependabot.yml000066400000000000000000000004231477404575600172000ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" groups: python-packages: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: monthly hishel-0.1.2/.github/logo.jpg000066400000000000000000016162751477404575600160350ustar00rootroot00000000000000PNG  IHDR-bi- sRGBsBIT|d IDATx^i$Wv==rϬګPhnCI%2iF2 d73`>P6CN[sn4BGGs@h6 DdFF??y={gy^E@PE@PE@Xs1UiL*& u+&mxI0yI*f6M4=L*=K3Od0OA0-aTߛ4pӟEq/W-_?81Y5Ll2 16'`ݗce`_7+"("kO3WD@g;nu<1W.^VJԛxg1f+Fb)v{%lƓM&ATJŒX7%^~QA?-WF1 rRuc #vqI.vL.7Fya˝~GdbEgee~c1ӅɊݳR!߆&6l9%Uߤ,=Y pPLh/I4*K ܓYl6A xb>7$ YFmlnMV6f{j&U 7Qp:Tʵ3.S? >Idy$2PE@PE@5A,:&0^29 c ir5!!AP?ۯ/KZ-7&ꚗ$S&y? #in8Y"|8y ;4pY"ͭ;ۣՍqRJ1>ׅӅy92(U@&i*1"ǵFn[:U m_됯1A!jݙt{$I|ϯu//O=.<<7Yd:3(ajBIJET:f7ÇwMZ6b1 | %χ3Cj;?K^kv;6#);w{ݾ1|q$q,\pa\"# }85V^DkwF/SPE@P~ )sH>+:cBv O<'-P5(#I_{/vObVƷ1HLǦXkL(=>95E@PE@P,vuy`7 #}h()^ ;(5Y5a_ |l@uB9S2,fiy#?1RM F"$2^aƣ۝;aVƷiK ՐlL<3%"` Yb0y9Lr 'Hw&.$tVg9X84N4H}@R.i ET+u#}y' ttii)~Z|mF:l6= o;3*/2PV$W9Ţk Udfpsyy=o`«'mtP4j/*E@@a䜰AD iJ'} E({!7ߝ`}pwvsoe~@3eHU1 .QX) r+z҉HN@V9gpX[9)`0qcN)L3?5<lr%OՔJIɣ/."ΤSfx<fE[HƄkB99~m 5p MiRVVOZ|A(2Vs~Bf.  ?z"( oH3jWRHPx6g& M5(l#$@.6QakyQREق;E S#ǃ'!Y(H3cA=71rIry_r{HGH:<r] 92u}VM1ıkBK~sy/$/_3`:{ɱ TZ9~ %LӒ%#K`wCı 0{;EtJI2FB8z<%ދ\a>ۃPyƨ$-Ǜ3@$&& {o}o6$2k\C$ݼ}ܼy\ڔ^@$!_De/$sBdbAlI&A`5&\ 䄝@9M  ٰLNM csr\6֠j2,d^nn#H,X"(",4s-2HRh#1~{+nzZ8W&aϗOOgN[{J3њ;&ɕHAՊrl ϸP9؇Mh|vS)u 8hFh,I؇LBw{Qb58ߝJ!@z|йc1ůtp<*H#FV粄jXl sș#5ĮŌcrfq..4=? 7F/`ӠbΞ֖~~j0@Rq | IV&.8u%SV0`m[9/'oǓoGXx<GgZͺYX7em*AUHX"W*=z~Ÿ%YQ$@h\e#'F+b%98T]boRr(yt$b%?qp>Z +4hcq.vK~`<.B$+&FH@,"4Roa>(W@Zr_#t S`C:N_/M[迄:8E@PE@X82園bZiBX vl (!qp^.Uh7k"^cl0@l*q;8N( pFx6# $ߌ(0L8t`t0n.t)]@QƷ90AXƄ-YE 1(sZrH/BD΂FXYRexL+7~ěCQ$HDuvz*O`Gggh;2IQ\cͧ׶4joϿwys'_|0GJ'Hť/߿ bKZac=rWBޝNz_9;;٬|xxb]zfmuՔ+Ex[3,}lng~*-$Y IDAT^8FɅIe\y|. KISBXE$9;a ;%d׋DBM0IeK+#E}E0Y8t+5z(!"U$/lz8cчZ+Vuj'S 4H31D|c<"("(2\mm]!&aQbv׹,/7K[(Yo.jK@$-/TVА[L,ĵAJ-?I+y(.PO˨\Vi41ʤ&A^{ǦPy9 cLŖzMZloW(ё-3~mػx%n%6 i*QpP0v Чׄ‰y"V⢡A W--+Kww$ Pnl~`TZHfƤ}(\눔sgA*#,y3Aˏqn}`ңcı67S-ՇLysr/Nq\IP@>RXL-$^}r싄;k MM'oDB.~ZV{49<<0#YBu!~<&$ p&ߋŊU20rbBriE/qvT^ `7щ,LGRye'pQe822f*-Gr_@rl&y2vd/Ȋ${Ѻ$J$g~H9,Sz Ӄgka:yb_tݯ7YZZ ֙WP|{;J]N\'s-ig7}P`E@PEW Dإd1spx %ݗ?? <˅\n{k/ Mѿ4[u4dD5HCA.8KX_$'̅&8,,q :#Ȧ:c+ g> X#.,0fyx,\E1>bױu(#-.݀12d7fvN4*8v*%lXdX5őV#S]ޘ?|*<|p[j8&,f  |U㖅:gM,b9Tc8٤*T+z`owPda{e}}Y|u] *yRG Z6e0vF]^FPE@P~Y *y*c3n! M l࢓|N7cZ.YY" 6ܿk"ffnKQq 3ŒD v1w|/,ql7-)97D/K;$UƑҖUL7 7혱QOGpq̰sc I q2_ʵ'j~!#e9}p[v&XBu$ߙN+r♙l./NGϔX1[*t{62լ㽝5f-mVy4]]>{^9 @RP C 욘w|}avpgE@PE@r"0v!z7c-w>6OoRɰ{%tt^ܼchovj;Be!'Ϫ+5jfwTނ xIbQSiq=a:Ӌz sæ3g&1AKf Ę3BO2A(m;MzЧĝ֭bb^9Wl`  ]I-uEDⵠȚ <į2P|{Ƥ֦X3\{dq:^PJ #aZkB.$b)䲈@LIEERx l%7I \fi{!(/\(db\(j%6.WJzK({SqEG"=ysIF* D0\Kp"87nc1V$%ؿ.Jcy,"QFؼID;i" a>DD41<{H^퍹AL21Tgo}JtP<{p=}۾ywoNΤ ,wѿ')(,.sA_gη/hnmj W4>-._U&ד]5s!@>+3/.i,A6Ѵxb*'1K^`2iTqKڷ,K6ǿUѓ!Q/N^}$~<HUhBn܌-y) eM`(qpXލ !Cx،QrX2+癣'Μ]l~长Ir`2 &9ψ gSA-*&8d޼&jY4Xh|o ؖU2gyɌ#d 7i;/lS*͝Nqh4̠?2?3E+WL?HFa\R+~7[׶QtR[YyI!2 1/uO"("%@`NYWUF6zdz-(AXpryqwj9_WʅQA+5qEN# oeXr9hZms6rZR c\ۄ17mu/k9c6vKfw|gr{? d A($Q_fTp\bU& *#v*0vaȜ[E)f&;&~ޒ'Ve*|44.:ؔ1g𡲛XYrɣf& ^81WxNP?/V {Ŕ! $-ٛŶ r4{6_A!zSPd@`%ؖ CIbƑ{6qd!!Qx9&?8*S9%ý_U=ĦL>Bq k,X\`.2+1 $&ɈYlNiZV0 _D"=HLQr8\0f\u2pae}ϦG "xCaY10 4;Ry;ȉj%8=?wҞ/»nǐq#/T_/~SE@PC(7ζL<> Gw^]v+jF\6f2-{ '͇̿ 0Ň`5-F|Ԁŋ(u}|_3k+ج_C3M.KjG m-Lטt ˁ#[6w9'b1 y`^8%ZY]e0GZ4e&̓s;BĦ%7rY\ؔDSϜ=̥YP`?Mit63O+V3Gp^mrDMd+*p,9:cxDshBk^XD)_v&ؐ/TgiP9Kҩ_n~R-?QULOpM.]$-x4|Ppxa_To[j8E1v3aeBЌ&ǰ"\hȈsۼ7Nfw-<AREc+,Уǜ$267G ,xDX3&SBˮI&N^c ARs3= #KSϹRzy"9% ѱy2{{G L`h6dtV+!&o޸sgJq /nY}l_Uȴ?zE@PAsigcCV ѳ/X[y~vkz I46D>6Hم V<{~l;fwo,m"16S//Ѷ XV$&z3FOwA<;m(lcMUSxt(I-b Yr l!wqef54ƈ0IeȊ8"`D c~+%I~>"6Ŷ&7By\7J `JHKVl[&gmcI*Qq" (r9:Wb}$-Һ%)"2F?eےbc+eⶔ1Y‡UJxD/[U>p>3䆑 T|`IgQZ)7p#;]qzbkfn7 N묢?/Ty7Rf(W+f,Ks" J4jyIXlqەc.ăh\o& wnq"X=op,VZQVњ@Ė6mƭ9-7H:^$Pg%qeJ0A륭0&%!"c"PṼخl9!.FU=sE!a ]N$|u#um*[B~i<9Y*~ !x 4 o g!2CZbTq2 eμ,`ޑ UĬ rkbvG'E4|/+#0`RgPAT`@&0ReͅEqr bNoZnJfK!3ɋs=eL|RxM\,8m 1ty,m`PYB9.'F)C^;(\qU?}>HSX(={T@H9:<2gt-jwfĨαgnܾ!jƵkiΪB.5V-ST̏i~X7w7io=OQ.W%󎛯 qTYJ}"("KB SUme|fi(NeEr ٨ދ{'ml\..9v0܅EnmOL?~d?za~|~CB6i\jt}uyۢ6TTB*64ooy7_iAYڄ[,9|+'8Qgg[ΘU#ewdž14b+ Alj'k\ߍl~c7AĈX,G|KgkWh>ϻ9q$v$cé.lA\{IRCǙ+KJc!t4Wn~ *gA O~^B k2-/:;}/GJy]͚{h,aqe:v301OZh4*O%eij!'s=_p]OL0&yՇG+L`7%B"يF$h]r|?EGsH?V5]%:M6璿tH[N Z-K ?sSiex<? ϗ t#5'gNL@٩kX(S oooo65ҙZSTVNc1C nV?sQX:xϠ.rJ%/={%UE@P~N0cT|Uā7^oAf zlهS=(+}e,A6WAUb.,O/^b#-"sqNkLΘk.@m̛&ϵMQ,W$~)!f(R9#/yZXP)##.'a5|]*BPW,dGH Ybƣ$Rt^LpqG9[Xq*tQ'RVڿ\^kacyA25#hW4B\*7 U X 00 #X.!:_LsZ2ӤWW7,1{xܾ-60' dw{gH*\P r!m" !(& itg|$(ia%Ax?,a |Vd弬 )A%UTr.d 9\KGfDM'e-:ۇ90[GqE>!XR*9<'Kr̓Hp y2`ra:a7ax=\/ʓg~5 NPǼ/t IDATN1XьΎ?6+JPQZH $-woEY:>rbƲGNʻ Z(؅`&6ƓZk%9>9a+HB_bU}9я+"(*y\^(W׭}ۘNGk~m b$4kDcq>g({)%b/_p=/_9k,WkIѷjbimS7e/JeAѱFỽ˓o{u-y%Xu!P"9**ue C$#nbٞ\RإEd~ jW9,d@N"`J$ "4\xv㺅5~y~]X Ûa(Hc۬ μ,NJ1Ģkvč%Zx|V~? ?kCx#x Yol&.A 3'hh.:[7=7-=/{]_;۷MtYj>8~V5N-Y!<,^ ~<^R魬];^^];SL'X} }LW46Y V_SE@P,d}omG{^ԪFJH} 1N ldRkUny[+^skxϞ¢ƦмBej:?x$4!AV^^>' "'XJDE @i"Raڻ0d 9>LΞ#dҕ#]bw b qJ B& ($3i~@ƒpeՏج8O{+F ɤ8Qq8ݜ9ۗu%{3;deG[94C4"D0Q{Jaឝw7{+ןVˏR# 1h``C!ʯ֟E@PE@r\ x3W*U|!;zڌF];:عyr Hf\KM||hXw=V+2*66vW=%v3clß`@BB7_:fnk#9#TŦl- br̍7{jѲK|8Dq]m78^mê OrSW6Q —42 CXSL3s?C72g ;eb2"f6Ikؑ`xK6##Ҳ r6vN!ϩЊXIAC;+"ip)i[|CryУŋ>-n.UGw48Ix}Xr+cs "==o3! { JKI4䞠aL+W(RjUV/`3,DEϔ) '#r|!EKдz *`}b$-\| [Z ;͒拰YO$1O=BȨT7uqՕBŮ%dAhT6kV7*pN%IR*UhË9Do`&A206NtPo6Beh/ԱȮB{/E@PE͠ ceK~rs:nFkdt{o2|+A^7P! aKg/`ϧ $xm˄Hc.2 \L8m&3Ʋz\̘ѱ4d1ϟ?ljah.JZ#%H{~~e]7o4 b (4wnӐMrƕ34&iaB'z窆e3D_!E"r*[>A{D76޾}'qm>b(8RiӒp\rrNblm k;N*50Qm qۘBוWLG^W^DQ6\8HkK_4>.1&dPynB"H%Vsg{_Iϒ:\o$-e&,F{-&n&6ed>KC`Ή=ͧ"Ʀ-\@!0CBYA7V0wUpM,̞5$kWΪlf;l7N5DȈ\Bꐨl*T6qgJhieeim#hIhʍ6:c )SEfK"(g#v_o*p~5Qo*]6*I8Az|$T3HR5"P)H*،"&+!#1/cnT|9{rNvf6s*b)&3v>vtiMcB*c2?F8 yaȔ*\%лesHS\Ċz0%_bcd6C w{!X% #YÈubs2@q26eԆh +s丘Ҏ%eBR6'! Ysu1r$HT0vssU`DLc>F2Y &>.'a \sqQ$?--}Pm|%{cq#{2VD_?+a+'wK9S.[~ j rUjC֣rP>_BlV)#7db)ŰҗcrҌhKߡ c_""RYRO bKS"V]kbc繪x.2︸8>NCuWg|1w * I<.od)>':iEܠcܠ#v±+I]QXݷ5.a3 v0>-0ݝ]9&߻OlHFFU<;E6e83Fmp ļ rynZB`0oz3E^յ}>4'X`6JyPI?ӊ"(OG;+==f:ZAD}w˓[;{G|1n6J-]F! N$_lE.6vȦfl䒸N m1 K#'$?!"@uA7 ء@{`\gIi&V8%6"-I! B{18g'vs!KEjCzpwrsy|f~w~GT*6>VVp,ҮdSv/~MMbZ]1ocEa'^Wsnca8.ƅT8  wEX9G4ęCQcb8y.+FOT{m^V0YـwyMpcL\98pZb,BTX>弄0 jWbbC'ʓBu#6qnƁGZ$frO퇵bp[ \ś =`̑Xgd. $]bӺv? $1^@H]X uFPӜ$,7}F$5|7 6g3HDK46-ܼysOuDc4tR q0͊\r1jx8+ 9r%潸cT_8&Pb\>3, pe)݁vDϲmƻ'iaL8NRVl(I"SEGp0⁴4h:R_)ן[Q)α~ve݋Zm40sI_}/΢ѭJ9IZ_3+`mڃJL`F6."S \+6hdfn6:}*- Hq<$z'x:fgWƉLC`a;(,||ka;2 b<ćG''ѧeUrZoܔبYcӖ -̩WcψLH V QhcBtX=+SaDHTp3ǴDGr2wp8"ᔖyBUn;`(]z&OzʲI 0`s su&F]$\K?Kƃp6AVcyV/l{ 0+e9b4x2) yo{{߿yxesslmn{[I%0T?_Ft$@[]KJ8"rxQb  Wܝ1,L%٠(Lz*t쑋:Q6Qud(D aۃD&MUC6D1AJϲĸJX@r2'i1߱Pغ[Jҙ@~>URs~s+|Ux"hfp/M̒N&t\YPMX ēM"ñsGx^$$廢 acheGJlǐ" f8WTF= \7bqaÔS51"SY)vC6˳ d3H8ll %n&)E%q2H b<+jieW`E2dCPi [p'E2I@$L.J]REm49*VZ4/V ^p q~n%oꯢGYkXe֤FPֆg=kp~LO-⡇$kn_~fnA^ IVeLUWDE@P0Psr{< 7 pC{hDRRa8\&_|L \]A5..AY16`h͒~6[vҞf )qT Kc)dI8O6r!( 8_^KGNJg'<2xw~)VO 9Fo2QHZV*Y֪-[~8SZG'KX `-I\ٷ\地Z.5H:@OBޓ83ŖuhIsj#^G1ԔyDʧn,|qKv3 [ؠ^z|~){iDR)';I2,'k7| 4{!eZO+-aqRO26L}^oDC?CUxU}e #lPA%[6vӑM\1Gg. dlX;,rJ)ZX,e6H~"L\!4^ f `frw|k+İUXR'r8W-* bōcAΝ\8R H`($Dom0ۿ5밃_y:jj 힊p̚U*dG?YaBnU1XX"fW`T ![s^JH{pyi7,9i9dMrIdz&O$jq;l("|5&%A?&g#J^_<8:ܿ܎# IDAT&-yCQ ^*X,!r^l xxʋ4^dp-m]q,ls9aV- ҩTsu2ޑZđ)/S~%T\@Tm ~5[565 0tFX7kHZΏeLȣ5 Lڰ{i EtJ'.^5lx 𺋈U,s x,ƙLr94gѧye9$&ֹ܆keBNl6ۼn) p[ʛe~3~=b4TQh 7\$Kw]$ǧiRKm㹑^\Doo]ww8>r9Q\iy"T0c&U,wA ow^0<û܍{r=[X +kѮ嘪)$gRPӒ5y~"6i7'ԎFLY| g}h䰘!ǁWL*D8X聐CAUxC b5crL"nV.>.<) kxqs L.6|%l*ܴz\{a:(JTT˜ǰ̳]٭NP~wGd`;0:i䑮 W|9]fQͻhܤwِ8'&3e_y}6,~9$=H -8݋)-o(2^S5b> ߷^6=$YO,L:g<0ޖD@VO[/(Yg@$@@82$U*TIũYO'+$R~ !/E:N"JүMĉ7M0b!3 +cΗ#$A!8V̛}uڛMc>5?ʐ/^7j2>MATVD]uA_VCT(EG{zHQfi:b,.-0m_]KF,+pKVĆ"< pY&qke 6t-(6aUtXe7v2^>}*ӻTPڢu+˫͏*Յnee%l?Cy~281hiS-ANJTS|W>g<}8Hʼg7hg^n[g/]WJ"1 mH2CYA$E>%cF0֮ *",$ 9Y&: a N2sR`DqIdk}Ay-Gc@Z]4Q`& |!}'5dă9@h LNģ,&a.e3 ,=>y$9<'X=JH)&\x=" i`0v3eL=$ĢJwA%qؠEr8?ce$F,(>GRTyssWk7ngs33'ZQO*uIR3vv .xO4@ԴsF#w_츭''.v$Iʜ%u̮t6`Cp bq>2{_бe{%0%x#N-X\XΨ0VѼ\S[ZSVR{K jcX4ϴ+OO;mi L[`_%*xPYt.n(6fAT[oۍBq\+s! g=B{1OvySr$e[dCv `!,zC^ {"n6|1Z9aXԁ^|.2r*?,{F 쉜bFqJ7*co eBP|W#!y/-9aDq=+A~MvW#֥Z[\g$/g8HăǙ@V9WUXD!l=w^Ͱyig9TR"leppCkmbo9 aw8Y\ Fd&  wAJ޸zQS˽ß}8+bdǹb޽wY_淾G -sE{-͝dsYU& .2h6qrRaz}\( ;]wx+q]|6:ϕhqxSgѩmBji@ssږLd5mk[h38(` dxP0aИ8,# :`9(l  :[_G8 3D3-L[`b}C DRPҾd[v `my|\o(t^f\R/ԊzRs:6}n9 dd`MSZHDZ"2fDdVN5P%EĄ}٨ٰ[m# cGN0Cf E1l zʁu-o ]<"|&aets8z)r""ۉsr 7Ҍ~lMkkTG ccIomopE;m^uaO:OPȵzwqa?**w&q-sSN$O0" 3 :=ѮI#@Ki^.`!oi(X,ʚ K0J 0F8:>JAƅ(sItw_/DW& "-~٫nkeO)odK_iտ?ijDVJaTEBz8KT} ZR" 5R=<I<@nX1GdU~u&| 3ɑ!)^F<{) MLa\\8.BɋAaV_t<4."w5ȕN{uG NJvgnKޘq,I9͉lmmIoT$K\_PeTTC$E#2ً qR;FX 4]Y`OU^I8ÞIZ˵_skyh-玕&½]aTvNE[FȽ cG;Oa\䍷|e-*rzPU(r^@f j-0mi L[[`"2 XԵ~z݋7ͳ/wT=bX+d^?+Eژ0\dlo}x'%/$p0&9 9$RweȊXñjNJ<: NfAI{l+Ӡ3>\ZT?;7Z}M*&R FBNc^..%h 7-џ))"$@Xw 2݌Q6K~k}Wy< <џ"Y1ӝ%w1&'l(lIKv^좇u̽{eP>3{fwR\Rݻ}ptt~]TY|Ga%i;y2y ,!3Q;%MqjP."'$̫ p*=%@ !@  D1p}$vh ПU jU .Y Dr>-GdNCGj <&5r &y*TJBJ^Q9%"@t=JIՠ$5Sc pg젟=:>5EKh <+&묄y4{|@/Ag%Sg疢߰G 0%Ey  :PhW}m5j :'2!|u_ݼ|X}4v>-栍8QƦ@ņ-0mg $§,~vzSlFpwɓl\- ᰫ<|5+{EDoRT"GjK%U vd˭lOΉYhQTGL?A)A BK ՜æJh1^;*qSaDDXnn`$:RN ۉbQ3@L~66r'=9j7s~z`G[7oF6-?xK̖`?3cqGt%/b0cL!iOX_M5vYJZb6 'BLM1F&#EV? VS$~y '5#j|h6FQ }Gǖ\-zs_gfI귇Y2@Sg#ؗ2:A@L &TYO4#qF1M:;O,8~`}xxڌy:T5M,Wy&4feZ5W `Р# }kU  HB"Ԉ0䈼S[p5pq @[ JJ{NMJL>Pb?<:4HPC&QN}2W&pQ-*Ғ%'G1(l7C4)z:ME&#Euj}slQez5?-crRY[Y? L3 qBq;w ?ɋLcVeVX7Ni L[`j[`y5J`+Y҆νlދ'>8;]뵯fN{ S`NN[7oKsWml9[D7P!BpM.f+%l1%q |3X 'WCätSG:b>N^6Ђ="0+qp3)`E1HK* | @S {:Lu_\xVE$kܒ=0R NN0,=/kWhqyrz1)vol?x>f˕JHddz IDAT-0Imz©_w_=jnΎ 3䃈F("SADf7_ΩS^A3kICa$o44-@2dĤqsLk0 $p]p%JElS{T3x?Ž$v$>8>{̓5## H#v"5%@9=σA5LIUH)/@a2P Jz,*"L]E* ϧ +ICs>rZt~"-4@1b?Ն)ޤҗKE^ƽ'pRT6"W&]𪁳j('YH?s^Wb4jt/U|~qu;ss$- q}aLs_bOO[`WZ'Ti [Dk#Njg*V++eiƕ˲.U]zSM8rH"ӾhʤVD}E}C[sZJJ絈-rHPg0 w;:'} \..{Z?n 1Т`*aUpSAgmox` AD~{.><'N^CP[$2ʧ2}TOH̻%$32n$e d a #^mPi ]qՋ p })58@8  Bx,PI6DXg/ !/<>w?T81d`j1FA FMQ[ /W-%bջ'_jD΍ 6-sP׋L6x TqӀEG g 6eE0yɻ o-SC[SAZ?_'@yةSՐIEw:l#H'|iʹ\=[^x}NP'y!h~*H S6tB%/ᙧ&-0mBZ`X2C)`Q ؂/նBڬ^k_y||nmFqbVrl8|q[e}yuI J͑eb;;-'+^l9L X֐oTKR(``+*9E#'[E{e+'L>(`fH[0aq:DsM:ÖN&'uW?lQqN"C6v!s<-* ,skiiZVll4Z!5i'}B0BfdlpEYb6tmLi7@XkZgsnXA"HrR:U96SsIESe^d޼)Սc%_f jK@ 9=o*ڊs:/߼qycCRm/T.)/}vQhLM[`/aSOa6÷[7}ꂊϖۗOdw=Űfk k/`l.]Qd ecc[byɩPy}EOcOx-%h1uBilqX765kED=x & f?/$p?$@KHdA W0<Cڅr 8{nf{/_Ȏ(lky[^]ha؆hf33: " dNy'q_+Aؠk~95n`'oZ_畿3%3)5aoE?K@K3A=t;8$}F!{]Y^+".v,TdzI'9hyGd/Nм:~G r\ U 2n:$<*8{S0a 0zM@MHP @EJB<V+Bi]oyqI ?>lTXH}&PYy t`q߁,cd{Ĩo160uaLւ&iCܻ<5o$+n#*6& a]RCtmȢ}q~䀶J hA+Wk ^\fEI=קZ[5Yfe 0K,s\ !cԺpoƸ/yNSFR|Ԣ4h>5 ؤS6js-))(ń{(d-;QVu~dIf`Vk㨽Y_qi4S=Ԡz玺ÙWL}/ZeM[`$l!#g~7*~pW{ן?}|!Wf;mVkeng.NgJfavk1ѿu'oO{kfLS|"$5Zzm+EpD3;s ?E3p6G`X^9DB>̛/1vTI؟)0*MAC碨 ĽO-$_SAy!fqVwL™{vPmFbEANJt=a7LjA+[7?iZ87;[PꩭL m"ij% ;3 8lc{XglG*H 猡e)Nw/"kqb>~i)vcg4<ٲ&"-zsJMndpvUJs][u|J5:?g!wZW?x^>\(g:xU΀1(BZ̃$,9f~Itx'Ƒ9@ CS/ڕ;$l*הL~$uTTDQ¦bꘂ<т1lOHj R\Wҋ8qTugխ֘\Qqᮒ1mi L[`"Z3\W-R**z0sO *gUO7k^GJ*$EL|ffߢ_)&ɩrZæбY ?hZ7qZhG3B28D*KG집>90:eN9Kcfߏ|Qw@Sg`JRH :#%TH|3NĽmNH[`8DtATˤ.*zy^g]92eoo6bqZ޽{C9V9"5̮E( be,S8G%'36"Lt`>E|a-9'n8>0v`6۲&&AhdjZ\BJZ%f@ 3EͿ-/,?ɞ:@c8ɹ< {0h ZouWϟk\QZs a `P)]svHm$Eџ@{@4.wvU&QtmORE1dY>-2TJ" ((0(J*2$ϪEenpy_d,?ҹ_8􇗦EV 3uy5-~t&Xe:W$E^E|@6 ;]5$,"B4T@!/ ipy!y"EF84cԣ$ Q̫.<"j>FQa%b~&8b`P6h2rmq` a&y=^_Ŕ̫MzYҁ<E$r6:=kyu-S T =rT%,tأ-""=D;yOaXX<:` mxm ad"wܥE&H%9 yYL XhV3չDa ޏ:.va8J6)5g$|EO7pĽr= j50g߼9ZB`&K)UȵB''Qݟ9\r}R+~>((%S1i/ZuM[`jI#I{nm ;'vHfٔX~$n `ϊZ]_7;uFA͕T|R KO]*^EbjR`X;7'ib9e @:DiƢ!H0Z|z6`d$yQ>EH|a`O{a%3^``&^(H% &9%ʫ ytyX)hc@:=K-t$" %!' 0uS"2VH?|e$nv(5ȭvuSn9HHXǩ ɹ''x6 H]Dw2'&?AGsRTFc Z#@lt MPṈT~ǢCK(GuƮJOȖ:Tw7nh0ҏuh&U:)[zį^xK۽IՋÃ/k##mCNUGS,<`x\${Xplgp0b5Ӱ,q]k2}\+:\btXy)T[Y"=R*$q6KPIs *?씓c: P' Q~P *R'@4OZw}BxaΉAs':`+iˎ$݁ O+N?NPIT{{[#60y6~09OC͌{Np\4<htTz ; '/lll/Ud*XZZQ36Sя64PQM6=/u`f+Ցr>CDـe"';`їI3G{dt';}S7^Ku40 e'bRdE=՗=g1?) _K=/:Jmm)^@=S$в$snx{;|8U}G#֛gnݽPچ{r?iш?JRP I߻d%*F; HE@T-WT\Dz1tJIZ&ri)t9gP^ĀsG`ψȀ:@:<#u_#4?zC,Zr~gzmV ?hUjj/׸]!-bH }$dL3Y!Zr*þA"œ[!E1#95Q$zp0Q Q\)| EQ@Fy:b{ aѨ^bƢ*@겡"U 6FA敔<\aE ;2̫Sm@5+bPU!=;y77u0Yo&_ڪWo̭{iK_Ϥ{]$&i L[`oɞr̮;\Q.1Qԃbr._^i@ M[".x-2Xo(aMN̗i/UJسHETœ8DW䯣9ό|r8c$i& IDATfJ;K?ٗ/-C$/ľrwwWq_J8gnܾe<&@EHuuհs%o3UE 별X1P;(ux*r#xFB @T^&'AµL\sCj5Zl@ePpbqѢ d׳Be:;96oP(rM$e vq``ѶX@ PG}d7]pyX<eF벰1hs$6 T3[N2^&<ѳ"L(} }P,X+[xʓQVZ)Pam5ͫ$JXq&QhA2d5>(lw2Gz,M؄]֦aT-'qmIO߽ ]zAi\)haTO>xy\%W$!?Mi޴-0mCxL"+ /ꏛRiugw[E 2#9j8!@fwS*kxCKU69}#fSM{dcZ?T" 3QS2)Lg"D\dh%C'[Þ][F-'i_v3Newűr]V,\EBZ,ߨ;-3\#LP}vzS7߁-q X"y~V&F HX6UjP[wn;͏99˲ϸ޹`i3\sl} U֨l9je[Rhgcˀ?4nL5 $쬸6)i _`k3c{4Udi*d)WwP?Y}9LQج uv2'O/~y|T | te_cw{ߩWVtr 2S뮦F~kff̠nrFl4Sc>yEoR&UCRr&\?Qa ]B$Ǟ;2.LQdD g 6 R<ft316ߟ7=5+̽'OёQ&]>Iu%[!2 A'h-/jZ5:!b=!\yQ~"xz&X6 P9@ !Wd 3MӝN|IT 3`Q1~F"?`΋ I'/\{P➹,QjHᑲJhaYXz,9 S }cJaD$5YPIrEU.Q,n\$sLLAP{{\G> eD*w#wq˭[ۏgŏs+S(a\wC5)BfDe23i L[`\T-um&JDoSb>wC ;ˋ~|(9,T/QII}ѯ5"ǔ}*E(!Ӈs QфrcwXxF _/gxT ÷}pU Qk*1G.,yShXfo~׉N{Ƽ]g _c`oij iF)}L|64ϧD2u̯/"q?4a\m"٘XI< ~rr I?alY7]r1D}Q{e`psz~Jx=`p"4([e$Q XoVLa l,ѹ 3AKN\eMT1O#m>0>ht;] ߀L+ y6!,![vJ-7U@;;W)fVu^Pd#6@Vd᝖ 6߀KjxԞŞJs3ܘU o}3& ɹ'GAƌZ9\msS;i2>Xn\_Zx=PHM80M"Ew 4$Ǐly13ffdc+c~em+JP-Xy58h?&mPLL}1Wa 沢ɉjE?S_ɘD#6YB|zPQ\r\?(~ǟrxS{t!O>̓=D9U *,-׾?xN~?7ꯟ2YcOE;{n̗+ 䇋KYi'^E5T x#Bӯʘt錃X$vs@t@L̉7H0= 1$Ց Ziƶ9\#@@mS>A~Zl EZZ +g(Q3E=J,A '3~lReIiQAQB J y$cRw/EG!)`+"<#'WļEINa:. a Vx>~sZX@1#w6+2i[4Hs; 48C( qIQ癮!sM#PDj^-E8JKkvq!U- aGn^D2T=,z&pv>cӊjEȔ{FÔ,$*Gтf 1)`K9+#TPG(. ϏX^\2vs-=PN,v6>mޏCtQ\% ǁN]2ܽwϸ|b[7daԵUe5&te "gAN;tYxnʤixFIFR{ғc)JOZ,x(lm >LME suSG&!",: oҦqrIܖ 4!rYl(1 M IK-}FdI^)}0o/ss(WzN@?&k-EѾD(:y"+t2ޣ+"V#LUQ1⑚*Q7@旮iL[`[`"b&@zX䩬ߓ6g'oo4/܌+TO-^]x 9M6#b5C f'!\}@ֲDDL^q3fhoF`f駏m/uwn0xٴ[npmclEH*'&PAAZ]_cecQ~kt2'#RN`o<%$2DjTpI:N lБh.倄F4̨q. s:ݍ|}U<#بbY3;;;vkDZ[^A H`W1n (S{HrR*p9^#R ط8o#e.Km-+JubZaTVgA}vo]#2- Mk_ Ǽ>WsPk@O+?'`d_9?z8r/8ɥ n;,[?/jLu>O"{-nrdn#ޖNÛ |%' i*E"B=l/iaUԿPI 1b8}đI}B QK2FyR IDATE ral$0Bĸ)#:T,Ȋp ]鐱/e/CgX`$YP|{/({̙p$pT j3b:-nz*fh2`k>DDu58HKp\yN iF)F;`  ZM?^Ѐ hg-O<<?' Z 5q'i]%7%%ijQG`(1( y#z:F0#5Mіڂ=)i>V,O$9VD_3I;f,-yZ Ь@ nyf4)e{xg$񠥾>ܺ}`0t~s/|U_j8뇤}&` Z#Z9 Uf6E!G?{%rkk+U_ /bQDEy+i7;B&NK9kkF=9=pLI "]TE@)}t2llﰧHD u]`*n{jDJ84wfAe.i߳RYqGD-س3M{"kxyg0tX< t$wJy^;10V/Q̽bB]r1pz}}hy-IhQI>`>z;8[Վ<)BN0LmvsG(64SC?x'rEs{3Ge`O:1IR.KNNqM 9c R1hy㊈&CĈB$4"$j? clGK֭V_?v?ҽJU5й$/]&A$`QI%"%w~˝[KTDS 8rO|2hʨc>AuN,|I%1+OdG F> + N F*MT3<9,P zȂsT@"!4KؒBW|$s*.˃py!mJ<^b6HncDc8Omm~@ňаh&)XiPp⑚-. KJGB\ɗ/vnUC~*:B7ZVe+*(J򩰫{VWUU<^LhDM[6.6JBT0?}bhMdX_i[(g<I`CS=D*[JٰX+ [3hGQ,Ϥ&͎c/]~[ 窰)őҕ ̚{Fݷ~o=,`d;vj;G'U^\'[/gLhd8pJմW:PLAmahH,otЦm>-ž{%#8;^w#5V'./ywY&q*E=wD$pyi w .Q)ڨ6'4,e,ɵdo$}xc8=[dfKޤvLA|>늧r ^ smBN , MV[v(Y?A5 G;aaæ(œw--kQN+IqH-"5f Y(G08l9\:猺OcWm`2ڎǒ?vZOz,i.ֶ)Q$%݌E:R2s<(c{pf(;yk?˗Jܐm=_ F#md}]14vteܯ"HJ m06T 36}䁰"XCB|f5mY Lw=IDr5T 3d0'*"01Aoϸ@x)hzʩ K%]'*VD$g08oCǒE'k*ܻv8<sgdʛ}Ɗy[, }")zIq­ס6 kbv0%Efq;&JYZμ $]^HpGw^$S"0mH<{MfH=sODugb $*yN g-<^Tҋ[|Cj/ vLYRuE_6Ikj+Rb. r Äϳ޴$I^6ʫےL(ϯ{y=U N3n-+/Z:<⾡7ٝךkAg֌-m>U6"{č1=-K!v$Ή)sz4Y"-Ҁ9`QGEPrQ m\ɦE^EbA>,"e)9RT&@E9,7ɋgEKiV\fO"`U#-m/ڷW8 Ie+")Px^YhlAˇ#ETŽ@'+&d=m3jRsu.=aDҦ(]_7ln>?Yc,**{!A^f,jA2G1;4Ks,KHjwsszg=ڷaoL?J:.]UHd22%h~u4/޿:9Η k>f6M-ɭqhd"2HJeDԃI\77JU̜: Ъz}a!i!E*IDG- 9@Kp9's8UKRaaROu<O$r=tp%k/m dzr[D=: F R[pԘ,=y8}G-6ژd6xJ:k~T[$.$k[/kK*+ ~PBE*0NM|Ɋ7}k)=DT%~U!ܿ[dޔ5u~-\WmV-n}=.2a@#RoS5z:NDߪˠ^꒲YeWȁt=&RdG)9#}Do7"B zǥv²:`'ADJ(ȣ5Pp٣K /*yK 2{}h6)}[<{^c>^dlOzi;<=p{:]Fp|3ѧ?}lYT4Ӑ`ricRΝ;r4~#}CfK66Ŏgwmc79 }ƢIH/mJ}4 ={f.XTQTϬlj*RI-}7r'e^Vt#;+xwN4UWrr>?zwؾ;\3T2y)L woxƒלHOo<y@PjYqmP:6.d65{p%!H%u\* aBb9 K;X&}bZ~& Fp׀DP PBbޓO2Aѽœ3iXhiӘlyrq60EEYGVsPǜSiu3OկG<)l$>h+ڥ~vn@HT`"@D3ZqRvS:\UDN 辟>}fJ*86D]oA>!j-o%d@^;8U5Q0$FA͒kLJȩ=Jٔ̋d(^\BNRsΗùťʁ;^6Q3BgGR7cMrZA׷Sr܆v:Q$)>IlJ/fz{P)@zӊzsA‡3=mptDb@sq (QˍQt%xpQ3U1bl%W*{a "%ўp)1V"+0RJS~ /J2F`@,g&QMwW6onWrC}\C G+pybJonfi}P%Q`:LZ T }$p{Sk-ʗ: uRBgpw6I6&L423ET {NX!GΦc0#tE d,dפhL5KX舴\UDdFbٸTJ_ݟ 2Z2[y[<ܹ{עG{16 9z3s&6IkGILzf"D W*U`83)ٙd`_k bVmy:vZי4 0,P9#4dM:b=I^8YǎF';6YFvJRo ΅CUuyfD؁>C, O-3A G<CĬ|1l {WX1f+RcmbmS-"ɏU/J9,:XbOGRJ/vnm=knwg;EM%KL(z88p~06p|tB>OM%FvT"bb(IxwnøfbetV'%c^&x2`[hQL)U.}{RQ_8PtwVe;>uu->a/Lf# PюvԷo|勇>_Kr9.MCFXAJEO"SYBʀִ!34lf,{ 4{͖2<K` X, бNR{?bDZ粿 #I5-+^]/Z> "){AВNsGCAK͠50:qt˶tx80 c^.~M8vjdmq0yk"#h߁?B>s >g МJ%7j&rL#R>Cmm <\]KdJ2O ]>}HN0ޅXO:`%f$b"L~KfCAc1pr8WD_Ge2(asy]_jt5l؏Qʌ`*t%kjhB}A/s5j?1Z^,KziFj8Sh~Kͅk14@<AwN2} 4| M_/g# 9zaB +q (T( d2 c?/1&NY_w*|z@hLճ$h]@Т~(x6ꤖA9Ϙ 2Q69 `ᨏ>CT(3 kN)٣%hSS1=gzvwDO@s^15Vd]AfԨ$ø%]@#NVT%BQ]nWz<|ihlZU(.IA˶4+Ooai窻O ֻ)pЂF&r@PGH{/#[R`[` LP Sk hԡEeeX0== wt>8 T;.̵%dOQ7  4qooTΝ=}p t4 tM5^]ga w`I]s;4|ì-eٓk fp `K9@;)]hKcUj>0rG" 鯪ڄ_dMˁ3Ӓi#zwc?[҉&MDQw4a EqN(>Y&#7rAiT[&A'?e<P@呎4ѲHH&E.5]bB:pNב!EouÓrAsb)LrB]\Ř|q]S@*w8dX[kU X/R?==؂ԥƀ$$!(g|S!iM f5^9|&*\T( AŁ"u$=dJJ#c@Vʎ~Ryq,DQ3Szt2,1#,0 Mɘat ZW{-)|w=hEd}ΛKցظJ $^h-ػfG Vh dBok .1O*Vz:;U"?OMMU8jY_jclv{W7{F}oz]aɄQ2_H8Q5¿C,$X>^ܾ47{yv:w͆-ܞ"j2hHI'6Cecg-~kg]yz MFt*r V8wׯL po+97uTկO8atW>zUxv矯|/TP%%ۥ=YcʼnLǞgmÎ`3ҼXh"\ڎ|@0lP\2؈}y/<.L5lZ}|z $S>E/>u҈$h'dWk>sBJ DρCDd惠Aɚi)6}aGT_1gg_[|F}Eyfr|'=d >$A?貾?[0D=)Χ4ğҐ%UjGͶ[v~O״_]i5%h)U<1846Vf>tE.&i64tr,"V,<݂j\[%r?އ[ VctZ-X#U Av2şZ Je q bs+:OINA R\QE~4yqO39D]hiJb9gs rQWCh,vS$))kF6a4;,W4ܥ9mJg`vb !uDज़}/vr=ȾiѾ$%+\z8}O{C#} P ) k?{{8y5>y5ZA > za!uCg>qmB]uc>{IWy 1X ^M[9G>D+1>ߕ"\sBV7e9bqZ2{(_XQF ,ZBݨzeb򄃗M=G aӗcvNW>ksu`:&& h Zn~^I Xy=7lu4;l}w{k:]]GЋ~ׇ ZZ+K__ҳ{[8i<@Rd]q)`88u1Уv}shSc{ɀ_*7Wq3ᄲtdAĘ nx?ǃ e'IRGsYW`WbT';~ί~[塚uXEh9} D?neWq*D%3ݚ,H3;RdqS e!1ŭ U9G/7ئiQ'sHA' (%ϕ@Kc\x)PR} T-;,,y.K~8Ӿ," ɹ 2C:!2,P?~U@Ts`0WyNpOo@tG_}hF`O)+cehgg}X⏵:%޳t PoF]v;-{U *?` @}$ 0%)-fuæ,&tcOfE$a|ՠ綪5a`}x96L%oIcķ33IזҀý۴Q9~7_;~ҧZKjzk@; f+M P8m=1~{6 ϽgP +T:*1Hm`e#go0QwkwYE彭AW;wx+k)Mb$!w0%Sg~Tk.R5lX*(,5.eњX/.hqۊAAd@| [Hky}2AkwRb\93Gy[bC{H6VNΞ>@-G5>LB(vBA [ _XTs{Lg%z̻mREZN,Y] JBTqyHCfp M7EեE dZ Vit/um[Q&(ǠR8{*zu)gU|DU>R{F@}t,J"J5 ZJF٬;aXo)\ @{1c2&2ކƥ!Exz:aɆ|dx_^[k񚍘? q<;1侲)T-g}1L’f Au\#e H ԙ]H3 >\ܽ{7 E!AR6 :S;"ػcq܁]z Î_{ʷʋ/f[ٙʍ7D;=@gIESm.]He}tASkc(fP$nX) V`|/w"xTX1EJL΀'d\BPX!y\Lfd2cs 'b?`)u|{@{MR܀-2r :BLrTE)j\s|=gO\W rujhl})g{-=\kw@[^7L#&8ߚIT@fg8,2SƅRX\1#TRY>U?uiJtU;bxtgm^Vt-YL Zʑ0RL[8齍f>pݵe4{U(;R8bvغ٤Nm/@L3Ƥn(sJ49B.|Xdg(cZtG B\Ä.~awDϠ1|bu|Vdr"h'"\YW`1kb0ENij k+*^j!Q-G.s Rjo,ɹ#P  ^F.L#H'Og{ɆDHfVD-KBLEҟ9ަ *V׼Tðc*v MFdIT[ՙ+ %r:^S`A_uΝnMm.'\{h,ci}6D6}`l)Pb SZY1e~h]ux[̶EYY\Pݍ{kծ͎#B?Su /*,чFh~%G%P2T->0;{ּOm.slcxxGJ`Kѵyv*GfzxAivg -d?Skn8iͽ4EUM:r]2$ҸOHUhrI.E;ɺ /gO$k߃kRӏo y{>V)Kn RYAIG?տKCH,ȆO U,`|_iB[( r?uǂ >4%1教~J;K&X{%"oYπ8-US0i ?$~0*?i)TDNW, BK%3/Jc6ӏ@R|k0{"KE6cA BrZA<:jk?sUVz6*Q DKDW[Q- qW^+mM9-IEB՘Vѓ[&]jݴm@HDBeI" DX(Yt:tő(FbA݉~d)؍UDiIKa,>N_(5J#;tOIj[Mhl7]J7QeLwkXA+D8Ψѱ U VҊPp-Tk7BS8)<Fѽ8srKMe8^= -q.2 FfHP>.~YSޕݛ7+o 4x?1ʭwdTΝWN_U0ѳPhSI^D~HShoub/Y*>`\#TWgM*a\^F֝b&'T7jR?QFFYd16 Q_up{)!-ws Z{ ]ᝂ(td}:G5%`i|o{}%mk2詇S7a~{v5VUKhUcc?cXǞ*;c'8 9!; h~ % (e#^yvxbTssΟJ:p LMOYmNWjK]ў Q޿qrBQ~аߘ{'hq&iYKE]?8-fC}Eeл~(ל ( ^ "0}.gd9䘷fH8דA,O 0 N] ?*V' +8/K,D=6Oͩͤq=^'7A )̰@?P0 c-Hq/!ٶ2 F'&MI =$~FřQL3)KdZ <,Jl1S519i(y`!N[vzWGV[7ӫt܏$a=ѮOw禿43u%;EY+pIV mGmKF07GP9+TlgЁv),gEW Xa)gbXIr]K% еʰ`d83,Sv65( q>jzt Ujt:ݮ6,A9A}cccqAK4+Ŵ8Z,:(ePy<>E=I)+^ۦNEƽ{̬Kֲ$80 q؀Ǭp)8;jlJ7J̣{F.Fa>99\͟35=<狗.Vj:wFL fU{"6Ɇ뇇,;[_\9p90¡ g$b96LCL6IF!m~/T/ KO߻> $ƒXAr E{6l061A6l&YZM{ 򵀔Psg;_}RҭpU~t[% 5F2YS5)ATc%cA*ST|r%%QŞyYtֽG,C;%śfvKuQ}bzK<8pk-O72$G<!(fm$"Rfe2K6}ޟv0V_-nS'3n3gZ˂>%Fw8)/1P- IDAT§IyS> c_)dOѸe'X ۆrdmqC ''}f4^o3`+MDk}Ejgx}}N Ζ(b daC f{lxwh}#ߩ_?.'6&h/{^y/K[Kt_(7E6qnݻUԧWqe*> xM/&h\B#pA r"{K=U~He J%uA ݺE’ 8t uWA A33VC] X [ZŘdQg%P)u$e_™'sN>m#d@653v *b^؋ 4 *&RÈ"%nrcrNޓ) $se1cA^j R!I#-N c= l=b+h 5%uDA2XOEGF{`:|H0a!3Ã#,{KQE2.;crԭxZPe]B2oL vr݀Y69t<Ύ s*E+ҋlA{å*1T3/F8B .Pq.;<47@Vgk'bkp\jr`O/O|6kFw+G\; "S> o.5yaYޏ@+cD@[q"xԍ:MǏĀ’jI?PA ]ĆEmC0/E&ƜO=ʊjg(=r]yB]-c'=[dF, #,{9>g`Y_ۏ^&bS>ES>wZߵ5=[Zc7u3UͼZv]-Gzw?g_Z\;/IMyM.7ڜty5C:s,[,ή2ٱ5_Pc&;MIGР|o*DA%RrǪSef<hc= PQ5NCB} 8kI -P" 2 R2NdU7EYQ(p~~砿pk6l6x(VU1Xűϟ=́Ҵ2t)p͎TB80h:Ww4P͝S3#w fG.X\ow^ɃQ?% `6ݍp3+޸ʦt:%ت| )!3Lْ]S&eGYU:꽫`'`0'ŶE?€~\{rdVpn";lE"ua?q(͵ZzW4!^Eӭ{pJ1}/܅}lf:@gyzQAY7ӂfIl dJ%3Afž'u&PطM? )uEb;* ~^qt9akvL(L}JR|y-gM!%rN\ϋ}-fd7PM0~V)Hw0ZjH? 5ʲ.ᑨP4X0'fZi\Eh~d;x T11O:G}LUϕ@1:Q/ed)4bi^?o6gfpxCM0k {G_m*h}҃Ρ/I7Xn_{K~K$?jV\[ /b7E( sعE sb[PֈϮ(JzeMYdwUal b8MEPHFPq9hZA}AKgֆsHpo 013&-I:lܙJϦN8>d`+hd@(c?qKP ..Fpv!oL)i :s!9e'S J!r,Q8AsQ~5h,.,\i)d B1yiѸJ錍ZwsS2QG/2=5՜kgg N< _}c`5v9'G0 �X?߾un _NuԉCq \trA\OUdLpnA_JUDӍR nrS;d TED hF>O+ o7\Y>UPyjggg$z'UTQe+essTbCksͣC)*&E pW}տ vVF= tn9oef&kO3( ({ hT`k 5fуE;m0kW9l)ƓD_=] 839 .?;8d/mRc{茈QkВ~dphA!kuo93$v]g9^q|^LPSx?]0p-a!8nҨY4 XL IZ53V;H+*m>A,K)2Y}tOf'!6~ԃLwU>j.P}sw؉{_}S]2~A-}뫾?+=;:[ygoANrqHN1)q[Ai#̤¨V (_BH&KВuI-Q-VLl$ttY<4@a#/YpqbEPA 4P3gJ67}q ܇7-x"%Bꕍ !m3-Ѕz#CqeӖW`#Ӛӯx6{1zYBx)3t/4^\'Rgc5WޮnM Sw8Ң8PSĶWCB]jEdu;27:yb;ӽ7;:z0Dlk]0"*yG#p4<Ys[[~zqv x VWV;hOҢ g?2)K(L.t趝vI q#HEp]P}>/ٞ&&y7fCq@! te%QS) H{-nF!OFeؗB55\M}h`SWh,QAE!+yl3\XsȈDkw̎prvgm͛68tDwtjc ߻fZ_8ݙaF.>ԍ V+Ww/):dK?7a e<CF;VU.tӁLωsP8pӈZXf zi&h8; Ǐwܴ0":B͆)>dDmz7z[;Fu?oE+5 ߀|:# T֚D̜Xبy DG-cM<@3FVxFzyL>Za0NvtZQُuu ~tO9v}tA^;[Оzm|^ɚ1оlhc_96qr{L~ܵyF'`~g~0>s.K3Z%(xz" ~Zܺ~kͦ͟tmYUY7- qm5utvuMnw֮7~ ?ͼѿKydKВ'h&ܽ]tWbۉ+\+z"K6/tM| SY:2kz~4UA'IH2pCh*^L:tO[[ͭcN~GcAdWEE(RFGeoϚ2}}HVʞ+W?;4̩.km}hFUۙȲ oV$Pws ԯ`XH$0-"hd\0.6UٗVEƌ#Q] Vuπ@EO!bT 2)(ݦ žQQ 62vWCO/3d)Žaհǎ2RR{=uzgA;h)}dRWڤLq/lE{-)A}!>xC-g"146.ـܧM[2Py(JT/ޓ2_C 28@ySzg)n04Lk` 0fO~r_ᚒuW~hL{E 0>@3:Lz(X`1<%̺`&#coq 13lZ0-uf0:ή%afRئ$03|嗛7{|qS#(NP[SW8qK~ѐOe^φ AKCb3;+ٶݍtٱ>T0 jz$C̃YQvP#c>ȌVcQ\ >isPSlnS5U1N C>Kٖ2NN#(xէu )\J@uE`59Ohh9z'׀JPztv5Ǥ FwTiIȜB# Վ^0a D@#}Kp ZcD^&0ĥq, IDATi;Ơ@*Y1b"2o2:qx~F@24cTMhhJòZ`p!7q)A~b )W͏NAyw :8 E jO4:E/;1uM:X}Q %=MwE ._E!3RA*(\ %5=^,Kz +?gPzڪmRQԓK`qZ.YSc<ԝ*W,e&em10o~$Uߎ! . ^j^~nG'+`\Q;!噋BNjC83}[Ǘf=32۱bZ W'pFYքll70AvnP1dh8Nڬ@S9y"|~@q{Oia\,#զqSY5fX" (`bB]Y.'&NJ8q0Qg@!mkb0pjKs*{^ 1=7Vp, "-[~Yǘ.\BԭBiRX̀+Ӛsq| }OOLwSKIXZ#_t:H&l lM8o=x62z s /Gj"{+R8 pU C,a,X$ Vy9= 5Oْ7uA0drbфQ($KAɦB!t)6*b$V:cC pz-B$)l$=~+Vt}%(]pmW8Q޺y݁˵k'aJk/ ƾP\ [R?b83<%ϸniEzk)Ѱl{BNF'm5[c|a!8AZh|蛽/2&!y_MjUHC\ٞKe20J a5nй`T]hr=6>)fJ'n#HA*c46n 5aZ6T| gJ7ɬpK˜N?2GRk4 ìuXqNK op#}`S&bP7 '3dÚ+Rl޼qKP՜?Zj+Fj{1>yjgp`o t^6h1KFuV_W4<_wyw5PRń[J)[ͯ4ʦd:1MBKc,DzQ79>/iΈDZ(ASpT&#1|r|v;OAgPnoj4Z#\6~je:ȕU|y G\[*~DlΝsM,woq"܏>"9EpWR螋7uRLCrs"Vmh}TIcs |Ho+ 1CG_ ,yUQ[_xtĹJ{twvnˈ4WvX4{MAvۉ (p4ՂrȬR0*?Le./O~86} uLp+9xPTY1ZHL@ %d|9U;&x<1R0$t[JDV gRvԩT1׾SY{grl`d "{ϥxY%cE]CutJv{|dT $_^{ne\l'NN5X;v28]RSV,rh6M%+:Pbʤk%\gEN1b@m=8 q:}l\k=,AA2KBԒ CO?k*MgdӀ|Y ҾL4*M[P@(G"D/sen€`xQ(5ΜGzT\P򳌁gNP?=~ +!LA~!;$H-Q۬Cj+f}T7Nsn6 %oޜ{4׼)F2yboW}odu?ֽu(Q>ׇ ZirD5O_l҃o>hI4լH'o C~8w@0B{:ؚ݈dp2`ɖ"NG(b,sA~Zw BՈi>NK TDZU-yB*0O:*(*F . ͌EhX?|[{նxc8.Xq 6BrR'9U/BA+iLp$•$`N5`Q[β㾸hP\)e4I5gY춌`JB >`HoEY$fP<&PF#cqYRĘw bmtE VTp[{m=}{եs^83-671(W6Kܼy!ENgѩ_1U$qɾS`K0ĨBtO5wX+\kL?mu >6m)guI>{ڃlҀc?]O"IU nԝl d\,vҴdE`h (St8 Sgɞd}X(al\267s AN3v}ە_CӦ7ʅsMoG{J쳔/6R}S/8LhekH6{/^`|9a_Y/c@/gd(9vA:1h|{$ dv!uP/1R/Pқw~WEͧ'>釔00a=}3 ,c<䬝!M@[ >^ XşH0Z"BcvXO%t頹6;Kf_ѽG1rdB F5?>!8_kIз!#SLnu`t|ĥ+60}{5]-_/F1!{5/7WɭUHΪA@SN g UOєd1~mpbC#[h!39yCLL;&ͬ{g&ϐzk㏱sQr9] dd1⚂2R/dS"nے\8`< Sj9ni Ċ;%Q}PTN.QȋA0H*j79|O"ϡ\s+-Q}&2Vߛd_I8逃xe*h 6+1=[ XOu"1;zy+X4 g $*W(X2ԅ' #hKZ e/l\0 lJX NM1 Bfd>&c%yҨ w e4@?7q4d3 U-m2oZ"s<x}p4Ac,W -y涂c' DX 5?/||#oVQ77>23ξ>uk>tF9Ug>ɏp4bX\dYD7{YvK_{#ׯ3z:mI?]d&6 RFpSߦI ?{!VJKJz =,ժeZ <ԥ@߉R<35ؐm6Bەl+PL94o L+i2"exv\NdWx*o5VAzا&Lj kf,spF0(qW%XwDm$O|hcNlx|VGó7'fY;Ź4} g 3}?[S+|EF~NВ}x|8k6=Zl XDsj~84u)x1Ry%}V ϊYS_}]K+ZSuVm9FC@e0\[x'k=WK!9yخ|_ǜAAO~FeZ7- gii}[؉S/]~sZ{e&t:V¤} `0>/>wacenTڔu({"QLFQ¡3O<;hq-(ZN.H W:F=)O |8dڍ4*)Y(R(¹{3EE͚79/QڀZe9gkÍ'xp O*}S꭮5]Haփ&@[sUA{X~e1_)&' B5EqFNM9X,nGA$@sQ/tg6ziDZ4 (AH9*3&'2AzХhɺ,y30>FdT"KUjRxN 0 jsN0laa2*ZcIoP к:겴7 +' :]dql!5-9=TՄ#4I$Xb|"%@vzHپ;;T5zNoUj?!NC/|1*/Vzxsg'?9qzc+tj8zӯ<d^*QIXwF??ޛo^_ڙ7_ 4W[ʸn5Q!A!%_o^W j.۾t(`+uv~)h;d5)W粙cJg)ے5Po9 Up8>ɬY0յ(3ʠE@‰ZAO7ɞ~T)մTU|^pt.8CE0R!}Rb7)q*}4mk3ր,]:j<7ۮ4s60SE7 .U`?or,5ib *5cN+˹]xqe Ĕ 7ݼ97uZ9{lFi)Ӛi7IXY]^VyithL̂~s@U/%s0W\&˃&?VQTK^a~]{<~g_C uQDAw\;{LZZ>(?1y%NĹ#9Qo2^b=Bʬs("սZpնc'_oAk"!ƳwIYet!jk~~qO7}m 0JޭgҟJ(LFTDu(.@MAz8Q|@i9(Ń']D+QgzltnԜ4@!UH/f%hUa3OJXilr!9x]# ԎOkҳ)N{ĢF% B 8U;B!^2Ki0 Zt3hLԦ] *%gWh}JYm;B`v .,/If h %')9-?"d͂d;x`tMsf4wukBn4;3- -ҳy9+I8YqGÌ} @({A1 )dD,DdNxxV2 \@'6R:[VV}牉I;oQF+X&dyE̛DIMԸ98֬軟tV>lOTrSc ]sCQk~G>֬nwp`S]>u߹r2.-ů蓿#PKt-oYaֱ<^k먵_Y]ynjn7ZS؁޾ٟ&nF%{Ciblۭ,FW/#bgV%@/)8Q ;ۨ^ 5LmBIy5AY0^ }]tjn'jx&=|65 x-Az =ŁNʙRwcgy|rN[zG?:'D(g'÷Ӎ { ٠uEeq*` r3^;9ºgx!Й2Iqdʢ>q;+vm-{=")˱y O1fh" 1ϬJA5Kl¬H#3s<5*T!pN5?|cQs~gd|',[m8KScA k }(K +̌DP|] ?;3y! ߵf+&/Ԅ.Ɠ_<"#&j% 1ۺvV6V#Oyo{_^:LV_MX&D/TXv IDATO.,vu6Z/Pf:N&He")L6<$qfyXbDf@ҰZ*l~=^@2&CZKmAY\$*fru$sѦq4 zGsӕFHQZ$tRh"P7"#E+%h4ƃL{;Eu3f:DВ4P﹐28Ĺ/]4BnNc_!?#u >,Ǎ3)Or~L><.$G[F{%GfPGq 1#!'6 :bZMLo,c֥-8zc'4 Q'QBiZ PwwVy @")R&"Ei)#sEuhNN505e)idm!,%vsؤD"C.Ǝ<}һݍ|C'n7 ?w;vhcS_MMO}wgn Ϲ)~e{x"h @Z'|_~7Jv59X_{W+;Oݻo}wQq)g_tVPĵր;1:Z=}k0?} 4M^Q26ee $~.S~+IGqS 37l(,|'kqP(#{A#}ɚgCM-CdВםZ}Kc+ۦ#]{ ӧNU~4 `^>T}3̙cAYG~j\#?LE#ΞA 'kw0JϺ )eQRϾ{fWGt|Ř @$74ߢT< ҇Hϧr.j~K{? ԃ[X{ jiD(k#cQi}/1 |9(xW ϒ8NK H` Z] {)cT8!'*XT8d̸r&S?|~^(43kF/H-˙jrSJ6n z&G'_ެt=k-KVs'Р]Y0/>zp;=H&Gi]:b2aԼ8RR8vn#Q;AS*z! (ExlL +_DϱsIS`8Kdģ_28ݤa>H Cp bX!w/.3WςTܿ"c12RWkb{sˡf:Jlg̔-u v[31Ic3CM مVNT%Ҏs h*D|x,b~qJhXϥs0)㚒*#рcgeD(!ho~I4 /6r65 G*#מ@yuMlC?yIagkI_o CVt׫= ǎwΞwVEJF39%Of󲼻˷_;'hs73z0\ߢ>:/a~NR}WglBWu<{{ωO9éFldh/P3GbloC-ُ99rshZ.\:+jبl2۰ 3~Bʠw@Iq$:S(F{>AO({"+ o('ZnW,Sk!Lr@Kz ߃!\4y\5CHxAf`g8QC ۴g\yɄ&12/5(ST5Y誠%Lr5Z* .LK4/,Lypy^ ϑ$Ϡ%{>gJԬ Q#Qy0=`҅s> \}FRoi_.o/ |g~:K~)E_Sa?oi~L_=o@ ^lcG`'l-eY4>4.l.ZQ3kPjq.IE >[y7+Z\yK#R|l)ҭqrn7dk ri3 .j:[R ,ޛI~}s1s_;;3{X Hdɲl,+UI;*rTPiK.Q.R,ZP8v]{_;}\~}fɐ6_x}S5dGKoVt;l0KzT1~]Zc0uH[`N=z+XNPY~[?fǖ  Ds?g] TA^xvz ?J|~s̊",OlѾsB$?m O db>-Sv9sZ"4'a$|9ldԔkS"<3LP]]΄-ů3e7o ʦxtmqRת׶0I2C m&hlbWzw f]mc]O)_Q@m0|FEV%-LZ0&4=l^^{Qg̫o ]^|$;#vks36 ]g`FfYάpR &1Q|sYVF=x5ޡ7U=~ZihLH ` FpUPVm $7Ap U7-)<w_#MF)j FwH"+T#Ȉdb\<7/eߛ9.ޑ8߸xg{I+g%1cM@VD\"xFLY Јɇ Y 7E(0Ҳi= GWQQe|yi]]*fUI↵V,$X@d7p- &/)H@$_ꊂÁ~Bd82iՠ?T9pނp)bR `yE0-/ֻo.]޳gupc/+5zq1dFy:ek/tYQf?_#=i51?N>ډWܨ:~iwY w6'ONN ޾ξ90ЗPt4YZ~Ag4931^'10(P'keZo O53п''CobZ N5m2MONJ؈ KRS ;[>VD+0vrz ZU |鿆YUdiЧ!ب,V~Fz}JHDcӱN4"#cm A}Z㺝 ;*~@L$hY۷n[_L޾}sG7eA Լ4|QvRuLܽ7lM?uEѠI9?t(ϫw2U:knk %`{sJWj!#0fB ȝX掠tՂJ'Xl2M @g̑AR+J]@$PDƜ74@VpYN=3;A-&~+aӤʮ3[@hX, e@N`'5 ;\X}C&D so@yJך `S3 sˉ.,1N=YuɻL1cι^hI8Jg[4LE eЫ~y6jck25FF̠NèEALvKMo?:9~#:MCi$E"Hd)D^3Y0ė-Ԁǵ'&*dQG l9.aٍ~} #F,k-0h1(ty=DuR`=Q W$7.;R>ͯ/5O$*Q_)SjݙG̹u[봿=;B, x=C 3pkMLȒL?OObq1DbI@ԕxps23uת| )FU%JtmƓ/Cҗlld:{UMDcL|~eW"k+䚌iqoͻ sɴ(`vȡ?hTFwDTM`A~eSDigMX}fI|#qItCqS-ImCB?0w>3b:6=w,c H:l'XfRصCBD<$Y~; /k_dɊ##޽#P}ƠELV3:"YnXA L[d^RnwA ֥KV+ѮfJI3>+jZ)=QʡK`.]  u<2'7/ Ŭe" ߉fsI+ZH ]:LۅLsۂϹp\)h,݂2-cVmU10I~97vc}`|MVg|k(p]t+6ٺ-m[NT :jbǖlAy~7CrdB<Ɯ-A`+S {wx,nYI*r|f||\ꖶ&r::%+Ֆ+9 O2!7oIL8~#J QaZ∜ OrEcFQKOzyQ7kavIebC ΃QKfEm(1@`7!J?2)!89O2*qWYc5<L#r79G䩽K&z<=c`H5P<Ɲ7 ,Y@2ʹ \==]}=EB *c}D~ey̙shu]D@аt՘ ^Gja1'SГ{ÁwREb+Z^4 ŸgܸW2">W)iq TwHp/!˳mqdxm$6; *9)i 22zЇ?9ej `i5;bѡ84%2 ؈[νഠKqJ9H}ݻcRpOnQCBȲ לsjf $Sz`[P5:礘sƺ&iO\%KzV զ@{0YV #6dn|Y1sitsЀJn}RN˧֗g깒ŭV5x <2Van:10f O '3X)w#l5+jNTc80>QD kI: Xp Oԝ&i!j6;-ҟGiF=F/# &Ao64=XQdMiq3!t) IDATaP3Fc,qdŬKn)^H,yEGǢy4 xNDPas J)8z) ek")w64in6e'H;?#XC& WHl;g.iR+kVԳϚ"5>=8a|ºDy ϲqg<`rJp!68>H+ =ZvMsJ|YY hfBJ"ǠaU tϞ˸(ǼA *={N ώ=Haf9|n?lH4&^~W1.)щ ։O4a(KORO׸ٻg`gn鹬Ҋخbi TF A̵_(_`ua~nmٟim N ^#w@j%M )A]C{2yQ֬d`J&g&'7onY0se{{PWzpz΁{-m|E=j(ӷ bSYhkʼu^ ߐT %1N?UplNg/ù$h[[8]Ea"j7^Ouʒ k׮]3ـΙ]PG3-d}A55 ƑY&-r vP+ѹ"p=@#-X8 N @̹.PۍV> {= Y1 A\Z%D77mLx⃉/ɟ&)~["?H,)UlBFQeLǮ\="T ! ρ:y~>|誅ԀW uʫG dGd@Z[<,gLl=C2=BXƕ鿈p (eyu nu JhgZiy਱r;Q?@u=Iե66́ joNXkJzǁZGs(vSI Ǒc!Z7>g }s-]g_Xp,N9eW_y1j"!Chf< CkR$='Ӳ:Uv zD]U;'uB\ U ٳ_D*u qZȣAmOL>dP+bE!OG.b2ZYၮEo`Rb#R1! 0&^k6sעyJecCm[%. lJ($^DmqȥT 4 %C ~B}4=S/`X'U`wʤ&gFX䘲} ]iF[nd0}J`.(bGԳi"kr7{~gK<⛨NcU AkbZe1#Ws苄.59-bq88늖)b>)fĀ&G LiFhQŁAS$޺u3qPMAV&LKD"s%ߗ~TpDD奤MŌBǯ2 ZitvAFχ ;OMc4Rj>1[#‹2lmErnFk>'84$(Xgw]ۓD qv7/'m G8N `,'^15O { e<(Et9~5ӊQfUSr<'ON5w﹮}]D 7F,xn MͻbGZk3tݑ}p>| ="TK=J܁xµLLaVԿ*a [k52ϒr)J*gBy Wj~qa?[MkUvJ%.=^\1LvE>K?id+u4ݪ{ӓsw{Wo\~Sm"iMIզ;v8q~1[ MLќyսttXM3Ƃ2Ȉ4c:zkI;`#琳``,/r9 jIY{gdi10z͘s݇3B΀fd&3Lw׃z=/@qd;?0"jQs.j+UCG&Nkn Zi%u10Vd9vgV*dq lH *Ț<4)HEC˂ Xv֕poxm& )Y4F:k8d!rL*iX1w~. (J)^B BqR={*#u(ѷkZKjp.[MЍHWЫTbBQ5v 电e g(.<s~8]gu玠;-~8ߍVc̿ceX;N=͞!HƂ= lr%`E ̭T:F}%rCsWi幉KO&V۫ rZL4mMRǬ`Y`dK%ָഄ,K!ay0w,&%P _Sp Zw8.Qj^!r3Zj(EF&C 1lQi:E0LIXD< g oC%e!U^];KYqYi>itajuNjWSXh╂>PVEYxӂLM[o[C`#yP>,7a{T .xqhwg0 v0 sV_Rk7L) 6] 7>a'#ڵ. W,a !XD׏'?IUƏS fkejUS`tf ”b[!֜YCz߱f"H 9duZ]*,gHלW2`-[Oُ 0wgp7H+I:t8F6:) .RZWe7U56'ξh5cd+*+>`H G1G}g^ɪ ފ+b"gQaOL/D[J};Xj&&b>m( К9m޲1g= ɂۂ&M厌sc)REQ9|f(+fXfmNx6"r0ZFk`Fwhcj|ڔ1%ѭ۷U@K8y"q;/5/z BSȳ<փ+bsO if xV0R鰵08Dv)a\7C&ʞ̗+s#MXƖ{z z%m3 !htذASY-_I r9\Rqg1q F$EPXU½˰(! zaaÇm%`܀)$8?{Z͊'ą o&Z\*(SpGpư_ km"GPNl~6>%eaT_ѳg홏>x3oէsN~ַ׼YsJn 3~/K+5uɶ= ~cCo؝|G]Q!|HϠl~OAﱞ?;qxl ;gEi̕ZnݾGù_Jݾq*ߚW&XU(WJo%շFKTS_>x([rOwGG/S,QQeUNtNN?hZ]MuIy*IA.XF6e2fըV+mFz B|&.TлiaFw%9U o adzđG[Ġ`ԲI'vA!pؓs#վw,LԁN3z ɠP<38 qI01#w#L"i~`Y hvzGA {!m tQ(Bi!c͝CF0@9`47\֗Z?B{ĞAAu9-d\T\Kȁ.U?9V|+YհY]nj}%mR;oH4/󼧵,2N3 _K/prcmd'=֖kJªcL*ۛM`*XcѠ+eujexxXD:l0bLIx)Vg:2#YF"Ԯ)N& A;&0.@WJtqjd<6Ja4L-z9V D4w^/׫&F왍!U-בrx"h5a\ݝfNNOѽXv~>ؤ} ˚!0y<#5ԍ&qs篤J+2lZ oB *&ձHv^XstEKTu?;gqRP8/O=!ˮ\~ qR:mlAy0|?0 MQ"~!`=g:(-EUMi/ بi߃3: [ 0qz0p$hω0=4z!v^c: J}v˂1N hS69)5oENSg&q_{OlC=݆_ Rc2}Mw\B7gQGfT(Rq]]5j9\AIͺ_52#6{X1d5nh; 88|-.CoPr,eAfaYҘ<q9Mwdhr 0uS콮hTEe'$9M@K=zTٕP;|dØC'0c7F3"62-S9lknu$!PrS}'smr7fI]zt%'&=+K"] @ :+ q&H )"k>9I|!zaiMvB\{fkp8]eb;ћJC^Wšp{ΎcH8o-k$d5`mY7ɞI2[S^byuM6NH*+{NH*( Zȵ4b*$ځS|KywDlؖJeԷe?]Z/=7TU\]9tSтt9 ҷM)̇N<( XOР& aYDdIچYl2ZQ{C& G*,zVǢ5cTJh~L̔M8゘"&FŃ.ܨtE>&I9"HP4͆a*) k i9Y9_nՌkEJТU"[Ièv/`9vO ~R`"ŮQM̐ōoz0  L^xz&ň`_JS4.AףjYD+yTED Ql `(麰`8(!.Uuhf0 X9ueQ^cHK4 {O}-cX vxҌd P**y}^dJY_(Nvt\Ux,GO(z 6Q;jTp.ebrCei 9psfѽAXmMB~ 'I&]Lxx)D?mnT IDATx^ Sj;7`pdtRM?"ZR-a WsRzҠwy0ZtzBɡzVͧbdA_y/7D:mv`ǽ~z[޼h6=c_80Q!憹9w:3 ϹCysu;5B]#]//.iDP 3րOpcpT˺HK'e4Q^T^uY KSJISwƃ%{Ɲ Bbl{EgaG٠3Ȩys;-k|6ndzFk&Lecš vUlQ!{INI@^_(.,)bVo*Tرc^9pK[ԥ殯jAnHhwp! ͗ [h(U@ӋŽo^:X[JI莌<(7+^IMGYR`T1X1&#إ4^ ]yNJZPNp6@h(poQ;B̨ j@p /q~kѳ0V^Z6n,Hxs߉fZJ5Y^QdBucy]z ewqjd5CcYJ,,I ;VVJrQNE&M22$Hc4 {yI&%q瞀;Dm?ji4dZ. Weյ)4Y^S}a62Kq9ueG1z%DX=c]q1]#_d!kRx)DZvCS:,;ѸPd]KnX_ o24Dg[ךRֲG=d)'p:cLjrߌea'HY|a%jM9u]]v7k)7&S Dž$8p1'ZN%~w_v9_ʋMZ] / ʬ28kh|ρWHLALZAS&rOAOLNNPB={ycGRcͪUEhHΆ˪o;6e֒M 5,[wn|l_ ^5[T#\>51O|j~8,PRUݬ?~ wۿ.W9)J䔱nrRkd_|k)DJlS"UpnƲ"<26:/ȫӹ-Q&}٪CǏ5<iSAv.Bu8xεFFe=T47[V]}d̪Qs V 3~0T`` ft44>&~-##KƺYuq";Q hS@:8q괡 TC#xr}`{ZdF5-CY 4]s)A'dmNL^)DedL YkjL%VDq%F!ɵbVY0LcfjFK  y/ǝ A h#JGdW&N_w7]BGy﫲E?&QWJ<1od> y˹B@ k)@wMD`ԢJÛq#orGǏ\fpdX#GpO?k8rfVdϊnBA Yuғ jҗL.B-0a yYv"~W!@FĘN6Tٔd"Fvh/X] XM״Jv.d 6ˎRweP(@\^vyHkOlZM;m壶6"<RŏL}>5=; qZ.-HQbZRS^{{Uo/,%9TN[8j4=q)!ElOzHzMلoh9 $, l>w)#սZydz^أHb$3ۄZ0@9F;5#.N`z^ !{ Fyr& S䴢aW/_R(o :(*jMRA" tՊ@J*LԒ$ih UƤ Y::VܦxfKF cx[. wQ0d7yǨrwEd uVT ӁzCmlQMι*a8B86y'<=x η/[{}G#f[^+?կ|%#C*"庹鹪 зknclVC.'07*R۟\kW~s=?Vֶlc6 6Wd W5&E_QcFjF@.xkǾ{Wr-8/jj( (-+jnnaDݽb{},R"3Fd(࿡17 )1O4] Zz0ݮQ"Q kmL`afLj>q<\"S`\D1Ӳh{э3Ygm(3ZY^_+߻s/1>3_^NܓA*^H(]<[-W?xdTZسȣOq~AԔ|#] owi ֞?NKז J3I'}}{ˠHsk(qmEA,؟ 3R <30/Z)G-GF_7T@ĠV~gF\( 1Ynt x…o\Tơ0.QuK E{bT4 |Y#ga YsƟ@.sE^'3 bjwQkjFrxc60wÜYpL8RͿ9 .ɜo'e+0Ƽ B?M*-=ye*!e %Vo ~J_PX#p~`@Y -Zn(;r qW&jh]dӕy^]smW_9x 0glfE"pEW^֯2FeU%hAJ8 ,qTWM][,h4jz N<:n.+ lXq`ˎsaM+nwyҞɎg:ϟ;;|o(Q1_Z^Z\:yO䧇;:G%Nv٤?As&A$7;1wo]L߿7RQnP WJKj rwW熀?3.\ys6<+}]Aɭ֮mZlwtY231_r+?w{cK]Z2굯K67fU"JV]%$ɍh4pgaf?O0w ]?q_VLJ:mP -ȍZϕAǹj3c֚+@9f>'Ȼa3O܋gl`P;C,l{3B`ƌmypZ\>pKsCH635LX(N[-KQf&&:4(]94rr9f":1S"֕ϲ7!ܔկy[>czHQ{IJs.<0K Jqqeד29-  N\Ap t8#4p$𭜝q!C* 7.ڷVl3̳o)cE,"M>ׄE}}.wI+ESOue,?)`c'Ԉ+oF9r*0z7I $Mb=7Qw(XcP͝qE{B>K";Gg A >zd,aFܹ7W2ra+Q 'Cqp c% `vDՎKKSLFN $LE*z'2.czig2H/>9#-䌍d3xR+}h3!T:LQ8} WoP^kѶP tӁ1N;*Y,:٢Ugi&m\˷w?wmn.qE L0kP@擅3#庪rX,J$gqaY> P)k!#"J%1Ly BDqW&أ0q#BtIf ^UuCD@ " 01 C2Y HVjby#AF-74~>ʹWd(S^WZ X=3?SPT>_|mK\]q(*[-$KbO"d[ sjj"cHuU3yʬiRlNZcd4C.bߘDhu AA1C3gй! bRfM5g=H|@q "k,M*},( FΠb&e|0*C&d: |ވkZJ0:I!}Jnw>9gQT`p),cCgvlt*BBfHE-8فe"E3zq c_4pɽ+ pkd('C_{KCGsP΋=NG0w鍂՜gVÞ,U4_ ]Xȑ㪍gN@ R^Q@3w=of^rZ0&!Ќ$F/ջNNpZ\!PsZZdH)sď(L[4 (L 4iV6+JNMHbʶmLj^p` 4OX2؃Bơ)U ePMҺq2*)έGѯEUMR0mVb"enM@,d>KRמDkV*Vo`D9Kyo7sZ\N{%;5-]ܬK 3oABmՋov|ƵYAZRUXk=>O~jç'SQ@BBqd 굢ccϸ{ nyyqd.O/1m(ҷxU6:|ֽՇzʳ^XLsdA*~BN4k9ؿ]X=X\?xGK3TY tfq:-9AYRV0#]sGg:-8I+1/$e4) T/0UZHgsԋ-Y) #X:W}ץV,G3 FEܣ}oj߲ljx #nWI>3QwFDL[oeը c('|at>Y5ƅlscD2=,Fn!9>bhSG_ ԋK DM80<0*k>\_d[2AwFI9-%cCgn?s^ Ra80;{8>ZM?<:sܭgcfqZl-V/Luƾ';|}p߬qsGV/kHyjq=" P 6<"ct2&r\L>ej@({2F pA~ȧxY#IZog]qE3byYwno\ZwOeK҆ p5Mv?|_饩Ս ƒwi ;֜rXJn>Cl}iUu-6g"ut\wgQY,n"}&B>i6LXq \఑|dFƆ c&58lZ3*cdoC$Cy*TkE(Y/CaeM>#db;E^Lu|N xLQF #rRE+*&FFxwd 2ux+CETs 6/hRGСJCN*:N+Jݛr8l/K#VdgqslHg3[QׅjݿвEP'S=o@h:361t WMfh^}A>4 s*s-_2!ϳp 2>GawwX@]kΕ s47ܫ#ܗ;n#ǫ <Y$[em~a4.r\+d_]uwϾUBu]:5J'C}z7{ccD꺪nyLCCԞ1V߶C =i4u`$ q2 QpitC0iC\QW4%d#d8w&NJ Osop}8xa^ D]#6% \Es%۰]&;mB"U d6x}Q Ҫ{o3~/v}ҥsm2kaU*_*(6KFә!׎UpP( %=ƳZV%ܤ?GEzs2Vӓ-XFggzU'\W^hѸn?h! soWocZ_-њ&*{rmi[.SUVէ$4VBό!]'sY5Y0-#$ر1| ec\pI:qvmW6 ( @ͼ2QH^N<6ҡL˪aeac9`Ыʑ^2k]2ʂ` c߲ږoY8vXyr+|V́+bd?sj*&*f, tP'!0YRf&?-2tCG;v7"+V7bMJu vStg6]'&0#8mNw.,pu݊@qT²A go%:Ohym;7dDB֦ҰwAڃglk͹4Dht ـu׮ c}0Ϝ"Ɛ5 `Zΰll:ć>6 lo f^qӬC~=;ƳT<+v_P| =8WV*36fWZno8-;\7C2.dx& #F Amk\ͷv̤r;76^:55JzGNK"񴸍tj$Rcâ+xf Ct(.3f3jIƂ.jKj`27&\02aay;*vg~B$7A&h*jmeo=D8BԞ wxk3 7VtZ.}W CJo؈ZRi̩-͢CEdv}) *iklNb#Z/!"KhE|0X(UjVlq|K1n#x0_=Rd.mdz 0q",+-/DpZ<-b#2Gav)oJ-"b3F\u'GXPN6@ FŅ4m < LSP:TQLE@tOp Yu{l4A18-x uHN!&ȴYq]1\h8\"6ʺ]OD.c)mSVeERAӄ "djچ}}Ь&h: ;M~gDyw|+"E<;10i9|ClP֠hdeJiAk u']7/jQ# _-G#׍:ٜ,'[K_t]CP5?PhH?r|d]VuA|ӒѐR.֕K;ٳ rk堤Mvv Xqࡅc =]s}$hjq /fEo 3{I+5ɍâ;93rC]a]6*YB5+-NgqpA/oy`=<. MlɊ#Sd {HkCv8#ˌ\jQ̤T$?:7옺 [ۡ{KuZ;FQ 0TcPMj'xנ &r=N )4"O_~jξJ⦘S`Ե&DL=O=AsZ Yi)c1so:3=QnN͌c'r [;5#*sYL}y LXv7|Ղr=#ct!ȦŚ =VC- P5w\#:^Xw 5`W;4tsYv4533oo&<UvWPݐ }k{%V1u6}K5'& v1 AL9Y6؎SA؇=5RCl>7 Vnkx;4wBo/7$GA;^8%BmD&-uPɋ|}G}Cfjw ߭:.ɯk9!s-.gO--h%@rIRΞbDflj)27L6M@v>0.H!&G98$PK^ky{|2:ƹMAP"C:!w>y,,,emcg!!YŌE84 >`螹2Z;:mTë , s*RWiqZ<SBǥJ]!bV!~"9#,1T2-H 65$4znSc2;=ufmhO痍#^( ~zp} zоnChhmz\5-栰N"4溎s O'k<̩ݱ "}{"ccBԕ-5,GGŕ[ #GOv0Id YDuXkH8$ J5cƑ$m uwLl9+ yGG'RƉv≖29q5$+'^D=& |SYMFYj{}'gxƜag1]H0ʳAVר%eUCʌ?Q]TF}Vs+{drWFyp^p\(V2U^ih gei~_˶|S63:2,6ڢ f?0pW{vRQAiPI'}W'N 㦡.g=`W5--/g68Y6 u[ɟtgO|ck9}cjU~73A{ZgujN ݹv|y~|0]WvSe #aSd^5PTLnadvÌMuL {H-fd/dC oFVpdOj8QB#tGE]uVfc$d녆iDYն!/={_(zCTd;> "9%Jz`ް%f2.hKupӱB{4V5SxV4>D :?M,$y1:G{Xb 䴠(2ͧq c[8m2eН<nhzV0bг]`co!#BF1+[#pu<6M2Ѫ]lYVo=HsJ1tOTpV7!|p~mY06g!>/3G 𽰮-( :YH~T(b:KV"hI5𵭵bNBlC e)9rGm7ks6V4yvǍs:\{VC:/e1JeGpT5.{fβ6Ѯ2H?_߭}RCZMaVku[5-!UdJKGƶ&ɀP$1^N`[w 5_vmmW:=ȁεGQU-! "O%qx9$4nxSo/7"sa֊(8[68:+e1pT,zN`@xʕLv&2ˊ^poFsS#$jI15sӸ'}n:HA5/cii N q˻7 4`D1<^͈3ɴരpˆ"aݰ B.u=x(l ۆiwJY..* kz:ܝf(RPJ0,%!1naUy7ւ@$,Jkze=rG&EuEvsmIV"%n/ R'b{8(Y\?0ITӬU!t+ɪWJEƹCOŠ%)eaBR< k]hcs8 >+[}r/ЩjL ^Y+PS2ƌwL Єx +%ywUE} mJhEPvo 22=35L^[ZZ%5Μ(75WՋhӓyodyӳc0bDPvl).R)ٔJS,S(lWdG?,Iq츤"H A ` f}79a%iBI>pw{=s}Aذ&e41)v͛}8)KG?ѯ=޳tnwη4aZrpxpCxnXV6Zav cO~Y6:7fő[+k'֖v¡wv7,UէgATO_84Eq.NDXOGpFFdV?紁Rg,Xs4vTFOh){ġ oMY6r_SYWP)#8j#TkEfE׺Xi IDAT 6nd#K+gϝSmrwW~?#bU썲iâa&oI9HJ9.mni6fxF 氄:qpBV|@ *Bqa ~E*L2ovRXL˾SN s])agKrOvѹ] gbY_֖WT/O=Q&eN N!-JO78I#<+GFHsc:\Z͸zKjW |mFS9U6 $Sq-*E'b*ESrƬ5`\ˆځOqCq)Ǐ@z'EP,%aqňW"B(<~6t{ i^Mϥ ߐYg'@qlʹ\[y#Q0e{.>B9JbL7z2-xM@ a#@p&_FTE8" DK5(s>0C, $v%X;[(HOAh i8?PAR!CT ?^R{lH'[2h?scWVϷ%d, _~2s~en/}aot`Q`*v./.6-,ևFwn6՗rb}ʇ>i-̘eVfNKcP0RةM{lui➕[/-.(޺ի>`<LL=ۙx`2Mb[y"ӥp& 2 CgU6fq4{tЦQ@톱\QC>dԺ΅c09x(}~:O jC+BE%w3Ȕ8 U :(/?xj`A;z>.ˡ9qqoќ[n b}^V3*5jQp\T:+NIltv?#Oea{ҿ@f[ H@)hWF_%BgC(dm#9pY[ cA lˮԿtZrlYsu+A[뛂Hlx `|[[>39`fQd~$"@o +诽|r]0veakX5 𚔎ڔ}enc]8HM4jTAHV 3.l~ ,ΜaP(T΁vj숾uwqR,_ KE̖a KҨIS%|oggӽͥ;f'Vf.;E>W0]-sV%|\9B(8m# `YٮIh9oR|8^ G {d 8Lyh9 łti)8=*7-? AR`V7K1d' k/Y_#:Õ*HmjZ]_5={(93) ~.%{(mdi(6cl By fk8j;jblt'bazE a>("tyF8~""kfp*QCSf [ --V8Aaf,X/iS3'GĆ G+#$PiPVְ1:1ϭ܁s.B7crHHxߑH LHk dV(낂tj]OGEN*xjj݊r7w.Y==z{VXN5Tto|CH!xO/zIGk'j]kɅ &juM5/ZibAnvqU^ZgGw5+b bh쮉b;^g<Ա$'.*D駖9F26 1GUj$N,GFg,h ^7c{\E,!nzjylvοk@`^C ''[5E ! M釽G4Ca= z0%E c _Ajbl5ee볬XGEI覜/}ryc/3bnGv<֟rN6s޵DVͭӶ*Q>tKW/WZ_0!@3爛8-EpϺGwKw<6?-Ŏ͵͚0Tf.UL MŦ=uޔA-kJHfGD?xxņH/-,xMǢOx͇ޖLӹU/ରxIate:ZFX$ \veY7/ET}P5)W'! )n@bED߉lGyUa3PKF"$*U3ws$UK# BxVf)OI9vMߛg[1؁ˁmrD`r޼%[IЅ>GsΈ|5BH`\bN1d{vXGr$)ODBVq=WE|I;B@#O.}~VBg)މBTޥo umw*9Sr-'̪H+"N$u6>3rXMx TgtLyǵOzcr(C1`_êQ'_?3QLMʤ̒d$eV^JY g]ʥX(~YFEAڂefurj^3ὗ=lΧOiJ=DMI̬l-lYqkD Dfvi=ē{~ɉQ5q3}?po9[l>sA@fM~g,NKqV椉zϝz33wi毮vel_6cR["X!-ʵ[̶`%+eqKz啓\߿{H[hV{HT>Iq pVIjGg (]B/bOXN";g,!%p=Axڳ4,y";qyO:)=1~Vuhc^rhZ:%/8n@)- SdBZffw4]:e!( /jf/Hp|ĆQShh9FjT`KMH:ه+oɤs6 NR5y^r`$R$[K\3"BDh-xwQDgĴd%p>樹&ʴXUD.vƌ]zB>Bti,AQpK0#Ew"FgoU.g&c# h|z(qɴĵWŤ+&(:Gkoo"ɦH>OŒa*=(P*j qZs 8cVA''9=¹8s|cxǏIũgb?mek3 gCMy̌+{0z96OȨFk{w}I%-΁[>/aeݞҮ\j 0SZvZJeNƅv"d~ח[kZ I3bU~A: $cH=J`Ҟ JDP 51>27s:`v'sz |9%R(*bQVu"%NMrtu(Y G'⎧flN8QIUFe1 .-43WNJ_9b IT5 # E4y[,(lpiIg u{8fWA;)ۀP.s$b'?]'Im4$K \)b;2dbcYtfDDM @HgQs3wD#HncDuC6QG*q蛚?"~DE%sbbԁ8D:p@ gQS'eCg~rKֹAտh宻SDOMFU!ͪmc.3ﰑ:0=-owM J5u}z*\wh/H7i}`4u1"&4;U/3+ igjb;Jk"4fJZzﵜ)EXp*Mܸp/6VP]@Q!PެC.)YbH #'sv`,m]fDGY IZqIƴk\@G SAVCh\{&3.",uV] 4 BC>d6c %12'y?뼆wL/{X)B!`F#?4k3$G&CG݋":pH>?#)6KɄ E~] hjx%2# Q3U2oy23p稼Z?.W2;&9mGGmH!k4lW b[`ƙ3$Q0&1ֳZgPZp6ӎKHol"fe;~`u(<=.13tvc:VhͭzP[.tv?=6a]YӒ G4s./=?1)$A]{Yub ݻT:T^faQCC ~ eZ"2TJ{}pltgl (y(ΤIrkM++vG?"\_^fK:-$EOror_CX92ӣ\Q$s:u|}\o.;xT5m9X|X騤eJq8σ1ERQ[wA++'zm_Ņ&"ξ$|RFJnS"JڨT9@QJҚH톯q:ȟ'FEl z1fg!VypfSˎ?}{+S$^:c Ywl() tZx `88;Uh|wP#DA^ %*NfWc 4vsi>440LZ٪::`&hL8ΤKoph>&`rDSLk_/<e:Y|zSA!:y8qmZ (E@Vσ,P6oEQH=h7B S&]?X]\'2>8σ\!;,â2,BnÒ }ISdZP_S}[15\Mev^J *\XW(,[=W}Yt~a5<G 7ۗ'k}Rǜl;+Pc;Rx7);il̂yAE@|x\"Q#T!S-[ˌ 3A5lyKOìQ~~fZrT+^zgcuݫ wBXX CCk+NKgb`ù!-HftWB>hDW2asݛ^"XA5QRNGr("'rhTԔwJ=߉d Av4JH(p޽[h*zĹZ >%#pe6Cd X%NSaekR,8R'DjLD0#cq(ΎE 82׶"8Ts3Ӫ{7RpBEI {lnd te>"[=sQq ΌR݋e,悺RgFd IDAT 5,v4d^` Z.}GDQDv0I.zhDLyx  r+ a¤BսZuwwC14F6lAmȦQC渮-xEƃrY(X;ݚ,2UL(FoX"k9v\Dm"\b]Co 2uаAtT\o. ÈOM z|.km:-ŵu'Ngn;^m>[YP^z70==!7qYk~Rĥɏ{s@vRSzz+ҟq_~{O|io۵g;os~sb$\:+7Chy/1aԖ8^س{g.!Z7j+Bh,vV ŀ! HҨ̪vNӜ8WD{.Ck A^^{4sLD \!j 6YȐP%ɞ)^/yAi , &cG.60>͎i?4w&+P։X:sE/tq ]F;uGf ^3T(DFtݶ!j| 4ea{@:P(@1s5nzG=2Z&2;NidP͞<3@Φc`"#v6es&eLГ2øX!lف<;lW$l(w|n~GC(^o_6_%_Y7# 3k AҤ 6-HQZΊ ڠL5ܓ.3vB!%%I1#*68 t49)砌c Fa&QVY_eOݓrBt*8^"z)Z@GFHMdf0W{%j=|=yU> ew㮻di~Dp]~ Dž<:[e.q+iBdJd)%<+8 ߮s'#%KŘ~2?0*O{g!Uu5`K碈)Č9#K!g{x GkgLP18Soi9oty҉S? g}6Bf]ZE0EwX>{ݻO'OꙟVF\{$,ڟ1KBめ_x#_W{Zܯ닕y= psnFN ָVe URyzn9,wIv.MtZ_ԁ `]g_P.쁘dS)ձтEn{ #Ny 5+kJke ;#9IƉn!agΜudb)K/Vv螃" A.b;Xf[̒p"ƽtO C׊㖠$Y'9x#$,AъL) : ,7@ҩК ,;,0>O920wJwt :Kx,Ydqr 얳}l*؅X01ba^i5AHS#3֔CfxD5N 1d _ p!T'K"d&kldjb:' b6:7Na s5Dj2) r!$jrs+ 5_ fcu̸8sO2^z+2Dn4:[د,K $FM)L YÒ銱3K)) 򞳌!d3?GNNe!,N~ؿB ܩY916bA`aWh$ Y;H2Ǜ)ΛvNBAި-wmqSWF:VW e'ƃaL2=3T@kljl v+R옠.rE:po9?/*z ܃s1LCv@'kM COkW/_텊}\[C~H #rNK`jW$bv<;>?מ|`OΎjfWWsz3D2f}CQ*P$*]zUQ3nH!Qo/puwD?N4e +F#mcAFJ8v!ך9#eѬc_PߔǎV9lt2ݥZ`GbS [9 %qs=>ƨ;L%^FB̃<@pl:\S܈@PyexP#- JA|}  У 4i~AGc0Ueúw9G>'usá)`M%{^s<σ֤Nfn>χV zfda[Sg7Kpdp'@oҺI+(asE1 0 Kw~>y{Ed e޳or}FXik]u> }I=GŁC-1˰vgM#-gV'Cug@Fp P8C3:|_n]<JK wyjh186֛T[}C}i {Izi(:뾖i%IqoMJ`yn=+ T?ZXWMbx7? T~zU\JU궇 CE,zH: >s^H[X 5 x[c6,xv5I@ʂ0Cg]8Y!px1&DaD%ʊ0 j Mf\fK57kb1-b>l"]Seaƹnsf# |&tYP|Ga U91H ^(-\K&6!ԃR6] V.EEuMH(p>ʺ&ДD"'Xtx9 d ]DK,_dP<\GKQBR&- R-[Vb~sld2TêM2c)ş{5ݩ1xq^Ӿz4L2BF~Qkc#_.w kA D! p$o1!O0R:R`m$jEM֦i+3s~û|u>@mL2:oF5֣m?<ό7wYּ\cg|{YwrA.2478,*O\wɾ_ԧgOLtt.D~G>>6;<,]`̯]y3c~-}>~훋3ߘr{{:W:Z @)/v T$ jaeLȽ#emY 7\M]ǐL;u[=d}-B,c[sd@@%C94gP̔Wgʹ'5Adx4+]2~`@^V}1 +% 5[{_ AF]F]~L00}(rPb1GIc+; 4g rPd`Y,FdmK3 l8-|?y>7ZQg w[LGY k˜8xE/{Vr70ep;3v< XɮI>'L&v z{s(!ٮPF,*!zE;bq%Dķ_n8-N+馱*ԾëX>CM6dIrjk(i!gZT v=7 !kYn. Ln(%BߤkmB!iO8i|6!g\ҎDR"8-,, . SI9- $fa v"J$(sRY!S鉈O~#)&l0Y4 J;c3WIB3v/ SNI)gGнѧѩ!T# wQtE`I: L&S"W"bu3ܒrE >N@O,Gp2,'b@Q;\l-@;(U] 7XDbi7&+EY"ܯ?уc2},;3W@k4iGAӂ0A( 7)5^XXoה ɝ=Qin99.M-Q! gY󻫹6R3'U&|ϝ:GNugM(WdWr2,]AT^|ud`xs/_K?Qc; i|4fU k:|J?-9zgem}+{v\٨'ڒ !lk%X0 !Wx) -b!Cpbve:?qn3 a2<15W9e/+?4jYR9KΆ`ϴ&.jl|YҢCs5tZ'j$&4Rʨ|ʣ.6r\3aВqF?yv6%uG`,=l³ \)he"aU cXcP)s5t eu%jpvw6AQw3{zݟ٠d@W&؉ *2;%!GF5Jzf/r33)'ˀP&n ;krY~o7VUK+u# fZ;f}䎂.OB05}z}[I Qк zuּ:&wcFygo~p^/<,绮u]¼e@W?dhm;7yv-L8̝sCfy:dMpN\".V6 0q'[e>:;\`RLޅ͖˝=:H뫲=H IDATWIk᥅A)WIfQA6*~$ w TXEZS3Ӛ&bDts#7^#S!P|H $^Xy8:@J ǛpH;{x]c^Q^xqbTfDa { p8{ NaReQ,fyMѾG2oz`~Tzh~sML%qшNSŃ~t3zoh2g8RIq^QL@>XLK2.ij2dJ33?cjduG$PSNI x}B\p 9"~4ٴHWu(C%0&56 WwWDžN<I2ݳg!7NUaب2@SӓU7Z_s>?'Cx\d䤬R@\%9MFBi 267φCYZ,7z{|b ܤ(MMw8V62NߣȔNm'Â$MIkEt]]]F&gdCʬS/v 4z#̂}خPFc.KWRkgDzjJv]{(Z4>Uy0>Y@CDqJ,G>s q`}d'#o-,/%PŵR xӥ>E"3o( :|ˏ8Ğ4)k, Sh03920^e3Щ~ĸss 4ض.CBN. >%=y- =7ԫpr^y#~aG5OIwfnr+8tL 3aN>g$$HuyQV v"tO0 菒`c݄m);enx< .{f&e,#9K ̜];|H4 2U ȓ>0ϙ6Qn_QA0e̾~׹Fg1~}j1$rOJ:X]@g ""k,Yauڕ}s;H3C7k̈z&Ưٺ!07>"y T }ݿgfpSMTϛ/m6u^יJLKnM@}A;++Tgej z?Qmk[&0V`x!!L'35pB͙CD"]j׈PGE.̦c#ٰ8 dhx={ TAxkR4%BTL%5V<. ·ϵpQ8xPDQJ'jReUh 4ƕsBtPx/ `g-n>J%/v.xgP_003ɁG=rլNn<lp5i(k gΪ(>QR^9(Yb 9D9#iN Mߛ&F͍,0 y$d<;itT'31u5s`icOl XDx0aX2й+ӵ3G->3KUK0@ot'Xvo.kn/_k+c{ZYQ)G\l(H UF#1)A lbSjC [ZO4u04_ *("hkJ^_ig ЄO2L*D a7~+2\> C@Q Ȟ㷞w#"OT3j490o b܆Jٝ\@Ʃy:*% l.>p5.VT +=5?WdT rp.,ރ}^5c!Ҏφ5g25;ԏ -Եٳ^3FٚO- dƊģgs]tC>q1Y9\kaqP̑D%s쥜P>Kr_5{pفA5 9Aك,ϛaXf,r0aW*]ct WJ% kk(2~k2¹Nm(0shcA0@ fO%=E8TGX19U?ڳcj#߼bbv]*o{%y׿{6gtT},,v]6v4ݵY]eM_a>ڴ\CEؕh3e d'z\U=#]X YE %;5:q1IuÐ'D\//ZFƢ;Jq=WdO! h鈬f2"1ֈ:W'zhpErK#DRR:s c2 D-Νu(y>γ"k'#Dr '."!(/hܰi z $5gni i\Ҭ).م{ V-ʇȟzBrgP =pDj5W4yR4Ulƹ̔z0X^ge@6#KXA yfQTnJnAnK<\_^* $$ǯֲ{c~(U PB_=qZP>JjKr6n.93Y $.HC`֥/ٛ6 8%Q.?o׍Q Ι:j8n<12? n5QdcHx 789'[<O вh-Ġ F luE g޷hQ~q8bc| v{a939π!#@=, ԕ4/vj  Xe ̋36rֳ^go$64ƄhH {}b=35Ɨ5tNj}qfJ N ioHK7/U@nvG/u>X`ѧp]$[)<4::-낦(<)\h>|rGHŅ}RmvOPя}# 6*4qG֑ "kHv R WƌK2%m7"<Q3$jB52&űGn;xNeTGay9gſh9(N,ꈚ4FZ{99H-㼮&8DA0& i+u@ AsY9o 1Kb M!u/R .{ /1D+*W'|Ko+ZX#q[i1B0{fwii!MĮS֎-̏ s53y];j"5-,.T?V=zX*'1hS6lY,$eA`˴_n(6)N > fhiփ4#+B^^.VGdZ>F`FNgjcE'P6q -WT\21p4 "})GpF@-iK3|r# 5;]/7sF\x 3M4 .><|&)⥈FyNj|fxl4v 0y9RN2-h,E#|CU}+L)JێkdJ:X!jKM6C>Umdfi=Jx_5R9sb'qxkJӂ89\ˌ>j ,uՀ@Q֜* C,/81c9]$c^[cwcr_Q27$Z%ZiTp@XS?2R$OV!WnvCLDFe33 F+k"w 6%r;Թ́/+-W6ӥX?WFӐG3Y]\|>smҬzl~}Cjr##_-#|{-t5/C־<5+KH ls)%.uyK W7gÃ}yϣF S×0 > IDATa #TWݔNkQk͉MX"eMoSY;۪ ժn~na&rkv+ն }b^fNXh㴼Tns@eq7-Lhlwcy{WhmW?Z) ApR8[bu8< "n*DY/QZ QZL}eE> ^v6sĢ!"QKL Q>"DcE;9du.5Pf{EWf~ S@[ogOrMQߦqT`SV&#d6e)f&:4 NץynK+ ՙ%LgS ,XoOMBJ [`$Ln9"ICJӚjPK5A%x HpYlʚ}a!]`Mk=Z "KNSss4T Gl:8%|}N]b7@nu:' kss^j]MvʴQ'GٺH#lÕ/ʾJ/̌LiI XaCǶ^Å"Ζtc8BQ.ܦ6NJ#ؗ]>59e,a RԾOɸbGgMu+|g$GggH֚ک˩_{W{/ϭ.~IMd(jV=4~Pwfc駞<>{z2Mr^TDz48:,p}JUN d+b!oc3p{.#R,H \0^VW HqZxB9SS88v@R MƦ G;46#%b{wA\PBb,0d嫊ߨ,ȁAi޷_ݕ)z4Ńs.nJf[R)+zW3 [0y(Vcdz#GcΔQ[>gXuI(7\O3MJFc=U<"d!8-V|j%J%*VaN Ū4Cg@$8d\8) S 27ʩY"7¹ӿ6"|àiW.FrM4x5qۨ LdpP2IB#I\Ţnֵwٿyv?>?wF#g`p#:J\DY|?r 7WhV(21Xwfߵ+FΠX1"kWh(_3!siLaDF,RIYKgy}h{LV}K~*%k=Rsqy{P8ܲ!_7~Ӈj+󣽝Zypih`U$.4הڒ"iqyI`\%$[DYY5ܡSIZowj_VԚ c+{쟻vczopDžvn]56cca.nδx;~~oH!9gwUk=̓u+- i&ý_a@>aalV7)rU5"賝a?ydonZ:۰#tVHd\)xYZ[L?! `."Cyr#0l*k>F6@)qY9Ie݌{]g> Vl]Pgso?v,Л]('!ǁ zG7YӉ#$Yxmb;2/1oȑcK@Jy&q0>Rx<'N'3vU5m¦HThNC0KNQO9024KT52Ap*" ~⇸o92qkg^b@~PF "\xg923͞byaܭBTllS^ K1y-.W(6xvcrsJXZa @832[1lL&ώB|"ej xf/X::* g %D" 9H ::hQlQyQw ` /P ,+{+ gjz_oP}];]=47ɰhZL7V[9~&Ev)?z\+=_>_RՍr d0UWכ8P=6VT&ahԸ(RBsɺV<ÒF$nn\G~{SO ǡO? - 3Dݻ~Ԫ!-p(%%5Q0iYEb5Pڈ /\8]o34bJK=V9w洄RfF.1nc jLϒ*Z>'GccYyT;Gϊ#5νb G! F8Epp^|tX1Hɰ`; <.nE*mQEO:͙,PtI89I@":ї}L#{+X i~ɵbW g4O{S#&3 =ב}Is Rj{ڄ;临E_{ZV1fҰno%r34˳sklkkڣ>K\9Wt+ +CG[6Cjw!׆1~[6kUkU Gk@ ,ef4(uYͭOLNn+k=3z`J̭nn.?3ug;<ЏLw~rjc4@ x#=v W] N(~㙒}릁#Y"tS?JLb3 c0tl2z2Vq3Ó6Zdu : c6 ƬQejsT@.r%({xy}Y?~wPgSc͜{N.go\F(~) ?K]PתʠeG%:;%Sq\LTABpN#`0;["Y?{UÚu]e\j"'@m+|ǽzRy'z1wi)|F('kdٿkO|'ƃW_yId Ss8+P.UתmmMcTʉt'nmuklZiGA.lRhJ6Ƥ֎eacrN*Qp739e!φ&<( zӗnID8rR=%#h - gq!bknJϩGlvFt ^ ݴlz-r@P#+ ̛f8,qpsmq܈¡NO:"f+)–(%nX e(j آ0:h6qV 3%ʦt\ ,5IC("wk[FmW2l$ólg(p4[qY%#BAԃϢ&7$n =x:C teD!缄4!:G>Z8#Ja_)}AֆBmՌ"X,: *pwgxdw)@ \~ FM96Ѐ!~vP/<'ϓss.gWkR ƚ_`8Q`\tJrehe"Y|~ĩe1L(3 GZ20o*-ĉ>/, ɋyN*״_fVjզ{{TW/.=7 f 7XWp/޹١I=Bh.];o_?{/;}E-==mث{sszت9 umǟ# szuaW޳;*.v7;l 9A#@_/  !9Cm JΈlfݕs9:t{dÀ1{?|p>xv?y4>;Wݣ+kGc*XLFslqXa$j7o~ϮMMugfO.] dZҞg? k ƝS@ŵp@{ycoϷ~ЏV7Zϣ84A*'GSh,+6 >emi\gIp68P_[SSppTGd趡"җm[# 6~B7 S9hf| he)Ob  =ǡ`jW{V9WDCGdP5!IY2*?\$G.*%o(Ρ1W׹vf.}`Qnp(C#BWPyq16-AKӋ K-2}\IEɝAjQ:`Q57-šAo5`*`ny{zSJW0@ $m|ʝ6P֤t0 Q ,j%QUK&$9z00Q(#o0hk|؇ZbNƤT_<|@b?$gVBȝ5e^H>GO+zNjk-~uvxB!\9-HhDm"P$@)BOC ahiNPl@IfD6-{wN%AJpH8aL1)T"/H'';/(*Pk:qeu2IG^Cxs_ (0QrptAya֏eHS^x_RGW;z#xgA35KhT9BKkW/_MAwZ0WC? 9CmTQB 8qgr 8[Vߤr۱ӹp.blrPN|Pq*O|K4r\6|4KiqIFAEn6&b Ju1}G> GVΒi# MvzRGٻ|i6ܹx(agі?/i>lP{KW]{sί~/g7o޸>Ƀ+FՠoD;Ƌh(.)_o@ &sSkO^xy|_wTt)ҩ W6Ο>|vqq3";P3OKE EɐYyc;yU[_/?7./m89{/\syP`28v)iubyccA*I&`h`c X/kLu!(d]jPe~90-!"AdM@)AW U-Jmo)K_P7zXǛ*k\;޲ҒKTC#m|'aꧪ'dI<ŸC>^:э1 ~|WSr4L,:c^2HCq:2vXz(!c,\1Elr<$\; %k\<^987Q^Ԟ C[".t/6?3NJW(-)]}@>UyQc ~.XWQd%ml"W-ʼn *fћ5h5/%M D؈pC ,#\Me7s>+GmπCM~Rk,^^"`ycO'0`0aR aQ + ge3*'º= ^o@M& ޟV:~L/>q).JZ E0dL-Pb1GgaU)Q o0kA?m,VÙgPq$HJw7{.qD|)/8GBqB2=:c#@#9:Q1]Ч9;+K0@EE{l0FNZoE[xuvDʼʨb|]۷*jw_ G{/qUu>{9STO?tFG S+a,oph$^Ο![4)A;ru|7r=![KLd";b'ɘD=^7(Hݏ,wiT_d2ǝ?ӊ2JړDx" uo* PɓǒI˦BWn؛ IDATĉdɽ~tYϛg_w<{`. 8|#ؓ3tO~FD)P@T[4bMO䂿0oScC\&:EcH Qwe!>z5L1Ti^|$bL GzWD*RK&]K$TqG^@їc&2d xLZ>Ot8`qҸN~-0점l`,?{%b9tr,߇A` yz;PZ4*=\Ų)6Nxr䴜@Gܕۧ_x׿@y{21="1*!JXtQi~~M # Т}`c[ͽͧK{b՟<}7/i9Թq㢪GܐѺ"HUR Q' Ùpre VN4ttm+ZKr%\ zl>(<{n-b^7H=F4U@ (B%AVNK%Ik߸qEG@!-^9-l-zV芜/WoՍOwْ݈.G%S)5̚vn۷wq. bgg-cǪbD>%JEA|(9[};(hDX`X`'j(뮆w{kzzarS3 o˲⬘'p`dF@[N/_iq'/".@_E]ėE \G5K(%e##sK7Sd ϖh^ߡߐ9VE#%I\{@L1XTcVd ziߙ eWU"9An/bU9S8\˭FG N9* LXW!eja>oչ|r92үݸH tYs d FDJݲ1\}I8弔/{Tyx5Ł V^{Lq*!FSfW s9s`5'N:++8 @cj3x>B­蟣rr8~!3Dߡ\yO fΠpDdxi.r@~p$UE7_gP`i$WkZaٕq_,pv44}Rxr|s/u,f3ДWBm|"-Qe9/(˂'o78#NdҼto*_}sY/JqJThŅ8`S &J'AKЋ$^o;h>JH`#N>N7 ι7}k䏰yQ_]fU/߱Cp * "ÆB41z 196_|0NS\' W2}mnlYmB6# */n) {B #>Ic6h4ÛBɆ42[JqKx5)Z9sPpSo YaTFV[Pv.@ș/ͺqů*AɊqUOAwBH|/AāqH2tr@W;4pV^sr]'QhXJupp^H1a#T7ǒ2$qzr^/>$_]8i*dJ?ȳ5B'U?g}$051 ς{v*P %ɻVnϜ(:暧,=1T}i0h8;W33Xm|'P=,ub%g(.B?:Y^Tʿ~O/?L.e{xgNn{-ǡ߉Li,TXgcK{~竛oL=>{=Ҋ;Iwyq hhQC(gje+yW!G1Q:on޸ѻg¢ɿ=BoyLgd鹕WyݯQCGfokcRW&;=37;^:=;ҭ + gNݻ|;g??|ɅS5w:Nxm-@B׽Bld^9TʅD)QXgkT"ɗDwhZb nhpoBa5ٍl B\8:jY9AY|sXwQT愭EK'@!tjENJcfKd j-el1aQP[_į=Qj5s8?Q6jRب"GNJZcP;uU8~p7W9ݓ$(m$E7 X}ԋj6W>-8՗8vJ-U tᔒE t9ƨdl:h4։M 1ES( 蔀 5zjl=(AxUVUy k~-f;I3L-Е\yXI6!VBܜ{U$ٳPT[oJrNQ.!?U|9xs #Xq";6u=h!e qJGF6#Q‖4Lmo5(Kؼ9O`daܘˎqS*Ds)ij+)UkaCq. +Eudŀp2 BOcPq@@aoDWc,!ƂrN8 }*щJG";Yř}B=pt.Q ƞ?[r5+O9`tNG'7%c_JE$oP r2+c<:BQ {;x^|@adۅ;Rru#qOF;OcHH(2I|BSe0\B܌>}WYq\G .I97Bj$myoV g'uFt޺sgG?z21JHj=撨 ʞ"{!^s7?cIQqOJrا{RU1 5mXUTz,'atO.>Wr3g?p€mhIƤ|<{rNk_odJщ%Q?{4~6у}g7;WTeo{/O_]=#F B|4bTƴ?uKkYDkx/B.\TS-UrCyaÂH1aRC/W8kG/{+S4JX W5IIN癨S[: *ySd 'M}Lniô׊h7]dw%kA %j-?gp+FNK*c':&:o,S5,`3QS8%(23m }Yzy25 {Pkc^V`ޏ^_} cG!HC l|}ƅ!i]+̑&qWD_gmPGՇR~># (uҕD^X%J3xΖ  nc'"ܥL4CONrs"a>#ɖkΨO>UQ);<.{p^ QT:DT{H} t*79_k|.rY2?N{7B`gJo>~_?:uxtpk.S>-]ϭtmYID%Q^|(!_UM: 5e3FlKUAMFv 3N >N(jOxmn>T*< IP2#Ƈ6X)rĜn:?Pi!6#Q%Hz籠U yeQ@2Ƅg/ dVz_5{l:#w I'j4 $ 8NJx){KF((Rb74 )_Oy (Зyxr W< a 'Si=@P"q<0d+qKP*nB}SQ2LKю0њѹ{wڈbm|ƜT*c2.wqgΜ#3`d`Qژ2بѺsT{QL5Vl:c&1.RAP5\|) $Eڴ6y ͢4סS_c8ߔo]cB#%,%wE;*)$Z?hUK~щ:år{?8!k@cW dąN/{׫_6%}?9hO p8c8"Oϩzs^wi3'4jIP X(qb?U!] U\?mȕgH8 ࢱ)=3hE=|)Dx~綡`_,)SK:흶μ6HiD9BO8v9-vuE'SHBp迧>H;..ﭮn|ᎆO.KӋfI T(W^U<ngo_}/S^s{{mŋݓ))q)]>1,9z5ܫpeyN~%ocdЫMNp#,HCTOa#E&^b k ڼ/_y9-6(@u[~ u]H›%([Wwſ^x&u6펿@Ԃ0qW JyA 2ÑUxUBQ6LռWsHH5w PE. #j,T -! E8v8|6ٜja7 bD IDAT9ĬREte^ۖ8B0Teeqv+ kNuѡYl*hC a{(.4{irp7&my\tgܷTidaca~?u^F|Q0VApΩ®m'DUц: kDfKYʇ 7ī' q һ 8cD.&NUh*ߐ7+*"bniB~8rTTLRtZs/I7rfl(L5)ȶeCӝ;9` ۶IјQN0mQ9uOVz'#/ֶGNF7\q~љ\o 6*ڟ?<ܝ> \Vqsb_P{{<iw}"֋ёC-M+ӄe,Y L̎paU[r"xX_zyYϬޏRМ#r0N{\P&fF~;mQi0fuACxPXiMˈu z~qEE$%*xA~B rO:C[h>j9&!9N2wVg)PZh\^2K> K pǏx-u@>|;zE8MG1YqB4y981HKDx8)myM; cvC]xÇMe)3r4p8-C^ sVef`l@Y"qG"#C{,jC58$@$-7U}/f]귁UR}\!4^.Ɓ)P`Lk9;Xr+R~- =9.y6ܖ#%9OٖW%o(/H$M/= fχk\Xr ;VeQUq/T7;AS5vgȾ*{rO8}f?䨊jOI"\N=iCZ?d]\bt8;˭ʛD^L kXFc0}H:Y]8EXTfZ EnOS8~UNgv{tx{cbccu`g_*P٪qP|inR87"@VOa"P%CzEb nV7>V0v!-%b|G!dk~kd -:!a'DEL-5.ܣѢ{aሐ{wh"/&BꟍάQ\UB 9+nZsfOj$5:eL@֓ h_&V,|/4@ Wk%IK*Q%qfq87R=pCAs<Obi*E}A\]ݒ1sga c mY{w>vjId?v5BH;`3VŒ1I F!s: Ji86Rtpoۑ, 0}jԟ dyX{:L60d٣p]~h>W[2.pz摠O (WXh--ve hW4?>-ZhppwD8AW љ4w^WZy̡Qb!릔]Y2 :hJ6D( Fիɧi:N$_;2A 0e\\ M*e؏8DG0YQiPٙY(~#8A }ba~Z}zBbo׮j\6'\%p~W,gX _1k9ZDR=]?}Z YT{KLCm ӬX3 4&б G_Ǡ<1l3R80 gcog]y)t߭_o7Ӡ_R `jXkI!׌ag8ֽFƱwwkpNT SG7V{JȳQKWrUcmz&ة-R7y_7=d5T HU9oQqvil\43Kwcl?@<Ϝ@d]#E69Rn gFץЏ4_A~&d9b8ـ]9G\ W 4wy J+L[GY7Q~f͸"PBܷ~?M/Od >Ek`l*jNQ(˖4m 1{4;.o<)ep6E9I)(F/K+b83zrck葔lW8RN *%4j^`ciRUElɺ, CK?1^TjZUr`miԌ8qq'8W * kp1NILY[rE@Qɑ42k={FQ"zCz*|8-¸ZsWTqHunV %sBKEЌ qu?%HksS/#zPm| fR6("ZPJ5ۂdGF r`8evȸs,˙)!ܛƅh=ja?yGwΩeQ/Gruժ=ѱ,p٣G.]p{w,oX|.C UIlESxn2Hir,<>=>c>P9Qũ.[{Dz){ZVVv8:9# Z1$am*{6H9<|UmDN`A]ZDy(d*{*WԽqKcD@33pLjFǁFfG_H+ (pp., hp/ lĔG3-ԝw&#k(!0%ȳ{4!+r2Vngr1E;1^ e=cQF 1Sƪi+b+H[d>`Ck fY{un!z,NXtF[A愨FjP6DhJ(QN9Y2*+* ó:ׇ#4,iZSʐ\AqB$]$zg˔B/V RoȽVO!vF_%*pQ!*yU%c\f@NE S2\FJQptHIQ"I[D]ًBȽvu$@ Hh?Rh1DNn}d_u!yrB|1XbNtm\>3TI^x2g"3m y|R%5({Dpy(cS nOFf&UT':? TP grK3R'B9iʜd׹RMjuQXP" {R8t RQZvX;P Q\`rrbF!ܱ5dC, [!mʩֳ[R~]$,SF9 ysS9N50(q'Ĉ8FW)?(˽8Q9`4qߩJTcJE7wJ`T2V\6k='@x1eoH^#ž$t9O $NҢ$S+R1J$)x.A7NKVꗈ >~o[CN' q}ƿ/5!K=Iz%/FE"|4{}.o5g.:QϖFcOβVe\t 8*x9ʣu+Rt)'8acZ-*XֽJ8em}৵ZQ% UJ95Ɯ|yCͭJ*yhQ^_V˖uY)޽{Wڮ;\tSkd.Uqr آaϪxEv}:b]9fd}dF%gTKs*I{qvOg)mM+ed7NE`&!2Nyglh=MTB(g\U9p tߖ+*2V)3n8-Ry 한,8*r-Wij`!8u퓻d"" O,)kJLZ$,JD/1kg(:&:!/1,}Mo92Xrh~[qk3 X 45d,-)Jj8DpVcZ0fܷ]9LTAʳP0o 5xEvOk9Af`IBr √ު#TLl:z>9lH9K-XH~|]bS5ЪqSlgM~zW *5hJz#OLe,jN[r%2 HƤ8i>SLz쑖cH2zD n5TVsc5&"nI$D!r-~Tz߶ѕ}F*(g/ pq[yrk 1' D<6ɑm:|u/+eHԹKf㌸骰 tFv}oRM{J`MHOߚ1P45@k%}ϒUs^!u7=6~Kr:ݜ6gF;PUZq;h1$pmD6$,!(lh/'<")>kd&zd:P]~md: 5eH}2h0InUE`#tʎ)ꕐNF.*Z0ە1[#dOuBh2g= +-ن!`A3b@ٖ2w&훗cK=rE{gLMTnXkq41=~.31"cpBNidtM-FՑWLX}䙜4yc,IVR}hs+QS `I$}i0k  ErPԔi濻)T`l4ykcWOh皊9DF&xGYwUQej`9- w|!ư$wPh ۖ|@="҅`Riq?h*0DRrJ80vC]3t4Tz)`(ێ@d69aor}rr1rcWC3CC`'Y",~jgt쟲'L.6 iQ?hXT\4^1 נdߔ#?~{[?"9TuYfgɽ{609[EՊ|~O,k`DFisd9ȈDPholvRlp!pAP3Y>!{*#2FF&O=ŸO|ogE\7C,9-8OT1,śRY*Цʺ<qq9&v ;7 s+<%a]`KjX {R.^ୂA ';an 2S9 * Oo4 Z- jd{JS^ ƻCPB( tIc҃yA2lq9☙U/*=l;t~fԺ0e$pWhC:UtI!VF44es)H #a,QfڔO IDATI9'c0_(n?w3n-X>U@. +48TFkhǹy#yCqcJ= D2g5~=zܞÇ®}~:_mB4#}_K@ݐJe6~h\@TJH4:Av%2\#o r/*ƠD@!7?y 3߰E`'sh㴰/xO PrEJRQ.Q#{rV@? mrNQ@#-~Eݮ5,OR2EVF7G&GKޣ"lr|4yB"+ Ed1a_@ҧ!!sHb.)ނf3"GXOXP$ {WK3(]VGmf;4jmWHwTřG t? : MA+Ke#*rΙSg*@N"b$KeT 4d}ӂ>+žqlKQx藆i:U(e-ɨ|-9Qa1zΈQB9D<;SF%.\R}J[5Yyf?Cc~,*e!^kJ`]гZ".Æ?,0\? in8gfEkjUlybKD4OzC|S*iM¥ }PCV(ɵbq`l) 3r;X)3hb{"N?Ӧ ×%Ljױ% P7G'{k*zEfqX=?7$gGɊf,*VNٸsޞ~ѯݧ=e=QյUκspOKxPe@kܔpdH8=Z=6u!ssWn+ZsJ*=G4rT$cX{hMd՗ YaD8Js"[ê>mkE4# 2ޢl_cBk,fEW>xdU"{D٨mk#F&5]k}u(_Q튐~1&%;+=)FidpQr3Xtњw}MdJyUUĴ%T:rX֑aF&b>m73kUuW) Vi됫8s9*jWZG^ǭ]3kB;s#) .A>ž(q}*)[2tt}.g{W` ٴ_?,m!4*}~~H|FAG5u].uDxt!TQt8.l2m #ai b(ԥT Ry[͙6r_i*%,J>  ] djP49?FR+fؠ jJI -P_BEgO8xP$ hcAvlT"$hZAǫ6<2<fô2Ѝ]BX 6%m-]ynCY; ~( E_znђ/eR420ʃ0 \DP0AP }-$t4i?qMmh ̡A5q.|eEт6VG#s6B*o} KV,5 c{YiAq+ .4U׫ DH%NJREPL0εJQúxQllC!M*sAEʘE:^k#p<ꁑR\lkF0}d B adi=aUGc~(W0s=f^H fɪϸ0c U5-C5N r8|%Ӡ\9`Dk$~<() U/M ij[D,#r-C[Լ|EpO8F \ GJƆal$LaC3cl1he1a٫MQY|Bh]hvԗDi1ZsfD ?NDJas @>;5Z` xV5@E4t:#<60֍. _@U<QәV@ƑCˈH 4ڥgh8a!\Br,'r> E5 " =QP b'$y]=9SB$S* E+v~"F[J#'=6Ԇ"PQocg@˸|FSQ b"J8-_>(ߒXgԜ n&AJE j -&olD`V|.Az8oɃ6diEqz'EzC#Eg4{\s,МSխ%Us>tq%qG&mJqXN"SEncJNg{?ҕ1CDj 44%[yf$Hg0sXV MK!}Ih|Eqro];ߤB zXQU@US'oX@pU*:g!6cUGU.ȕ-؞@6xh$hSmEC79hkMEl0b 7n%hyY58D4^^(p8Ozk|~2Mj|`"YoZI:"WugLά>9kҖwlဋy@m+oX'?*3rk*ͳn ^B9Q@ 3t~ Dy]mբs H ^SY|>:-sZm 9BhE*%c5{ =ssr\[k ivgi)(_}xw[{{_GhA$'ٞNZ(ZYUZӴP5At #.vVL }> ;fܳ8\t/$-kXC{k_UX$X\͊ LLadʠŸXj~ٹt%Qq:HXaYpZR"8V3M ,)ݾ"ncXHʠ<"㞂{3pk%kY5$͕hZA`mLt@B(bj`O58~1Li!UCE$J9a@h7b;(yp%+ʞVCɪQJ(R3N\r Z{ JBI0 t.c˜p1t|v7nL9gpĦ6pf~7΍Z*_X\]Kۘs͋B½WJ6빚%"Z>f+U-oCH ")@uJtzfNJ |Ds6y} 87:-IՊMC|M5vZiܿB̸3Ŕ#] 4U0ӢRlc Y˅Vb<{:Jp΍0ԒU!D{@;$.>TAӿ0dYN^!q嬣+gRXg M? DZxn`0 D+a꟝l*gЀ~Z75#. cQr`oh,'bAz)pI|#;ӐngmP#+c+i{{Ueespc]{Rs+]ez$9$`kƐU8- 669QնP7(.٢^C߰60(" aOȷx#?8X4zQ2/8}LW^q9|K>>ȱMBF>{pc7Doy9/|џyDh_lA8F{H^X>iyD3[LC LZ=m0I2t=(u4@YTXbth|jvktjnkni7?x;q VRs.c<ߵIRQ OA4>: 侤766Ї{t4 `0;IP&b!A!76ѷv ;@Ӑ.YA?͑0 Vφb7maHjâmUHe _SI 2y-6fxfm6J!ABۂFuTטh-'&ȥЯ[vb(kC]m9pߊӉd+Εh/ ]0 h$0 fe#aS IF΂*H[ <._Mΐ jS4FM"yayMtH#7H5 Yl|Skru"'HJ x%ΰNE u;k i4+E1N8-[ KΜ>RP;*h/|л*NKLE[Rj1UE䵪ѻ@+㝱=!u`UjΤȶzݹs[,Sjr|N=/ݼ*;"tD5΍q:u.U{pf_w|Gpݮ}~iĶ8rp_NoQKތq.;rZc/ 9AꚢnDY8[ShrY]<uG.=9镟PHOT)lvGÒai,VQ> ^~8_uwI|Y9 [|ϥݍ͗qq;-B4ꨈ cMBcL (O :D"D+a!3{׆>B%ey/( I |F%e+ᐐ=򓧏:>2 i؀VR`']: IDAT}ty(^rL%ly18(ms/PlY١ljYm{vv J+o#whl3p{8-NUgv yg)LPו \;rp(13(>r W "F'8j 䊜sChi }58x /rFJN+A[58-%ӫh) bYNԊ8yO܇8I/Z* ڌs{1-W&9;̻!"1]!Iui38Ac>؇:i2Z q}w7kPh36#qQOF6TQg Jֽ qk8-"W47:ǻ_o?zv{Rk&5Gdrn.tf"e%LHF5#I)EBo'cװ٫ǼJ^wzw[aTӟ3%{Rtp.o!͵gZ7JB#s7v{"+=Е׫g6"+ NԒY 7"ٞV8 U\>oT">`ؑR8Oy;$T cgɆ9Fy,+a6'ţжK=,j{X-ĩ(5 l~VMUac$hal9Gb XI2NCI8 `ZB+z#z螸;V2r Í)óUJQ{L@x6wCðS6.ÆKّAc@/6E + 082]˴øV.,mnϱ+DrCb [?QOؕ8)%=π •X󕂾2 V&'j(/6Tl9 c(der^kns: X{UTig9-ާ^4'FiC~rM=i ZQ#ˍ֯ nK|zH/~XnDΞ=[g>D<77!kɕEn$JĚ}Y "#6:dAK{Wϝ~^+Hd)YNE6cpY6ܶ^bs?9IK,k%-L;$("RѦpDŽQ9?DkF!1; z4'&vG_eDNm_S@(2` r-;??Z^\_qS)83}5j'{9LAm,uj7z4sx\/]ƜO΍e|a}ֈJ.AO90i['wνۻxŋGN]ӝY^T1Zq*2-<"ʯ6'Gp/ѷf(0#^c{A7)>dϻCot=ڈ<IT̀-QURk@W:jgaVчrEX%(0RG^ VQTпQG`l aĭѨ K=ߑ0}E}hnUձiL4/1QcF$Ӕ_6p^G:+Ux)Y`笐{ ۆ6B)#2\VX- fXYr:Ze˧wIWRyPY4Uy1ro`3] HvbOA {2n݌ U4l D w8qmxP/Ə9qר)\Za5-r8?J<-@ ׯݿ r`și͸j$ R;+,a6`D86s%x)cHP 0k#Ȇ3Mca Ļ"q U;-'GdlsNhed[^ju;5Tzo;[=lGCsG3 I} 7}кp kh.T/vԹʥکY2*{ I֑1w˓qz2lJ+ 8B.c/To*Ӝ B}xk''UW‹k w+N/_hjin2o,|c4kG-l~=!k]G{+w:_7wrKN~ͷ'_}͉E#JVC5j2$<8Vt`Q0Z`{]A%(K B*'zyV1d>dAFL8"W gQb*? Hw0RηO_[qBuy[(UQhJ883mI1QiaѸ5|Ic~{'Шmև(2ZkBH`IHJ JU9\l&H =-!% AW$\=rO?Boca bQ⦇0! g LQ(|/b/H#zO^ |Z^ jUcb+ʹS(aN<׊)a)FE\e_g=y_0 .(%Dɢaڴ jg 3Jd.]lG5*Bx JcB7OaPBDeN,fsAǬ{kȖa#Cޒ0d]F'<eR9nrѼ5lO:ٝ;T?v 6;'{u)1Ty;$#Zh JA f٥h>ssh;/Y٣O,sLS/.%- ?!FB=hO-_y5~Njndk֒CU% ޗ|EkyJVpVWQxW'קb}h! ιwџw/ymկn|tܩܛ9}o~{^<{+RP$+$zf`s;0~ϣ{SѥZ#I1sPYSB0Z1TH[!i>(6HEP=CsmD(-!,d;Lz ʑ^p ;g#}˺ǧ/Ϗ=rKٓ1{pH|Ŝ"@Z iaz ιbXy!cxV,zet׿bК|!4h*gI#j]( PxQ+}YxMt cJm)=W>kClJq5lTe`Dsu3 Rs(~i:*vVFT~K0?LxoP8N^C6Հ(>ypRZuhG>PL4䓓w(PbB'Od S㵣?lfqa~˯t/*Wɕ;͇1ZF?/"%??~ӿ?ؼwƝOO.N*矿&4=%[}rNIW.^uSE0(I L=#fB(Q8 MXUN0HAP0'=贻xLZ0<FH^}pn\2CL諼o Q9#90xJ!x17F 0סd(k[ nyi̴p[ c>#(6smuU#fZY͕}([ەZ)"y1ٙ?!i.>VzF CaΊ7&*!U&)iTJL!_Z Q2^B®=aŨX31$yqZoht oc遂$7d#N 2A쵵RE]$oFb)uyE B;TOnҥ(7?tbSj篕_Gw=Y^^d Z.M4BuCPex]|LRi Agl6˾ϒGr KsMIȂӧ0Xo]_-L*%gR+U5RPƼfFj4{t$f%<{dH35 p&e3P܉^6l,eB do*Fz8JK)q5FU]߻|R3qQ5݌qbc(J':m{ʋ&nO j#^Z{^_(E!;߀[u.+~C zal))o%֡(iI 4 =r W:ox0zL}Ιyّۣ,ro'TAkBJň ;Yۅ:zAg}~;{w=^Z^9tѭ;_ 9 .^sꋿzo{qbz^,\'p1ZFmц Tצ?zkslL߽uS KS`m$FI4B"Ru$7<wьʂrSL 0b^󳡤s}>}FŨ$<.{~jD-NwYwbr?I`a8U#9r5u|͠lO $q{c0bjgqyg}1N'@lm#{uF6 as bijFJ:͛7MSeY)7BG$rO5TjWqhި44|:KKЕCWx >rύбu=B(h ![| _ }}3!+tԅ)y7 *~A tB.1k#I(.w !n_d?ul#&4`QMZ<OB;^W~tHeQms;=Q$̋b Sܚ-ޭ5xmT^1vRyQ˄Yvw5ag U]TPR9#0 /DVP$SXU y{s_SBA3{ct9g nťuU7H6c=\3ѥCOO.JJGnKQ(ʔ{'C}%wP<[򨋫;0b1`jr߱Q`#F2 ofɱu:@*=?}tr050':|W'|^%Sɲr ]Vu ҄bP&?D#H+o@30p (Yy NZ~bH T> b2<KgUB~%a/u?" hy=Ӈ=.܆A #m'?0R*X- zQxn ` qd£굢sYY+򒙎̐µ)B琱R$q|k&;ija^_ A&֜q8!?Jx,۬{14ZF:`*zyj5{mI4_EH4/V> Fv->Q/(ُHa%8x)crJ|׾9Ve ?pґ $78Dqر=U*:H^8$Ccy.̼oYS Hs٧xc1^yF cS)zpx'ӎ{xɧGK+Go.Z۽|ʗg&fVYE?ט4~/rZ/"JIU;|^{ѿnꍏ??:yqWUk?'M^3Bw\ƖOX܀0.m;!ިi郒WlP _M8|ЖJ$FH %ϗwñ&bjTHK7TUNE%Db3{.S2[\#h ŵ!@4#=2t\ +$Kj#P[ХQ(AX u|' AoD%I~b\ժ64œ\1N'aho^(3D!Bw̴ZY ޹78JGlm(K1\/PLy\Jr-aNnʪLZv_֘вK3ֲjG{1i (#>e8GQ@ w^\k甁\.x_r Պ#Te90 U:R[}{~LR73ꪺY⸢` BRFK*/E7^ѬuIHc2qgq Gހ'S+>İ'<-A G+RZZ,xr7 I2L^A-! (~xUB3BKj}N=k%u(qVsVxQ^,ҨaY (o.?gEJƀAѹ)~67]{c$E f,Y22{W™^,gLƮiGee Dj`oHR9 P%D!gȎ] h/|_4(*pf؋1@#+t9|HO:dѸNxwhW?w Afr j(_,^~^-IܼwFDٳg᭛_}_G=nFt00%E&/8yI{ TeJ ט^X0%Y?|vp8Rx#C'*6nĮ,́E/O{R,X+AC)w8ȷ\AՀ cϤV~)5pN7%.u3mh3*[L a ̗9y`'bN/E˝ٵIG'[*8@3T9!y;rsPq5br"0A\b2T2]a|$0BkAS= \QuO+A3"AFRelGkblMgQ1g(N0$OfS^x҆=gn4:WRs<ƍ8hDw(hKHf6Iq!+cFL i4Uܹ .R/ϬÉxՀUOϥh.PI'r9<^fGB'2heso ?~v x;؃ 4aL=Qޞ2 !OϢxYgx6#n$5|sJxA(=x縒ZM OI!$UFKϑCc5b#æLhO!*xXabnWC!A3P*BC\#McjQ"o|Ec}hpmx>]x 9~D>@Faسj %`s(VF \ B>t9UQ#_応S#jvJqu81sDq Ugݗq|F@[Qle ibY z]JOBkN{y683ZUYK&iePlʫ2:uJ9;̋0u"?#Nlpu8fEEGFT^9{t&5!,3NF)2ge+~gZۍR9 ԹQ{ț\Wt>4OЍ}]PIsȩ\tFz 0G;ݹ)P`cPRxy1e3 839}9 bܻG _޽pÅ|,fM-~QUrY5/#hd4'F 7ycvM%.w>x~sу~s5upt+I]I$(]h)Ic!2ANb!aĉv-(疢Ph4)j ?F ߩ4k%Fc5s(Fç91BkL2bȸ*_ R~*eNFp3(HD Q(YULB[3lfD},4m<1h!vY=y܉,)0wu dcM`7d|.B\ *{RVV9.C:j}ȣ b4dOE'Ldž8F- 1($MLܾʸxI{>=f{RȞ( U^*zsĺs=GSBKu ձB'sݑŽSZt3ƞ$buBvB6(Onӂ"pA7~B)ACHͲKDv">3v->QF@O؆R)UmrWh+J9"LNHl50#gc=GnȾ-1W6EFq@ΆnHI2_gKDYndn IXBJR`ݽcpQ6pO X)x<*ePtA{}jzᮬĹߖǠ=Ⱦ^ByAo]/t6jY v@È&6#KFi:gOĪq&6ss9$[ʓ*Êv™{:i~$ qi1NLNG9Ѯs~.QF ?z $Zx?L,Pؗ$BEd1 a$ 􌑐PL )FW(|Dbps(+Y/:Zk̜lH@bUnt& =m{R+CUDy>sab]f.[JBWk%׹%}(<{K΋q A91`ϭq!YhaM.v"4_VmBQJܒ 5?wxUKUI <91wbܐs+($s!9Q<-Zq^!1*e6EB~P4{.W^B2LBf!ZBЎ'>SeD "(x|yYΪc3BbjoO}ĕ[FY)G:Xkֻ΃rR+싟iPM'b'eQ-MމʃU629K43g7zEp3vzGrR|&;L%ͻX4ߥHM㥥\>#νd0%m7X9IN};0u<UT#й{uǫHYM.Pqrb洦:@Q("\sB bEFZMN.4مٹ lʙPgV~wx© vSJE?`NAWD/H(([۪}_ƽ{[^SirVI)ʒB2Rk>$#GDb@CA69cT,tqwrB'ΞƎf݉w#Y5%|N00b*~-0h7=2ڈ(͚<6@[`f B!iKF4=GPѳ(Q6%e携׶ ǚJv_e4gm5 4Kªb) 3 t) ԛB,6JB ,<1|}h MăCtk$ZAgQqwYY#FˮvbՌֈwovϼe+@XPkF0+ợ$rh%yYtyKƱOQ@Vy:w%Vh$l9cU-ZqCI)7!x|UM}j胢wia輣XE a2C~#̔-OB"6'GmSr+ltbKy<1(u*iH5;+e~Oe^|ccO5]>A'`$B{{:=^oǕ` >JU?!CpcFY)eP рML~P+賋k;^UDyguF )va6 FƎbw')ާEՂG `2_G]%GUe|hd _ Nu߀ܫa,Ö0L{Y,}~6IhO>7T`Qa?R.$|}'+wHcGi=-'g9+ _g|/`](ç-{P1r^IVMxvJVVᛸqa^ʰLEB (w@\d8{5Wex&<)!(Pҧ$#{Kc/wiE"ʂTlcE_}|$ރlF@+Dv(Y߿ (ΑRx.Z2- >QO.B訌*KM@9zxk<.;Lj5ɹHvw҅ZZ7sivt'4kEUq)#1z7qp}Qyf=Q<ɬ`ϑ4R,vsm{TY8>$;Iؐ2^#rT(U.(( 0( CiQ#)gT* ($ R1Xr[y?qѐ{ud_E)5^GB}]WIт0# IDAT5\s|&xI>P3<)*- vY= U7KtXÎةM $ 4|D湡zG " `"3Fn3%9 W?th&1G-On_Lһ"6-B 2'abl 0Ѫ6u%_?8{[w^R$*x,EB J܊NFD:G{F ܇zxaBm,q8בB"DCK"3}h4h*XH3=buwx/*˩h-#KԜp%lZ߅f)@tzNXc*8&1^gōVHFx\Ȫ!F,E7 P[R v|* FгFY3ޮ&$̊}͚bD=|X93d|'a 1yNsux8Z]ꉹm@ʋ=|^Jk56V$@Q_dj\ڏ )sC1s6f0@o+bi>"E%_*!bژ(/iom|$2U++Pʁm,hZ}eBǼV:0p(VʆhPYBLǝL2 ܳntA}~2._v^NRU=Wp²aǤOJef*،rXPu܀tHGC/Xar~fc 5mX30 }fWH'oaBңҞ<"%^b[a!^I{B'ͷ(A5}F 3ؗEH (T^3_[!q1ZtziHY{%B<ziH(1(FUKB^g6-k*C[P+Jƿhi Vy}@|ycsCY1W<(D_A¹6gd䵡# 7:ԙ>@p8Ney]uħ~09d׽Wݖs{|S0,G%٠3㉷/EyF2T=~J+x89/ǶJNT^.Lq4ru^W+c\n91\$R"`mIػ)oB3"%F#EbOHcx0 .MNTeD͗1R9U50x~ƕbS֖$fO(g8^%.#DIu 0.V*γJn4#mq(N:eա_f}UU^J\<gB\IUXQ. *hê[B}0ZloXSgSe=^N,>+ Lm=όfSR:zc.>k)!k;BpwW6NAƚ3'9qZX4R<*!,9\ק^؅D /͇"c>8 {]HB#s|2B'緧f_hv)3 ݂iR/R.)CR9'{;23uɃݗ++:p'>:DJtYY#0D.iBL*<y]J,c˻{-M7@\"Җy`{_`l1t9.P]+]3pMA RB;8o0cLj9{Fno "F+FkTnFD![1XRÐˋPYU\ƙFO֎dM|p5+g^hd7%KdWBZvw:-zgN ,ڳNd'9o+XjCk eb'19^+.+hEF0/~g=)r|)la ~SwΞK)Y q1BwZ mL(BZT)焟ڋdaU*kaf%ل1QJaDO RSsPɖV@mHh e/C22: HcxhB}EAFKsˠ\nNZzB)4*Ks[ 058Oj rhKU=Ն;ڽ;"P l+ڏ(T1#h잵lԷhbzVhϋ2b]=M.!kb1vYâ۽JE*`ݼƯQpȹQ Wks-[-<4w##LN bET|LV8r2NrL#yc)Ј؛xp_E.6/'{5y(/H{h6>{2(3\t%ٜdry]a8YE)s-2zd'TUkUOAsgߗم.^rKc qү-mt =..JG`?~Ǜ3]S' Ǽtu$eT&Yy.$"ʥIR f¦,e~fBA <}JUԀ;Za\q]HB}3GKl`؀ΜS<lgzq`Q#QDJuүea0zBߖ6.o3Rv,IZ:Л'jGTSc(?*˚gn/!|W_mP^2rHρ;a1j'a(HgyhP@Y^/ώDDFGp,v\2j\%ԻU.N.Ż It}2(P])M":-B: @_x+mʽVBbVhUW 0+yzV*oq=tSy%BV)Y禍5>CFALVxn[CDI/W!2\~ɷ) =FK TKx%8FK34d05l& )Ů!@]* 1yVJR7K d1a` پB@X?Po`b j!@n.9+h,ÞYL}_aWg^=#^7x=ʹ~RH<=q5{]+Q]뻉-J;x(WĬ*%{4'8£,4Ș!s/QfUy"(pTvvU4J٤ZiQ^EeWFpnHFv`vze/&L p]*fo Bd6zsZ=Ϻ~GPx疙fk9G9 +rmЪ!|*d:MpޅRN;kWW,="<\ {v;a3e/=' 7 ~sX d$ 0J.LM.ݔ7W^seZ o% 'ӽzrZ.Y6$ <0\O:712TP8GV)($L.nmfḞA%FQŜƖ{(F:L@=Nr™b򄠸 @(˹2`Qd0ۥ l#fzO%rКuy0^BX(ayQչpZ)̟Z`V_ N q}g$QsB gyk2jyCwdEt!^#w<%MO=6W>b0|쀠|j ?ghgEfE}Kep\X$_T~4ي;!ix)PDA QX4$Q!G*~2|87|ojroWV:1sIt/AքgN9ej|u7ƞ.A>1.fgWgv3UFKݸ=;}@Ѩ]. zz48p6^($< SU[SCƏ6h+VV{ 5UϺkbx_<(xkeg7'<5|E`:&Լv,SgfEtgRr O V4F=9@rrǣР]hJ:“3U݀#x/`_Ƙ/sO͖EE\z0Z>]z>gJů¿h{#cPfYxZJ~"fx?<>hMTRq ?J(`&yc^ }xi3䱘vO{Nv rpLʂb sە<1??zf`=w4K@p1V +kgdz1Z srؾzx`o'OV(\-T:$qI+W<, Z*%A iK+{])%pr%z;- xo#2 )*R_W36\\HJyv~O +éJ/“ΪyyCd0vhuuor ״8;TFLMu(w!eI4{"{̽1 #h''4zbmex/{EGz]C6K~d8ɞm#h~^/haM{ P:p?bz.)^hsݠErR1uƃq絮gc=Rܞ(f+|ˠsIX I91hVO9D]Z〒ڋR^tҊjgɰCx!cn5RFȮ鳍(;k\D'9VU%ФВFUwRv9Kct4Fˈ<4µOua+|{`,Qx>%2g 7?s>3 X)nOॽ`ݰU儭;jQT_1BeH%; ԩ!4#.v@¸~EfبD:.)D^*(DVb$oO8iB 5E1N{b{@8(o=춍R3O(Qzm@bx=ƀ o+CϹ/MN Lx(ש )( % y@>PH0y晽?MFhjo+ɬ]~KdgkÉ 7Q,9Ogܶմ5p /qTHn ox؃QFhzV'‹^r<}vrs@9x2 0mȋ0wu l*94蹵r_F<33s0ڈàŘOKu'{A^l*@`iCxis#Yc]R͟~']O78& @!'̑d~=cc;. ♢ YapG'?8}˓_}𫓛7n={ͷ{K/Z]ܝ?s+^d+~J', Fgbao~e`Aʜz|`oxw!`Fљ{gάLK=٥%!d.Z&a~Z&B儸vb0t0Or9rXyʯwqtr/*u3n{;K 7K(Ef鋰5q7Ǩ1E :>J,+A{X)f\B\)9F\洶ٟ7D^"XK2(y}..ObX{Ƹg GT*(15X/ڪrB# B|00x "Ї [{P>^ڹx0`)ޠ.u=q4WUBFMU ˁqq+e|ʁ[j e_Cud/2a֌bIl'FePaއD a# c+Y[^Z+NDٙPxHb 0蠑BMH`2e@^k2.gI^(xJa.g;nP5Cf&'}xgː)'O3J~-5mM @ e 3S" ?ۨtI t{oܻα,,+mT ;bqC6 +F'dާY=Z#l/B;wwHCD1hmN%tfmI RTVPGM|uܮʫC/𪒫OU1% )C"3刬1VIsh0c&Cy3V:g-ȇ2ʘ 0b<`8J/tSё͋W1X`u+4>(z/tŃ0C 1n: lq]X]uOSr3ѹT *0\ϡy bg|Q{9,d D g@AF>׮N֥gvZCs{26B[ =\7g@Q{WZFpH8xcɝ;N>쓣EK B/]?};ko~4"5LC{i71Zz%?FK-pW+ :(9zWnϿ+OMB/p}*95E$DLJ1TrIA0$J(rȒVֻTn!peuKptf"'*ArD!/@Ho*TCgX3p!O=5C*76ab>*RW;¼"ЌZH=szM i+?DP`z.\(f,(<1L].uӽ: -\fYLXfJf$[V6#xSq4=)\24i1(gz/*[v-y%~kQ}/TE 9yMHC;" NdJ#E(ymDEc߹e5h(SōRChiBfe*xZ@Qmt(Z1w8UGc*hiG:tjZUr|\$ /JC{PZ0 $GMB+~t#I+KAuw)RQB)Ţ-xOK'gK fzQ~{N4Dyf&po޼鰸 # }G aZx*Zh"{#㩔AEzM'^gepU*ͩ^*Iau 'T̡a0. "JGGy<&}{(tus:Tn )٠ U1o#7p"JM %QQ K=9~2do!(M^G ?qE%A؈sG~r+ȍމ-#~9hd].ə*ؒ u 1ӿɊQ1fKeo߹oD ׷f~%W,㷔ˀ>>BxJni0 0CIgL q~Lo< +PBh猰DJ{π x5/FYbEU}hA⚻_m$澡%%h1M/^lwO21L˧faњa5j#ѹ]+g\ MfC#`,9֗h9U55N$7]:NX2<*VȾ Yh Vg?zTZfFx1DS~KiUꗿ’A(%y1JRNa(SVV~1/EW%kUTEέV<{X1N֗RmG>}e(Ï euFIOBƍ?8_ ZJ,9ZyjYGw'las0vB7 =RDX<+=C\ !Xk)CHoEߡz:s6ZR "eT"lkhAQ c+Μs N_"Bw^T8efEkkokS^^U4 IDATahuKy*dAE+ ZJa|;)|k_>qڵQ;+^Wƚ[ZΝBB*.Dҹu|Jn b9^ƙ漧_rϏ9 {&(tuOp%4JO{Ncb`\&4{׊h/(E@˪D7Rt)ٹ^2Z=u{r2L}m\a!HdDqT] j΅w0n :NvH*/x4Bt8`6R$b/:Plz/r0hV a}O͐s@ø^[=& Gtll< G&<粌iP{UdAg]=FD lA_΋q y%S(yl#[k6Um23N7a.]t1ӀPslhDr>G B5W-xې`AJ-@øbIO/5 `X,xsy_ Z)&Ƶ ;X@`bU`#3!+edk<*7}y?wѩS>w>P 6{Z=Ѥ~&<, -RuO[g?wjepѽ N?reU;AѮ%R&ĢSz0 [:t#XG< 2r+2tGAo z%!#C vB> cȃq*/Nv2*BAw I0L| 9x\@!,T߂),Qk$Y%#7caB1 }5#h] 1~28qJaFo_0&GH)TH,bwA"!|N s{2 At0W E֮'pY$hRPQZPNʲb&뚳IPpF)DcBك,c Bz @QkhT"Ai!Ja3Z[ULR$cݓG\Fx wC76RPjM۝TA(@<ϴٹ&#QկR0_;AE).?w6ϒv>D?3oPyŤ}]&(D}^rOqYw9gM 6QbrxtmdU_>ȓQNIRֈ?߁G,OFEҙ4x}@U!C\!Mnά" j[Jf~B=Eu\ŴJ.(1xZ%xXK.ߍ{F0@Y-R xcywTjzb7I6)RL8'O;5$J]յ/ (w2Ͻ۴,hYہF}Yr9y2sha♗S3sw&ff'G: ITX%~6:- oI?_O__67j,M޾ք~6<`os5D3h90IAf8ՃP[(8-ډTII8<"[IuGxc$U@Tp#A.]M)[x<{ ]{^#~&#{0]GAH=FbPr8NG%+WHxs |rd|RY{C~vX?f7oje;mYHɶ8^_"@s>7-+4AS5 kqfL<3}ݙiSGhYJWЌ++%G s馘_)fG@)XʓG&~w:1j:}PW5SjսRGk6jmN)uM;Ƀ߻ݻwV'߸ysbqiQ{mLcg_(r\.ô݁;Y?>\p)%᰿#\ݟ-?;I_B(E):xD SGqx3fARhx.1eJ?BoI}(g B{oC>7%%C%}ōN #t8YotDє+g #ĥ?) h#z]qIF#N ,Y8 (%gG(52La zH— EuQTHYi5PIha Ǹ&+^Uy}tFt1Ϝ=?ӂQJ ԍ5G0Pc"MФv7pAO`@8 ,EL isT>#X2q`3RB=%(kc*ȰP*9YUIRYE3HL3̝kRUbg7U>s#} #-jx8󈓩g~f(_MFtd|`?ڰAGB`OTRg=F9QCWAVqzX5QT64Q:U@~9|yPB~, )t Ҳ֏m o P"uF-5o~3ً8-n\յ _9-8-\~8(0ǓgWNc hبv.%=Ό}'!].\jjg2Tc~W,kFD5 z: }O̪uڙn|G_lC y1~GA+OwޱjH{^ѳ=10YrɢGO^DT$,ɶs\B>R(3L.-)xvsjvٹ&g>[ݘ at~Ah>zگ)aU[ᴌm8-|_V-۟>{gkW'OM (+uT W(dM۸0DHCaìPmhD8D.DQg$x&ƼD@0Q. jE[!A9BٺcpG-\٥Rz9GE⎳'Fѓ@vw`؛f'Cl  M*0_(tZ7ƈMԔ+M9AvPGemPI26JCn*t^H-Ebb|_뛪5d'Gk|ͷe_2)%Mc88Ŕ^d|zR8BƵv(B^n$(eA2; N`Z =971vpZ?r<< }tߓG2 y~ MQEp(qZmĸ)$uZA@+~.Ekk^$zaI繤4{H9·ڨLwNiEHm qNQhpAv %ȡ4eQUW|YE **4h~_YU9#G{μBE=v2Mݑu4~<]vʜːe/~T>偉d4}1!FB?}/rS mdʄ_n.P@"PrXB  Pghv T/O@WdϪdK;!3oeu^8M]83{.pܙ+h 0oҞw9SG*Yؽ]dg0ϕ+״o}Zퟺ/r2zCеIͧF'}"Dž1_?Q.^6b1&ub#RPMxɜ#6P{ɍ8rKu+Q?և$rRhBy8޼h-SSڪ^(xR X+In^ bhRLs ZEjqO MA}{Kз8gPƹ ͹ʲAHj0 2yj*%~Kwl<cȯLqy>~Cm屑[*Jx&r 5ՎȆ?ډen!ͳ3 L7;D3] Jkg% O|:-<7mhŕř͍ܿwNJ 8y K)"ť.$O{QǜGD o ⬸ NpV (+b}yOh\ډH Vh$j'a)%Hb@'BW-,V;1Bj+XJ8@r+nK6+/?k>$!\M|˜P%\§?y$ 8oA0[iѪ5ER.o2 @ YzLsb|'ʔ9Zh~3 FF(x ePLFZYO;>nK]Z<Y-$aUT$3c1b71Y֓g JsQ':9 T edq2@p?kB$cb{y'ruUU^[^:kJ}0^hxe3 G}2*θ jYZyTֵ4>QFøp?}W_|5ѹ[J.p(67h@9{Cϊ`Hi ˠѪgx%? 3xЕ -8 ҿ yX)׆([ꍢcߜSr*p#asc,]i8UY Cu}0GUa%qĹ.GZ8*lɖ8ɞRmnfm-kЗ)9N|*^v ;X"ui =T*T #ø InƆ3gkssjDbpwHVegwL*rW\PbJ{`g{g[o:=7ocxnWH)] <}QYYd]1o̼^rzF^M"׭0gZ윇0e Й |Nи(A%d)߉P\\ _ѿ'>%E scCCe*qo@8RzE߮BX1p8#9|\s^y͹\IQLYUal"{Mޣsc?1}3X>`<VNc)9Gl)HQj"I:OM/LP/{~>5;Y|Yј_SZs}ۜ wfggw%w~x[Ε/wfOEqx$TW(6ƅ/$[q %~ Ɂ?%[ūe%47 A2nf(6[DŽjH7SjiUy9Vq$N'*R#Dƨӂ0$/ɩ~@¸fN 6BZ K͡ d(l+̼b)c)# "\gEt$jaq.v5\9(c/Sч?O!Y¸pYPK|vnY<'Tqh ps*X쪺X|LF^ȧIGx;IE,='' 8vЪiϻ"*2(ITί~=3VQh<Dɑ E )ONu )&pV<,!N9,pZ)ed8s}T sFmK&T5~,Ғs:-`{g3\GF㧦HzI=..NsUEX*Z¹~,^?b'9`:ƚ?: H4_#򁪚ja13%Uc&0!nd[s\$R ژ$ήDf6Nk'BTPZjYv7Ml=N ωmЄ˖ Ne=@TYAPyzNЃ"T@T<툌'70-sz#gR.Q{C ՛U C5$xGЛZ.CɄzT$yw}v~ Z}>Sozwpg=Q !D!Hfbu,_)TR5/A"ˊ8O=Ku+B(GJͥC!5F(r*U upX$#Yv "zĹ 8T h}waQajS|vIK~8,ɦ="{w-L8=~=rl% n&(D;,.U,kr̚hQ)2 r6gh~ G`0rfqT }۽>P!D5'pT'N-}_) a3k<5Ak*XOƿ۷iU}KE6ߙ[bu_ K9-9wd+>@ !2LK4J.e)BXN4%!gϞYpbtڐrJW1O 1l:\oHژ ᱲR2Qd H0qȱ AbGxOdظy%NZe6U|U{mNA^E1`&ZSߕL)C7k#'PjI J{ _`"K6ĺc#AŢhȜ!‰՚_y2*k~)T/{@dVh0w~9s@0Icߧ>^S!*ϋ}մ-؎fk֙^΋iX"c yGs13mj^`#q1lt{VBpt*j_0nQv*2܇H錱c'QT}8-I-$$ZWr{dT =;h4<9/_2ZG/7TΝzFeRDZ"ri^70Ҝg٧sJ*!{O"&)TمWӳ..빥sujjt7Ԙtm}{ ?UN˘rJX98.XurM(I@?yq]fNN%otM=r Q8i%Nz_aWAu+"n1Ngm@#HM*`Q*ʒ݃FH,ڙ,o bqj+Džrwl"LƕP >jrjIk!>yt5Vpd>uЦz5 SaTGIh/)P,Ȗ( Eϟܸyۜlm"`6l@E+lg3mW W3XTG+x;)7ϴ]M5iH+Q98Q󩟓oIx4j/J qNAr2ݽ(e ;$ZWu8=8+%D^xsKD8F % p l¹ b= 漆pzuT>U\9`X7"26f*\ =YBӅYc)GKv(t{G;KCESHZJ*JCz?DY,sڑ`QӰ|[~Q-kFiACB0dB eD*ɈCTJ$`i)"/[9\U`_;ҡy48i|Aܹrȇ( `"!Yw~{uE2-7j q:HwҼ@S.DII4K7Y_@qs>fZdw6LSRuT{km;oyjbU^gv@txJ/"٥ U{6}XYRXı+o ! Ɛ:xW=?Qv_\j{իiIB9%Qőp4^ PA\Ux! `sk9"KT%XNM%\ t‰)N8 e|K< 7A%G9 xQI0.,$+! ~DܟV|zߍ P{Ca+%|Py G%NW;9)݉W6TҀ1hPr*gc`IT% !;%DT|۴xicUvl֞ 4Nތ I,pE$wŤXU HUCXNńGܔS$hF>,MųӷNnXףJ/Tb|.ckUt+gR(v0ζrCtT c-a}'y9;{Ϊ!srZ@˹IB~ɉt9-+q B>.]$N'>MC<9?qLbH322hz]b1x6u}m%{i8h+`cȀO*03p%c\qmCG" ˋT\Xn`nfhSBKU݉H4`R{ryx鼈8NoAw2!9}!qsoI l{>* \'Q_]Ǿw]v3B =oZ3W`DOt֮fǬ#_w;N ϼ%P`\s.nGlg*mI(y^Fdn.g NK QDU\e#HBQ97MIg-5_DqU8GhDɥә_^qGHQڷ|MK8֟5KiDVCgµ,0~WTY'):a㒼Cx$'ɿq9v6 A>RLcyr3 ci1HZ孔V9mO>^|xopKko|xǃoby'#y۾vZ5߿NK&Miק:YH9ƌ1~})ST`8OjHL@? R+Ǒ #c" cro7T9bI-Q+UKg* }dUD;nrR(#=$!1W(z:tj$#Ɲ(@ʢz5pu=ܿw_>*t ~hV9(#8O θ|Q WrK!IK ^EmˀKU|b c.<D,h!BOEtO{q/z)g(" gMbAq ywn(GC0M򭟓; T~QdJz?=Bk]Fa%8đ;xA̺Gd˰,G(ee눪V~ =9጗sTrĹZ噣S~Igtp1BpP3TNKE6 kAZĵͳvT: Ahژ=2~+ 9( m(GNK7=Xx%t"zˁиy&Q4 Xc8?x#՚:Ɠ z[T.9;䙗}!Ew{i㳌/hq2x$&▱Uԍ"O>=XxKhdn"hdRnX0~_P HUoP`-b3raaI.A^кRPٹ׼{P!EwTZqSiEKиT!cs3H'E if]bC^,N ѤZJ>$Sg{ TDo\9z KGI">K4Dޥ5 _sG swg?sDFI`PL9r1AvrFUe24!.rCr峜3M4eA*EgU$,:(/rJ=gY>}'rմӜ~^RBڙ8Fе ̝׸~H4 |ESqNI ;2 @AaF*9 1Є%qZG"#y4a]v%"?\ҺuM˲Țq֑ tQm9Ev@裁E.[#Gfpv՚//p~' <ݹuvF3:n$'dGK//ڳ#jG)c@_:̧T;".tS1lLB~E,f~Aymg*AK J"Go3c? eSO? ޛLK[/UI Hޛpq" q+hkQI6+! XBm0^hP&Jp=ǻ>; Nޑ&KҠ$1O/Ab7t{T$vqH7Fyzn'3联Jh_KnkJ*-oK}N5N>J7ΑHxv^'G}L(tܰi7-9(ЛlH`DBFզ);TT&siSFQ XE\'dVP5xAO+HSF;xR\9h| 93[2nCɾ ʪV5xP tƪRT(h,2ӡɹP6 DC Z~Q;8->{ZSr}o 0{)Qh J8nAmk6AH7P*? 15"]<ަϬR͑G4)vW2iƅMf.\)#g#b h-I $S/H.BG ")c>;Q>S~ uW9!Ei5v( 6x:O-N[ ½NotXS{'AK\D| ?}psuF^Dy|g3^RьI9s>FQUN+t"7!CR! spOdDPŦ"4y~BF,:If :(J$NNgph5եvvLGC~-@'X3@DK@2u_! ) Vd x;*6]̜g Ns"qмlqkJ|=?k6 XA\ǨhhShL]Q}q߳|3%;=uKGBD>=* .C/$!f\%%[TL?t>YvJw1bsB)MD*ԩ j8F|96sd ZG8ԿMރsvuU5ĸy  D3pZ/BA*QQV?CEhG;ÅU#qt:6БW:o; 2ιAO2h=АH.Xd!?GL>g; ޓe_L@;h?׉~PqG)|d Qi! e<҉'mB*pbr aoJ~/,;~l_03dnpXE'඾+ǯt|;ۛ?Q3)U8A`!)ǡAi&!ֆHXJ:=Q%26(:("gVx"1@HºCʘR;-cqT>7Ic(.ƆHeQz_0즓MyhJ+$yϿo{h@hAgGѺQŚEP()c$e7({9%H}* e98% bO_&v^;q2D-xh c`\D(ecG[T;(rkJ7/r]0@V,U`M9W 'sMRJwXa0 F(3qcU8-j鉢=D栜U2{YqI#F*J!A~"Re:'47ˊ\%zX]QZPS@qT@Y}򫃛*?#= )uI8sva]  |#)3kۢAXzj5@5 !~ZƼh@&a>FgBɱ:±M-|BX3JÆ`CE#8$3>L_#|MPu[rd~@D>jR!r._QTqӼB xq9>vypyt_CH0>dD]J{YtcȾ0sG%q4xpxs^-Mj\Ӓ×FA؞h~/#mtL|DRv=gP~aXGI.dluP3my=qfӿ@a/?N'ifdņL/*BϮ]ۛ|O~)iNϋ0 -ue{zLg;.$eIB~(/'?͙݃_}u7޸i TAWܜIFYOq We}T:nNv.# 4O]-hpp J1Dy.'ƹk,'D :،>ODh t:gG#ɭ)mqpu)>ȟa?F'&jh/փqP 9LoYhܥ8-GIwR?ΈfѠsK<7lHpܸiԼ}E~&>;#g5+ FȐ؎vt#c4h6N$C}Pv H&ݕ1vpȍи#H81n'I)nNFI*Q⪿P<' U-$9;*DcOZXYT)-,.?zgAGVM#4fs%AM{{6 ֚hb6w?__}۷<?ۗe NճdཪDxMԆ_T2ȳ) }2bpC9>^BN6e52N\<6 p.y+* {vS;$VЕ 3cFX[):JDLGLRP)ЖdeNC: >`E)y޺ vCµ0qJ O=y);̌\z}0!MGlIG1%Z*Ih-TN*d9BםNg." VT͉yWu3ݚz(Q3pژ#RL_ wEztC5= M˖2\2:Qw H엣+hydoZi<AC[;rB;d<}@Гc# 'Dٴ>(r8 :sD Gs h|*(U]]@e(|ܑG%!:_̇ϕ@S/)xim"vP]}i69g8}cfz,Ȏjљrlb@dD-:T[KjavˀrFzOO&'g_Mʶ}8=ɗOW}xJVNڧssgꑔgK/PN?1dPX$D\ސ3%eɾP.-)`ٓ3/.h4?13F kr5, nh<( a[]Xjܖ ݙfe\b݇#SJ h`gx; (k%C=ͣB.q+/ vdxXcY(aGqsppDύo](%0qDrpKNNyn I8( ̯)y\^|xM9r~]B1McU)2W1BWxvih(2cBCyExrWsy5=c򔆒rF^ƃ|{5iʉY}lxڵ'OMQ]xm0/}j*ݬW}{Ϫ@GGŪʧT2 d,tDQ,9zs 1)vJ癨2z{F( vK,e5NcSty@Yǀ6Z"bC#ƣyړe ! ~EONbx^9,'i(n\aZ3' {33'';rƛo L|!w+p^;*-zvZ&zqa|6ww~08{I֥ӣ孍'+/֟R{jAvrN#PU ȅ? ) +s0U*X `bJ+ 8 "|tTTİn#%oxήԜyJgV"uO? FA[Hh,뉶$ 0,m Zz( YAٮTdDe#QcI`Hl{gN$PT(ۚ{ոQƌs5e7aBj%}h(ʸ,l1wR/b޼QTT*ƀ%H(Qs(T B P ql#,[yv^꺕XՐbcE<3hE(6~+:"ϞDƝc_Ņ)F#jbBM1[T;yiN-|GB8-ƀYԩФ;s ,B/ЫIrӦ꺻8N 0Ū?K9`'<"_Yό|.^~Zd@ϰ:]-yƊjU+¡Q~({$*Xxq?hdXU*W!bL#o'kl BiNvE8s(bo]CgG|ʲW4k8|WM t  ddXlr$!opE.FYGqɧR7{/ 2>n +Ux;R /ٵ];ipϓNR|3 ROt۳Sk&˃Ý|ԜSU9Q :GR+> OZ]PKtq2 Ԇ~vGGc٠}@HG2 F'Txϵn?\ m2&luRjo~TP g-ώ}FheFh2QJ BQ+QSQA}2Woݺ=8{~/XJ#dl,pTL28DE1_6^Aڠ5j*]E^f\iD={R?) .b& /a];N׻,]X_j"y{(4V,Mj*wڏiaH|=kF",Aq)uyuI'qƎhs22+(DZ@@TI@{IEX%[#Bj]܉DBӜ (kd #-nJ^/ -쟑P sٺ"H< Q/ClMM5EDDi)7*F?NsBX8 ^qQ Ѭv uN IDAT:1HK=C},ZaqZUE6!`γ%/&I'I_tjvǽMQZ٘_!xUy"<{^e(IFٹy\$'*޺rW$U늆X{EƋermEUq"#(+3IW~N8Q `Z{s[Q_5oPpNFlYh{Fl5gcrFx98 ug'hz6EW%5T;r<饚 [vVN&w ,t8;FfN ^yTDi`d'1z33*MwT@gurv Pr624H]9x*OE~ˉb?3Óaxq (j}I6\s#[=!U y/{&W\k˛ 9eޫ2^ x~z>a.j r6;b:<$~IrzQb<d+?l!nEm#JpnEC-5ODA y&@a;QAp*ڤ`G}q]vՎ~Fh%g\4~f{3lγeDC|uneuϫyMy(FxW+Ŷc=Ƕ]\[ >W`y|@FK4q>-O5qHp^;F2oIJuNh4)ɗ2&!MH؊E&U/g<][s,;fRfi`F׿ vZS 煳Bd^2wniZ v_^[ww^~擷^,orJX%Mi8Ce;A n;4gMoc^ ;wT0C'.O(/$>ZwX ! ץQ/}kh>ΧբiCa Ge"ύ5! \FQIsjޔl HwP5+e!vBh%Jx"D 2B!')qbp@SFE*yzllK@.?J54}q%!Һt#ZO~/شuU;6JFU%k`;{Ajh0\%i'\FP6"rRcHP2Ӣ1vq "ÊyACII%xh=3s]^sD3ӝQZ2>c8Wu+C)gқP9ѿpV~2'ڼ毢eJ-FQ7Pg9a#2Ifְoq!H僯x%uVzO~$eGѱ he2bޘ/FpPDReq) `C^ !޴EPk=4= )BWi!6Cc7΁lz|U 'ʟ`W{G֫JPL0|=g$0uGuSa1sՔY[QC7n'9dW.7_IU=98ٚ9s(OiʎQh F4i]t8'Dow5'v^ڰCf0[\־@xouHp9Iɮ83I:|wvcdtWχB?W"0%?튊\X47N)*2(y -RrjhN]E17Md=N^ᶊ.dlᇓ/-#hlh.ݿcEJwxW!D00z0dmAS07G])0״vR%]W?舎4ik=}QRw|nuYUykpS}fTrSÈJ( =%'oy\04BTBk;j&F~뱑[ 򵁹IV'<?e'F̿~rTI *9/=x *Պ(UO?MQW*tj]赼*ڢ1UEig7F`yQ9kɎPdDMjM})3,]ЁrL[5 ]Qt[Jr653k畇sQ2EYu9悵Bcc 42D2>/|( ~y~"j~TuJrᓣѢ7cU"RM(ʳمo#'ewZ{:UP䨨:(d@T:<[kcqdA8ĠU<8ݟo*x"WLAO-_TJzP ІR8XZMȀ;zAdC=Цd`ڀAqyExT"s0m%#"A%vQȎ e D?{y/N | 7T9.5;By ]B9f.sBh>ѧ+ڢ2 Aߡ`Vo=v5DQ \JQ<+(e^Jґ( 8Ž'no_ԅ 5q]Y׺{US wG\[or -[A0Qf?o.{iD4ő\)̨cf"%SgwFm x5 _A}cqF-':_Wת=#ϞoT %+qfب]Ӓ*$EGFڒ I8,{27וXUs<Q_\p2I%ȭiVǰq጖76092@k_B?i#~havpue!PL!rQ@s1^(**8yC'N09p6gSE8Pp\@8-.3o#"7qY&kwKw?4_o$CpYr 76uGtN&{.iܻ*׍LᎀI1h8.`_ 1tac'F)"wZ|&_!շ(z(r20Aǐ`@s~"ɝ| G*Pr*>wAÅfE?)J|rʡTqHoB!"33fe֝ۑ㇃8Ⱦ~H+?t[7=8<3|ș8HDpR J'.sEV)2l7!u'j* j+gq}il7zK 1/*(ݧhD9\-<\ˣD6pR܀]l nPCz{|f@\N Suq^'к=@~=%O ?igE{RBCqf\1&44g'ڻb'utv^LUW(푑q dB; N!J )A@ߡ ƕ `x8B~$L%W4eP#5A`]t {K| u4!q7:u# ٹ Ղܙvl#YnhN*2j U@^E@߀F*.2…avt*C T "%c(=\aʈ(礤󲄻m|s4(q+ ͭriP3g8(?;:PSgO `Cxϭ[\p 5MSֵ+چ}+P '.ʆvH%ƆCL EgZ8l&R)c1ejI o$E#3& n?#L8yD6;E]Fo#/yd{8zRrUD|/E[%q*{@]UOI-3S؇RL] HQj^blP &לY|T syP a<$b>zI26UPzb`p1CEЉpZ1.qѠ2^ cb oT"]6*`YťIѹ-Aϙ"vN0Y+V |@479COS8>6O^j3JdO]Zߡ`HK{z1UޯfQ CKd&bGx52}(Mɯr6C"G;$[ pFwuFi 3T.gPpH?S~6E%Z$o\΋<"51WcA&]t(=o|ON/lf il i$v%DbWcxiQaD6B < ~&Wz_ H ϗJF2208W2Dv)䄿t#$1Tꄍqy_`=v\uVS7jnaC!HB73 yF~N" =ϓJ(=,*a՝/UQի)򗃟g +/Dcbcd"4h!F2U?ReT pKQ`GC?h-}pJUW>?8(B-|P p7DA+|΋JNDbPІc"FR|Fcd-Fj Fkӆֿpsp.%f)czQ$TGJ䢆w8jF E.8-)L"IǍXUf@'Ki%Q"96pg)5SϞ"gQrb\XMhi IDATd rD $y@u:5{zh-u xP PJ5$Qдx&Gh Ĩdꇕy;YrZ(}M]聪?F9& G:Éwgq"riqN y K*C8LfLTQh+Qǐ27Dҋk[Q@ yUjlӻA/ ]Tr`ڑzxRYvTgrZ\!Z_"o^ nbB'ri<빫T|U~u$gZ&Aߑ14_kgiTb^(!g?E~8%ZՐncJgÑŊ99jQE?3צe|z]BEJe=ZH_XE}ʩ>(.?{*t ie-|J_)@捳J׌.س{O9CnHBEEDfltcy9&b!w[ , (@f"7߹M$KTzTu& |}{=\I& TR8nyص^!аVf -!g{8LӎcuNP+S5Q)G'_3E^nեϵs jY۸}J\%r( @%3L@(ZʻQ/bxuDpA(gD;L{znQ+ XJ`yas2&\7d8]kGzc[ϺZY,;/Zc7w{*^g]V"RЛmZHT@L/.xR[Z?7&z{{/xGR(o"WIF6,Ga!"LKb`O6<4I+r$ F|8('ҋ,KY/=U\wykxʮk7oN߻{GPG,2?,xEBlKDyw$m쨿^*xq8R *BOHrRǺ:lVuA z02CA#O) lLJ#M们ʫF L7~6:A+ADa AU@ Y?|}Ձ@5 =RmPwz[_ofVG}?u}5.@C=ha` Opr@J>C}a1򞼟:Cv;k2cx+y$rWI*SxKo5D avV ;@(>1n< N5 Lͨ#}!aCŲcG^ݾ(~P?VoBFDʼnT} >wMG֋>^1iI'H2ǜdӘ3:?"08Ϋh][*k]&Ϲ8J.Yp (tϣ]--EGEH"> A@Z}#E(TVIѪA!h ##BV2pHƒ}+y  h"rWDhA}`6?|xob|<$ճʚ"c%f0#ª˵r$22Ғc Xx!?3KӉέ/U pKy.`DO0;t U)Z}m:=c$):x_W͋& _?7T1ލ[tn?{ٝ}Y !)g>_LT_DUtrh+޽0ν dc!hyDJpo<\{*gDCBa;'#}M'+`}qc%t` dĀN8Za$IMcGNP$-v͗^?^39(|$9zVh6+HUEО/ m<Wz<8h#{!G/m$75roxCwV6ޑ062r>DOy W3F>x '%L:feޙ5^&@Oe]W> GdCCJ>}'A_aO$IHh-2 S 8/s\:S1Că1#Qäft^7|8N?zh(_,?3 a 40*)d(PD8cQCbgbZ0L>s ڞܡ|ӥ#ꚄW)ɵqĽT -5KppP̵N iV)c0UƊ6GoVZbدxL*ukloF g#ƍŋ{gW쫰Ծj'G+3use|J|sX?gwϐIk𲌟^_띞l0tk47䡨CPl\aѡFwVhI=4%^AYq2#.DVԫuFLp2e ’I5ᇺ$oS(-R⪢߈hE%_{e>|E4u ;7T*IX`MBY-ΫNgaJ9hxO/'ODGzi5QO돗ZWdN5qI|TT7yǶJU *?{'Кx떀#SVxZJJtL!N_ P 'DB4Gx0uv/AKG4|AU  $ZYIE] <ڌCNW^hJIZR?H4 P,4eCix#c=ugP;_g+O?)![ Ϻ wlH{1Ri. GT &%G'G )j+  nDkܷ֞P Z_D \E מqK), z@a-$CAr-}Ax@#!jk9T|]b+];Jډ).A+{5l ǫRvO`1@,~#f (RL6\Zzvk'p[_㊁AW )9$yN6J?{v4霚8 >HR";2>"\ +U[I'9XϯD s?Jxc8,J|fZ6ٛߓHmBj>AP>yv$ьn{u.񗣣c 69MSr_N"i ǔzT Z=ȯ}YAhr̍N{+S^]xS1KNHv&Ar&O;++NgPm_=%O:z{F;/޽5\{^x>zMWO={rbӇ{~?ܸqQ@XAގ/<2ކX?(΁Z2TU+ZsNR,&jVǓUj(}ԛZlAeSs?U B9xiKXEN@F2UV(Niw%StHvJ@`pz\!D1񺯐:F>exf^t{faUb8Մ*A ϏNjz,@ ۖle-6;z$ա, 52VW/%!DpwM !9|U^Mա!cMޞ(8?tKsgM1G/l{R 'rx~˟iΔGtU(m;KUTwnb!/p6^ ak"U)ɡq@ms~rOh3yI1"ITB9yR>c4yGdv'<|aX#4@Ȅq?TY Q-|Fx/E#Jn D3Ѿk8zi$aFD)kB#WKrO0v- 8b)-Rx(I,vdTܦ?N|A%\{0⨨y(r>uv)b^pTV.xn"zxP`jpQ ZN4O#\%)ָ6N81\a/x_ iЫ6xs[j]Aӕt*"y$rȈ$MB?jd n#sYỸTrA=Rh0)Vꈗ G):NjeDBgMD|eƕ=ŴĹS) 32ZN )v澕Q;bرj ;\7ɺ@VkU GSRhucA e j^pag &IH0z:1Wq+@ic7y<|s ?/["" ̙~XױRWL~}>R.or%BY?lhjrDHB:7x&)Jb{0&mSSTR{P_ߕ _yP-w۲JncD4rV-Qsh R=z)oB6o+D22Pj﹌W/iZ0v%CF0^Kl#PT6 /g@zO=Wyi٬n*'Q#C®+]׳E5x+CC8X^*T.tFN|Rxw0Z _|[^;8@ H rޗKD-=έG-o%Qڨ;E1F\(TDS`%' Q˄"d+¹&Q`!mqdb_PgCH /%FCc0UDק:T.uS,9\w_l jbPܬ^dmS G%h>3]? mOm 1TΥ zl G4IH&.7y侠E"lxơ&;VA??p*kc@"3 mۨQ/Za1k#=EX ]'@'<(_ tR@{EԵENf3hk7K+yCuGsn(_Ϗ*rNh ZrH^\zy]T~sr9=w3_:iYHw怺h"`9v,nC.̅KǵfYr*r_nZIqlST}EZzAן͛b,0K-3p(ɼKM}sK_' /PZ&[Or -iZ a0 +_ͫWS&:qήm޺piۥw_x}R%T66/z_p[w?;:WUW=HzrrHcj) UcZ4e,򲓆D@ymѪS^Yi5+^m1?|˟WN=u˃\?;x#*UTK'bt3 Tǁ+ \UdQ˒~ )@bLz7t_ˋfثxFG/擓IӺ M2&m b,RbYd1O<gJZ} F(OsG s; z(* ȩ)U!NGvyc]'4d \T:O传pӁ>ϱc ep>s4͇LzSd;BԜL?۷"*M7קfG>n+G"V7V[ FFi# hNFJǧFUbU$ndlrc\oVhW50)JDT.+uU@GC^ -]@ cQ/?k~ỵ۷?Z{є!޳E$j{\B EKC;'6m_G3juW:FZWNݝkɠWR^hԵC^jUЭ~dV)D,WIZ+_|kV<7Y]P k1iqTUQ: _O1yhۂqR,ÃuWC$B6{YQw!0UwUJ!aJIw,#CwLjK=q{~NM-߹ȎI 0S=n-,-!; EN=k.I@KR,!(+)ls؇"縢8C⊢Cfyv)Pi `!D"za ։ #YE d-餱daŤQQAx Q`6!ߣ8+bjt+/06cH6^Yݞa8Ta'ӈ{ &<v~ibP45dO(3g0jcЮ0l`<9)Rs0SkhlRcq2lFX ʍKϢ,и?-%D0n߹-u*e16Qp!ET wA>R|6VN ;PJO}١XHX1Ku8&ʪs$C?c=~C REH\D_+!|C3Xym  6hO,/\uXi<nK-%.#k<88{QmYϑ\evs%Ԕ: ƽ}uO],&"RLeRq*t eIa@3.\'`"tI~lP<$9aKZ4ƕWR~>ڐ@IR_ ҈Rrԇ%2s,XĤá)IАLHr8߃r`.sehO+PYԋ=[A]Å 7쭌* yMWӳJL[GL^?9z[v*zʚ[^Y x\@SI{c}{ ̴:$G*:^m+bZx#;O6$wr@Nޕ|y=FĖx? 2^$AƔ W9x;M }[ɠ#ϳ"xWTUwz' ZKX~u=k=ddU|ٱ,b/ ݷ!eT^%jy!hp"awא1PUJ?]WT֕d29X]zjYV~/^(GhG.(IxؗmtePzz$AK@RP.t:ơnZND60H Ř 9+;ʂ+i$5sC).CN/tڌPaס5Њ|"SIW-YSȔ~yk (8+gaJqJr3Dr%ʂԔ)1U᦮Ub}M=$bơPxyhqJN 92À=xoSCP,i*M O}ƅinsU5*N><9D2|Pߠ`634ԟzF)!`D -$`6Π\R0 {DѠ2j+E]QKd0 0iS &xօ1`I1t>v=;g*"*[QU48WU6lt)or¾'+ǍvgWԣW_J@@eWwtT8Vk5w{@%89 |HדAgb̪x9/~^?[Z -}B_[jΙZ1NP\("4#.e}@ևԕ8cY?vNd"p'. Y QV|$ˮm7[%WQqnE*JH$˸9@=s%Hz D2h@1+aVhMԃ>ÁEy{"J ! suyG27#DDechclB_8kef*w=)iDyQ́=G%A /#PyJ:&g{2y_Fo1|jn!#e^0v/\u/] >>;WkhŽTjv?mT;<2\gNɄG<-wI>b{9ٍX?Ge,j9l󿟧YĽ:VOZC+c.Fc]|*9$RL1MHJgEű[22 i:GZ[yWUE쫝vC6H"Bsz6vtŠf,{P&a}^6A) b$a?G@tzP0@"JS@D_y0c&8Z"#@PHO'0׌_]_ C|xI:&"1ʏ}d468$@0 )=HQSߟwqk0IA}@Ϳ7>@џAã hX3`5Ƃ<'Q0#bD] SأIAFPUCj #=xy`/҇Jbк#͉DF<]F&<ƹzo7}v(pȋHf3 c+a`@@F&BY䅡n)B @3dճ|>ƢyZZH=<Zf@<3h;0#?luKNwpH^P"@ߒJd.Pd~8(~ 9?r|D萆_hߌE? "7c{8s?<'w)kH >,c3ɧAJ􄈩PDZ4_qHH$9`ˢ-~ ydύ\C4_#r{mWFqbIȴSC9$ssi_d,@_6 D`s&/hЭO$0+cڑnT)ע$ERKJWB/* `WA,oNDj@tUZG`@^c8I?:t 21<-#y/aaXJ/KC;NDDW]B(ASG ?Efܜ/Ela쌔\/\ȣRS,=`y-|v!jދX/p-x1C" :,u'̪hq,~W%%¸4Io(bzB!sHv+Er7P6P2-dJ9C'y3ZVbeIkY5sy@P݂c'T"" ^Z/{WHM#q<FG"dSsUQ /oPR Rvuu^1_H!PWFZfp s=RU*%dvVGԂ|GaدPS;]Vȯ翕1dL~=H}FD@9f\X1b ˀ7JԮIJcFxO_7!]H;'g@m5?L뭕@"AgucH%챢O4'dx'҂,P>9OO3=q3@}w-wd7 f΁;^?tս2zi]|(fI3"tګ\'oy#\E' u(fuhpIIYb`Yv:@@B=)F$!2%Nzl3e$9" "gS)T{=Fq'A"0BN|pd+B.4(XΥF2[R>+Ҵ!AK[x퀏{@P~a<|ѓdzZ^ !%V&!vJq"ZV,q᱌]'tt9'\J0I^h0 o<hiÊ IDATȃ CQ>iSE_/m2]Q><|aXށJrc>FZ9cA=kD8gn3"'`]JN h8L1Z@*Ry3"%0H*45P4~G~&FŞh Q$2=sqz$N*Oh 966|wQg5x DFxˢ[}*ᐈ`F$X 3|v9bհ|-H1Řǘ7E&'Do86GcK:;ʽ9i+t$3m9I:+LR%:pVZҘȁ5hvVvo{zB{Bbr|4tMlğ!u=hc쿠\^96zN ,F̿sUC$RMJS ׽:\W5+Q1FҨ  {7MØU =3\@ xA9 M<0PX68t~0;큺A$qmfOu1m *L]RxRnPD"!?APϙy$< 4D(1໩Оz`}rYM Fab0cO7@ T?ZN,FѳںFtG2QqIo.TnH267iB3"~$rU'Pܨ`!;GȞ2\ MkctߔyƈuhMKל-6&璚`VWT7.#O{0*-$J"U$dӓ980_a j* |` c3ڠ`0ggẗq]`@dCg+ bٴLETR܉Y fQW!_3 )L7 T)Kr>$`\VT% r<ޡDy&?)cd2eQFN]_[ݗ1C5(q)_5Šze4mGg{hPU(Aw7Ċm|Dc,%5mR1(LYr'܃kR{j"M%]<=<>+\YWj\yygFsG;:TUKߩ}ۖf8^Z<[vAvi=O<3ڜH@;Z`LhGlV~ ]7{G6:;88ڽ"@)wZ^eM:c<)XR밒F A?$@z`xK_2~00|@HYpDd(">˝Ʌƭ* 6f=*D+:,pF$1Ѐ>w"N9C؀T`ێۀJWs5\ W/*M} pJ"W<=@$Y=Ԇ"40Rb$@=jcd|N"VY`xxsl#,ЧcϤ 4E1_xn55 ;Ym@0Ѥ{^%'9$3Fpu!A,yY|3@CD>.ԭP &PϊNR뜇 c%-E`9Ḣ5Sz=풒G}|#^.YJ9)9TSW*=H |ڣ$)iE3FQrN$/z-Q+IOܕ̃B4gΜހTHݣbWhg1s J Ɉ3 h^L~ι:_Gn5732QO rHg>EKo2J`dp],n~V֔a䌅0^ْ5ǵ%ሔCquOyHߗF [# V hO5rve@K[{\T TDScXt.ԖW_( H GXWo x#I7x3|`Y-hyMxQyzˢԔ(~iSkUtIY{2R.Vp- {UblC9( R$9k}r.i-H|$@<ڣ(D"eQM _9(@QG\LbD/j2Ohi@Ǽ"{0яBL(ZGR^'D @ CșDY9+(YNy=I=T 9{p%G z(+L;9#,E~ꄹWuZ>=+Ҽ0W@>Rjj޿NF XVmISBLkX?]_/k@f~~hH '&vAǦweYݒEx;zhҠwrIZJuX][iVSnHM}99M..c{/›/$ix1ڠO6)]S>3]Q aajU 0*&a*~OD×C7E"pR$9*. ]VN|!:<xɩ~7XFS3Z's0QRfүH؆'Ġ$` <Ǣixt]$oDQpD;/G wU aKjػ,q6d3.].]-4Ʃ"Ҁ6 =;/l,u/s+IKm 1.2|$ D/Q&JOz\'fyV*` hv^h6u}UJݓ,NfYdmm'φmۤx"u.l(j9D.Z)Ppp.SґTO{+b`/6/jRHyD;)-yWrE6kBDFzQ@Ps̠A]hqHpeLsR2s9}PK,9 2^s%w"XDf .YێG.}]AYd$2g>@:Rt5rn~#{B rWGwĚ~sx!tur'ɞ^|hYdΐB;u'? eMH9F˭vgIb2}.͓F{_}JUy.KHeUso9ML/=6Zf2%%4~W;GT'Wr-j2fPbbzܒw3r{xrژ-'7KD$81W)3IIeOfPA[a) R\Hkm\/0j.@ZZSԢ^exTT 8F^UJγpe[~ŗE[Tz嚍h+) hۓA@efܚK/U DJx$Mvg9NX|bQTSʮšȓJ,}U~1>-E0|zGAq_s8rrd *gMe@HjRÈ w פ+SWQ$_'0eTBfJhSLpzpL`\Iu,ss^FP\Ȇ 頥\pHcBSS8|?Z xErCM :`e4, 80l<Fx<#Y>fcܩT1?c$DOR*5b%Ȭ*&0WOkV!K؊:>bZPҩRGDLВ]ij6XwKf3<A[kZ}&SL3>I5sQ@`;1Ա1(m~طrKny+Ҵ86EP4nZB0sU+E;SrK!."1Q51 (0{Mʝezc*jR 'g*'Vu^_y_u~}.Ky鹾Pր2` T|_D]b ZX|=uط[: h39o48;@wYҨ׮ڒ,ifU,Qtk6:p1:<ʈ "=e;^d{ h9PSe'HKz @Uryϫ~111Rkai;*\|=٥8P^cYT2 q0ELv9iyPBSKJs! ypӈ6eC&^j v+o !A?Dc]/M3XL*Qz>pckDdDݒý}_h{QN%ETtD s kMz$#[vBـ/'Dw. 3:= :-{QRtg$g-@N( %kQ0![Қ<'ۈREd]湗ǝ!}}vU2=AI%6ʪr,ׯ{J:GYJ;ҮFZ(rJzn>+"?7E3kRCu޴TFGZjt.|%zK-gr^I7"&>6A=-P7-$=,9-utr̅w?9:xus:\.+>i6iS,wEc,R̂lna4f,h=| :t RtE&e.TOJs0fyDP7CJ!CS 5 b{|1 &w.mɨmCR$0xY4e 0$";oF'u6Y5EB;FOGܷ.aaAJe$sg3X*O (ce {0e әCJX$)SA^RFhzB͉HuFLՑ PrUwc :^B5^ZsS,"M$iA؜a] cQ",% P 3 0풹H%JsyrZ s= 0-QꣁaphNs zq9/}Ei3g.+HuRi$+FaE}/TD;$ⷱqL g3;4UY`KHW>86N&sa|R}\IioҺ/b]M0ũM9#D35&9ޙ}kְ='ԩQck[(J*whP$'X7RdjXR򽴑71G<5NGkd;Ańr䥐{ΥxƱ%V?yGE\mL39M\kTk}N=p:hRn6:ԛTwuGl.|%pw*S~zz`Zޢ|[5w+2<#$n,URp:8p߭L+ʨA"s&v|ίQ!+m) Ag`0AuCp*P# |!yd 0yk6F&`$h?iHQQo©%WgR'A^LJ.F6K@?ץ86m"RpM Go (ap.9ik*cEdD F$t06r(t<(G[5~'CψrЬ0z=FIJ ReӏTcY׀BE;1A9?G6P蓤}?>:ŻV6Fl,!ҡxݙw f8 PLqZ#\)@5M u ԱbuHUY}ޞ=xyS Z}=4|أ VnTxXҠ¸Cp?dM<PN^H}؅z,:$C2mTIRW'uV"/ҺPm蓔`Q{_?z2 4ǀJD YT߆#~oAabx #LQ"z?c^gXVݎ19x,zaJNDcxѧ䐸3{e9Qvyr4y#H60H*/Q?@KlِET O8)$ ,ua`Fd*@HfkyBܵY:\v=pKj$o*ON퓣WYh/W>9ed( ;J k$ Hwg0|OjsQG r 97+,[ /<>{RD!Adˁl:E1LgS1fl O`T,|ersWx䃎T3grbjգ"^Fņc4u xi_"2e- ѿE)Р7ӆTxx™ڧ#ZJ;V':;DCґCx_Pk$er>Xh8ahKᑞA}Vgl(ki1i&}1:w'k4Zm.2hy}U.# ?$e%؟F}H@ zCj-"#.;~b񅽀4-}]ğiJ6O,cspO XP;:>iTνP(mRU[eO Nj.kRDB- #<<<YiVS{Gn$RSLf /`4vDh`CW+~A.ʑk:4As @Cx#@QD y0"7&!ђPoJq3#&nSe,#,SZ⌲0&gVGǸ3aDdfnmZ|jO'Q:{Pp3L}@. +@=I L#*җI7͏>9=3DRDm) HfYt% Y[FD$1O|ؑ0xU *4(_O>ZsʪD 0}x$K9X{DpP|$;)=Q ަ6wE~!ILc^Wq$-ld~$kysug9*Q ܛg伞ҏщa* ]mwgv e <\s[D"BWѸӠos?"EMfl~ѶӇ{Auһ4Q^4>{6>rDG`8|峉L_ hKi@ .]X`e/\![9ὀ@=|0PK4QDJuIK@ &(75݌{p}:ʦioVwj}7M>Rwddex,^ox,@>oS+󏅳jҨ+ptxSƗt(4d؈=4q·=bd{EF3]H7AP{<K2y4"3#H:S݂R:FQ-?+0{jG.F=e|*I`퀓mOx`%&Ñ">PGl:9.|d ODTPK5NƑ CXӗQ@<A]gH QS2~^Y`G"A7"d Z /2 Cl`)H t#"kf=ڃ! h.Y",y9SyUE>S(Zx T.d[#Gr},:њB{ї _QD%( <ўs9b',ݸqPטoW4+?^dm`ӶgOsLqPq $/ Bt-j4;x3yЂ#ZQL'ӓr1q:3!,!5b,M⓻5e8c Tׂ5HvPBDWpj/`o"R}h.s0~GT/5tΎ/`%H\FPtrVZ+yTZ>cr@!ȵlLɁ(6Lj@J'㣾OK>fHCy5Zf"1VihO`PtV/|>Q{K^_11.X7rFZh`,Dý}hk}}341ju(OEz{5 wPX2^ =EbS˃CBj/[Ȼbr ҤK87<C "?){+[\Q,$ A=ZT(|4*.k2Fp_[Jƌdєψa+/Eemm%BdNϨ #gɞ}'/5#<rPW 4-['1,LZgM Z"3@5#~suT>y,m xrSs"hc\Lmy42`Wv_4H O jx=^-M~Ÿ\|E&Єikq@c!*ÔqI3a!Kt!J +IE)9v%IϙXjz 4l(($ S B%3(wZStE(PQxf=ۑS eH7`|]7F KPи=#!`\Q9(nٹAd\NPV4gG g{^Kv30`LS|2~ 6}/ FrБ?m;<9-q滣vK@ n8^<3}ÃF|SE ^:1JA* ݌n'm#sg#Aj9uߣ@U݆^9{l,&s&hP 2O(k(#N}3h!ǻJ,k/|^iu<{+Dt~W>46-!֮*%-0#اbaI{dDqD"VG\klPD !B pԂHU5}Nu=rwN5'=% lwP?E}})n*:HJ̈oKPP$+#|uL6ϑQ8J$|+`q2yK-5C*}tkAC\m>4ZNjq|ueg/5W~)U /Q-,-.^of,@˛9noU@K|t_J{$R_kjʷ?p%MdP$" OM³3Ɣ0wցx(9(#sR0ofHM5o#Iya;,2N+`uW|Ίkp| џ0ʠ `MF `A)¡JNb#^OYR|{0ŻjOgQpx+]jJH}B>\D ð 1꽨3NK0c$mMx뛮hqx~Jco{YeT8 @29LJ0`(4̒6\PвmBⷣaf"g phTށy !>+s~2G#ТeωzDZjQ!z8|4Q=@HQ>uMqXk^E3fʆnb '}/9 <1 5= 9pxHs H^2ym81ty/.jZ)c &0ՎyT6gõ}m?>і"gx\GT%`< =QZ?StY~nvD$dU L]*1VYR-.GJ: VE"@8-ms&qGS pդqki?!/QW;{Bz- &/ku5Fo??ϖ @ R\uSg~N8q:d3 %XYJ?#g=ׇ wuQIm2/CW1ET+Ќ+h,qDtb$$^.'C!ᝉ#WD5UD&1#)?=$Fxy/P?5Ai;KFSU"-"Ҙ aR:,aDcW H։f)xFWlѢBI**KnKGBzNm¨NO4ƉA2)yʂsM̍bWmqmoVF C ~xTtnQ.8:B͗;H'e C`Q03 e Ώ?296bIY4LַW/rX60P+b[rBsa%c/ uQlY 9,b$b# &JJ"rH*Td1#6ϲ\u #08a_ÚҢX'& WaafϷ=ܓaEEJdLqèՒt bE?IXjٕ kc=/@=$gj.C!ϰiS?X.r8Hstr>JS:f4##,%褑E7~3ޛRZID0YW"w"qD] h!*3M'(`NcDrHWD^U\]7OC`M{;2zh'"2>=B!L'G^zt玥jI(=qQG"ߠWFbr_rMf_UDUȊQ׊b&D̵zg)Cl[8.Y<2I kQµ^x*GZyպ즗:@9x_s5_ŕo<ꌒ\7k7ƀAIGҙjwDU⾫馍|\#v@)/{)>x;xZEr!b> 2/@YvE+}@`{3VwBaRk\~7_ӭTe[!kq5ϾckpP(7rLX$E#bШȴgQ@'ZL";b)H$EfvTMDQTu\Q7#t&BauTPpz'1jǹtO jfrBԞ>+(6;sP.|؅ϐIOΏ]kŏC5OYxjzief!uɏl1t!Ҡ?دF׭F6"!XPځW;{WGzŇ7LO?~{5vy2%Ű)6q2b&CDI#y8Ьc E) `Sf"%mG}ؔ&dأ>p"bHsVo kn,kʘ!#6;?^}RKH@Ջ6Bz85%*q-޵_rdp8*S@#T1\รPRU }{k1 B1# t8䀻NƠ M _:b4UN4A;A- 8:&c}Вk(5~ Z<]pD C񜬤6;h@,@UKI;z"-\svQ8RDQk€MG+STr$zX ZF)?.)SsT&Hԧ!تW_1hnQ8P̸a ۱!>P=T[YGhB{Upc@<~ӈ*^J7hqDrVA{>PGĜ|\'bAy%s7: C 'B0's\Ԧ%壨'2TKQqM8m@̧;KI "jQ0iP` [*zzT!OR<9i/|%="-/ `ʖ뱳G: ˏ)1g+AhOyyc,wX:Ӕ9Ezu'ncKWLR<4x2o0~4Rd Ñ@j#mH'^s)43F?-."=H /u  x''q><5]1NŪsQcLt#Xq7kgoy+j+?So5,yF2ǥB.`$+BZZ)BEGEt DW^/U,HNZYGӀ?`xJ=+y:if=_ H0 &5\E%y"G"XKɟ dzQ?d_1ucT=/ ZVO>wWN_xm(5Skh̚9@8XXV+ b- ӌ۲s ب.*gYh08 XϦ5xwҵrr4֔()Z2F[(q]'*Q1]J΁}Mq{ [C:pDPR}oӾRcB?_C %2MID=8)hBE3]+gtN 8 Sd{OuCΉ/zAAq5k]q@i??sh}4}0' -Ξ [K͹V dcb=H`yM;lQے+ȉ .5YkSw%V?9beIcM>ŏyD8/"ADVY:8ܽ=<>ĻQ7O]Ihpq k4=S_r\@l@,%g?v=0-_!y$J繎[NG2 .O>}J*s3+3vyy:tЅh*Rn)nlacLɰa J[XVu{Ô>xlTBcc? B$H0!r_qto< ߏt%"e_YC.[zf )m RA@R$oE&yBւ Fv 30=ݗ;& 0|m*.^e8մ3J35My0ti^"$P/%!Є LB "G%g!T kD\$@^'a릖& iq sv$LWWW Z@bdPHƋh =gfLT^G߬ HсeE dV&ԋ0 xs[ \dp{]0%^G@#$bގhT11!OJ=SD;1ܑ E/( dZ7qX^ɇj~ǰuRGKdxׄ= ">6J}&6PO1淟vв' 车7iJG83L[:R뼕;":yP#v_0fS9?&'KxφX✺~qenC<==T0 CBL8m|{3ƃ^X%2 IƲ*6sDF=0-_x[2aaV now[)6P8ٟ88;>0>;0!pSbN`R⽎mV 31{IpÏ2 #` cMGhXu$%Ѽ]=GѪ3;]x‰Ĕ/Vi]()H虸/0̩#g@H.}y1Ao#8H68 ;`paZOE~^d)RI(m4$fH &!ѣT5v"0>io4G[q \Ww1% ($3NYa92"GzO`b]%tw|.IӇZ0`JsM3hQrCw~^fD:HKԬGv. \kh9S@?P+#G:E'O~qo֔hUjPPPj3pz\!NQ ͓.U1+R?I ,k߳%r$ k549850p@D}}j9,` Id{CD3 PTڛ=1}^:vG"bLБP$d]jDOoPT{\n/..Opgy/V|~q' ( duQ\GA@`K EV|urWlMi[;=9IB_B0C!Be1fdlj$V{K쳨LlMWtwyQUxJդx\;!Y P#0pcU[K6Yl'`t³- |/c\*e:18t0Aua5= \`sp{w@'pqJ ={WEJLwF=UMK3_x+w:磯Ak@{";2&StE"-m.t$C!z@Y>CNV|:soa\Mr~$DdL":rJV51jEͬE^jRȝ&D8ƝH0`s~2]R'D*f鞌^,푄^$Ʃ*:BEY8t-}{2l_xaPCoݽ-Go^Dz`Oj`V}K+pL! A ƚÇo+y[wM#T>xlC0 )y?("1 DyLLGJX^z=s @9gVA`Pz 7j| c+1;d(,r@G*9J@6 7@q{x3XST50GA\k1SpcO]76e% . Xdpp-rs9,}b@i Rr:P[ILm8yL"piv#=6PB+@t:*5_q*>Ǧ2aCeM-CW;'(k^-hhTF~_!|XNh%Co$i5]$K">f:viv@1JZ=D~"]"& !gK,`9ˆΊzNҪ8=(ǡ-+׋1Q+8 cgY8!ag-v9J`#s5s^{9_PT$@HmKYxʼnoo{/ D"㔯bmQgϮ2\f'?lp~Z ?LQE]F?3@\mڐNkJ@Λo^hoPrssqW}hͫz>+?FI˄^Ǐ=aܬ2Dk(c,hE!10(x1h'=XW<0C aQѻ? 8VmM ِ^c(mIn#ܖD0ẏ7 ԟJHId*iQdiM9ri Z֗q}[dUojWwhaD # A{ D,PuJd>npxD9DOӽܴ@bhGPv8Q"8H˸DE-;@F}9$\'{,(qGe*HZ$\GM&{]A+TQG㟶VfD:+z $ZlL]"xQMijs1RÇ z\<" =]6xtub!kn@pd<^M#LGL{ڊ{%56S8V95UX' ]8f_G CoLtkfu4"TN'W_:D)hִL]: m`F?,,-}>wهk- %z mSMKazn~oqi'i0+Mߪ_`4Ul9zz`Z\=w(Srud(|dov^H#ۼSoL<607pc]MMMlZw=j;8ÿ5Vt55aB3['Gѭ>s;z?Or7S7;*EF#}!ʍ|mGcOoKs~f\:'nߺ̬,m8N"w%-fu %𪃠THwvd¹': rG8 , Tq"X\q8H`R:PfGx=yaxE*YR|Z# aAg@HD: R @1.HWOn k&TUbo[BWqPh{D [ŏ~3IaIc>DE.5e/8%;.9 l.>־05xMP%FBa{#x,D8#;$RhO~x yq%J!U FYY|‰o <)|/7/}6O- G۱髅/<|ʃL?LoU c%gu^=0-J*ߒx,ӳ}⳿=::=OL dL*oE+ˋ9)s %\m6_6k[FC~qgc䕧y0 Y^C G>tRDb +u0 eZso.Ln{?Cm9 "H: ޕ~CSXY^3C0V_x0btR[mUq%H0_Hn-GC2lXQ'^AdEx 'BwDx?OЏ)5W Y &KD˅( _\Qp+7ǚ| @5(.dy18sG%FeJ b4*c@bbLݯNwYՎT7pNA;!Q5V3:89DRq3th';S)+.͜eR:ݭ7yy6c3see 1LWGh!3hLcBn>*D>-eS\ʀv!{@cw2< uatϹYr%=""QqKy dr_ "C<%z0-)J$ePh~;?xt/O76g}M oI3'_;3%KyQ8,qd[Yȗp-lE|vGs4'"7VOJ$R2=:Wʹ:ˋZ;=A8p 79/+'r~J7QPډ]UN5cy\r"n@?6ϾXn\)TicpEq|.5'Doqh(b8{>;|jqiWEחoffh~s`Di0ztx,mѿ|L?{7w~v %K>`،; =^xZi :gڛԆgMꃀ)h'ڰd@BÏkѣ crgkh06Fcp/$ yu`.C=" 2zG'hCH>3JF D"`h\jNtȫC^aCS5Vj7hey|[u+'s.6k]nb!5[ݽm{ IОD=&Z$'_ q)7F71X8h*c4t3e,cIg_<%C MŠ/^T^=)Z 2N߇ԴӎjnaYGdxI99=tqC컾 m4M6ȒPK7`'[G nooyR65WWަu@#\ xZ5d U 2G Z:" ԤDjZ<ĖN_EK ~gƗDj(e 'h1E!N`- ek5A0M32((N^g^MkgJG="\8 => -<(CAcFlW<,* ?[A; hI-)$BQ^H9wI)TV"_p@Ч݈X>.@B"'dùb5.0pRq}@f{=yO/+ z-bp"[7*zP=W*:a6n90'Or3_Bs9VIiZ'T?4㱩͍ǯVlbfuW_L )gņ%迍FFk:0_f !S\|oŏw|xuvǏ,lyy#Cjrii40bLO-y^Y wIirҹ+h'0ÑRS7{d#oC'2ڐv>o6qu窔 ^'|r(uDbu5xD0Q/J 1HdM›GRF)+phl8!`Wqѹ:?S_M;BuwR6m`sZJZ'[b7r\E q+ c8 H+R)G%/$z?lllBwa rGdCc gugܑ"ВU"۠% 2EpژyAKHewa;rdCGhdG=,02Zs/ "r Ή,&Xzc_!~e#Rs|oA6q!3}>R'aL;dHTE7:ayPM-*,a0*i9Zϭ$,'Z}h (Ғ\,23G:ʁ@đwuYh[%'ғ= Q@=ݕg)Z uFaԧqt-=J VS%]mp0'WDk 3MU1")ZZ?ƑVj\s LoZ \E칖]Lha Y"RFT(rH(ldkDqԱlwd?32A<5A@K(r\jn#O^VӔv8+DE"{d?(SLZ^Z:o 8U뙇cBGÎAmGJ;xϱt{4},c+m )d{S jV7=W6:gʆ|rr3]҉Gų}#wkGXÍɱLL|wG/{Wk+KJVʇ=ѿ8TG0ȁԇFX}O[8mƈ/|Mu;Eh^x3xϺNrM{;> 9~>70} @^>'e hxۃu|簶Ҵ3PG9R2+ZQ\"T9*RA(^? l;WV #W2,Mi"̏) |xUE 0{9zY#%s鹘DH`fPyωP@+1F4K@͛t"\z`oBz6Ȇډ`0x-)!#9D٭"WTd;5S>wWys^UQX5qԑMwai)Ok_"Z3\q&Z@?RE95<' 4o$āc-B.%JlW ⵿j'k:D-أ$LN/A"v-ґ(-ّ,/G\`O9-O=WOϞg$ qBbagkzTDlѽc߂f5/)sE.0@ VqB3\ Yֆ=7Q|^쟋^Dd_ygU77xopsr(idfQ1(iZ8@吰ƚAG'nǨQgvm?οd0uᄇIa^^?}UW+)Ⱦ>0oGo^Zd,brCvUy-_7 .{ M {$s S皭oɍwI:ŭ>' #y0"M51'0| ɸ (xP[$ mQ;Сoç-a ܐW2\|ak+ƍ .'rQA П..?;q\ߑ϶AM c /~@"$/ѱx".WHۙ*cDFN t>$G/1gP{266KOm4<2Ҩ[mɹ1԰0N % LƘ́ޔxG,$9 h3(\(1gLR^EKt-m{E%O̜ *`2c3'OC@=7]h Q\'<e"^GYGi(V_|#W?- 2鹹/^m Q ߚwmi[DL-'!gyꊼ\]啦oVg/?E^*u_7|_"C}c)i :407|TΏ0@kyRR}rwȪgIq_m7涶^U]}ouWw,gBJ+֦-82!E3dKfYs89!C͝gu|_|$uH >Ta\V%ӊp@d)g+@@4f^TAǟC@Ւߦtd iLIFȞc cJFWӪE 8$mPt̠A9.ΙQ\\QFDce]$cG5i22CLsD 8P1T뚪`cutC=_#M :do˕"oojh9õ{]0+a)1b_"aJT tA1.)*vu^.ܘ/߫"+S \h)ƚa\r{v¼Aϗ`[Z  xR=' }(mMe\Jh8 #5O LȒ\ÎDh|B"5HGQsVW<%M u6԰GB>#o>ge,w/9-kD^c#wZ.ثv I;2(|>t}ٲO;f lͿtTs"]x&zՑP&ov^%jp؅R*go>_Y?I7jBQ3zya"\<2ןFOՓou XѾ''-ګW\wv^}tr._߮,i㽥x!,jD$H'F_<fngK-N;n8}@~L6c<}aș{ ET3Ok {lvI7L|߿=eZ} x|+ocA-غ %LC~vƣ%jTp~=<%ݪidGnln9 u%DsG'Dq8ΩVl)*_.$nΉˢJPK³O( d|򢶕#ъE@7jpḿx <`>e-D a Ԏۓ)1 IDATZD"4P0b+BSS,+#<ꙊM6i0X@;c[yU`AUF⊯C£G M zKF8!:UNUѽ;D8j'Z->,Ӟr0в6RwBtrP 3K{I^8HSXW\#Ѿ$:|wĪP0RmU rΎnQkB\x+r=kAK>m0uaS2? ZZAD:o!} Rq"kGNu ԉ@dS Q `zw"9+p6]џGRr o Jcɱx+Ժ}`vE*R䔽 kI† i e $Q[g&laB{N ԚPTE(f8/}n:~ݯD?eJ&garKDD :kZ/*pj ̸2R>yS[t>XtՃgQ1#'Hק*'2֏ ^UQe`zl~$ap6($܏rl,eomy;򂷟M6>55.emT, Wa9@TsE.0+2cxv'2Ҡflּq/UGw>?>@6^mTT" CU;=Qj G\7=W# :y7'Skɢp_#y5VБh=hi`G( z8uGΘΰ4-eZC}^H$P z ȱV'NwQa# P%BqN. FEn QqS@ ꌖ[}B@IQ ?@Ԇp4Eڐ֎ƍg.va/fm'AZ-ʉDth#o 0UhG{0ANNC]5T`{i~IDXl`Z崴ӈՠ HX=ʥ;}oP[K]`5%<#;lw{`Zw{-P%H'++?<í+ ΂&>)-ļ]7E{;pĤ6wtʇ׶omm Wtz=o2γ!Vs&Bo;0{6D;1<6;%'0P^-+GNЖ̅Sh2SN~#&R:2%(rcxzʹR\zOWwTB 9Sp s,.?OkBA?0f\Y|c5u5B*RFhh\Ӏ?M)XkO:N@|_46I8ȠHb$xDNZB> 817 +z##+T|=S@9E1 L{CEr8@Q}ݴ>LfQ|" $S+@_6z eg}OYIB4l*' E(n=Xw5VK1^u(ڐ#urD6Pom5+uFuqĹ_j?li|sIV9ClBXOΡ*jaGZ2hgEߟku}zWN:B;uo]qN^T'+;?<9U|nn|im_Ϯ|0>+u4_ Mų<#FQ=0-|hR@^>HG?~'(ڻ8ՠu8<||h VT7{tUDWvpw<).aо >܍ݯ7C8߃@wF>q~;pvy5T4G*ÇBOHCo/Ru.q<ÒN=()SUlifp}pe HwpWdz4{H!݇/'հ9dQ"Ix+xa P;>o#LJ{E9OB➈^5fė>}K.Q?E:|٧o#G}ʁ:@Cq M}{YU#% KYX}yIhH  9s JNFQVSs1Wv$n;j9Y0V n^|'HVkb3Ƭ>ۨ;i/XМ@Lg1,0FMZ.@2{m,>1j"mmp#rSq" .F0}u-=^UQۆ ol 89|2}ԼYuczip?h4R)2$(@ s9=BY( ada=JQ4׉ܐ˂BcD4>VA>HEջ1V= {B"h顨DY>5M[\gH?P>F_DNmG?.5T#kRWj MD>]Kk;-RL70HD~7N9< \.a4^'.]pߕQ09N#|Ǫg Wn%t8w9;C evixuu'K~:93KvH;ר?޾EW tW닣 35Vtۥy]gs;\yQdDan51G[:\P(Bqim9ԡŦݛ>j@H]n8l/rMؐAs8WDtHk:4#t0F2MHudEJxm&hG^[T- (ll W}EhjQ}_;ٛa0"@`5 ^S=_ԓྗH" P.0$R(ʘi1|4u\H҇ ǏDu:`_w?(gLܴ".(<,]WA"$clB&&6FUH JF Q˼1Xs"}m3'1jTIm&bAKk`N/${s*{@h ܆ԉh)V$?{]";, J"/p?9IW="\`,L0J8yq/p'w@ԢI6P;θHgOMt֑ub~r&X*y!gY+-W_Z͌6&:=$鈚 XjjcͱNww keDl@ {@/F.:%Ghob^^IB~օ 6k 9RQiD.?Snw"bDmF>zPX|'JePG1Mς|1MNuvRSRynZ2ODW}yxoY->Pߏ9"k}Dxn,^䑔9Lw)o C\Sg=|%A_i;:` (ZTXQE؟/d#"q3(Hs/\nBKx,Lxu㼚^ϼxCo1g: ˲97rh02N"fL]ko~_q4`LQqVv[GnFh[)@KEW:̥qPڏWg?ǿy_Gřӓ+~-b(LVmZR"°  >PZ7b3:|mϒA+48XP7RDS YYYa~$0,_)맿|7:7ƆG;/|Pȸ Z6vEBI,慌䝑Qn8,Q{2"JaE5t"/QM8߼br[qGcB k9Si*H&6XQPcr`l Oi+-A!J2jԚhs:1SE#_1 Ւ㎘i/~k~EwKaKʇ]sK{ II'8ga;Cf^d?ώTʛaC+c&.n')5ʣ""ڗD,wE-PЊk8V>Y,gU5 8S*u!^w^:9;.돼HrΩId"1:-LY~ŝys+\Rdg.qLa#GtQm폤Ã41;_|7J̬O뼕~60'k$EE;7}..G֓N2Hb5vup;5&@d# u*o(w}EkNG%Q"WVn8.Ș ?C/m'Jr!MxWߘ(< 2hp@)yo@_XXxFE W,r`oL CհrFIoGK,Y-Ғ -g/ \%Jo4N5Azд,O=mn#wUU [̝C<&†҆Y^(il8=uDv&O(FsjNcG/_Y-xlD*1QBѪsJ$r$l c@B `H0L; 6J)϶ޔi5N :< >r:.2 jZK-93w!C H4xy1IY04wTyR)bgvr,UCxegˏ_|v{}s>c0nBsb۫BƼ׆9ybttewZ>mdvltxH{$YU.<7RSQf8X_uNL/[m<<&&ߎSM] { +iJtt CmiQ6)ÚvaJu{K "F 1̋](Mzmiq͉glݳy9;;#(hBY|67PooS <&!l$#O7QeEQ':sE`"J \-imGq€—kіHFOT}K?Qq1>5jr5v[Jt,92B@)-Vn n{A[@E$rlJ,8>uh!&GqNsAIA,(AS;h{E ,NˉgU+"8 }b>PG~Vb%(1>5_]T|ScF19RH#͑dD{l*_>5i\N K[WS8yas?@DW!&\8dGNRǩ@qGP{-s rR+J)V3`I? ՇD̿P'Ԟ'9r ߴxE; |  ˫Vrfq7?784-GQoǴ)z˃ӧJN?~GH.,;t{!c1;Ia2~ IDAToټL'HXsj*9d1PF0ug [Le NؓAArq!p^rv$9gʥ>SmC,*.!j}2VDu^Đl=i71I.]VRtoqLRנRadעP4@;%P9'iZ6RVːGI5E$9Jv˻qDVYMut" 3xc@Z)b<z=+FBZϿ@ʊGۯU;4 ڄ39Є01m4 bv*Zڥ SUӳ'~s@ Ȑ\񑤪w)YdhR2xNXVi|sQ*!`gh[TǘB]hH6bZmBDo$ԛyo856}ut<DL0?ws[$ j glٷHpE,Y{:c1}zuV qthBB;cӄ9K,SUs^x]+94N*(t6.`-0t|9B9Tc^@3H4 PE}$ulh^2B2>E&-\Uz.g&I= A781ŠRqaqm{Vp4Q96e1s-ΠWk镳axM1(H灁Cs vv ŗ:2sK8Ƣ#q3!Km%$*'_]Y{mMK7hi1@ %3-cJ ݗ)t=>!Dr|znG|9𛹹5uqƥpwSez`Z'6q;X} 'SGG?_}eueSTyfێ֥I:>O#~!i]THktx :>ؓ8y})RXԆSֆ*'Z=Gw:g8/ʆ# 6dMr}R5i㎬⥶ wi";$Ī}32j)x(9eq)`.Z$>CʨAy|iY(ȋJMnjhDo{yE0PxD꭭{!1z_Q8x3Ǹ⹉%NJ(XXQu W% eL-.jEW5HD$ ZԴhhQ1^mT |;q(Ԙ+Ji;Y*b(YDKxUZ}qFb$In .xƚ+}r@a0ʯ7!-ԴҚP&e쾸(B.8=cx&-] h^<`>0+qp&jaѿ+Kwk"`)uP!Ȑѵ/,ELs8`~K:ZIg]"# V840a-裖 #zw0/~WfIO/v8ؕA2|6H7`(G_lYK2GD0BIBc.Ú&@rp9 -9i 5'[p+&O_~du,h3Qr3 +kڕ)Yj/ed=>qQw{KZl$xayϐL `ӏ4\"b 8 M4x3meEa)Zm 펶d~嵴d D\X G֑7E*7jXq*l0>Ed=cP #f(&qs/әDŜYI-@1j'(j u8b`7Պ9ͫ#%g/9Z@\j k!@ Qsrk$1qDrpy Ի8x*Y80oUG*?'1Ō|(gٳӼa@f8B؅>g5gLk%:jj6{NmGEʹs ܄hgU<"VSط7ǂ՘sCzwߢB}=SEe^oO".\o?=3n/5Cs5mJRy՞q{ x&{WG?|o~ϞlL,ԼZ!$O61h@F JMӻvap)Rky{nǐ ݯ\$1P {B8<&²`SmQKQM>dAvFxkGCJwT :;*\=t{.n{B-0.((9-UwJw9O[E31|#PE)o$B`3[I8}% bP" d/P LI ́g7$Q(]PaS{.X5wjD͡~~@K6hD+͕H#ZL@o41\ 'Fȝ1bne@BSZsc|b:JRܕy ;KƊ>~uPH0OmH_kuK^}T{SGh\dDv&|7d,B{%U3*Roa3O@ߨ~0͌)4$p8 XvER[5XoJD,Eь7;P+i>X_Dw3ƞDMypc=גUDbFV@Q<ѻ׃*xCXb0Q<s<%ӉwkC>{ ؅[=QƢ ? (1C~չXJ1NayCP)V $;6h]Q/ԀI $>10M193< 80(A-z%7Jhۂmm&c ؘxyqg3IJs&ۍqBwQ ϟ?JۘK7 O~zPq!ܢ(' /)v InG΋ hq|"RsD5 Q;1͸vB`=!B(A" 0DZ6@5Oo MR4 %bs@W+9D}3gOT;jɦ'GȾGT'EPNkA-ׯ_Y4G+9z0ٸݺ+9,F!ɍrmy0OO?G(~5Dۤr77dP4Ɩ8T0QLQ@K;3z/f4h鳋騰]'%i ڭ?6fs-(rYEu3oK=ԘgAA1u 8xh˩ DYʘyoÙavRHTTH;'@]r#7oY*_׵ڀs*kGSqqXn.ȱG-=\MNt1d^|*h3{zdk~jjr|OjI'Qlfe$LMPe^) s:@UP&?䟰m<(yn\? ps w4Ð͹+s5`I4'fCkɫ$S*j@H3d#\c1(3"jz=+c\epG+Tގ17X6 *r'eSCZ?⛤ ?ǪJT;J֧ F9 zAyRѬϻ,0W@wiq嵧U[``ϙ6`ce0MҚ3HUSG-)>K<qowhDZBؽ.ܗAHY]27vZ1SJz k0C598r_j&_leE4uT$˦$̪M#(9f7}u򫀁=u$\* iq>sa8P„XÊ[4`M dGfJfDSNvgM+0y^@ƅ̙J}=OϟA J.AB2Z#NSy+҂SOˡ9so5Vzo!\@r|H({u-5D})}#1}rBO׵I(6tE6瘠0N9돊#Oth [SVkP XCs5M&H3-uqcuBc͓ω"TJѿ}XkH+,T2mfY`ѓ=v=1NO7uih3h=ȳ>֐ϙ>gL5ǒFR e:>i[@ةxS"`\XY ;.( {85/_#pkP0x|Onha _~O|9` *2ddZ|)sK&ƙe9arB lܐs]z _.HSC~Os>P,LKNHhϱ,i3WY|{a!0I0u>{NJ B4{7P{υoGN˧~"z>v( 0 ם ia 72EPڈC$*iq& e, bxs.zQT7 Bݑ#'  \((;‰2= Zs! -ZQk $/"5AmRyK 1lGPH}[dSz^Y"cƑ8 6KX\$g讳Y.lj7:`mU9}dp#B"@dh.RKгcPXd)y('=j;&xi@ݼKQJ%hѳJƱ't~Yd2^\MO$hם zP‡eQmp$PjD`(YZ ,y>柱l[=uA܍$bW bރl|rТafA@n::VjuŃG07 ! R <A;:txa_a~ӣW9r%77#Y2bdraqR6 cc Z|X:wFj 3RsH1y;td8)7As,p;ݷ"]KnyGC4'3 I\gFÄgb8hA?8wSy] z.TngqC;h%9,F6(xVܥ8QT;_HAyP+5h8E7zix}BX0+s%[H5; IDAT&kɜYAADk^V4NA:N9N ajtp! W9]}IN*kuD=hdőH'y޺n^LsjDcȬO7@RzĴbD:,6cM-,g }SQ >*}k,QD۷K^hEW"Lp=Ċ81erHF2U*M_1]s;.KT"j2DAyd׻'VAJ,s(tR6d ,#dl; 9N!"!7};ÞB] iCi7q2dLh؁R;K =mIkڗr0442jDx}K&MH*H[Y<ܸ$ YfTl4n Z ~1iҼ-3kRUJ@QwjzD223QS`#=-臺WƋkJ@o'LuA] CuLIh3NNmm0v#"-gN@>%kM` P}(b%ڮsO%~B)nP&v DtW>豙 j];btԔcA|fT[Fq,InlmUSDNsC++Do3M̙ᦠNRagl#xaH6;Ƨm,sz>&yDz@}B@*%5%gtDOӤ0ݣO>WVPg{K Ze6CR=˧_WGdR $^p(&z,F cL,'CHt Dx?\V12W}!.3FT2X=ZxaT  0`m(C :AӣBp s8y߹y )d RjgPJΐuѣv3`uj: 1t8JF %璳DX ]MB@M-kDmERdF9d9=}3F'#ͧZDtj׷#spm# \NL^TVWxPx9.rnj2T5lF0;&(ޠvfڙ lJQ#PyTTM@h>&B@[rT4㑝c@{O3ce<Gsº3\8p^rԿddLMǛ@PkLr$Eu8dĻnڰq.WLPGB^'|#mcw~?=II i̘F xCe98ZL;NzPk lBwM zq$IpL$A7M!pJ+Z6c; yQI![.}d#1K:lTڦ}qVZ*(&N)b= +{K{^YZPh-'qUɭ+*'z2<"cb]\1V`D@'耒 jAb!>xpTrPP*%'Q{dF%<|_mVF|߷.+^KUϏ0K&`N "kq־Y%$QQt"O/#PoUg*`0zu}&Č&:@ {M9]4E m e.ht%8AYS`dȭbNvy%#n.U첸q/vvtqmST1j\0S=-A}qgZޡy`9:.^No˷?7WRY\Lvv،fywHX A _B84 }I14䉝A +;EqV1ZiM Jylpig\S9lYFx)ő=>x \FqsQC MQi~m(K3| nDk}s/}(AL~Fʲ OÅ{Ra.Թ8`PA6J;ڊLqj)\}ZؒNc T8e.=Er@X(p2:IqԌ)@F(Pb\9l9)IE#P¦;Ǹ%ƠϿЖ3W#;EFNi< puC87lp:#7J,PvAk#/_O\: j&}䜢 fP~3h`bRaCfT$픦@n*haډkt|.PHγs/M2F&RsIgomWZk s{Bޘ5[Z\&G3*_@$N]B1uF-.8nZ;44jk(\;?%&R 4IEQDcS_Y8Ѡ n\ $P軕L,(W\T l*DlXB'8lDRP/iE Y׸@ E=bP|g$aBhbJq7 ~'̠ j~w=KݿvuTCHƌ+lis1זͮ.@EZXtOU@tdjXeIFD  7I!9's{RCVdNbL$R5ߗhS$kn:;S?dW!DZٵ㋹7Ý՟- *P^.s.ghb_}Ly,M c^%k<͗g9W}sdQ) n&6S6۝;S6_Q-T#Wdvd8r~s1^ø_(s1J' HAOg3($Z8N2GM0Y8TP3AF4$*rPgԳ8n@EN t BUBo ey&";0s8qwM- "4@;c~2dpdGYWi'BG}DYn ,ےY I@-iřs-TdYsrH4K؟ dT%>5-Z\A >a>0 X!&/C)%(DU9|3 ilI`gp@ h'$[?WIZȬ7W%i{-ȾVU߇歛<7Eѵ*\>t֖y=X?$?O0ʱs5eA>Ps3۽ 6kj5Y}yZSG%^{u-UH"llpsf49¸]^Z]G? QTFa{y=_}w,< VGJ~e<ٷ}|q~-n˱U/xdRe9*,ee-c.2CCI^Kp>08Ao*4 F qfnV;%r>ȀvuH΅%mG= paȽr`:#-I&{%D8xi}6oVGs&sEƶή[e2cLOLѷԅӓ ~t@@TO ՅL>XrTR"748 LvE8:@ $]@)隼p,4 edҧ霌35XU  "KfB[W suP.)AiPl$⭛Q?rR< 5!ce;ROᆣƽ Dw]c|xFBVgnnGk8qdhi%}4'~?ړdo=AˠAbu Mᦼq UqS_d}~H&e$KQb,F MAtD 6dZY>ڎ3q;UG#>Frzjt N)Y@\VX}P{D;\&O61} !JZ8˦D(HfzVJ0hN6#'ZlAeiw!> F8J$ub%.pS '3!Ko81rȾݰ_ɛ9U3rzCR"aE NNe Z0M$M}[q fYL4 ({z<[P:$Р>w{A<H;$BpiT.%B,gng,*ܹ :}lksevCݮ3NF =[b;i :d._;h&*YTL)@uHQU)&]Cfq}`Rmyh6E"3eGۻ%||٨s8ar9ADu:~RE4G k>q&8>j-ppA8ܬ8UqLΰ<Ǹ qLZ}8$+)4HN.;ug W?|g,q8șF!LbתYuInybTvB/e;(ƚARp"U ru#@UEaS٪I8"ִFk^P!7*5̳`A,%PP±5b^ؕrw~?/#%><GzOM= kjܒy\^},s_<'NNijB N BnLusF5B?ܨ#jJAv:&~^>w`~D㻭 z{vvJBp~EluS~ĥGA{μ<-njkrVAe\?&XqɻAb'[#YDR/tPɸxOs@G\qbHnO3RrN~(M8$dL4Ԛըگ%0YsBʙj [.ig'Lֳps9;a炸\;)蕂Al ̳uؤ 4A.,E[S8aFiC E|QUes8g=;)dEXW*X>an6j0?[`0tG UfdSitz[ԹI5:S/k B*#%O )zn}jhf=!ILE+↷\wMaГMGֺ`rVco3|[Ai;M`a-=no?z4u?ZeY (0X$rO|ghaX:*D&W_YRݢ'UQ7q`@($HZ ` ٨A^:[dD ~GZhR)Ī Ӈ!OѬPd,^[G﭂kS̲~Hp8d;h!ywȤ`#̸8Wz(kgCiI+i6H!` 3WEv 8pM̈ŵ94&8| 0-蔂O!:jS,GkĘgjykV_#fǼ @yzv&Iv(d%A^8t ߽\oU#9 Lϳ$g'A9zp)qX*Hl:eI&O2{"% ,ЍuCF;68k}ş{Yn9@^55d Tpȭ )48cF&*a9#xo)h1{ hSˬ4Ï"#zk~~^AF&]YQq36l17 b]սP'({R]EІu-g]e.A&g>%X&!"l7k lon)g6r}\ѯ@Z~v&@cŦ9zJRQZwlbJvNZ@VkZSu;ݯP z/^20YF7>MǍ- ax 0|[ u۶** IDAT#Y$M/J(5C} ,C (0E%(N%&% !I)Ov_>x|40{kᕞWf*I|ٰf۠9)9=xg~m|qxuSJ@yTC`,〴#xtBL 6Etd,i/w:Ԗ@B).̼|}*9?uĈ kYNdj-o%O{@lAi4TYoJ&}v+02 ?_etIjp4Jpt"u *q'3;i[{yI4+;r]"yh;6q݆yPcV.8B=M ߅Rã$>Q x@Qa |_; JԨz*{ݫQº%"З('&v&y"`fl.IZ'H2FXyВ3ZCA|4Dͭ gVNNe%vAI;M}[}jx ^Av89Xjڶ#I1?43R/t%$HB` %8SdrS#lfϲ,%/87`Y>V(@ ZP:H%A =8ד <9?k3Wg򢩥ס?zs+?%O~5ZjR _tQ{Z^ԧsy幋Ý닓ҙۢu*GU`Xtg #+?Y7-ӑ)mǨp'V!khӇ1jtg̥È'!Ѣj&&Bg>2OMgܰ:b]Q}pAL}2Hq~BCCW99_6]83ZAU fdѐ2d $'aT0h) /HawA2П9cވ ;Pdy` -ȟ>TY>\Υh&8"<@B%tH9=TrȨjUv|;WDՏ!'Ց{| HZԇRcO3|D¡<]#B/ǪJv|6E@QkxlFe l0O\=*9v֜1)n 8A"jY5-h&=fXNT;3^'(7(mK@P`Mp??l;BWqv.ʉ(Hf;V꙼RNQlгZ7Tϣ. c)盞c1}Iz$hphQC 6CT] 4Pju^ͣtz>f lDj )c } |$fFm[g-he7EqI-Q ħ嬱M<{7!瑈 =9L@1{$0CMa*g0w}&CF.d {TL'ْqz1V3Ͼ! V(ίAV}`gI|PYE &oJ2ď2HQ Z:~A:PdHq emm*pAi21Iή.m׈N *WDw D}gBR;CD-qAdNeLC9.5gC*ENgM }F`zB 990&Jp_FCq8A W#H!4ϕΌrP]ہv-ex߶7t#9۶n:'T\D 5BTGQa~,W#UW<󣻣zׇ@1!ZCu>^t}쫮czI[Ίz9׺na{(jFg|.vVϻB$p2FOȁG}!rfwBsO<~s=SQ-C PcGsGJ*\Hkݹ6AH5]D ZO? zn>'u +TI5'N!JɚyqA6Z}YQ9B/^PM?//%gA$W?Rh%' H !;ǖͻBdҒDImļ+ ŵ j@@d+1ݓ,}6hŎjڌRBhEA<=m `;v1Tͬk /\%Tܻ ~+ԧ`DXנA!1Zn+Սay *B)SCԸ0~,B!Ypt[Q 謼SȖBv?JDn(FJ+'h\C"υ hyK'揠??ѫoȥ'`®R/LNĥy@M:>z-/EqL֎qZq:H jSpȬ/D晌}Ltlƈ'g1aZl1FU0_ }SPԁ?WgQ}S艣u7pf\>Nk9eQ=|_!ɟ~ ݀L9JC8U#npFzBp$&3ZFkBsqVM-S{k͋`WF 8WL#kG-".C+=9Ou )l&HL2MsFY#u1qGBJ17Ϥ!ܧ|;W`eT5YQt[׏o`ii'xcm.ԑi뜝 u0ҿL%b#5[>qᐺn* Дm/oM-9=VL툏N @ 78 zܡ :NkK.NpNJz@H י֘q(&ԔNjY9;όA1=5:*{Ժ\_{x Ӭaqlnqɚ?{&!D $6NH1I ʹ$"`Lޙ]?7q`ֹ59᪫e ؉GqI[!EݿшyfN</7,ŖP5f&$XI6%9A4 2BdzMQ;JcKlv*+2B5FqҞ`* @bM h0?LށC½\B%D*2wgGtڂFmg ٿw=j 3έP#g$yt94U72>/+gRz.Bք:yY .jmokUW“zmnl 5LMgϛ_Uo۩`3npyp'Th~_^ۼW/ݿnO-\^V6gLSatep\B~_ҿY|ȍ`#~agdwZn7.P'CGl ^V^^ ˜8hAUb?=26RYTXD JA,PW(i-_'1*Ol[>-++2j ݂ GlP$nȋH&atƒk%AW)O6tĜ-i[ 2(h;YIT Q.lyf1^;fC~Fy _:Q羃[퀋0N)ɵ4]0ZkJ\bkN bsaxϬ}ζAcظ(57 ᢖ>xտ[7|sL{%Xwu @VQ7Gosr쿎GǏVov'z9hĎ`p67D(c0T8v,e8s`(SC,4~ʡ"#hy3 '`=?O/?ytyseqes]D&.:nPZ\0aBYTF i$h]r>liHgNy4g#iGtL utBeFKuЂ#xhxe\ ogGWM`>q7HC7Rz= IDAT#D)ݑ[n#n(Q0?RvW5 Hάy2yB9<ʌN|m-k@*h@ێ+[楧D@BIH]M?e,ǎ#dBE N0AasKﵲGsz&ɢK]Z $ W9ˬ >N, %Ӎ 2^X |?4 wvmWbW4]Ѵ=?j]a$i9̑efehqnFBYK id 9j)\OLJd6|HVWS})q*w@@F-  ZBi*ѧPG, jZRq'zlp#SI&tR*_>lC`,GT{{I2oV%fT:I bI,6+Y(Q9L+erO/]L/;_DDzm;MT2F_Lq&%hq򐢮Bu7^ -i*hb#Z'J׷n& s׶Zh9hb,[m ;9A[62n~~E_7߼wo6v9+N'[9\6W1}2OcL&kdmd+ Fb>~-0#C4"ʱN:h:U55$9,:cgzIXJő16q6\EFs֎g8F0hTjeXiqVݩ9hȵ:E+%LZ3V!E:qp}1r0Ք.Pͭ$B{"AZҚ\")H6*<Cr@GZK*U/g, 6%!;rF^vr)o.@^g Π^8ćDS{Ϗ XwR./,9pPSu8c)PB+AAh!9[?Ox^̥3,:H |c!mo\@RkeoO2vh741%hQ#QC=?Fd%h mAZܼVt$뽫ϲ؁#yl”ݛA^)dMSDP}Ǽw˼sQM?9KR?rJt-L)g]Mem>+Z߳/泿jkb]B`8MM*nLlU5,m@>QZ]ߐ;nDTW7~?2?k࢟zUa65}M&( 6ڧ#=Ƀ.N&"r .l C. MjMY Yv4¡?2 c^(M3N9r& gk$Vm/cK Ң8d+Σ ~T8p P._UcyqCd|4 JS$l6jT8,puv-#&E`a!e4]\c\A _8 y 6YM-ݚ@ШIQ^ףiAxaK͊3YP580Cp@ p.ztHO[Sǚ^C]NE2Rm CШL^8W-<R sQf~Xd@6 > sԋN4*^!ȶ iz3e -.F:ji1{ɴF;=wX{Ȑ0u(?j?4<>IE54d筒&'$ò;P@D%%9ĩSjj*g#1"J:r-NW@U8 jc{'*L!㲷0z`ÿ́)7jLyA#  dQ+J }kDxV|yN E3 @MA9GP ϫfSMj#RZdd {Xt'2$*0Ѿs\AK7m;#aD8K2i2@ıۮy}F$i{ e/ rmG H>'#_llQόJ^8X5Z#s3F,жy_#Gh$>Wi]sJjG'z&N+ 5M{4{HLessV2}͝mťKɡ_:^DKPzWt y>hy&EK- _szշF-/V[H' ?7ix8`M#Ao7/kʁ!r*,,TԉsB.+^+;#jh'Պ=Ao:ZHN^{qYJ18qh VA )g&t"^F%pujdiƹ[^uw(zl@o w8+3ݎrZpAT gSHW7fv$ʜTR B0r!\qI2k_G :Mh &pꃲ+`Q q`]wФ2A:k_ܡ:\%ƁM*Xt$8\u5W?V,e*HE L= PT/85CCL4Q F~9)g:JX,gXu/|F(w.NMǛB ='$,Y[AJ*= XG5?_>력Rg"s:s7k'pz;Nn DC[r2^'{v~썾O`$ D9i=C~^ӵq -FkK8c̹Hk(ر# eK6{QvʛI5z0jj;Hj'eH>,V\{}XNc + &:DA;tU[r t1%]zjgckdDg&켶Ft\g;su>?2Hf?HlsF29g9L }Ҳ_ge!}>$VAKi׎L,DāI9{|EYtcA}bGVEY"{. JQw>Il`@W,,Br#FEs㞍6{_iϩ{AKs 2FmHI%S=u./j>ԋ\{ .,,@Szs|%}x-odv@_9kv~O'oϏ' jh~'t&kHAg8x_g˜!=P)nrFpUP CrK,xVqVOp'G˝UKfC!NP _"F1M;^d7t<=:<h[ H L$<5BxOв (cުGq A :sOVXYJzH*H+4l25)ħpZ$R#։r"t;o'S}h8!5Gcqj.Pwdt1+N:cRU@ڌ9Ӆv=1W 61`9t+ k>=A :D{Jo: `]97=;A ە!8EµxzH*n3 ⬁L{bLTYkk{F0I(!-Hd*XeoR"{K3'8 OI܈.M A\i4{ ,ԑf;dDZ3#<ME =xv(ݤQD{>z"ޅdy Řx??Gt%ȋmd' ~ F %& aO9!1J*;8w>oYA@=_9[s<}4qZ;8Py{Qg cЁ vdeCgWI7ݰPʦ`y:r0@$2>s5 Xn䨫 LW.:m#GN6 g7!l[BAX|mqul=+h[^S@I}eƮxv9ykdЖyQĖ;Zjy_`R ao| |C^Rz,˳ ]Q[Ia+XZI#K@mmWY-,8L݃~@$(ˡHtE P/]ûp()1fk23-MY35Ĕ !!ŋ,N;v ^5 (ΡW'.37$QW bnNF8R ZB\phg#ӢϲsR c W#OG e5cDA`՘1WNygyԜ8<ރ'Nd0W8եzNDISЅ5w@hdC2O1gE>bLM*AF/(QVR{jI-QMШ *9!A$X:WngNUIJ0Y b,h9@jN4ze'+uK qjRMt {kӦRz`A<Ϧ8SqHբ$dV9t1j{pL (_Yӌ쟛A&ƈΏaqan9/Q,&ӛW(SҬ&ZG(l_B>IGQ,:1ʗtDW.Xǃ7Sg=P{>vǖ#ЫuVԹB$G{ӊ wࠟY86XÙ:x# i%$=\i?H~A7XkI\F{hJnJq36N.8uVE}  Chdkk~rrx={PE\Tӓ 2g:'ݚbe ,.IIA1v\K}AnXe#*)diSLh9bX(2c{8xX;M\K;dX,Mtͽ`RB0H.)xL"CnI2F)(-sXt/d,Ju] V,j)ɕ)?78NIS ΛAAi:Hpz|QSԊÍ_P-rM;a88JD!*0U J Nֺ+9hޑpq>PHMˤdzo8@eany^rdDUYW!'h 2{8FBDNL> #9g^7vN8@9niaK+S9AĚR243LhGX !q4RyyOxOeFK&4Pj|vhl3 уGv ςAs(z.L##9xY}P#+> ihtB6+I2Z{9g p(X1acVkyLB a.VsXrMGgxDΡΪ jQ+ "-ڛl* PAv}xl㔗RcPs怜=N3ȢM Ծ=$RdDuɾѫ Wú8֚30~ #T -P65ʴtEm`{ɉdM*d,^NNMV:ohʼNS{g -7&O* A˘`CcP<߅vږPX-sK8TUQI@axaCG#pڻ`|K5K!. {A;c`6ܴo1vX[9@uEZc.Y5 BEq#߯גH#en{En/`}Zk#, KVqdlvDf!=ϮU;7P\=V)TT;xDUN}LdWI.D grr7V7'Wb2*3sp)lHҺU n&FYTM&yJg*Kh݇^6LUlmG^FC}p-AT;uMd\*hqއ~!ARs%aj@Hv$(djuO:`Fg6 ?ES">͓&Xd$Pt=SyO6bƄ2*@SFYԘ"B{ aСAM JCtH{Z,g,59+'"3܇y)m>^B߰nD9*oP9BT8oIJCqsCN= Z67Y^Sg.72tEC瞎[ejf}W6"h JGqgR 5|ap2t~)<~P_!iUBp&p 9W̼zsg7q̞c|ζtCOiU@giMM"$)6RC*cLT1y2SY*Y-{i (3ȡ&၃C Z#k-g)сM2ZԮLdS.֚&P$׷>tnS(42^$ -*!V@[! Ye9eF]M%Ңyezur5H0ϏLڧcK :FYvPhdXRC `׬%9FHe[n$J⁹7Kjhj*j1EFac5VvEXTs}.w IDATŇɍu SGiL˳BWMKP:d@Ttc{qvFyR=ؑe|B1C @ Yp${\U`Y,21raϸ5BdEb詅讻=sHjΫiNFuϦGC,ވknWE}$戟qoiF{IX+H %PH4rjܚ`ZlIM]O ē*y#xmUs4* ۰;r [Ǟgi(gpWHKM:R+I*cgj\; Y'Rz/Qdfq\eg0캙8ΰ2<<ʦvE+9ʼ{dΪ`I:NRĽX kl!PII'zAFE9Q>4숄Һa,YC'Csy2vޡO}!Of4(|)5wu<+8'E=:s8|L.TH :-S;4( Ƥ̥\!Ι2[ۏT*xUp Vqpt-+\{V"Ф+*j+ךj BƱju_=|C7;jaW }X;gU;*(?,%Z!֑+.%1J"N-d#=tdv~uH b {W-lKiS{#d -} eO}u ;yPDUlj/cƎ`35aPy&na|6,a]qcDJo,k Tv"KzpQF3 T#`D`b L@졟-׸tro*}pk0g-|AЂhy SBɜ憎_N|o}+ѕ6,gVd8Is@R%rm`!i5o0k,qNLYeI:X@pRbZB-L. & # d5| u;jK D;R6A!PP6etM\#a̾:;7"dz\I݆\΄l17O/*P'PsT%:|ɄC9d`1tQg9Ҍ2-p@ceEوX!S'xW/#=tf*btͽƾ-ETB>plMc*GEXuׁJӘR5<A#,%rMwE,j*<ų.dOU󉳓F ~g4`G߃J 88M>Ϟ&v[8.\9SSΡՂv٢ӿ4]SZ$$ag%ȸw1ϴ-8cBdvE! 2Ϟ=@u: zD\H9εȬϐ{}<{Cг#ω ?j=rQS`[WG7T%B;9G'M`mub+/-솑5z/TY :4t E3Jy Q٦ DP|^wAbp%zߵ)bFHEdAvRb>+%%v Ԫjy=H @ͱ憁86EF[X(Жle pΒ7RAJͫQ ݫ֌)lmzawسqUTeGU@U&cugm # niHWޓ=,H%B$e8{P@kq=uj>\>;*"r޳#I<+FB6@ɹ؁; 8KnAI?S' :09 V{gkP䓵IFUr}Ӡ m|`8 Tv{v5$+[iOz_ռY1wa`+hQ%~|t;ÕFVߪ4!1Zn }\Xaa3' !YuSs,ygT`?93C׆UH /rΌ,dQ@8K1вvw&6t$Mqvzo]7bbY!+t*|ss$j)\?f!%dd>AZ`^yڠ3#> #d89PPvC\ÇbEPaϸ ʤa}1,jD2OP@aϔ:浮#yIֹ(tWɽrqO ?7YDS,w!%]KSA kҎd>9TTN(g @RcqV7JX@I9O/k9J_j]3;@^s,<`d>zrF?ү~WCuP$R c s+rJWtݗ_BD>+c8=AgCPJM-y)&9{v^F3Ggi̝% YrRH}g 2wMm{u@{gkxA#(X)V&{VHj; U6GI;h lwAҵY1`B"(#B27S}t"BWЊ%!lՉu7&P2FXkS讀-a-u^wNdmazZKECydqI `3)ToJ-ڶ,_?ۅ-kh| ͺ.~LOءdh<NTlv9&CIҦ5\^\PC%ZQ2M,T_gA4GP]I WTB3@z܏$~¸? Ew\#oO!k(j%QVtj'' u|P5?-Rc;D4"k4Hk嘹Ggzj0+_8g{<]5R%|ÅI,sfJI hmn]ˋ7KVVwE&Vpx6ލ}̣6{Qî?ɛW~zקYCmiIpWl`0gB8 WKΗcl/g6qgWk2}2+^Ӆ/C 08QXFB ~>8aOkt(qkxcq0}mc#o]n9wFUz)9F~Bܻ8S@2ݾT{rw)a͡C\S|`QAE3 xq&M@J tݟ59,X!'vJtrI$ÁIsrPPڙ&bfkpvoB17GZt@KgAa-RPj]^?=ϭ"W @jզxRLg?HէFD'o\=xK^9X [[]^>?Sμ{$y D) R(H2@$7 T UUM=[`Gyяpn(ܻ% ´A,xN=K&wkE|PP!pע%iȮj{A%gW"">@~u%suNy$  ѳ}=&;C׳^3 Su>oZZэNUc4Md-$45T(оdl35Xx~lcV?$tZK Jr=ًͶ`=&[>G'J]+}MiaM~IbAs#QEtr}xaix{6`?['*_xel- Z|F+߻=?7/}5:z0MV+pЂ,ê]CL;Y;t0";( KkiK)T8w &7M증r8@ [ga4{'Y+/3++[uG @9h6նla9փ~p6:!eEحf A󝧺5WY^Ϯ*IbET".jsη^{vFGKYK}"t%;גq4rS,Ōc=& ~ޤPDF|ecLS],<~%8 s(&c@"G}4;r[%tb&^#j.PU-+:2Գ@͌ڜ3`x컻^{,Jc:jڒ3i<`_vP:fDSLz uQ59w|6HZ5J:Ekka hYlntX"Y @ Ϥ8 i6Uh p&6KZ?3drɎP+J͇AməD<_ 2ϒʚ/RAed;ςTf0~ƈ\$߉J/}ŞmM{(i#" 7 {I )l kjb-9.`2ӚJ*PxvI!M9W2i?w5D\iLlI]O0M"xsE>[El+} ~}3XWKA#י\SuA`XEQI7;ܟ}z{07h9g ̟PZiItw~8M&$Ĉ4k3!v񽌝#i~gJ(J-x3#CRK9 I꩟)iN#@n!: ^fV˜1[d\ M8jnJRKc-Q}y#t6p!es:pv{ֶi(bgHF> 3+XDdh\N\{[ eHJyˤ{ hP-C#ءϰ"bTb~Ja;ԵCr 1Glu<_iʥGh`Lvo9c>xO԰u})vЖ2AMGf3Pt&%RNCו5T'kib?WVWW@%SVS vpU#Ę!ou6-Y{0j2c|fe4b2y+~F5RKA__ɜ8]6u/JЊsk[ ( "$1gɪ#'s+'g?*3Mm E׿-e |-$Т@'ý~P Zw{U%8w8*Fp 4Jx(-Cq>=Y (F;fkbQ ,gǼhS!=N&_<`l#g|܃״z~NkQx:PCu1?%5ZI\ϦWZGv5:a5mIu5pe<BV44בe'(Q!@.k("TJ蹍 `35!PL@Vc\YUZB8[+=Q{:KSYl*#$l7b% QC XD܃鵯qm>9QlJ ̔s[&u IDAT|CMDL5p:vf34d,#[!cq6ūH/ TٖAt]W9ȽUU+}dp ~ȹsAǜ!tf_9>Z8ȗ;’Nػ}b[5KU+r.7w-C%037s{~@u}YaH8U/@/ׯh_N5[jds! 6\umf-,<%e!a:Ӝ4צK6Z+z_ Jտc}6g~.@30\IMKeɠ{j6&ѮȬiW14rjpigBh!5F'y Vҡo&|B/#%FJxh# L`|6lA (iDWβ=8iM pGH }Α#8"p̈dEFV-?s_RUbӌz*}jq9f^\{x 9Z]u)əHNAü6Cu9:G2٠M rrn;.S8wd +#TakꚝH++D F.7 訬^`F#` jN98ZeSub(N u_*J>'@aMY} tsOW{ςljz=.ԄRʃ#3T;tEP@g~ʹF X&Ot~)P\#pZi<Z&OFÿ|o;XU2mdZ08azg#%*Ƒl j$@9M3A-Ԯ#š(7}]&E-FD٨(%sܫ\.="Ɉ'Ǒ_858!4b4 K u %2Q<c7],C&XXSmc+&8Hroe9BrUt3]+UQ$E$d;j,N; X58ZlY"qFe&ƑU9G@İ4J\,AtG(ˑ6]\PF פSDiwdGlTqR21v Yc'pv&cZD"Jc3˜ 8RA 1$*=^u$)vL[c@%z\pSВPr hQ C{)*#V]TOV.oo*]28 `4?8]=j|ѻA瀸53g5 $F]4Xf_GX=8rz:D52C9R=s?W--cHnm]:k9!:(9y MʼJmˀomm kT%iyu<~䰬nRxf(#X5)rR{& A\t\Y]+%E&R LAu"<8M;WߜF-ѧMɱy^FdrD @YNaV #R]A%0Ԣ fb hɮؐ`@盬3t_ze'Z o39O ̶<;Q4WS@Y S$y6Sx(\5V=!p#p5!<@NŬB :S1^[kC Y{-v;sa5B8)?}Ɗe6ЂPAgKf$ je0_"zBDc @i- sV!wY2gLa;֚\"XA>a񙘷]pu1mNy?_C$`p^ZbB\7(gAAbNeǹ>͍]c=>1i+.@`5iWNԩV[^ZZԽ?ҿ r>\g` t-`ɴnhiu:t2E1j8oNѢT"Wd~331tD;OT7 4N\eAۏ?vDn8lI|L48S^_ ݬPR1GNϠ^@K΀S"6j'5HU'0G4SVjC *L׸@}&=, Mi]cSt9 */Bew!ZV1q=h B:7Gݮ@%G{ϯ){>tՏMƞ^EF<ʃ-!]y@ ڔ rtnW-چ 4D h@SmТ"uKנDA @ XUEݮRsq:('ǃ Nhk3N1[VccLl5aE@WGJ)#rS˼Z4V7TLE@>L3 i|Y{hy{:dt{ l@jgKL3e$CF֍5 +$g>~s83J\E4@;x welUYCuBY PyFZ Rףg:Q6@DĈ% I0 TqpBroqDAoMzoD-#""tx3g\%WV·v%GL6P’s.N8KA2`{=l!Ֆ(iM݂;{!8O'@'cg &' ybwfP3ɡ`1!M&Kc0~i:(2+] *(D,! $q݁p\8S1pf T|L𧒢GAofGyϗʤťQypMy(1>n$h0CFY79˪ubnQ 5LP?z9ԓ*qT._t_ f>kK&H;#s҆A8Ç]3l7B(u?,Ϛa-g5[gǻY ,h:DN,X?QuAe mDq9O@9hu-Y @Zs(#Ŋf}f#u)r2LQ ? *@[nI49W~GFVm 0AP0'2!N Fi\A9xfd_hֶjؑ԰zmkj='C]L[XFS}@羨iy|K-9_= y|C5-?>}^gQ쌶AvNrLi(1 dٰ(ƹDyV)@a'W.4Vmq="'ihA?P7[~r0phNֲDa}Ըsi5 YF3ŝ&o $a(`_JK%6Gb:Ι{;%Z6ь^ + F% ˳Оc>K/@-5 IÌ"g0:%_y-LZL BHYtT^  (p|ԍv ЄSW}CphQaLT(er%1IIbx. 64>:gMPYgk1r(C w9)0DRQ' hZqZ=@mԥx}l` (D#g~efyΠ>BU&{GБشDhD>7/*T"׳UkPI%s%_R{k ^ +w܋̖öh'CM˸C9kW1$@ SѻM8TTS㺦}A E* tQ<[ZB8< !2wE嗿EVWVY[47 C] Y@'2F8pf5<6r]XGX`X_ X8g,`j%3@ 6-~lp_7gR 5^b;P:7d*߹7N5NIS|UUUȗ5#kY_ Uҙ%*[xbEֲf;,g9Em#c˵:#3?GG|p A*}B5tcYRz*8L}Gڕ؏eeDw-saT6NK34GxڭWU]]3?B=Hhtm2}|[5 O[ H=`iC$[֘Sg9hr+gQK蕿KciLӢ ْpvQBd+k=wjGѭb`p.>]U .\[f&`f- *}ME2u'e0 ;NjbĠ͏CDsޙ倯OĘQ2`"DQkKhE8XZH8RQ?sXu=q|ّP+IIUq&jGS%9"Zghʺa+CȮ0|ZBW93йMJ 1:BXXALNՌ:1)3e:t9ZY$h901AhK83}`ܣؖzaiJWJitqqi*JPu(^* )\dtm,InXD,\ߒ3H|8e'3?GՑC.P}Y]uxqpISwBXT&^K=n`E3kkc4prCwWv玨*׮niMJu}dL;O56V=@,&3Pc|@[fvP_Xy{h/7/5hKJ;dP >>S9em{:`5gg=  QzmPAJw2T]tisӭ2ʂq3%[P\HJf2EPR2(!*SOɵ  Dx{mooSy;@&p`d98'ȑ#vՉca܉:YjJ6xBE׾\yS||rFY9wpI'H09$'Q%$*Bڤ| =$h$sD2y8LF=!隔t1N!V$Fz^&z+W.}sDaà4+gC8# \{\]f?Z\Ӆ"6zO 쀮Ze GAͰHeP]\2}>}DGph#hQx&͘1}'^^p&9[-Ew逍t=QMDsju+yB7>s^=<گ޾}۽S; #`eKWs-3ZkD8'P74eyyM݃"5#ib^O յWe=a>pH-Ap@nNm(E`I:_KJ|{Z\2u^lw+ۢ .IMm=j\>DŹi@3.{֬>=_ƛϽdJPö"4ݨO/w?}>߬:͖Z3D-1>Q;.9&D{e_z Z͠y{GZ1(j Zv NqP,{{ IDATQCp T3=oGЀƯd=Y8 kz6& !Wf{rH9eAC@:ݑ Ei. TPSs >e+c{dcD' N_(ZyԵWD ?MoꝮ>3w}Bbyeri^sj3}e}PW砊y'z>؋1;<%8@sK)8pŘKic;|nuB40.Y~ cl 5cQhm[3ͶB3GHU9!Ϛ * hV# [=B˳q htyyj6œt85ˏsk1ab]dZ~\g` hn=$9??}7؞}tV˴VSU3U80./y(M..zx( ٙ_#dyr;Aʼn6Eȑup*5JY08QixGPP럁֡h<|oVP8F[_% g::&ɹp!-[GCD)h iT=4xR_Rthcnaf9   Hu;d9@ӲҒ;`Y/ E96cUaD1gHHg՛Fr"QoE Ee᥀i&Ql f2Ɗe- 1C3pIwEYhRk_ Z 5IXg4I!{upV6E>szqOV%ҘS(hHقݽ *+Z5p tkssTbPӳUDpuOo]s wgz{J!yL> e꺲{k׮qCNu4c"*}ymmCկVܾc{0O+1vԺ߸5:-ՑJ1(j綕 R-/geVז0㗵l_d`5q|j^6Es4Gmt2%Z 5u~S DRٜ#EgdȰ3 9ZLZ)f]?W _˻y~r"]V~ݿo':O>x9.$ֱձfYsbU1@ uW:D6P 9wI@̚&2c굙$ T)jIM= Jgtz(*{u@_qyS D ~6xї9 {߆ơ+Fq;M"3$i &gX g`}}WtlBw[^/j.mY )HR{>Ekpfy\M>@|&l\9=ΐwd3;% TGr]>3O#U~H]'?\[/g}26U`ɌlNJjYN93 (lߙ O8@PU8>s eL~fա=:>A9]sEc̘}د鲀矫^ںzRogRz3g_Us8]jlJl7_-9D#yKo^k=RmRݻwd+K&}EpnoЫΉ2+QMZ86$6Dù #I<,2 aJ164'v 1t4<>:ܯ!LG:|.jg1\?8~o6G"hY(KVkc!-9CDӂ E,Q<6 j^dCfqwkgJ LD HD4-\90"\;ۂ=-I݉"0ԡ@]ӵzC#2H4ۀT$nFq:EF,26>tXYBt324^C8ϒ~t߃,*78dP@p^OuUuεaijDCHm,(>1jNy(sԺ MMMw0Y ]purwɱ!"M Rk;P4H`\cAM5_blnmQN<,}QĎlV*DAá"9ޭu$^C N hWwIw "SG5HSsNuOH%6pt_NT6)A'k/2\;@6Q_|p|ϕ+ǺgTBNZ vɲ.ƵI"k@+ꇡ`5'#s쮳[4{[ <(9XnsJ8g޴/3V>K的} ێ)Bs Ȑ=-\9F Ja>'3{V#"4`۾:913l rӑq Qz'7J4: ;kR4I'p*x+惌:* hh&qlveu{x_kխ@5籍} S)>~q;R\BKX[6gZ>ʹߟ,ˇ{ O_.@x-VϔKv}~;_=w񽴲8ƣtO?SѱZWP`MΖ6|XAm9w8NjsЀ`CZ]]Wp-eqxF9M0wq" I<;\{dL(GsfP7FC\~Wɇ|PL0WU+FՙV{tW^߿ ̶ߩZ? *W]]&RǺ R6ůė Lc-XK6 {ksl=Pv3ٗתD.%@ەDixPApͲPRa1cEvD1cޗt*Knr0N,BuGzp|gh(& ]ƙ˹R&qL4Sy|@P73L[+tK0#XE&M؊*(j}-D_\lIw0f*}Xpz-ohIvO4ĒrmnJ',9l^r6dq R t&Ӆ孍o>z~wof:*S; "߉lƥƌPcy;B|8Ow4ˎxd`hҦ-۷n[R ;TvBk!3~˦=Z Sd D@Y扚. nxC|^)LCgBp2P[Ǣ-L?HN \WQyj2+Ad|sZٙGow53/JIbX}#)2߉@ v5t@z*b(9wyR5v33g-+EB#}3񼻦GNut1@՛nz1yHIub?JSSE>dpG&Gd*C $E=狭w S-v@W+PgƩ'b;mHܶM̲] @>yJ5XW!rxW ژ`qF}4eLy>c,b &]SOvWV'!{(55 TC!fШ<~62gphb(ŸT."/3 Ζh WM u ޗu*96>nF VgNؘRě#W()F}ZL3Z0lVd88/:pl~f]8}j.@'e  i~R*\Q"0H2or"E%"0 ԽNκߗĎ"9\|GUGw4/͋svk6*"f,0 lAxI3=pB("2 E _T@iz -Y͸+A#j|H ]҆?ӉVQQ⍬j_ kk_9zpώGxg(ytZ ND}Qr:8d`5 +ūrumeeX}6%_dРu])ņ2ے:m9]6N8eIMP0DDt9քZn)j{;@8E 8jgp2'; 4hFRxRGK+F#, ˢ2@Y2ΪL4T6GC^}9;ݣۓ䤿T_w`c(bn%Al,*&Oi@0 hq$Ytb޲GR싓kЂ_2-L hiȩWf4dqB]PR9԰hʪ%h1xһO eΊ}wt̛ұFAsDa8A"n)(%_Qup,@ YP+N })=iI Q3h[oL2!+-8dh9Lc2HIg=@LV}&'u(;:9e !~8žyalW@ 󁌊3/!@ \f8ޜ d]1;PܓdNM9.A>="@-(|˸ns/'@@2-$lgh{^9JU^>;ӓwtߴZKA E%&+-ȧ8/N^O7lsѝjFϫ9}s+޲!--^ithT6Wphj0Xp ڑb?UdvΜu #,GX8]aDJJͦ3t;.а08rBv18|%F~Qr^/Җl5G<D[}Q y, Ȉ B3HCdU@#<BlXQvq`\**g >6DnܓEpQ(1 ͿJappf\h͍^^ΔƎMR2, jLGCK(ҥppF1pM55z9 s\FamoF/GS@;xsf$b#Uy5A ]0rm^~ݹH?ws`}rК<21NA z׾燨#, mZ3kOvnOt(' E99gǏ<ɩ*ԣP/B)m?cC5x\'hFBFNK xXůCUTHV%z'LaG=BxA ܟ2ZjhZ#ROR_R.cF!7c9/N6c|(ז_ƛ~|8;v~_\Y_:/fuY4@QRHVd ZxZ~:M%vP: tGE"+p2Xh!k jQ<3dZM:Er`)){/]RW9$ZLa^؀3trIgSI,s CZ%`DLs-Iy{UK4g$>\AA?(*1zh45Spݔ%@\:sܴ^qL@ PK01^k(wL׊<9;~_ikϡ~,P>C퐃usmf8:<ƖT|=(3"JPD]iz6GLD+թOʌS2f{q?񓅍ϔ2&u{c/^_-ȧE>Fݗd_wOK6惽GmMA@ !:)쵁%%#l Rz#OHYEjj3t dFQ"C-OFL'9sE<3)I#3I~ 3g8M {c,9ׁ+bPcq8Z䌍Ha1E'B~ǹ0|GNW:p)BmKoACζp=:ޥ/B_62KnQ_Hmtܢ:9R~lPsQ-\wdZ+C IDATt)/2gjh#؇ F H_G7PePRjqbL >h^>8`c`.ВM.:Mu8QkOZNƳ͕ ǯ~Hǟje|9?>X|1UEKL-mdBXXmN3yKl3J^<+Ձ26<: t՛աOթHыܓ.J^;O|nq"{Hx ƊgZ,D3jd4`Z.͠. ܳK15c5!)sBN t?@"I 2`Һ#)m-],]˯}̴0 cLDžl@cFBgoR }V@2MnffĚŐl %7مrreڡbRlz/lQ%SТ 4g(#[V#LsL6Dml8q\/xi$yԑ| &g^t A0i:1wTl4dT2爺^m2ѽTwlJA{*aCg:r饐MȰIƣhu!j̽jNcFPqVxYʎOi橋rc"*(Y&|o95N&H**:aU;Q{?}Xc'>1޽^CZ#nj9LYz8&YG3qZ9qf h @BvUԳ(xy ؞S%`4%8GT^2af|N,evKf (deܙkYׇ*%JtE<3HA" 8Ru @(~(q(4xp%.\F}#ujix6Ȇ0 ރn%"YL_kgDLE4)]j$C糈%~5,.YG^]8@KH;WXQBy#`Y7Pи)MQglpY+{R:m)d۲=Dq{fh\%4` uK{$()54's2A! 8X)q34K̕;Q3K+T;Cz$.R[yЙܺ}~vW>]Z_ |r]cќWj.@302KS&)+)k՟l߻?0xB0ߚMf`*S9\ vU2DM{)"#NW"jHoŘZ7K(tȖl#4}=>4q>$7A-2c "瞢D{v/4Ɓvܿ_*{wV^x%S3޿[YQ${G818jd "JyJb>"XN啯I9iY+<c-qeXk+.Q@O ݠ\5P#u(%Rcґ2K:״,ecu=ؾ:;%2, 3{Ϳήn~06>5/TGWoO<ߞ%~2 i3cJGCMX,z u0jXE2ajdrp.i QruӑEH`h;'Z][jIdRޜ5+ v!":;NJƝ-`Q gf`fKg93 8F섁ͺ#Q<%  ^yv/KVE-b[.5~z4edKTʨA;w'ǽsK4g~=85S"-T#RvJhySr=6;֫\9pvM yEzgVbA!lQKaǟ6ggHUC!k@MfBq*WNaJ!FNr\]A< @,ə?'-\dK' K\T2r(] n!c'H(R[Y/Fd-t2hEs06HˑOj4OGt ^*,K:|{4'Dj}EQfgBJVa65>/4?~\YQ1M 0}=pr0F(tl9^ dDd&'s1-tNamd}LCcNTo節񠦨X 4W,-YDIu32C&-x BWHD!DU#dHG|y$ mzƽwgkL{*@]XT  :SP˧r AL2.yr &xA1EB*YI6*"Q[v"z~vfN.\&H(5/$ea2PZZ8v>YAzlFzu\>؃6T5"k`IY$c=9y4wVWǵ/Ti/_x[ )X0o?hźZ'o + 􍄇3ADd o/E}N%ڝz [I9OSI@K6:/!۠5BM 7-.lʕi!Ba>}M .%_MH{eL${gIlhV ޫQWlkhIm"۬HN׸8sa@l^f򡮲W4-2tD,GStq ZCy<36!MSJ\g4:xJg~Ja&͢9lk5#.ʌ=\ϔk@T=NES]:YB~0:޺wW?T3ᝣ~{{28~i:85Pg$,>-}>sz&xZ\Pbc( yxɌe h-AdΑ3ud]ըW`S|ccd)ZCZ^|t.~7/LƯ{ Fd:\Ė L(斈n(@JduR!ВB8\V4XI-`YPY?UC&TdXdRt=3\OҚlO Yٖ}X@YY hui{ RE2xn-Pl{R:㭺6T3 ,-qM7y߱[\5jK`mU5{F|H?&7H[َ@kgTk̨ԙojAAߙϿӜ[$-u rDz L'v4vx/ӏX~4Q$4 ;sBx9lDȴ0No8߯08Ґ%NQpÙg|~P:sh*`p-+Z|U0>)@l8G_y)h.d\HZ@ ~pu qXߦ+F+KPr9Y/<ȉ늵P6V;b=:ytÞXR>ǀm8Vw幗{o|o~՘/>;?wh=,)+FΎiӱS%Ɗ ht:,<bSQ̚/Fƅ-uq!CkS1*qddVsGJ-wYp2Ӭo( qp]ݏy-2Ě5@jNƻ4c}nZĖ:EEEgQ9IQcX8d |ɪ 0evZ#eZ3u;AƖΚظ=AYs%<|싱O>„zpzűqp6\d@cfɹn1Am MS~(SÚq <7ʒe!ʞutCdټW 𾔐lrҕ~D%=b<ʒ}73B\th~NeFn>l/-旟jO"ŻzcH4ha3{]g`N-z3.wm Y8`2]KˍY=rěV>1D1e,Pur q Sdi!AKf/&(dtpFAbB-(r֤D;w%lн;lYہ%m0E/@|Y;d8qJ ׏C"!F(|SH5-M2ꩰbg[%!ci\QZd:`%IEʅ84Z(ǩVsvE1h,kw}$o|E1~6c}X5LI$[9|%%BB]H:)Cщ,(pBMĖ\[hmx"Z7N2>-꟝Z9U|@>\; ZrJf=IGZxzjrЙ\~?h|lNX;xL˯{xɢ-CtI)CdC˩Gբ,qdu/.HLwPg92]OzdJ}083d CAGZ4ğ[ȶzFƐ |vVDYb|:qAZnOږpC5|ZxlW3j[DPtt(Փ;*]2Ӿ#pkB=|=|qx[}tsR9eDu9ϫ/ZRd9ӢBI 9G!녗eMl?ˌ7h;~Њ kP%S-VhkH XCmLZd^e'Akqq{qYԑqAk &liwZҙ)3pg~(/qY@Kqq=8hE"x!]%9HzCύ%#ϕ3D[ _bqǵ'jnk^ߜ QgU1AXepx [Ë1Ȑg}Pf`3QY`^ɪ*pmuTw<_j}d[b/JF8(蟀Sn@*jrBWd[HZ9QMD4i 8 \Q̜բ/H5Adb^Zs)yl]PrI8HBt! `)`UؾwV48gPiʮ9zd(9%ѴW+_UF:PuZ\t Ȑ4Az%md geJy<ZJGxm0WVz x1J`qJ!02xΦ8'}&*lgkzpqƵ|[Wŝlq^_߽ika J_ R>|2J""ȴ$"J/@ l4Ґ;4pXoԬ%,ΠL |v4#n5'++KʌIf;.rxˉt881Z HL#Ě ׄ;@2(@ ր_cУ:G3(]vRp,@UeuK?鉨dAJ R3F(yCLp]}$PvƄW$zR0F(veA q(2sTs#[bk|}ʞ与'K*}d8^f6(LDxcelKJ)a ndO5ZL.j2<\~XmU ڝm-|ZsJ*RRQ ':E-3C/CG hI%1_kͽ7>b[OHW.2/QR@by *r(tg zQ˜ٱ(ܔDMf BF4C2֛K>jW{wj^u4h[lY;=u zݼqqzIMVua>2x웨I7|C*[w#;-2 Tcs4/DsI:m>WAtn<řAa_eMsZ-; RSLGG8.#‚4:(hK° c>DdR݃Q؝0Mt1llɱt#ȞdЌ+՘-[gj`?Oͮ{Jk#[R?Gíَf+ M̧ Eƪq٣85kuxf9jLfF҇G8` ù. pZA:opF#"[ǾkE덳TЦgDŽ,~GDCZ"" @>I\NsI@ˤ-Q<.u BG&.듥l8})t@$7Eg"jFi+nUwgC K)N C` y\Z:^'a̴$7y$ I<$ [!W2,Ŕ<ٮ}'i M& (tPesj'J7qƓ~I$;Y^nh8e9s 7՜;{` s!V(T0FeaSB?N6yg3ZkyUok_ѵo 'NK:qZ`K+2ca1*ýw~vVm xHd֫JШY])N?Up3c~P$HN 3ABp,1Ѫ(0:ZLPW!( 48*;ԛA%Ad/GQ{1Yˀ7 7cVAt,(RhOFՅ>(G?D-c#S(㠐1pDߎ!D+ߡi) F= 舸i7 A eg 1 DJN1O4)Ok0Ͼ/}@B<'3(vd1 @wSCkYbtlÈ1cJ\7c\~A#](P*DkA+ ;g( S&sphϢdY+I7,Ϛ Y_dȊj:ᑎTK~cz_yGzp$ݚ@~HVpMڳ.??Sd6l^|+\0Ƙ3K@#9Pgk>|^D̚.d2Ze_w>-<ѳ}+"g-Ry0] h^HLqW brͭ#ΚQ6H-6MUa'3n(bee2280 M{=9c z$#{FQPOA:Fge8UVWۓ5;KO|gW>QSj'%M+_pykuw8-ޒ`xqJJ)JH#Tt=qF3zl*g/t4P޲qWD$dCY%Ԅ@?;&MA$;q OzwFgٌ1jJˤk5M졵 v-KGzNџ 'A=9rY&<12͕3@hs],46i8DqEp/ /Mr0\rJQEb``t.+2}t-M2`rX#@ ̊9?Ni{9\TO(WRHҀxߌ|jjE<.J΁); H8=^})M3:lNN׫3 / +MIu.外 f iKC;ۻѠ-~c3ċpTP*2d%6n0j{2.;"``Y\ۭlʨYZYnQM0%,(/316zaRUG1r`^XPY'+_8'[B߂{1ѕŘsq 4XFxgS$L n t$53":%?l?8&avL|JLk.M1\DpM(t(Lf=F%"h21{2d1AUsVq;+wG5C3Qq-k= J63G(ca `,}8u9Mp(N4m 3(V\<=r {f8NQ$PYSz>|PWBfd9 l" d{osZ%[{U"Ƶca|~~nμ7|L;CBC8Э M?x/_cە ] eOџX̘49سt gr& h0S9}B- z?k9 6Yq3O2vq.(`3d0QV‡ߒM*EM7meEda(feuT꧟VΜ>%''bzy+ثؿslqEr+b<AnFɴ@Hcqp:޾:}~3?:_kwwׯT$WUcfYLIl: 8$F202 P授7F#A]k=4g6 VлC?N72PQ.߁B|@LzV\pF7nRHdŘa_869Dz9I@Iaodގ YSi(NxN?ud3X_jԽEJXk8Jy!"v7s#J 6թ#4YI0Yk8..|vd{gX:R@ciE(.˙9Jn]CƓla~I:'+g,)hL!Ft%\ S g@'j U"f0,4-  Y%d(ǦFUսɥ[7f#w8PV9woyg6g_xtTucrc8! 'K/i8A[7>rFi_tUSX P EUu<"B[ $0 ѷcK"{tBW PHyQ E3%\PX ;XRYX8ZP;wg,$PI|0ֽf8EمsQmO XV0 \E"ʐIicF %ףv0~VœB5_B+Ԝ`U10$?!{8aL=PVnF@{P*bI v'9%ڔ;1=,ÝiYPXøAH.0řz֘ع"mc#Ę #( fnPi6(˂N GB4 vG%5@Resz 9p|%o؝<{;ͻ1j 5;c@`%(> VcV;s>}sxttYiZc _>'/{[W ?=}ԿyTWW3I_jOVљ¤Ef3M@%o rB`yH61YD`.;X Nӈ+K)]gE%jk]Zb ;XAݛ %"̙JNBQ[>{s`4awT!81C}Xi`#N*Dc&`(k"˂O;53 yc暇 y?p^#e4akjj>Xxեwk\G{kIt, qc& Ʒu7-eDd ||Ie -q^a|+q8h0j+0s{ dЄ{KaFJ q Dյ#8M׊sF(`I+!dS21+SLjLjTև QOeMay7j΋ R a$42dQ4#ŔV@иS3hMFs]џgsP j֝ aNI-:t$U7;y2Z sQVj>"A QvX{vZb 5tIEE*cX gdе9T T8E[ qt#C5ttSp ꊡD;7sq~ Q{b䳦4d&SP "L1n["@Ei C !sqZhH#G8{q,.ڝeDlH`~9Hsѐ+@0Xa(9Ɋ(Ӳ+ sI5 Y0RQ@]"橰xa<#wtoURƐDNu12R^vv}8-ztF{PB&Hb>j}ʻfz0M gE/c.p, 6p*4¹ '3?;IzV[9uճm*]+w!MqV 3R߫v_AVm}q@'m<(l^&E 8F8", #3 k }{C(qJ~@dAFo*Xu΀h}~rϽLYyfq v6޺o4ܞ>v0_ɴ8m$TDO<:QtzHBRF / k}<7OADNPu-Sm[9%QCi**C3MJasB:)4mA|KZ ,]ɦEe8% dBX5%# R\Aer:&-xg IDAT!_8 da@D A`Đ,w j.;cN:Ν# ڟvEPJ(gyD!"ӆsZ\ T!C2`~Y`d][#"e`&N Y=vZwMYi?;.`)9[ztOP 9 2θkJFY͹qVUp#h )R~abm[+9Tvz5{G==ΡBMQ @+ľnϽlϽ?<Β}i9qZKI%3AgߖtL@E\ Ik!R)0ɾT7ooJ’(UJ ; gDD#VJT~gB#)c9h0)@ P;1}3$ 9P"T%0]Q.4-Q c:/KPl/A :{x|TuSl4!t=Qhjt F;8K4 ~5'`_9g8>_5um.yƺ߷,-z;qWVΜ=]L엋m* }s{8)B6;axԪeMt;F#fvhv%FZ:zJ:_%:fѽ-4*DiKɅ?VW˗AŀZn|cgO#7nh̎""bq;A {;U1Toߺ-f冚K>wSO#ؔPK_g!"BdCcJE3 <巢Sihtt} pe"R"z1FXdCP+/ !;Z ^9;!N ڈjA@8i=5-tiq2dBz'%a2<+^8-wX_w<NLyƍʊꑈaX@{$vR ,@0\Wg48aXK./g0:b gfƁ-]t2&Tje a'B'B DkNH\$jRc BdzaT38$H% 7\x|ǣPsZ pXgWQtߣK.TyҘr]a@z`Eah)O ֗GQwVLt[,d2x9{bǏ# !ƨƓԳ[3rnc,F^󞖲WpkmNy#clb2-E9cHo^?42o޼1sEI'7\JY :v:;j 8S64߬FcD+LA&Pd0#+ <y^7SWKrLU7ɸ_QqxG-\7xOvxX}i=k @Q807LJ{={zٶ,{UQ+DKYuY+2HpM˝ C#d]/CϡajMAH ݇}GS}ICx4=RmgpiĊZ9ج`'(2B=0HDv5heԲS%ӂP ǟҝ?QC@TҨ8-I82Q }ɴ [>xh,f*QT9FV#Q5]Z>k_@ע"ԸAp=[\*'aiI w?,2K$?6׃@׻z@a98EƵl-/)PPWgS3MP;c}u HHힼ3p|AX!YP6 4'ƙ<,AB22Z0 Eigu,k(' >E7e8`QՂbuM4sm&GCȺa ˺BžrsMXpVizDum6&F̹֗.vzkMMU Z{>RYn?}3tqt4n*@}tH;vkAt\gYE[3>k D- yB8?nNZ;Aڟ^ixuqB %Ygݴ|NGչاM+3=.(s}fsr2bIF6uLvW Td`0Yܲ_=z]8^B/%˨՞[,=ߛ[:ǧK 8{WM;|֍+_XXzc~Ԫ*ZG9ne[YJVd8,F)pπX24'Xp%+yёF}*(,xm"$$jg\eŽJ6x,!?2/38Az()@c8b:b}Rq&]P@Z{Gv/t]OjX^I쌽Kk>qtm3  I[kُ*N*mzs.SBVz ͭᾚ_v>k#L#ZJeH,#NPu lDwdTIG|,8 :8M`;]pOeUi|UNs=&:3i1=mvT/ n76hqZNJiU_(V?U? ::<}Bef>-(XmJ^Bڧ]KZY;wQ,>R%@; %ϻ72~UF#ƅ Mb&50Č-̆vYH!!!10u ̊D$H\t܎$Ik#JXD|o3e8"lLJ-o^<g XW9+N6SȬwP%zKڪ1 ȣ%ra2CoA(ۛ[wO;@p-݅f$K1o |Ng;GR,GPt@lyB_iA"H5}{heDC1>noT?g@ c}G$' 8V+˧z-Qx_FM=;FC-0z(*Uw/ 8-_-N 9}ي8^yxd^;/*J%9-4FڕEPUٮn߼e:r¥{*ӂ1É9,a0AIpZ2n"鴠Nf1_I5vQc)0eV G$E`1"k^. E N)lO/ Eo(ْQBǼKy ŊKBxyPORtlIG ,ęalQЯ"VD]WTBUCY"FI\8c<d!\}=9M6@#Y XBC]\c2>`9v[c!Qxn]ŵ&;2s8mj˙"JAҀ1D7̲p^{(VޱC:3N oh\oʅ__z?N =5%nٷosǏmolOsqqKLSN {jPF>]Ʒ'aCM xs% ËÙ7lS2l}ݳ%cWɕ1zq c2B8-$+dϨ-Ĉ߅@@Lr?"8~4h kf+ _GlZ NYwE RTx43?6JQh֌: r~SL2Pۛ-Q9A%$Kxv=ŌzmVe[a|¬dP֑q#HD}3w2dWzG[k'a^Td^s lLoI?o  6/B;GT ؛aP4^YRbTnZNK_F2qPy8;>+ONk.Zio t$2CF5j"/)A!;:Ćh;`x/*x'PlcO+`zԳՅx~i׳a<^##ŒÍċ߅cxC jp:~F Tmw1H b( I-=k{#]oSAx6z؁3J%r9ϰJ_%uԺ@7d 7"⹣jd`\ڸ}6^-jקP_ިCV-$ 3SiRv 9Ъxjnr-.}̅wiySٕWL\|Ƒ}Bsu*N/*%Z8*tWFG|DNˣ{g>}͹+67֯eԈZNI8RGa=.YX҂ K_JiJ6"}2Tw%d;.c5iIhQPPRF>l+4+"1Ch *+]Dx4s@1qIa: e⬀6p o&( # NfGƁ:G pZCiX3NK# %(&Y*;BI>_SQc&gR꾠8ڙfO][={!eͲ(P[^{m~rqk-Qde<0.p@1 q Tڿo 4:Ȫ IDATf,H> ,SX$Hp !7rhs|S湮AA u732/D]yAlsH!2R1;K{>g\!g"ӲOqh-ot!83@'jzO5^*oͮV 7ʿW៵1ȮIzjYS [~L˵9#>PN>)0(k͠qy'1='z3 zA=2#Tg_rf}}C2rs8 88>3@@xl#h \ 5E2oy.pY3dJ`S*͋yD(D=JuĹLF r0Fǡ2Q#S z (?ݠ沏#ޗp2oVF;6 $grh讀?&1ptGXV:ƺwOs:1BYg1к_O(GН03*n*esQCKk8ETqܿ!LE֍3A 'M oJceq7Z3V^ ^NK:l$Ɇ ,̋z8-_Ŗ0x^ofhD3ͳoώVg[Bo:nK_xIrDlWI6ȸ j^9(J <1^D ÂPfaF>(A*dÐuĽ۩P` (} w ζcaIf/J xq}UWF>YRF{8Z}}"b T7 1ެ I"W+!H-a4cPT<{8>rM$ h"{s"9aA1{~JV+@F\Q#Zbx=L:E< 0P?'|, W0(?%g.wՁ}Eb< }$F-U5[~!!j˽'Ϩ "^+GHi6HHH% Hދqserrns,F)'иh{*+/w7nݸPmR}jpZx9֚#80=A4nc]N 8#3ba J U4VŽ27Z]o(`TgpJ:ؓ}\qRd|g cuz=b?Hp_21y&aхڲ_pZ̻Qv&%v b#իmWϿ3rZ^xG2-dWR2])mf&Fg߾߽y]`^SP X}Ā\iZ=Ё <И4`HRc'njR'P=`Bʄrhe-(xM{:˼ELH'c t:S{NtW'OFdqZ. 25\ܹwdkܴ5P_fzm!=x88n)9r-k[&Q4%88k]2Ax8Сyɜ+drv!a\us^k\w]!N\eQ}ri#3s*D-G3GkU[61nϜkGٹkr~]C<[fx\' 8-_II?%Tי~|<>|fso NC)ɯALΞ" Mz)J) P"uBuc- d b.W(1Za\sXx6\%m++a(cn%Sv@ut(M5Bƍ34K‘1aILS~ð! mg)qC0!SbBi9:fڏpbGj{n@:  oQOsZUg2*0)Fs=|O< ك{'&l{W*Ŀ~o= l:EW17oY`C։O]g9-²+rbpP$=,`svLuEwaT 8!{{ dؓ|)"N;C9K:#>S|f*uf A(ar 6@; '`dݨzs,xFϛ`9D.jpPK+WΏ~|ة8-SohkO?|LY;5jGPsh2N !1`ܹ>IP3H ]rF1TaF6Ůɘ%WVԇGF2✉RGva[,mE J@8kA?aJY`SԚad ǀp|x @.E!c=YbȗtÞT8}SR(>R:/7; Yg"oYpA 3] N&/uLKȦ]j:Md/; õ%du)J(ɆB[\7`qjieaZyg6pr ^gfȒQ1p1? ks 7o^ӘoT>uW3~G?wc}Jz[xOh('N:qZ+^%/:HJm{~bGC:ҙSgAt`A0uψ e6V3z 0 q~l 9^D` 5q ם|KH3\`#=TaԲHHE"qܓqh#wj4+t6VFo>P Э!a7?xdw3C&F=FD_٧ |I!|O@4٨Gv HqCauYo(D뷼')f t UΤF Y|a0;,sF]%#&C*}%LDq."b B1ᴘ\IPMR@T15jFMO_ڥҘ>G>?uZ"j{fo_闏,Ѭ ՟G$W 1E̹*3訽!aʀ j=ڠSB\h[Y%gds| 6IJg-fE KMf# )%'Cg1Gaw$JVbhxv2 6?>Ë+9A,g,4&ƍui=,\Wgp\)4lB^$r6_:U2-/w8i)#GB E 9 Q `sUt|KGrJeqy}zk+SkwJm8Q k{GF4 ")*l ͆q /X6kK]33޻(SB955VP"JqAsGW3ƨ0ei{aXNB(αRXo 1p0֯yOܘ`hA#J}&0  ;责Z!8j1d٨üb\*M2nj =%.'O0x/"EBmi"]ևjq7M`浹o<ʸrYԖ x)[{;uj*֮LLd DD!pGY,7XkhaS*@1)%fEkrA]A\0oF_ 8;T`Y@O|eKPr gYGS4G0 }c1S^Ř7 tC`MP9+Q(~Zj*c`e3?CeJFs-chLUa֏=*_±xf_~jt6LbMqgiι!wZU dz/`&Aa +@FC"WD)9 (=/U;z8 :Z "+#$/0Y `4F 9UdŚh.z_B`$I9HcqX9\mK.xA>몯O8U&tGp:83+h2v&JzɈ8[#122L: M%F~I"eLY&hTka6o 7EʐCH!y  AwBdB&sL9 C/gNM 0Oz-}|%8`S '85LB"sHmz- L^d xcUiq8_]_oNneKF ݓi)+i,8 XUt8~L{;n<31.kV9%*!U%XՅ.vnq;$e,dxL,CK3܍A%C9'DL^ڽRlu\ZwGA5*pt(EcQpxtZQ7P%P@>"8-f]cQh !gN 2-麉ѢDðZS+>aNT)"#ZQ-tE8DKNaI26JdoW׊}l`hTb[a5-ue[d0@UT}>m[V%2EͱPvJ*GӒH7f0 adᴩS[;B!g‡eCOXGͰT KSiO_ݿzx`qTgFm $d`\bO4Pd,uբtSTpeal*S6'!"#hiNG&%p>T bAerQMmK o͈ cv R*RLLTPr1 ּR._&DKƓ(.y%Ć&32fd\(qfO;2 u8jVzEtj0=I5ulVciAhp. {5[o]+;n [HN{5/v(ɐ2Q@1&1*qT"SJl Aff^z Z9 l 1-f \Ky<1`S>2:rpyYOGsԌH<Č =5݂p ;Yv]'t1Cv3 ,*h;7 چ2М3>3<7)`WM ʱ"<?KY"(ydt鴸+}aqtֻ@Ă\J3`tT+,738!r:G󶠐d93lP,=a1 x~+rH]YAga&4p5 3?s”B{F$n@䄫vғ r<&Z TLk[f~+ᬐe',>-_׉%ZcY\K[̊熽gu6eS-jܪNKUe B+mAJi۱ m{VG(8SJV n9A9^\9C#v-F`E) ;bdC1,-}]BY Lw DlK12n~OL +rQ9.~}ښ9j`S\`v:O"6#S_j(FC$$Da*ӕ‰8~]3^c &^8 sxu,>Wr]\OHGɎE0\0-,kpd:*X2X\gOF4X]ńv]̪=xP1=Ws3el:H>Dg GT_Ȯ1o&DyT]X>̽>19K?Ep"1:9~ڕk#\TҨQ#uPBKuv^}bO`҇*>iW̚V0R0G4{Ͼӳx RFRWfNkL~8-紞8E&Q.Ύ{z~Y 5fɚtxe΅䈻w٥fbusa;v֢  gs:i=w_Y;2yF8uXMR1?j O}O=';w=شJ=p~V?'뽽oUg7lVΞ(KSS8ߩ%t9>ZB콬Khu IeIp?9m4pDs9%ŤuYEޅ RqLGd%YtۄB@pԼ>+69iA,=}%ȷ(.t&dJ%&,GXcϒ,U]]CPl>7)~}no'Vñֺ %/eQ[e[ 9phRĽT *  I۲PG,3@k M'c_X4WVؐN "-T2JAN;r@Yv2 ̣`n=9V[Խ!2c;7m@/%pVp"HxzSfojTv=?Jי|`mQW?޾y/>䭧'ڄJQLuNQ9?Uh6jȚ pC\, n0YljIܰT᭜Cz 2[͵wBKf/2jQC]K{kF~фIڟEm lP2EM"YSC6e'-7~f>RI6KݡncMa#< LfZIFci~;Eq}} ްp9cH_ԍW)r{o(xsZFk¯sӂm\~ﵟ?{۔O)AC#o],Aуg Ю.[]#9|OV*XRAh0rt &O/Z4 ~ <;,eiμw0K o"uO3x ϊ.'r "cxCF__%(@݌>\wA1{G}CHs{bS=CBgu(+K Ӹ1-yoY:g5%=Yba@_2Gqƍ}m^iq/EfglO !M14eL.33oYnp,[vgm^7_* 9Ȼk G·Lp #>نV(2r9R)Jz|Ei lY(qp)W ޹&>a:?}\PwM[qR2ؤ};Mrng{~砇E ?2"@σ#Oä/V4,̩S@Sɐ۹I8s0÷rZq&j@h,4\ׁ(%-1=iEYj-;kyM]A lMɄW:ƌW=Ai1ҖӮArZdkOt6zF} :lϚYʿN/(pޣnO+uEh6k\7FSTU$1F"%{ƮKHBY1&EN|Q(hP2(8R2l`\O?(W f6+6d r:3t P8Rd8 ÖW:-(as4`C3S dw**+炳N^:W:,vʳ qeQ@`__[ 9=%j@ڦg)PDic8sfbb1g l75Mѻ#DD %l\*6@!NK (w^d`>A`ܺRCpƊ>(1yaLdZ0@1zflŀ03ٽo'#98l5Ezھ% Eg} he(?.~<7Bber &pxp;/@G`o^uanaũbb-q!67`ifDg!;U nFȪ |Ш=0V!.8d2(:C0)Ђ "{X~ z;5.HG]h,OuV= 7؃l3g}v0!{pA/< ɶ)3u{vQ@l;;<^'q]>&:s_NKy TnDow7?2QGxR}Z4!/

=䘛ݭ4%IX PvJ0=[J bo+w\#h'ܸ5oʠ:Etˡ"`G[ ]qZrf}Ylά5=PuGkPFBq=0iovzhaqϏӲ-{iuuYN?AQ)O{iN:,9uN%xؙҋfxڑR'%2"{ (]zA8J75UZO@os lF }Z30͆j80dRmgƏÑwN-WΝ7W?;?Dg?isZL !g2S9zB}ԣ}dkL)4c:AzsnKY2-K2,Vݩp gMkY+9zO A3dw2ܕ:'j#["dME ODK_ΐy9rkleBXhpKֽFqFIV#EO {@ f.dd"F / ӑM~qIQ! d pdfd"#ʝ`^9vJ)g`J>J@:NDz56zN|F c]"cJ%i%jxHf[s8.`@}`qAq$~U{`vnnކlP."|G]_X5I3z[TJѸ`B)?GV06͑!B ߼y '"a;'$}HDzͱ%tVFAlqZ^rt8wd\ G ń>Eೳbl> EwNDYe8ܗlQQ{]d>pvLJ&0N^(`}d@4im<vBesE?Q TY_R~tP.giIeE99-{}Wp1>(|ǀnƹZlREsH3cιߧȚ`_gZ2ouG*յ<4ŁSSN@x?V- (2֪+e MI“@ɸ`As;[/vxHVʳ~7ytdڨNakE bTqL8%heW5b)H]HI-]J##Z3VeA(C=To/\qO+TDn-CDP`[*rF/D*,}JUq+W><Ҙ˿{ hɼ1](+x3F4Yz?eE4~8DpQ_X NK4( x4`{Ar&A8Jؐ U)cJISZ@ _}z`*{9BWJWlP>S/Ԋ*gv$+]ڗsZì-"Ր 0C$.i 8,iQn⛬5gvZ^Y9ӥ3gpZ`׼i!SxB/^*^2kMmeG`ȣ^,c9@>%C65K YSWA=7ޑa@wX3ȹiFl!$8|5ZBdP5O@U@k;訣dxYڑ3vy#QBƋҹ8kGQz:<Ҙ{GSŵw_֙ Wޤ<Κ?)Xd{D+\'?h7TƎ<=up6pO\SQXt&,t\/y3dA>:/RGٲ>ęS6)2g:fLFndbM|'U ҴJ"_$>r; YU2,ǝU̾)H:b֕exo@۴^I5}:al_J%\ʈO~n>ypH4 u3C#Sk1,5`~:iyUҀdi)A,9μ:; /#PR$GdZ׳v >;":\Y1m0A`9&7Xd.'m`Ww-[S/azsNda9>)s+L˗|%%2W$~OܙGJ+pKMUS# 8S[7*.^~;y/6%Ugq枋AGqhyxqW˜5_Gw?G`K9T:g %_%P6@)HH07c5϶#HTB(Y{w1RÐ*Y<:C'Ҭآ`L ~Yq8=TVv/jC V, &0 -"Ppὣ TiÑ CnF S$@N. }qq9"2msFD|s(O"QtZa*R $O;`GLZԻʞƧ͑T2CѠbŸšID:tWۿdo]@|.Vb|h~O۾p}__oV|xO,~@{(=?{IvcW+R U; "ވ't8@~hFȼgFy%CAIFȕqcTF&Ƙ{iԼ*ᄕ CveY(=cB3Iuiy׉:hz7D-1A6 -,⌰mkmaGl!ɘ*;YOM?c?5?As}Z"/o ]҃k.gHr֙XX5O 8H3qVm9Ќ9c]>?y);.ˇU=4Mlx! -i9ʬ:x {MxcaSss<32c8"Wm0P3"|EPOx;K|&bY/ 6=bw:!kF4E)`bY,g$ Jp֭!J0a9&\N=&hO&=%ڋ&buz^=|^|F=\V^Pӳ5)jkN-Qe߬][[`<*=*.[!BQn4RB(<kpz QB̈́e rsNOc(gȗ跮_ܸqÑˍʩSB̂E21PJ[b{F+ A<3JItYvVUP^\]cږeͳyb3A= HiPUkIE3Hw;}>CF pIAATI|ְ`ؓHjZ+5y/뮻|p~z .'7/$R&C81Sd47"<\CO! ~r18@_TFNPԓf(BqjM IDATKmx$0 U1^=~829@; BZJY:u0R̳<3B@栿,) -ťW@ I-OjRTLS)4Jstl٪7&~x|t~p c?J}cV~v/>]~+M#$cpQ)5 MIAQ491mɺR-:gm@ y62Sh6h@zap8¤uK4'S ڔ%]˼F\Fj@<mHFpɔD=}p"A _ 圫QWsVW/vV>BУX izcYc_ʻ3'\ߕeGAs;{Z,>X0Ͼs_X[0Rt*/*L7O cJ"z^?ƝW:Sfٷ%+ж#@G^oҭe?;,5eG7ӈ=:L* [2jpy5q=i n5AwJмLY90b鰱*fAٲ /Lu1 Y$PQiCGgxѥkW/;⹩S.ԩtOKt0kfo(#2Q~hMKed`CΧsjˢh-qC;=pĭ\xR:SVoVIuhNٜ$qd lWNߕUϾjѿ_ok{?H/>ު<8sG+L Un2ڜ[x^18'菍}m6KEejPS~P`1A4{yo9<:=T89= :?!ÃaF' $@`g k:p{M +pթAS qቴPg"H׍ps(8a@ !eU,ȬFgDqh#pЁ"("#P|Jl]@@8rp1D5PFƪ) í]cO]]m' L{@;|wDԈ{*k{L{gvsE *B\7:҂gvTixE4(= +8yBc" $떋 ? ЏJ)daTtթ +ldl̓0Q=#*V+"_AA[R!2 S/MxϹI$ݗlL ;*6y;C5$[! e@0y;{F7y죟FZmTͱ0Z JI۹oܺrξuHVd>$9M>"km|A `u={e1girZ3z֗ڐ/Kgn4_6Ts njU4̫[R#@![7S7&vQwS{Q}IɲCFcFT#&,"1uJcd Fޛ]4t8 EJݺ㾴{QDXNш\AF}`@%-ߋ hU`^#~d>^kv|QLN3~n'Qa+${vqH*<:5{NLBr^ \o\RyW}#*"ș_=*5FaSrn_68'ż-󢒝^նVAӋj47yaC) *I:T(ٽKJ=gt[c6 7?ۿ|j T+׮HMLmkʓOxa]pXհH< V8)h#TZxk\%^uH6&hI0eC'd\x(p>"mbJ@YNh18Bg(>S3@9t$;!DGnn_\l8hw.?TPȉб =ߒweތ"X]v90$~g.:Ķ5Ĩ?*óF;(4'3yozj{A)zDvr3*kΩF+srd&b1hi$Icf{i`xÿ-E & 3LPZa؞l|?ǯWZ_kֺZ} HB!F{P55&/ ʭ۷J/IoDEr'U$ /, I!hI-MO=jL( 5$Y_@`~Q#bdZo \ۉh` FetqkIe "@]z'%v\^Q;R$ivm`jnWeIըB5Lc3ַZ/rܤ8 BH2^DX(Zi@b~::JUgCak}t\SG`# zrƒ.9tN"5P:V NdF2~UdɁq]$ T\ϵcE%C&׭IGv0}vM81@K<Yˊq0u29m⹙NWĝg"X#uEGT\)s a%0F9}-+Hʹ`ʛ#onolsIsBs?52f8 3C}ARԳfԍ:av* CW+g&gt?13PPc+}CiyElKփR=s?>=P@KT +t93djߔr'd^o5V jξ_Qɵd+nުHLݿ7 MdacP?drTUʛ*+ #%ug#%Ɖfzo"/ӓZ-(PI,.sPŦFc~H@ԅ7eI;eI94̲{^eE*RIΎ<ܑ`ٸ(\g[Jx8yXdl:C>JxUOxfW=8[90 ~J 6^d [#҃zAG9eu6][( ؂X`\B#ooNVԆ^9ږ.?@2ТXȜ5{˓d垡!uu_wٮOɫxnm3'PzQ:+j-ZhkXewk??}sk3zާg*"nb |/.Ђ8BK TaC4CJwzvۨrKQ/q<{" xޣ6! ꓬC#d^@ː uȽH; s3saʨpI .ᡶLYk吸$.K} '$yFnɣMTq_4?<5Q)RD؞>G̀\.]V=ztWZn>BB u<%ߡ|GfԐ;]t`1~Hמ ("֢52Y'Db D//w(Vgڈla^i?;j#XzQІz4qHzJP3!t}yo}FZrNbeNz"~mym|9OMV6ֵ^4ߘ#5)?͉yU>c9bN 9SE}op2z)`QWOqC wh߻׵x;ȎbCuA9RsD㾣<( (Ɲ?wΪ]rtG ςcp]zR9/(at(el8|F.\KפLQ}c5 d `s`jc\@4b Ghx-})?%8W;=9gz#_QQ$IBE )1?<)y`J1A Bggk/<{gсN\P$bQS]30};.bx1r}a0*Ea7:!HTਵ) =tJP(^'rOY~ZC G!SrH'_9@K1eNNɨ i}H˰$s"m pDb4ItǖkKy MZQb8\ltbQJQ;䗍dخ:VJįF7& Z2?ߜ=m)|K/GK6XHR^ل}[FRSHh](3 QI c=^lآd1"Y<7D t\#j aAC%"lR$V9r,T`g\w1 c0HݨC:j:j ܏ҀRl@@iQ!> ] A $1`Xrѿo=Α:}!ќ]@#gkO GP<Ơ9 tjy}"őg&dqL^½ݧduo;7}ݒ#kKT^dD|h4{U'㯯ݨ,$޷?g[kcgkȱ3XYWc<-f79G!h͍KdF _ k9-gׯ_]6~Vm{M;ƻϺ?dBǪs``NqPn G:JxJçD28!.xϰӇpPJwzX^YOs-tx:1T$s?gΜ5M7)9-FF8^L|)~@Jpag@A2"z&jsWfq%zrPޞ>[a)p]^``gֵ|=MDZXoկ]?}Qvn]NQEW1MEH73xacc, Cy(DHJyM.j%$'cxbޒ7Rq57ǎ׸sS^KEON)zz~G%RxR@#dNznF(uY`LW:".%k\!14ȢWԯ2v7VG4&3]lprjʸB$JJ OH9= Jwɠ$Aac$ⳏtQD{\KCę^^Eԁ 8Pg&jF2dm< 1!V=|?3K#m鈈VaBSSI!4BQ2= [ žU_anqXTP?¸7?z拡&U}DD0"$E0qE?6j8y0KJ>_) @_&PKtK 0u9i}=(IܪoJePaY-z8u Sא=qBZ}-ok҃>vU9m/R-9׵'+$E]taΊ?A˧pv C eבsF˟PG~G*bdweJp2;?_-n=5l:9C CJW*Rj ZCCC&fK!H'ۅc/. %= ޷$rzCP{dgQeQ#GȆZHOZX&W-C&ӫrp@SAjI惑g2^)V{a1[<@8HgTV.4'P 5bdW[FIûCSex‹˜`x~Gs==[ױ]y9IH*^;2}_O{KRk(jw! _[ӫ Wgg>6cE_~Qr3jNJ֘Jo_pl| IDAT͓Ͼ7&Lķd aTQ.QZ_i(Qw{M\3qѷ[4rP,|mn\'oU$~[~Mפ箮 TTGofg@~Bb"X2%_gS$m崈™pOT@e][k lvG@Qt·!`R$%ȴO oM#8 ;N,7:ovnbfY~}܏-́T; 2o\ڀj=QcF#dqĪh|32{GD䨨q9sਿ&( l#uF?ڃ1DyU@$Ž_(2`E(oQ9h9ޡy`mPA4N!Gj % gM`z, Mc"-4Cۛ[DpV`}S.TpԤ3+#ט/W| >(J~5`g;n5BwB__+3 O8i٩ R8G["gӚdMtDG&UZϾozw/Ό;.Q8EPhƈ@wbƔ2;a%9iEZ-ssaqh8ԗTi@ɸ$S4 25+-kI.)(M*BF!b~ ܓ UT~ǐ|7?H^;Ҩq\à mjDp-K;-W0Otv&õ:<1W􏴚;GÓׂW]G]e~gg󡡡ϯ\>ҕ^tzj\5Oх+BGr[\Y\mMϠ13&{Y}4OQG}gqRir[ VԄ@ʚȆk9oKGej&%ӹ*!ޢDD_tDL<"ңDZh@ 9a h$vRi8ҧga{x"rEGPAYگ2 aGu!ZE "Ύ"{ tH6o;s@b&=/e1(:m%),)C;:(3 vOT׾kRTg~ @s/׫<ώ4}M9^wz|\oM)b7<3{chb}Q;9wD*a7,DJx<_)qC'S/k}qz$Ufgku:\)ëCŁ /7W g} mjfFKxHfC~zn Y1# :+iɁtUop RlԦ.Jϙ;A1Q!ڍνZ0 F= ut.~j36j"9:ؠmbUu~ ɧ\PPTHh R@+$<z:z^BJ{ G|MMFd>,j[xq%92%?997Swb hs8| hK;šK%߻ XXzsyM{{cs ?m>2b9sYz hiŤ)*U}{C[ZA*A4mh4suMjj\C ^ 4F.lxQs}{\ IɗbRX5Q!`s5k1 C` K&߆91%is4ש[iqh¸ǜrN\D͠ =;wQ ^Ft⹯|[?/cH'hi>C Z?)_UO>V槔[#%1#DRa,F~KE`_ r69#.xz{`C*"K oxɽTU֩"n^Ej(ڽ1rb@xdn {;}=}r\sL)xki p~^J+ӏttP"3s2"32"ڑیx"~Ԫd4~ᜲrUcSl<t YAM@*: %* -8A"ⲧ/uh`Xo'Ғ Aш0 æx9'z1p{(Gޣ$n՗MmTu} _,@P'o, S1OQ} 'k$B56NJRK&[ii#%Q!59Zm0D&~A VBb9 Δĝkk}W#Ӈ+(ι%t=pZ'Fck07ء-lMjO1'*q?rƕvogQ)D$MTq9Eq-!ecV?^n!5tS7J(9ʻnyPa"QڑkHE1>O:6u4xl%OZH$0#U=n QKG1>ye5ӳ\+#U (Bϊ1$Yr3Z]2MT\`CFs+C.nOC<ɑw%i>j3qh/siBKT yq P ^3Pc3v%JV9g]dwBe*ٻJ9$t.L-5=䧍Y;^| KO-;|w?}y͛S#5ٳۚ֝n|#6!%c7D-IZ> KcC2n,ݪR>*w dX?#u)h,-.VjcIn@ l.`w$\M@ƭC='tP3@wWc=`E:tZ$CrIG[ye] |Rg-b$" 0Dw֥n N̄a QɄі~ykqdI JD^51 zc:5{_6PU K4h.,-:.rDT];12;߮mtrSs+(C݁?ZzۡcMM5OU6r?yjbo楳ݩFu{Sjnr=0,UJDY+Ƀ 5F4OQS?"*B\na2BF؄rK,!'qn"Mڅ*jdAn=l5:^t=1  xQ OE $ܻ |䔑s8#&vYQ%Tơ7Rȴi;ܑK3}'ST׺T=YM> 5k6E[v7҉kf/c-Ժ'9[x Zֱib%"ʘDD31QdσLlupN1\ydZƍ{-agQ )irNQ\_QZ6ӯyJ0ens s4-|(1}H73ߡ {@ZaxkI^ٗMFK΢y~` D0t(;σcWmK]DA .hrIK5]0Y1R zo-g꘹5}Z%3Osˎ!`$8g@)ÀExsEC0.®u%ݭ% \<48j_}TƵK /{b&#m=g!h3:u"]'{/2-ձ1StUMa!*[U6FXFÍjǨeYJ﷧L@5}_.1" dGn("ЎB޼O  FNldǐ$A9 @z8$"-\A6b%oiU[*㈌ WKֳ pqPPW#WChIFiHU;(Xi/< }Elý9{HrzRlnIzFM%$kwZYE-*3hS?SRLf8tz129舌!hݩuK0k M'gߞ[|19yUyS@2['>?~сjkh\Wi$ٗ($AJb- ATۺx*cHvaUsr"}#]K]+1:QE{4xǤ-4@U$Y#ߵMa,CxQj &1)5xT7 9Z! ˚]'F:(ECV3pH݋^_գO<3-*,c4&()7^ "(7P(4jX)yYB%"p=ΏRr7%i[D,KhC`-XU  56#t;XŦV @{,ϕU2wEBigܲR1W8ye^)~hgC2}>J PL6q"AH[u:>q/hE2Ե0 =d #R݅Zfs +yٓ 0@ȀC9 *382SdzMӗ IG!4 "Ź^YYڑZBu{9>}d=vWx<녅3qAAv>qٽ[xPі/m^zo=¢"7E% JPhL0BoH UC)-@jDWX@T6g=mg̴ iIs=! QW4;x)YdNGEӹg)w1>:+/G%bC/#Ԩa&FD- Z1g*$ړd}L-m^lP "Z b0=XȊK&^bpg"~x=y8尧1lOad$X"cJ͇<Q;;? CTTm]7AG xxnB:lnG3z!tLL3 q,gaѾi. **H~3]G뿯ErlRĂ~kVw% cGsd[Mj{HFo?>dd}M%;+9 tx"H[CkyiE?b8ݨVzky0x GɭS;xAcd_7U-sy^$ȴXb#<xkIP3$ӱ"#v4s 7 dW'2H#'Y/eY곤5rcJ鑞_mkkUWGސw0,t ;(g!h3;ZQ+ΎP='SaA:.~ y'UwJ >~ڊ틆vUBUQnW`+);;9;SY^]iwF1v)`RYY?{ˋeEYG41fa.ܣB~*OeiHHε+czƌy>5-2`il92+3A"6T ݒ'jr[neg쉷&玿뼾񺨡xQJN>z H6&[Ȏ(;n_}_غ}Y]U}؄QDt"e7BIO @RDNFtp @~JS%SEB qP/,,fΒA"`A~Ďj2jO]",cmB2w@:\"N\Dj,1*ad?gJ9Cf_}-`$ipE< u:Tsֈ-$7 #; 29yH#eGۻA}ji7xSIR1-D]oZ7{ŧ3Q[v`[^tgJAa a rZL+r|&i̛1B$\846 tKeںvM82Z)/c0o Z\NJ} YD ^-ZGHv:7*ՑhK3NE53Jۡ9Py״SE'NÌ7ӤRJѢkE$0&-- XyrG|>Ai: J/|F^5R|ڽu㪧;o]h`mPܮVe^ѴOO男)),% "\MeWm#%64pK:+Жa}Ogߧ+~| Z0 Eaj#nmy,lomN2غ7 ć5գP~UO8 'Oh#r2%g?[</ϳ'1]BF0,.kHF¬# Nz@ pbQ.2I" VzjE4hMt|c?&T"'[EZyX\:?-x_kӽtU<JEͶZ׺ &ԅlg`5Iy+b#0Pt0%24ΌXY^;vDڽ[7Ƈb6tDx*c-Ȫ}s.Ֆ+A"#_F]Dϑڒ4eŐq.J*f"QHo.)wMzuֽPgN{:I!I՝-(;l948ASiD0,T硞h6'-r ͂L_ٓ/}ic8.Z ZI"ߪJ`N7L=\;O3(E+3rNfd@?)" h1zkʲ[S74i0D>.hɭ@qַ֗/ɝMjP}TÀq*cژWב!,pڒ؆wPCէ'ME+` 0L%t|GRePEYj hI"Hc m&"`CO]C%n\9׀>GNw[a Rg9psM=1a}2t} Oϻ& ~Eښ/7o\wdhU;3GN^E5 _$hc9|d/ b pIN(A*PXD0A=zh/K~ A׊}qq):_9"_9/zϏyuK U+X(ʩ9?#(7i>;>ɳrT~ +/5ЯB򛪣VYҸjNW'u~0,jW\)5wBMP'dkT\W~\[U!dď8b-.ⵅR!YZԇ"YpnqK^zKDzp..(Lk0.Dz`bVCeBA2dNjVliM]7(aadqh<)ۗBuhtay~G{SvxU{GXD\f+ /=' JAWFy_F]=Y-'kquGÎ%. @}y"s SRgꕗ^âu}o쉓ʎN Nu#;p;ڿ-$_ePaF\CiuB'RoQeEŸ,n/=&QH]F.\z0;=~38vWΟ\[X\^vM.*=73[P֮U`EM#z6sR @K<6Q4FƩ"1xp!QȡpZk-Jp_SQ3 sAv|᠏v IKGmgzU9 S‹kap`ԓuPvBbySs ~IŮ{0{NwY 1#R/ ڛW($Vp-c6`XMFR O{ nt5n@KF0B,9yIeDG/B]*M˘ʘ~򕛕<ȃ}RA#|D>ޠmylEK^L IDATEN ((Pm3Eq] `e;#S `g%0+k"6hp$tT(!S8wЖTvyԑ{wR2umךc,__Ȕz ;(̩g}0)gۧS*g|:Z;1)j` hbb 'JJN' v{!M>͛ɢ%.ԯ*q5H?gweYyVso+7&Tmk{M-M0 ?RaH9: >\ N1q$ CƸ(^^7P+%!=f8vGO`t "T*diizgPD*>{+ߔ*{sklߑErNq7]-li=#o L )SSx`˚pfV {ORgѡ'ŶNJuGia\F^=oȱ4!Zdw\S<rb(YJXhBɊB'+@鱚#'CZ8#-%s䴼76gź+!=,',T!`OMWqX:J1Υ؃J˺F(yc =?AVZ38F"ކ&FM1 eTΫWxn8~{ꮻ+2:L>pgzv;=8FeAu!v8wD'rAs<(YTq8:9Dm7x 9)Q{B}(YFZhW/hqBD/OsG^]@Kz@+гF@'ЊFh')bZ|CSa[ 1~W/lNCn؎pAC[|_]ǎ8+GNRIՆ&[/kjV!/ŗ/{ԐR[nx~"cVsyO`ceR cqܪ,L~ɰDI=2:Y(f6BFeߺ֮.dC bv"EE(l Ԭ,;_|98ELA fZ3GFDyڱqFp9 c gLwXEUsgz:-P_<ѠYIChr1/Bq=sRͶ:M,N/Vj/~?LZm` f-|(B17iRm}_xVʂG%M+?,4r _ LTJJ+o7Y\CdBj&Hj놤n"Zf1R\->+Wɻv_z;{쑣%*5P(T5$_9i37  tO5'Eq߼ ?"/ .sqb9/C0d[M(*IQiθ׋`EΝ'π^1 N.[.@{&iz)( Ek"Eh͢Py]KׯUWV$/.ЃV{K WEwtU{j_~SgD)jW,[&Sv~u8-^)q wsL;;e[޽yBW/_XP_UwFwߙ·T2[>dxC:22뢡0^/sEJqD($>]dQX@K5m USBdzΜ[Pgӭ#ÎX! "Vx8@*=a4G"9F|֜Yɯ%mQr5W/Fp# w6hENJmǹ)sc4- EGg6!d| էIyO$U3\^RlUCޔbcԘ_"I#k9U`ZScGi*k*7>6xY<y!GnW!ʃ#iEX_^_<]J[%C.~+25{wV cQ% []#O2Nw7=٦h~g?AsDJ/7Ț4GS߄@K̈Dm>jڸX;Hw8X*ds@k :x 4),qM8~3-KNxqUzf Y(nBftㇹ@!e`!e~z]Y+C>5}0~Û[;BfvxCG1>JM @uܷ nZYurF]бߍ7͵swzE8gT[}!Fb\j80hd|#;Ȉ A({wg :(gCh(CR0ݦ*c? 'o\߀-p `!`Nh's[aș =QǎV%WGFCů{W^筯i_ VqfҀ@K՗Pg g/}XwosTZQL=/4E* 926RPB=jddN[} %9Œ D/u#ځ9$/G.58nPr$BE tnm9nHT$-"՘wc%.bh{Xʼn'6 g]5R 9k|0s_ySMgYC)~]shsϙ=edg~$ɮ4?:"#,U-P `fvGO|yQp_$w8 `ֺdVeVT:Q֘Z ~;㚠z1ZJU5PSUM'[!}GS܆ lk.g`Z3r|Ʊ+akIWZ9Ahq\hu.6+T^خ{׭x+17>OsH;.[qpCU%6Yb4g]uݵT!ҿ*Ac fF4%fd0 ^ 8%5&/S4msi;fw4㥇65-kC^Y2P7`B9-It0 c?" =LbE|=DTTͽ{ηȔylc巧`|eBTQ!i!6sb3*:UANhPp'r4\t:2UZ|sm#ɘFϷKib U㬾^>k!zZNPhJx4I|6ba o8`L m΢^KTak1].c |>NNGYQNue;o}t{SL=N0( DFv k4TrU`(\{g)K& / W5]BآZW]3zC!S,ƹ\Qу}\Ȫ\ݔ07@:+[50ؖݫaYra4\p{@\dQ ̍o'p=klA8 _6W^}{3˙Շ=&1u3*2PFP@l G뭢ki ZTTvMJ-xZ:h}6 WșޭKMUY(tkє\G放Odȉt]ak<5S0WpU[^st7y[.Q;nξFs%^{Vh98>QĠu[ENJ>.*!$@gI!n]jH}Fȍ[GTP-$@ Ք캋 8ޞ z!*C$tR|Z`b>2{!ѕ, [c@ AY^{!>@` Vog֒ml1HQé̥̘ҹ٦V1M2Zչ9u&M{w czrRQ2TF:'o?P)iOsqAD6zH9i. wf:S؛{x>](?_TqbM^z<Z=RzU8:YEZ%L2g<ݓs_yZ&TʊU'$kV)DX06}2b Uf.l*dbzhTpf4~F}$Ug@j>)w( nZSdpLō{ނn pϪ>6 6qymn+ޯҦto؆&+7 9&`՛eI]兡:~}к,7nZE=iBb]E*Yn Nic kV>zĺe}}?*-NȪEAsPeŮS\1:*)n\ NZuHrlSY+ק/%W38=wůBQr ^l!sd2jʤ6V}TD J9fQ;dHiNK0M442_z(| o}hC\Q\f9LDQ b_'- tUqe+C%M]3{Լ6 $KnaӮUSA@'1QDqBMʩ$R*TD-0gj<_rߦD,82ÀO u?7v;7Jcl hA3<ڀ[(߬ iN1m,}XSQ8U6ZuK?S.RF?ŬAТ @DQb SSQ:Kl;m纨;2W-qO1ӷƞ*)!;qkĉCZ[}WUU=%tp!a'2K Dk-jG9LQM/vӌ 7Q-d{CMpE=_ IDATZf:0 xѹִmFs `SdTBfeb29^+φ;_i|~\U4g.@5{tǶ㋅>(jٖ& FK sN?ܪWWY1uYU:=D~fة각ss8g?PZ4f;*Uj_/'Jm06y;| 594'ЄS+\0*PE(s,UA]\416h[iZFSpKA]O%A1.XMhZql* UY4*&x} F7Q5QSL=QԔR|*.r窚~#P*CX (3lNi+ZL| 1`So]GZ]ykIbLϺ>߯^3/oܽ}|JZU*o"uo5p`YhfqZw\k \Dehf[A~8m[J0뷞e+씓 @~ E틱_(]DӤ[xJ`T ld(ۯ{!g_ ([ 6٘õ 3oiS -i2$GGsW74ʡ&J˂h}Y@BewW hW𯬨uZWmr#ޫFqԱjn b=D-tHfZOX C5@ʾGn&Тl@N)*}@_B <=DϛhwNSYdMd ᘏ:^+-2;:8I~Ѽo@5^ަN:F*.-.L >UN+[>t~?Ǣqs3̤QCCN4@YB"B ~1Ech?1G߰q+iUq 4F+3בּu]eI.*eb-씙2.ZMY1:^U8⺖*9WQ1v\.MU on87g3|4 i: Z>py.o\y%GK>V U*ӜS4$+4Q,Ӟ`eE".}֩WsmPMF= Y4p:-ѻ*-kơUpt0l9^^OXp.ÃPƯ%(ؽqp{ iF۠7>ii>]{G= 8@<*c }Ch}Nk*U,P^<9&-QIDs|YrڣTr4hUWYR@}y,u* RU(%+ca^.Shmaw 7qBmn{-`1za}A'3[0ڽ2D}e}reGq]i^\`Qb0<1yY/{q8X^@'D \}BnCtU[Atm.w] QW0ly*;;.4^n\ ~PpN*O~Gðe{-m(UHYoXz .NneEӲm%D)qشh1S\RЯOΌz4QR)-&ۥ?iFA}YZYnw`.,ش &w:k򶽹eܓ>K,7L-?ν*2 ^OADֹ8Pji.UQޫx ֵ.IL"ӟ䬥chྕkYIQLc0 - )p7X͟}%~Fxm"? yLJvVۺEΛo~1+s7an *suUWiV3 (m@*2{2sWT%V=T})RM!EUpM,Gs@Bzic5K vk=FpG3I(aG5GPk(7J49+v`[1Э\6ǽM% 'هw8,X+9{Yc &\5cqsx ,`zomw.%GrQNZ*&ধ.Td2'9 yf 㹫_dC)ڸ$7-L>gkF!+Lm.we ej6&s3]lZtJ*8C֚L@c.q;n7S?M2v<E9 }xθ=O_] }>Ct,g!J\ '*Ii-h=- m 5HzS<=Etr篼\;nZTrD2*-e=`?_T|CuY8DP䳼UxFAQƮIuYh9'i@E%̵^KJ]oUgZ6ׂ3gmnD ~@*:>osci&&~pxttk{;5PXHc>ƠQdzeBPiIg]| n|~}̅G/ cq_ˇrW8NQܼ/t}&i^J,t8}f("k"kbUVs4&A|q˒`rpo:0UsB2F(Q=P,bUmiZuD.UiDӭf0hŪd1n[ױH`]_FvO>v6KaN(G=s4SO7 i=%ڈq܁sG%2U:р"] ?0KP24tn]DKIαXso;y톍rv'xQT(լb?u 5{a2{*s08m&km$ٹ嵀A}Xˇ^q|#<:6O."a8a*.JWTۂN71Ub~o NРqeuh V뇲9CQ\%~fכBMǀ6"8ezĕSAӦSdrHdcĩТ jςDޢdMW F W48Zvhii\C,K/yw$R1@c:0&Т(>Pw\A@̹LAd]^ٳ+fskBA<4@u5դC5S$=ds@Ն\1,4a a:C>ivSK[O}髿^xgǽѯʙjN9!Z\8x'1</`W^ZSŬk}*si3A- +O_MN'oWTŹ:%V:وZIkDeL㹳pa}U*̈꽣5985^auvUi~8O&[̂~A 1jă{zN٥j{?$MS-ѻT9P5ikk*JJ賔,;+S-vqSNp~B(e+Y蘻xO\~O? /p]~rtC1~]GB5H䋑nfИt,~&Wwu ި?\G˄3ؐUEZkjpW4G?1hz5?T\8F$.:Oc(fnX(zJrnn,%=N} a/U*IC{4BDNbk^8;9n#H B999}5Wt-L}ZNWD qG]5{IR͗A˕^ꍢ˹ÔPA?pl(FK^=TL2a3V*co*Z e^UR0 T_T 4 dзB,..jqi>{m}&ޢM ]3,hWd,':`|WZ4rT)uv}'ׂVUdURSuwTv*0[ KzzpUK,/ +gV=K-M*.OP9ENq0T$^ppX *OzԽӯ\xB&H&-/ k<T:٨mnU{x)&FZAš .܊ULUyvhC40{(Aե&We4ؓ36bݺUM9  Q?+l)M#Ag琢ܢ:AbhN{,t!OKoa7 ΤSR#F[X H`FiVW\*Y~G|xB=Eh\Rt [[@`Vd:󭢇&dž,bWw&*ec{>ӷ4ߵ;*4Y6i/{;I*`]6cOWPtCrpb#>( \ GND{e5Ô-74,;+c(s´:9hAs(b6A5-*[Ҵ8Y K 8`.m"L7+ՉBz"}A'>!kqEEZWSο?|py>6ܺyÎCit$ina18BP%CkoZiS*;Ny/UJDpf̭`yqRDX(Gl!QOpf_,A)A?JBNsg,KV=ۮ{YtB2R[O}Cjw@"kUJp+"cm|T:r5hC(vFURyzzʨA9RHӲ<'%t|F{E!0 1Vl\Yr+Y^IE5&Miqf'q״fuc|w\NAkPT_a.h&Jf #$ѓGۣM?0.ot`Iˢ6?g~ Agx{PzX&V,bmnL8U(LB9Zn"h ϷYy_;ROAeZ /36# IDATT&kW/(qsB d8(XP' z. K*jw0γfW)dt$Z4!䀌pcAȹa2*L 8)8E-qԬOzuNO,6]5f֨v[Yi_/}Ph`m9-{P|p54W'^.ckkwwѹÃww^_K ;Z1;9Cܲ5jEE'Kp GBU[c ȸ+$*A_r\:#5) "q Kdk<97^j؎UiQ<,{ͱ58FGt=thqmuy 㩡nr onT+oM<|/Ĭ.Z,ۥ{Ynl>TiZGLѣ=%dB!$g-j'X=l;Z"%st\BC$I{%斖(WQٯLԨGG4= m۸̤n1UVw{_Ue*zx>A>8복Gzb?<:+ʄXIhFlFg^DA:! إi1\b׸t*ОS]Fcn@jkA՗_SO?lm܃.0m¹3KL:]4aO=_ B`Q.ODa6(LtNo6Jͼ5_ϩNn 9E `O`pNs7nOTSfm!Md R)TIhuJR.rxMƭOuh"2 h>f1+Ւa1FVsZ,?F%PWMl}Ai|E4. VU%S^mm xBLuP!n*W(p5 ߪwFi㞓렀o;(*'ZEǓ% >z輕D1NLI-Fm=(zɴBgfff2XnV?r̦5Hm27kILw+cg[z}F` Z>cp-ׄRIذ.Rlrͺ@Cr*3ɰ:L#rɏ~Xjod C6,cd֧ѲIf@2a̯C #4TJË!1"h55,d,['*zX.6ES_̉exMV?ҢV""7lUXY[3>SV̭GAI#‡2W 2zw\O223|GGɾ.)UuU{xq@ש¢VXOAc(^=Ew/Bs31d 3`(JNⲼhy89ST?/> {v_"ЂJᯮ_'\j9"($Fh Jh4r!MZFѫ.KG& TZB Qc>54w@Q`Q@Rƾ9@96VڜEz3RyvD:u{߉= nҫ < #Mt3tcoB|-B9uk;46궎rU Je"vk00NUA}/wP֮z.:9[.5aC&=9:Fš\C_FH5N]/\SYxTstDZTܤ=9Jz  *T@4tpDlh]޴Dk>k4GiuH7Dy辇d` ;&$q")i%I%pJ߰VtVьjHH1Oq!)N<6@շ٩[ x05ZXFqb v~(* |ZbE,mE= ܾ<ȣ*f 33.s2ipfgt?6 U4VI(`Љϲ8hb$(*57߭긒 R%@(JT3*`E-*Wc:Fs*&]k桁&7_1FQ歮}>>ԜY/d "2@`NE4҃Xn7` cV]`zP*qRBF}1.ZO.ݶQP ã $Rnv²|&HbeXɈ ~ٴtX:cVck4Qe В ef 5kӥk:YadP[l`Ovoa9%*e!Ie@بONܺԍYPzd$X'(t drSNT6ٓzjU@ԯA*m.12&&'4>2ТﱠC{+Q`hz^*@)JGpӱ(hQI4 8UTuԸd'V&eDPqEM`%zLؙYhypETiqU&{DaآKk|[p\ή( 5K[>lf 2 ZiG\߾sX+rR= xz@@͘b W ˲UD*MPy*/,Ή̳)34$UĨdeҌN!i!Z+l3[3r*0lU &'ʙ7 l#`ŋ6&p2zǪѢ|^M@KN=m8ZpfG2S3&Ċ%4sabtx2LLN_v_v~L)# Tik9R^tyZ-Amd<5v;kÃ]2Pi8ँ*. hg5x1K~B5*(2/J"h+/CA0rt0iB.6Z ThRՑPuPPb^e戺g+Rh4{!l2N(+bW 7RnTTKSgI%^g41I߯L%4ձJ _abk[RQm ]ZLN$b'R$BB[k(uF)hz{sa*82` &V!I()IHB,.6MC/[ LSA>%VTX9se\Y-G` Z>ӗOO`^xʛN f#\`K'2k'% xd$rXgv̩J`&oޒWvD 8end Z)ω֋Ƕ鲽)O!a\ÔT6Tq[I@ܞhqnb.s^_I-?NkE2pgoi2D`p+#I<,7𧌧$rʚYa6#B Cݼel9p )$"3 L)VE R;3jU(dx/W75>N{7\Dg*ZoD~nS׃`֮lVfflN *k.:j\O#f4kI^'ˎb-t)©£ZlonܙY˴ILGC?2 t@i4Z"-E@\hT͓C(ܥ5ᡇ.QjSTfa{Td?f]I F0D&V[rB(;K6yn=Jdȩ4W1vUw [g4!J*=:n(؀]LȼBջ*uvXçG8n:"$Id=ȓj*,4Vb媱NύvFdz4F;ftBY=W'/5-m(yh&g* h?+2y+Os\26Ge-E~7[[QG!&a =5qZ FPj@|x!j-6K[L]+lt3HEI*p[7V^ߓ1hbt0}ܽfJCzC@Hn48PNoY^;;UgA(.$ QB*g{Rjh |Hn+BX3 xBX-+,P9Ry?Am"~fsuZ68uiNҭ[wl,/-NBLn-LM,'> H;#x>Θ5r|D-[s'xY*Ag1PSIbث I ^0s4PJJOHV } 쏤~}*v뛒45,< l e=9]6rr6Eea839V(MGs :7)gXjp'`434.=ExZE Hͱr3ॲ s(u@Jd浪3>Ⱦ54(k}aC.CUTּy p0*9ո1Da<2k]Y7ޤ9*ZUp=BoTq/M֨\@{Tt"I$F hՁUd& >985IkSS| iU7 WOrG1hOOW_/~5uvG~֍O{Au26@ܒJPW@K;YF1#):;./aTZ.)n.} ?HXKN:6"yd:jeM~kTrɞU&r-ǂ}26~ZE?@>Q\a*h2ViΝ`8lA ^С@:be.-):.S@ QzNCCu&qYHgΜ뗣9Ђ#>K$>lwY8t\\d89MΣ L snUJ~vkLHqO/-viZF'|zyVcKiWm,x Va*D Xsg{jL:2r|bp2d+\ &+EA\5r;BE @\`xJ=eTY9`>>!3נyC](Xap;m]W<ԓ Tr9Q:-Gt4]4D-#m Oa۬sk(UrOOrfq&q1ik ^ÐMZp}naإ[.O~7'.`hIU*opm4Ol@h95/Ne! a"hAD熃孭;Z6LUPvǤll1xRD5 rb o߼4XpƤ{{8R^\^~X7kcu^l/v5a H:]YEOU!`W>ʬAkCV%^ »vԖpBFQ٦@4x40|p41aq|_I3TT7e| IDATsv9vf|kZP /\āB2T\l։ ZɪQՑj?*Wƀs˥>0لL`DO>nD ?[EzӪfO +VsXwG1hG|}s*S7Cm=L0k2 5d "6aM[`E91ťNZC0bL˴9@`\RTy 'F ;}#ĠEDAQ޳e:~;o:rx:4c8_<ݾWMGJlM ҡ벽CsM4-y֤ʚޫPD}=Hv% 54Cu#?M6O|kwPJ+Ut +Uo-&%bc?PE *t X^~-WlnrZ%H@JTLY&hGWSsVTEŀ"}-#g@bbj;@"[$oa߭cj5sogNhRT Qj-947W;8;GÚPuKl]u67k2}qFhq}[D(@*85+\u&A;nH}8> q.//b)Rsi .D\u^?@`Cz( nUlxU4b ȁWƔAzrQ&0U(M -Tn/Ut S{ĠEs=g}wnU.R!1C_C9^10+]vwi <G._BwGML+":C{kw&AQJh'hay2w0DdWW (JPʊDr&z(׿Ѽ;:Q兗^W13G}$JGiA44"Jw2it]2XC^泞0͑ǶFܣBP=D+V[@@B_xgr%H-瓲<7?uPǮiyI[b 9Td=IRMy=~$]^|.PGBjgoxȜѵ/TMϜ8jA/'sHRơw!ڍz=BB /Pc3czF4*&b/qҪ0oG)0'a>mzU0ASc)U"C=lȵ/@E{49*^K!_5?j}{unl>WL,doFx}֡͵h*/TaOt@rK' 1L߃t7mb~M2*΅8RYE.I+6,|xʕYWUSXd[켞c}(؟BS[_<1hO?x63ۈS5q9%F/[E|^[d3I.K/ g A4_T&ƤXbY.xYPX(wcoJVX l*W+dlh)3k'`/CS a5Be F%Dzc9c*=Lzg$ &@EYpKVS!.@[sB%y2L9RIg2! ە钄ddoau-?T葇/}m񨧇:K ~Ha Z4I54W@ΡԋF ءlp"G˷9+Rz\yxĠEmBK̰̰ո/~+en ZFhTC-j]kOtշGd8xKIjȕm @b)U1.=?|w[вT!Vo˗/ӹ~̀lE^8wAi^+ɱdaMMU3 0ׇhR;đxW^s nzHgy"$ mʈacm` >՘ npW fVi9j7l &jDLjJoW}DTr#<]'T[O;9}~˗OS_\Y9{WPekvT- p%'߹Vv/yx#0-f|dbO򛹀~P/`c' {BΕL䡄dҕЈ4ۡ`wbm 2Rb]<ec{;܂okOD8WOM\OTOܩ* {/ WUmh`U`NpW]|8t4u(-:1t!ہK3~ *XM$bdm dΡ L>@0-G!x}\mX6M8;M{՛(f=jlEK(/ &dpMn"wyPs&YCơ4:? &#tGwn_}ko ~ua+b*U9ԌOtf:c!:s-GNܪH$M~d›BfR'&hRqϋ4䮄3[pZ[rsڨXj.P; ,砥_52UmfMFB 5cUƅah>P˨2R n[Uks=_51D^*5(bD/gk a^41,Tsg\tCOx4~,:.U0{%5 i3!m)MFALSH HvΧ&JTf)O.,dl>S|뵗ͣk,<B*Z,o\B ?8ʲVZƽ݅v;׻]tc{G3Ft^ҙ̈7f(IUaG, ֈ)g5SeF&2(ͅpFA";?>0K7=qn3=tV?Hۀ\ n)xg/VD7ymÔ:U^eL{Z#0-ZOlQxdOPąs!#~'h i>A?힜TRo/_OLTj/==tYq.y|H1:1hu$v_UQБ TqA!l Dc Yg&:!8|4pPM=ars2׭z=.޽LCד!r66֤QEN%wC-m!4Yb J"VJk 잉mō{=ol~a\ \G6 N3"cE\XS) < PU?s߽ygQg.I :rтZ4yt/wچUڤ=M5>.K_ L@S *I[)Qҵܙi%5Xw Y# pȖ36޽v Kw컬 ogW߾tkgw%P|2UZxj<촉jb,DsP(AYOB1Ĉ`Ur ]XpP x-X>sFqZ3 I49݁J pC_\X̯ ̛ߒ4!ֺ8qUřsɟf~%Usbeƕڙ&A 3Kg9ν4ٕf/F(ˎ' bnFO`EFGn ;Izprbr8<ʀa&]QR}刹Qz> }L!/$R$Y\i|<@#;c ooj\K5PiSGg+'o]}/<4(xDSOqS+BGZf%ڦJ $([[˖ΩC 캚s(q־ιPu)Ȕ.aMP#V1zTiTeJ@),v.A YӖ(l:d6C4&8K4qAWqQ[{f]DmPzn iF !J&0n|Ğꕪ鹓`ԠSExdLIi z7a!+@еC bza/gStrС)F0Fyk8-rYRJAv#*CMPB@gu}[v6Du =Xu_Z挞@憯⩊rO ~yg /Y0zo̕*nj^Ierbʾ5k I5Jކ6)[k^YxU!R%y VLN@^VƓt2%(A5rrflTTBYA ,,u.io<;;G^R2޵3S}+0mǯ=`|G_ Mf)f ri Ad&N {e/vV3q3,(wIaQG},ˉ6R}? ɇiqDHb%Tj:$zc]d hN(UbS|F !+u:IZ-{wg/^ ݋>[,v;A2=0IWF|.tEi}4G` Z>:.<9f)Z+B =uA' BVD{17?:'gzdS߅$Gf8H+V.0$nJo&]`@%w칑3L$عKqEh-v{p3a?xs&b"~~vVjIU*V{D"܏2D$5ZBC AD&~H{Ϲṁm5J0ע0; \>EDg3 8ıȓ'qWW]x%lFitUm*#bW-Q)*TLx]nFF%0i1|-X1=Ԥxpyy)-@a)EK/RGNҩ Ѳˍ-P#,C6f9i#'HުJdn}g(Z#6;W.|76vdjZi4Rii"bqx=тPA 8`Va R99/0o_ *݄Dv.;}9!UJQ*Q*6!~Ugm^g?e~T$*[ (88qDEz}CCbû"5 ,tGs 1xqx*Tb0kQʼ# HxTSHxhDNP)t65{ CIћϿ|bc)w)5'jy;ߣ}L==nn=k%<VЖq_ Ie Haϭg0?*Û :8J*x]Ld ޭ w:Lu.C+W-5 ;dffua=P.$qhҸP uMlMN6`liO-O>ݎAl:Z/ވOk6Gp ɼ *NZmTV)$IJJ/%zϡ8_yW"-f#kQ+I36آ;B7ze; . 6"o\VĒ8u}-hy''lWbcpIxЊ-BF//#ߓWCm>WiEb@_ 4&FGPGMt:'T6/bgwo"ǃXLn%&+v"m/[]x D*s+SMDhUIo߽s.^ݚ1|(NrnLD ‡zގ:<*@)"HԴNʈ_ "TT %N*++UIHPb@'1KN ӈolA,k'v] jGkx=*0&l'TYJ "``Fc '=qv $}=^{UL.c-!%nYXzZ-B{n*;LfUaX9kmC%qyT.cHj^Bz7Ʀ. F24#@Cj+sMQ?̿п=0a&@r_}>~G:I_T\h:bTkLġ:!񴵵QaX[bam㏽ǒ^k#PSt-+;V&LeV Bg+32HKLSmҷCѻKWut6#s\6=;i]#5,NO8Ì9ʢr AŘb5'Q"鱅Rɑ.|eR(e|FxMe1Ű94͇j3xmYc3χd315g}jsƋS#7aYD斴M2sh' @J@n?xxy ny- kcm1u.5*VCA UZ<|ErIDhVER1;X.9g(U%AV50[GΓ,boUK*:%UB.+Tyj|B})☘b!8dGB(yef&U_0 ^n'=(Yı!*b"xM{Ċ@_#T+Z]8[.˯{1=<9u>E畼W| ;o_{ѸR *@4YFyhZ@6YD42Mf%74p#![ '*S%3q}%sZo"䤃.NA*Dmaja ]⹇?8"笠 rHAV;o{*DUqVI"Jo5w04_>gE3_p7LX'ضm*Y7Mij^`T ]Z`-\5ZAh%dXtH 0Udf `.gejbbdPHauZ4[f2mTG"}K8O9UwOEߺyU:bY>@Rט~2D::2x=TY4k.V- VOZ ^i!6|J o߼bMZdh\*uN{G׈C^"=kӊ'淠xf=P[T%Im`%Zm fE[8~RE.\#DB ߸y@G RG+V0XtlLTƨ2xGdl'WΪVNkxE$0̇h?*舶4"FZCWʑpTޱ_^Wvz\3PWg5]bw{ݰ{^őD 1.Jqǭndl./j"nD/&dTHL?zA+9;#N&-4HTl?~V䎻 Ak |U_ U#K)W"Dc;w0;fWeWY7UņsB/ג`!\-;ޤiL1=j) ftʛoӟ+D7WPK1(}@Ƞ\[rcEWR51 DQmBbY2L;ǚY\A[f)}V]C7i*pso7/\d*C:ʩlj;i Z Fi-3f~VCh Ξ*ڟX`!#[%T jB|/n>o+5'7bLjBIT_Fwd򺏖,:E!htB(jf6$n,(NNo޼]*a]17JJ!3F6 =1ᐫ0qbQ#Z܆rnw_]}tܕ x]_.ZZ-(>@{bo+p.;N:;@I"#f6|3kb뤼+3sU2^S8f\/N0rU!:2% 9/Ur2#Fm9-̈R\݂AiWՖ8GF1C= *<6VBy- C`߀}%$UTZ0lnH1=4̝c?犿“ /Y|7ޣZ+nv=EIT+aOiލ-`Ic:(Z'MK˽8][x,d= c{IZWfj sY1L=7Ktu-=3*7 Ͻq k" 2/[䚵A[*5$eA+o]Lƪ7@;QZ9}bXOXIe˅p%.bU) 2beGm0F>(IBAiUh-}["zy[[]|kvnV7jQGjZ""b(_P=kCTK 219-[TeMT&$2yՖ渨 7Dh]%- Oq#hKo<+7.H􋕵eYv]10":^ZD$HEC"bS"#BGlq`_DD ^w5 @xxLmHnҒH[!mika$ shTcRǔ63 z673^خea5XSSڍUbJ(q@{G&hnT6&"ϕT$sJ[A1 &g&,84s͏f)n_cAL {0Yc!Ne(³+ m L:{xOa!BQmAs!L0֭ C3bBL}dDH&=FhD){Eێa<]]{n-r7,߭u) *YHܞ.Yċx6=U f 曈"FȒJ|Q*U,bwFk*~({ˤЪDȂ@XGs4u_mChAl9mJoaNL >ՍMs'N>]8TKǐ)7F15wW'_|' ge9pI;pƇOHh90/%A!%ՙU,8QT ̉ZKd2Ŵj, 3Ao6)ɔ˥$)ӥ1mX^;L;3 ca^-.3l&n>hE Քݭ_+)O0/{ZzE2*Yo@7n^xUKA$Hk!&%ī!mJ DFL(c%j*29˩F&Uu Ѫ%m`<iֳ!s"$mπU-CDN:;%L }lKKLnl!iEkc]]{ $! f :(8ͣDB(Ɔ UC׃|1(.d۰{~!Tq5h0}σHh9x)&2$fm@q,x/FF>ILki;q uc04Vp▪ɦ .xMjyQޅ~w%eyO6Bq{0-Ql &ocZ"Zn9l{u$A,^, t taL$Y/"J`QRg2 R)TJDݯDLibj$""dDcj"5V*uy1O̩j%ka@@u[?y3+"zû˥qba! !KC2&H%e&Sz(dw3r[OeRAu,- OH`_h/ø-(j%&cAZ͇IEՓ(7ZxB"_GZl(V,%Z"F<^Y}}Y'zexy>J-0{cL"7XE1"&PBBەTGjHZZ^򨴈(Q5e:=AۙxKRx iqL77$~# Aȡ)=6-{p"$]Fۡ8NJS:Df93&$a먶? - @ jJ Nh}na!ywQ!*09p?zlj( /{-]ϛHY'@B<>؃؝B&;& ފȓ8aQ3;"B{ N49l׃[.pU& n}6"|ψU0^ToA(tHC,CEDHD*+VTY.Q$Ha ["Qe jTkHIo)s>DLm*cgmTx N .?S?wsm ̏1I4z Z,5%J^rCؗ:jL=vfcV1lYlي6Oڧs_fe?ҪW!8=$@$uhHqHHk$P-"ZD-Lx^cFϭ=^|Ojc +w@Hn1h?hf3[CDKq334**[@puZX. V&$UM<0dcfiF_6s_'-1g|,HI-$]>6 vZטX^|7[ꦰcQMDC]=b?J0IVh۩mqtn.*:"$rH !ܐ J%>Y1Rꉈ,c >EdQeQBEsT[Z,мE`xIDATK#ɹ\A vmD&abK!LuQ *r#S8NۇqɏN/:330Ye-789) Eˀsw$@$.Rm59,%;|[_]rN'_5\ڂ) ~)ꖚ501!/U4b6 t ",<-*"bAJ?B&C,i](ql1~ZR $D̈Q_Z$xAkch¥JKUG a2jQIqM";D9RiȬF06:2y gGfz"6*&,cuNBޗH-C  詶HHݥEsZ-Z[^E??5ڹX"7 J S6 FY-Hb_"C,]L%"B&&GJt!6Zl/s\P|.JEK¢&գL-a {D:2 łY-b.Z6>1ݿw_K'r%mn~⥄ֳfǷ4mxUZMySBH\GJĀfRgFř]߸ |l$hN[ǡIƷjos$@$P׋GK$p-bzZ/;߽~o߼z8,/laq̄wŖ f]LleZ!EJ!"* q bB *&F.$渐ɪ0( 1+F˦ķRSۊOFF1حe rLŌIj+(,6Wʨ ~GE%gtm mLd`L Y,q[ CCT^4lӂBdv܉3rG>ŒqBtv~tI R DRߤ1J޷e ?zo^zT*n`mݿs'ٳՋmkX+!g+0:P$l] H6V1āaSsX(C Y*+mxYbe2I:nCP_ Pʁ8I)!)_2sEboeB[AтV(L ӬI0PR5+!|/P cd҆&55Rb#zMZ̠ϴt6LiJ Y8uq|bҽ]FrSREsωH*9 kfHq4w6]d7OW2v#nsPk/$*{Zހp<# ZY ڱL/|BE<& ]x\`쯣2RI!ޗ6.ͨ.y\^Y>IT^Rђ~Ea}'UR<@bȰI^Si DYF[kg^Z=zb̅X"{[2wph$XWXa׿ٻ#6CA @`F%B"X ՘8BG+oVk&T"L2Q+23EbEh J)f{ʈQ^ b/!L JGeJKB,"zHr0u8l9@Vk'ӺiڍG3'N]LGbͭJhʻ"Y"TEDzNr" <E O  'vn1/W642m9/&rΑ{?i;Mx^բ%L$p!Z8*$M4a,c oa%|.^K-Q=C8 c1oDhT1PύZ]Ya-]$9/@TnXw=GNn3fS3Go ;{ke@Q"e#k褼pkF'㦩hZ.?yѯW{ ۂMx(oR J!H oVŻ_޹˦mժ#aY4vZ^Ba%a Ǯ"u WO)+{h+;ٶaZ'μܙ{=! , L2վPQ` h2B9 <v HHEץxn.TesՇwJ3|˄ ~j6rFqec cݛD ;aH02yiXfUV,'m+cqLQf`G jΊ[L{0'ўڒ@ V Q \6Ξc7f&ܰIJsGH% <$ :P|$@$[ #\d~ݢ:o*'.]0[l* D-V Q橴[^(M''3#ؑZnd*FC[]//;Ѽ~l߸re PH%U5TE"e) j10`,u+ bTwlry˷Ə9+!!>bcH2ˇ[8K AP>HH"˜K% iN;o6޸vYD,?Fq\2<mtQEX&^8VTU1.ok:h kz = a"nhz5tpzv!Uzli+X#3Wz[``M nO$@6gё W&GE"GWs ۞5s++>qlŏFj~}x<,POfD+oUO-V g (NU}ٍu-㺓C$b+W rFFeih-H C&H3@kTF̥g^KatIO` %0>1ka0L%0w&>ɓ ʍzV?znMim=L&w3NVۉQ2r6c1OL-.1+N> ܽRNcNbÍҥSϿ|)72*KR*,%d`dF=O| 8(ZΧL$p EE>G]e,."1ED#w14N?}xsomݥť濾o|;Չ#se z|C*omsbkFقI߄%tD9{ѱŋ[N֞dN'M}_SΝx¹J9p0|$@$9-<)HH,ϰwA!\,x]tRO8[7 +/=#_0w/y}Z*R2[y (Ç:N"#iNNWJw''Tk0܈!~Q/^\v[w\s󚋅˜IXĄa )HH0h9L6+ "9?/d}jFu) :P42^eݦnovвJ]2-ӯJT*LFx3ẏ%g˛[rm)zmzZ2 B'jgϾy1뾯o*;EGʍbP|$@$Y-<#HHs-\eLDKni6 _z&ZЍn6nWRm1+&[Һ%BEHUD/?15hAcҒB.h,!ZB&T"}޿Ր^!]) +' =P  PD /"dJϝv=XH$@$4-<7HH~D '_Jw  EK?Ը BO.OHRrO >e nHHHHH#@7nE$@$@$@$@$0 -ݐ G?n܊HHHHH`@(Z!    EKܸ P 4wC$@$@$@$@$q+     hhHHHHH?-qV$@$@$@$@$@"@2 @(ZƭHHHHHDe@     PǍ[ Eˀ@s7$@$@$@$@$@h"    nHHHHH#@7nE$@$@$@$@$0 -ݐ G?n܊HHHHH`@(Z!    EKܸ P 4wC$@$@$@$@$q+     hhHHHHH?-qV$@$@$@$@$@"@2 @(ZƭHHHHHDe@     PǍ[ Eˀ@s7$@$@$@$@$@h"    nHHHHH#@7nE$@$@$@$@$0 -ݐ G?n܊HHHHH`@(Z!    EKܸ P 4wC$@$@$@$@$q+     hhHHHHH?-qV$@$@$@$@$@"@2 @(ZƭHHHHHDe@     PǍ[ Eˀ@s7$@$@$@$@$@h"    nHHHHH#@7nE$@$@$@$@$0 -ݐ G?n܊HHHHH`@(Z!    EKܸ P 4wC$@$@$@$@$q+     hhHHHHH?-qV$@$@$@$@$@"@2 @(ZƭHHHHHDe@     PǍ[ Eˀ@s7$@$@$@$@$@h"    nHHHHH#@7nE$@$@$@$@$0 -ݐ G?n܊HHHHH`@(Z!    EKܸ P 4wC$@$@$@$@$q+     hhHHHHH?-qV$@$@$@$@$@"@2 @?R%IENDB`hishel-0.1.2/.github/workflows/000077500000000000000000000000001477404575600164065ustar00rootroot00000000000000hishel-0.1.2/.github/workflows/main.yml000066400000000000000000000016411477404575600200570ustar00rootroot00000000000000name: Tests on: push: branches: ["master"] pull_request: branches: ["master"] jobs: tests: name: "Python ${{ matrix.python-version }}" runs-on: "ubuntu-latest" strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13" ] env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: - uses: "actions/checkout@v4" - uses: "actions/setup-python@v5" with: python-version: "${{ matrix.python-version }}" allow-prereleases: true - name: Start Redis uses: supercharge/redis-github-action@1.8.0 with: redis-version: ${{ matrix.redis-version }} - name: "Setup uv" uses: astral-sh/setup-uv@v5 with: version: "0.6.12" - name: "Run tests" run: scripts/test - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 hishel-0.1.2/.github/workflows/publish.yml000066400000000000000000000013031477404575600205740ustar00rootroot00000000000000name: publish on: workflow_dispatch permissions: contents: write jobs: pypi-publish: name: upload release to PyPI runs-on: ubuntu-latest env: HISHEL_PYPI: ${{ secrets.HISHEL_PYPI }} steps: - name: "Checkout code" uses: "actions/checkout@v4" - name: "Set up Python (use latest version for publishing)" uses: "actions/setup-python@v5" with: python-version: 3.13 - name: "Setup uv" uses: astral-sh/setup-uv@v5 with: version: "0.6.12" - name: "Build" run: "uv build" - name: "Publish docs" run: ./scripts/publish-docs - name: "Publish" run: "./scripts/publish" hishel-0.1.2/.gitignore000066400000000000000000000000531477404575600147770ustar00rootroot00000000000000venv/ __pycache__/ .coverage .cache/ .idea/hishel-0.1.2/CHANGELOG.md000066400000000000000000000126141477404575600146260ustar00rootroot00000000000000# Changelog ## 0.1.2 (5th April, 2025) - Add check for fips compliant python. (#325) - Fix compatibility with httpx. (#291) - Use `SyncByteStream` instead of `ByteStream`. (#298) - Don't raise exceptions if date-containing headers are invalid. (#318) - Fix for S3 Storage missing metadata in API request. (#320) ## 0.1.1 (2nd Nov, 2024) - Fix typing extensions not found. (#290) ## 0.1.0 (2nd Nov, 2024) - Add support for Python 3.12 / drop Python 3.8. (#286) - Specify usedforsecurity=False in blake2b. (#285) ## 0.0.33 (4th Oct, 2024) - Added a [Logging](https://hishel.com/advanced/logging/) section to the documentation. ## 0.0.32 (27th Sep, 2024) - Don't raise an exception if the `Date` header is not present. (#273) ## 0.0.31 (22nd Sep, 2024) - Ignore file not found error when cleaning up a file storage. (#264) - Fix `AssertionError` on `client.close()` when use SQLiteStorage. (#269) - Fix ignored flags when use `force_cache`. (#271) ## 0.0.30 (12th July, 2024) - Fix cache update on revalidation response with content (rfc9111 section 4.3.3) (#239) - Fix request extensions that were not passed into revalidation request for transport-based implementation (but were passed for the pool-based impl) (#247). - Add `cache_private` property to the controller to support acting as shared cache. (#224) - Improve efficiency of scanning cached responses in `FileStorage` by reducing number of syscalls. (#252) - Add `remove` support for storages (#241) ## 0.0.29 (23th June, 2024) - Documentation hotfix. (#244) ## 0.0.28 (23th June, 2024) - Add `revalidated` response extension. (#242) ## 0.0.27 (31th May, 2024) - Fix `RedisStorage` when using without ttl. (#231) ## 0.0.26 (12th April, 2024) - Expose `AsyncBaseStorage` and `BaseStorage`. (#220) - Prevent cache hits from resetting the ttl. (#215) ## 0.0.25 (26th March, 2024) - Add `force_cache` property to the controller, allowing RFC9111 rules to be completely disabled. (#204) - Add `.gitignore` to cache directory created by `FIleStorage`. (#197) - Remove `stale_*` headers from the `CacheControl` class. (#199) ## 0.0.24 (14th February, 2024) - Fix `botocore is not installed` exception when using any kind of storage. (#186) ## 0.0.23 (14th February, 2024) - Make `S3Storage` to check staleness of all cache files with set interval. (#182) - Fix an issue where an empty file in `FileCache` could cause a parsing error. (#181) - Support caching for `POST` and other HTTP methods. (#183) ## 0.0.22 (31th January, 2024) - Make `FileStorage` to check staleness of all cache files with set interval. (#169) - Support AWS S3 storages. (#164) - Move `typing_extensions` from requirements.txt to pyproject.toml. (#161) ## 0.0.21 (29th December, 2023) - Fix inner transport and connection pool instances closing. (#147) - Improved error message when the storage type is incorrect. (#138) ## 0.0.20 (12th December, 2023) - Add in-memory storage. (#133) - Allow customization of cache key generation. (#130) ## 0.0.19 (30th November, 2023) - Add `force_cache` extension to enforce the request to be cached, ignoring the HTTP headers. (#117) - Fix issue where sqlite storage cache get deleted immediately. (#119) - Support float numbers for storage ttl. (#107) ## 0.0.18 (23rd November, 2023) - Fix issue where freshness cannot be calculated to re-send request. (#104) - Add `cache_disabled` extension to temporarily disable the cache (#109) - Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated. (#111) ## 0.0.17 (6th November, 2023) - Fix `Last-Modified` validation. ## 0.0.16 (25th October, 2023) - Add `install_cache` function. (#95) - Add sqlite support. (#92) - Move `ttl` argument to `BaseStorage` class. (#94) ## 0.0.14 (23rd October, 2023) - Replace `AsyncResponseStream` with `AsyncCacheStream`. (#86) - Add `must-understand` response directive support. (#90) ## 0.0.13 (5th October, 2023) - Add support for Python 3.12. (#71) - Fix connections releasing from the connection pool. (#83) ## 0.0.12 (8th September, 2023) - Add metadata into the response extensions. (#56) ## 0.0.11 (15th August, 2023) - Add support for request `cache-control` directives. (#42) - Drop httpcore dependency. (#40) - Support HTTP methods only if they are defined as cacheable. (#37) ## 0.0.10 (7th August, 2023) - Add Response metadata. (#33) - Add API Reference documentation. (#30) - Use stale responses only if the client is disconnected. (#28) ## 0.0.9 (1st August, 2023) - Expose Controller API. (#23) ## 0.0.8 (31st July, 2023) - Skip redis tests if the server was not found. (#16) - Decrease sleep time for the storage ttl tests. (#18) - Fail coverage under 100. (#19) ## 0.0.7 (30th July, 2023) - Add support for `Heuristic Freshness`. (#11) - Change `Controller.cache_heuristically` to `Controller.allow_heuristics`. (#12) - Handle import errors. (#13) ## 0.0.6 (29th July, 2023) - Fix `Vary` header validation. (#8) - Dump original requests with the responses. (#7) ## 0.0.5 (29th July, 2023) - Fix httpx response streaming. ## 0.0.4 (29th July, 2023) - Change `YamlSerializer` name to `YAMLSerializer`. ## 0.0.3 (28th July, 2023) - Add `from_cache` response extension. - Add `typing_extensions` into the requirements. ## 0.0.2 (25th July, 2023) - Add [redis](https://redis.io/) support. - Make backends thread and task safe. - Add black as a new linter. - Add an expire time for cached responses. hishel-0.1.2/CODE_OF_CONDUCT.md000066400000000000000000000121531477404575600156120ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at kar.petrosyanpy@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. hishel-0.1.2/LICENSE000066400000000000000000000027251477404575600140240ustar00rootroot00000000000000Copyright © 2023, Karen Petrosyan. All 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. * Neither the name of the copyright holder nor the names of its contributors may 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 HOLDER 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. hishel-0.1.2/README.md000066400000000000000000000127011477404575600142710ustar00rootroot00000000000000

Logo

Hishel - An elegant HTTP Cache implementation for httpx and httpcore.

pypi license license Downloads

----- **Hishel (հիշել, remember)** is a library that implements HTTP Caching for [HTTPX](https://github.com/encode/httpx) and [HTTP Core](https://github.com/encode/httpcore) libraries in accordance with [**RFC 9111**](https://www.rfc-editor.org/rfc/rfc9111.html), the most recent caching specification. ## Features - 💾 **Persistence**: Responses are cached in the [**persistent memory**](https://en.m.wikipedia.org/wiki/Persistent_memory) for later use. - 🤲 **Compatibility**: It is completely compatible with your existing transports or connection pools, *whether they are default, custom, or provided by third-party libraries.* - 🤗 **Easy to use**: You continue to use httpx while also enabling [web cache](https://en.wikipedia.org/wiki/Web_cache). - 🧠 **Smart**: Attempts to clearly implement RFC 9111, understands `Vary`, `Etag`, `Last-Modified`, `Cache-Control`, and `Expires` headers, and *handles response re-validation automatically*. - ⚙️ **Configurable**: You have complete control over how the responses are stored and serialized. - 📦 **From the package**: - Built-in support for [File system](https://en.wikipedia.org/wiki/File_system), [Redis](https://en.wikipedia.org/wiki/Redis), [SQLite](https://en.wikipedia.org/wiki/SQLite), and [AWS S3](https://aws.amazon.com/s3/) backends. - Built-in support for [JSON](https://en.wikipedia.org/wiki/JSON), [YAML](https://en.wikipedia.org/wiki/YAML), and [pickle](https://docs.python.org/3/library/pickle.html) serializers. - 🚀 **Very fast**: Your requests will be even faster if there are *no IO operations*. ## Documentation Go through the [Hishel documentation](https://hishel.com). ## QuickStart Install `Hishel` using pip: ``` shell $ pip install hishel ``` Let's begin with an example of a httpx client. ```python import hishel with hishel.CacheClient() as client: client.get("https://hishel.com") # 0.4749558370003797s client.get("https://hishel.com") # 0.002873589000046195s (~250x faster!) ``` or in asynchronous context ```python import hishel async with hishel.AsyncCacheClient() as client: await client.get("https://hishel.com") await client.get("https://hishel.com") # takes from the cache ``` ## Configurations Configure when and how you want to store your responses. ```python import hishel # All the specification configs controller = hishel.Controller( # Cache only GET and POST methods cacheable_methods=["GET", "POST"], # Cache only 200 status codes cacheable_status_codes=[200], # Use the stale response if there is a connection issue and the new response cannot be obtained. allow_stale=True, # First, revalidate the response and then utilize it. # If the response has not changed, do not download the # entire response data from the server; instead, # use the one you have because you know it has not been modified. always_revalidate=True, ) # All the storage configs storage = hishel.S3Storage( bucket_name="my_bucket_name", # store my cache files in the `my_bucket_name` bucket ttl=3600, # delete the response if it is in the cache for more than an hour ) client = hishel.CacheClient(controller=controller, storage=storage) # Ignore the fact that the server does not recommend you cache this request! client.post( "https://example.com", extensions={"force_cache": True} ) # Return a regular response if it is in the cache; else, return a 504 status code. DO NOT SEND A REQUEST! client.post( "https://example.com", headers=[("Cache-Control", "only-if-cached")] ) # Ignore cached responses and do not store incoming responses! response = client.post( "https://example.com", extensions={"cache_disabled": True} ) ``` ## How and where are the responses saved? The responses are stored by `Hishel` in [storages](https://hishel.com/userguide/#storages). You have complete control over them; you can change storage or even write your own if necessary. ## Support the project You can support the project by simply leaving a GitHub star ⭐ or by [contributing](https://hishel.com/contributing/). Help us grow and continue developing good software for you ❤️ hishel-0.1.2/docker-compose.yml000066400000000000000000000002171477404575600164460ustar00rootroot00000000000000version: "3" services: redis: image: 'redis:7.0.12-alpine' command: 'redis-server' ports: - "127.0.0.1:6379:6379" hishel-0.1.2/docs/000077500000000000000000000000001477404575600137415ustar00rootroot00000000000000hishel-0.1.2/docs/CNAME000066400000000000000000000000131477404575600145010ustar00rootroot00000000000000hishel.com hishel-0.1.2/docs/advanced/000077500000000000000000000000001477404575600155065ustar00rootroot00000000000000hishel-0.1.2/docs/advanced/controllers.md000066400000000000000000000131601477404575600203770ustar00rootroot00000000000000--- icon: material/brain --- `Hishel` provides the `Controllers`, which allow you to fully customize how the cache works at the specification level. You can choose which parts of [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) to ignore. For example, this is useful when you want to ensure that your client does **not use stale responses** even if they are **acceptable from the server.** ### Force caching If you only need to cache responses without validating the headers and following RFC9111 rules, simply set the `force_cache` property to true. Example: ```python import hishel controller = hishel.Controller(force_cache=True) client = hishel.CacheClient(controller=controller) ``` !!! note [force_cache](extensions.md#force_cache) extension will always overwrite the controller's force_cache property. ### Cachable HTTP methods You can specify which HTTP methods `Hishel` should cache. Example: ```python import hishel controller = hishel.Controller(cacheable_methods=["GET", "POST"]) client = hishel.CacheClient(controller=controller) ``` !!! note `Hishel` will only cache `GET` methods if the cachable methods are not explicitly specified. ### Cachable status codes If you only want to cache specific status codes, do so. Example: ```python import hishel controller = hishel.Controller(cacheable_status_codes=[301, 308]) client = hishel.CacheClient(controller=controller) ``` !!! note If the cachable status codes are not explicitly specified, `Hishel` will only cache status codes **200, 301, and 308**. ### Allowing heuristics You can enable heuristics calculations, which are disabled by default. Example: ```python import hishel controller = hishel.Controller(allow_heuristics=True) client = hishel.CacheClient(controller=controller) ``` `Hishel` is very conservative about what status codes are permitted to be heuristically cacheable. When `allow_heuristics` is enabled, `Hishel` will only cache responses having status codes 200, 301, and 308. In contrast, RFC 9111 specifies that many more responses can be heuristically cacheable, specifically 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, and 501. If you would prefer heuristic caching to the fullest extent permitted by RFC 9111, then pass `HEURISTICALLY_CACHEABLE_STATUS_CODES` to `cacheable_status_codes`: ```python import hishel controller = hishel.Controller( allow_heuristics=True, cacheable_status_codes=hishel.HEURISTICALLY_CACHEABLE_STATUS_CODES ) client = hishel.CacheClient(controller=controller) ``` !!! tip If you're not familiar with `Heuristics Caching`, you can [read about it in the specification](https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh). ### Preventing caching of private responses If you want `Hishel` to act as a _shared_ cache, you need to prevent it from caching responses with the `private` directive. Example: ```python import hishel controller = hishel.Controller(cache_private=False) client = hishel.CacheClient(controller=controller) ``` !!! note Servers may prohibit only some headers from being stored in a shared cache by sending a header such as `Cache-Control: private=set-cookie`. However, `Hishel` with `cache_private=False` will still not cache the response, at all. ### Allowing stale responses Some servers allow the use of stale responses if they cannot be re-validated or the client is disconnected from the server. Clients MAY use stale responses in such cases, but this behavior is disabled by default in `Hishel`. Example: ```python import hishel controller = hishel.Controller(allow_stale=True) client = hishel.CacheClient(controller=controller) ``` !!! tip `Hishel` will attempt to use stale response only if the client is unable to connect to the server to make a request. You can enable stale responses to receive responses even if your internet connection is lost. ### Specifying revalidation behavior Responses are revalidated by default when they become stale; however, you can always revalidate the responses if you wish. Example: ```python import hishel controller = hishel.Controller(always_revalidate=True) client = hishel.CacheClient(controller=controller) ``` !!! note Because we already have the response body in our cache, revalidation is very quick. ### Custom cache keys By default, `Hishel` generates cache keys as a hash of the request method and url. However, you can customize cache key creation by writing a function with the signature `Callable[[httpcore.Request], str]` and passing it to the controller. Example: ```python import hishel import httpcore from hishel._utils import generate_key def custom_key_generator(request: httpcore.Request, body: bytes): key = generate_key(request, body) method = request.method.decode() host = request.url.host.decode() return f"{method}|{host}|{key}" controller = hishel.Controller(key_generator=custom_key_generator) client = hishel.CacheClient(controller=controller) client.get("https://hishel.com") ``` Instead of just the `hashed_value`, the key now has the format `method|host|hashed_value`. !!! note Cache keys are used to store responses in storages, such as filesystem storage, which will use the cache key to create a file with that value. You can write your own cache key implementation to have more meaningful file names and simplify cache monitoring. === "Before" ``` 📁 root └─╴📁 .cache └─╴📁 hishel └─╴📄 41ebb4dd16761e94e2ee36b71e0d916e ``` === "After" ``` 📁 root └─╴📁 .cache └─╴📁 hishel └─╴📄 GET|hishel.com|41ebb4dd16761e94e2ee36b71e0d916e ``` hishel-0.1.2/docs/advanced/extensions.md000066400000000000000000000070231477404575600202310ustar00rootroot00000000000000--- icon: material/apps --- # Extensions `HTTPX` provides an extension mechanism to allow additional information to be added to requests and to be returned in responses. `hishel` makes use of these extensions to expose some additional cache-related options and metadata. These extensions are available from either the `hishel.CacheClient` / `hishel.AsyncCacheClient` or a `httpx.Client` / `httpx.AsyncCacheClient` using a `hishel` transport. ## Request extensions ### force_cache If this extension is set to true, `Hishel` will cache the response even if response headers would otherwise prevent caching the response. For example, if the response has a `Cache-Control` header that contains a `no-store` directive, it will not cache the response unless the `force_cache` extension is set to true. ```python >>> import hishel >>> client = hishel.CacheClient() >>> response = client.get("https://www.example.com/uncachable-endpoint", extensions={"force_cache": True}) ``` !!! note You can [configure this extension globally for the controller](controllers.md#force-caching), rather than setting force_cache to True for each request. ### cache_disabled This extension temporarily disables the cache by passing appropriate RFC9111 headers to ignore cached responses and to not store incoming responses. For example: ```python >>> import hishel >>> client = hishel.CacheClient() >>> response = client.get("https://www.example.com/cacheable-endpoint", extensions={"cache_disabled": True}) ``` This feature is more fully documented in the [User Guide](../userguide.md#temporarily-disabling-the-cache) ## Response extensions ### from_cache Every response from will have a `from_cache` extension value that will be `True` when the response was retrieved from the cache, and `False` when the response was received over the network. ```python >>> import hishel >>> client = hishel.CacheClient() >>> response = client.get("https://www.example.com") >>> response.extensions["from_cache"] False >>> response = client.get("https://www.example.com") >>> response.extensions["from_cache"] True ``` ### revalidated Every response will have a `revalidated` extension that indicates whether the response has been revalidated or not. !!! note Note that a response could have `revalidated` set to `True` even when `from_cache` is set to `False`. This occurs when the cached entry has been updated and a new entry is downloaded during revalidation. ```python >>> import hishel >>> client = hishel.CacheClient() >>> response = client.get("https://www.example.com/endpoint_that_is_fresh") >>> response.extensions["revalidated"] False >>> response = client.get("https://www.example.com/endpoint_that_is_stale") >>> response.extensions["revalidated"] True ``` ### cache_metadata If `from_cache` is `True`, the response will also include a `cache_metadata` extension with additional information about the response retrieved from the cache. If `from_cache` is `False`, then `cache_metadata` will not be present in the response extensions. Example: ```python >>> import hishel >>> client = hishel.CacheClient() >>> response = client.get("https://www.example.com/cacheable-endpoint") >>> response.extensions { ... # other extensions "from_cache": False } >>> response = client.get("https://www.example.com/cacheable-endpoint") >>> response.extensions { ... # other extensions "from_cache": True "cache_metadata" : { "cache_key': '1a4c648c9a61adf939eef934a73e0cbe', 'created_at': datetime.datetime(2020, 1, 1, 0, 0, 0), 'number_of_uses': 1, } } ``` hishel-0.1.2/docs/advanced/http_headers.md000066400000000000000000000047221477404575600205070ustar00rootroot00000000000000--- icon: material/web --- You can use the request `Cache-Control` directives defined in [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111#name-request-directives) to make the cache behavior more explicit in some situations. ### only-if-cached If this directive is present in the request headers, the cache should either use the cached response or return the 504 status code. !!! note It is guaranteed that the client will not make any requests; instead, it will try to find a response from the cache that can be used for this request. ```python >>> import hishel >>> >>> client = hishel.CacheClient() >>> response = client.get("https://example.com", headers=[("Cache-Control", "only-if-cached")]) >>> response ``` or ```python >>> import hishel >>> >>> client = hishel.CacheClient() >>> client.get("https://google.com") # will cache >>> response = client.get("https://google.com", headers=[("Cache-Control", "only-if-cached")]) >>> response ``` ### max-age If this directive is present in the request headers, the cache should ignore responses that are older than the specified number. Example: ```python import hishel client = hishel.CacheClient() client.get("https://example.com", headers=[("Cache-Control", "max-age=3600")]) ``` ### max-stale If this directive is present in the request headers, the cache should ignore responses that have exceeded their freshness lifetime by more than the specified number of seconds. ```python import hishel client = hishel.CacheClient() client.get("https://example.com", headers=[("Cache-Control", "max-stale=3600")]) ``` ### min-fresh If this directive is present in the request headers, the cache should ignore responses that will not be fresh for at least the number of seconds specified. ```python import hishel client = hishel.CacheClient() client.get("https://example.com", headers=[("Cache-Control", "min-fresh=3600")]) ``` ### no-cache If this directive is present in the request headers, the cache should not use the response to this request unless it has been validated. ```python import hishel client = hishel.CacheClient() client.get("https://example.com", headers=[("Cache-Control", "no-cache")]) ``` ### no-store If this directive is present in the request headers, the cache should not save the response to this request. ```python import hishel client = hishel.CacheClient() client.get("https://example.com", headers=[("Cache-Control", "no-store")]) ``` hishel-0.1.2/docs/advanced/logging.md000066400000000000000000000072651477404575600174700ustar00rootroot00000000000000--- icon: material/file-document-edit --- [Logging](https://en.wikipedia.org/wiki/Logging_(computing)) is an important part of every application that helps developers better understand how the program operates. Hishel supports a variety of logs that can show you how the library impacts your program. Hishel will support several loggers for different parts of the program. Currently, we support only one logger called `hishel.controller`, which logs any event related to the cache. For example, it logs when a response is considered stale, when revalidation occurs, when a response is used from the cache, and more. ## Controller logs The [controller](./controllers.md) is a part of the Hishel library that interprets the caching specification. It determines whether a response can be cached or retrieved from the cache. You can configure the controller logger for debugging purposes or to better understand how caching works. It can also be crucial when you're just starting out and want to understand why a particular response isn't being cached. For example, let's enable logging and see what gets logged when making an HTTP request to the Hishel documentation. ```python import logging import hishel logging.basicConfig( level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logging.getLogger("hishel.controller").setLevel(logging.DEBUG) client = hishel.CacheClient() response = client.get( "https://hishel.com", ) ``` Here is what Hishel will log for this program: ``` 2024-09-30 16:32:34,799 - hishel.controller - DEBUG - Considering the resource located at https://hishel.com/ as cachable since it meets the criteria for being stored in the cache. ``` If you run this program a second time, you will receive the response from the cache because hishel.com sends all the necessary caching headers. So, for the second run, you will see a log entry about the successfully reused response. ``` 2024-09-30 16:35:14,102 - hishel.controller - DEBUG - Considering the resource located at https://hishel.com/ as valid for cache use since it is fresh. ``` If we wait some time, the cached response will, of course, become stale. After some time, you can run this program again and see that the response needs to be revalidated from the server to obtain the most recent data. The logs could look like this: ``` 2024-09-30 16:39:42,502 - hishel.controller - DEBUG - Considering the resource located at https://hishel.com/ as needing revalidation since it is not fresh. 2024-09-30 16:39:42,502 - hishel.controller - DEBUG - Adding the 'If-Modified-Since' header with the value of 'Fri, 27 Sep 2024 07:42:28 GMT' to the request for the resource located at https://hishel.com/. ``` The controller will indicate not only that the response was cached but also why it was considered cacheable. Examples: - For permanent redirects ``` 2024-09-30 16:43:04,672 - hishel.controller - DEBUG - Considering the resource located at https://www.github.com/ as cachable since its status code is a permanent redirect. ``` - When [force_cache](./extensions.md#force_cache) is enabled ``` 2024-09-30 16:45:10,468 - hishel.controller - DEBUG - Considering the resource located at https://www.google.com/ as valid for cache use since the request is forced to use the cache. ``` Or when it's considered as not cachable ``` 2024-09-30 17:02:24,961 - hishel.controller - DEBUG - Considering the resource located at https://www.python.org/ as not cachable since it does not contain any of the required cache directives. ``` [Here](https://github.com/karpetrosyan/hishel/pull/275) you can find a full list of the controller logs. Note that this is the list of initial logs; any logs added later will not be updated in this list.hishel-0.1.2/docs/advanced/serializers.md000066400000000000000000000070311477404575600203650ustar00rootroot00000000000000--- icon: simple/yaml --- Serializers are a component of [storages](storages.md) that simply serialize and de-serialize responses. Hishel will use JSONSerializer by default, but you can explicitly specify a serializer or even write your own. Example of the serialized responses: === "JSON" ``` json { "response": { "status": 301, "headers": [ [ "Content-Length", "0" ], [ "Location", "https://github.com/" ] ], "content": "", "extensions": { "http_version": "HTTP/1.1", "reason_phrase": "Moved Permanently" } }, "request": { "method": "GET", "url": "https://www.github.com/", "headers": [ [ "Host", "www.github.com" ], [ "Accept", "*/*" ], [ "Accept-Encoding", "gzip, deflate" ], [ "Connection", "keep-alive" ], [ "User-Agent", "python-httpx/0.24.1" ] ], "extensions": { "timeout": { "connect": 5.0, "read": 5.0, "write": 5.0, "pool": 5.0 } } }, "metadata": { "cache_key": "71b46af84732856e5c16d503b655fcd0", "number_of_uses": 0, "created_at": "Mon, 21 Aug 2023 05:22:20 GMT" } } ``` === "Yaml" ``` yaml response: status: 301 headers: - - Content-Length - '0' - - Location - https://github.com/ content: '' extensions: http_version: HTTP/1.1 reason_phrase: Moved Permanently request: method: GET url: https://www.github.com/ headers: - - Host - www.github.com - - Accept - '*/*' - - Accept-Encoding - gzip, deflate - - Connection - keep-alive - - User-Agent - python-httpx/0.24.1 extensions: timeout: connect: 5.0 read: 5.0 write: 5.0 pool: 5.0 metadata: cache_key: 71b46af84732856e5c16d503b655fcd0 number_of_uses: 0 created_at: Mon, 21 Aug 2023 05:22:20 GMT ``` ### :simple-json: JSONSerializer Example: ```python import hishel serializer = hishel.JSONSerializer() storage = hishel.FileStorage(serializer=serializer) ``` Because serializers are supported by all of the built-in `hishel` [storages](storages.md), you can pass serializers to any of them. Example: ```python import hishel serializer = hishel.JSONSerializer() storage = hishel.RedisStorage(serializer=serializer) ``` ### :simple-yaml: YAMLSerizlier Example: ```python import hishel serializer = hishel.YAMLSerializer() storage = hishel.FileStorage(serializer=serializer) ``` !!! note Make sure `Hishel` has the yaml extension installed if you want to use the `YAMLSerializer`. ``` shell $ pip install hishel[yaml] ``` ### PickleSerializer Example: ```python import hishel serializer = hishel.PickleSerializer() storage = hishel.FileStorage(serializer=serializer) ``` hishel-0.1.2/docs/advanced/storages.md000066400000000000000000000172231477404575600176640ustar00rootroot00000000000000--- icon: material/database --- When using `Hishel`, you have complete control over the configuration of how the responses should be stored. You can select the [serializer](serializers.md) and storage on your own. This section contains examples of how to use the storages. ### :file_folder: Filesystem storage To explicitly specify the storage, we should create it first and pass it to the HTTP caching class. Example: ```python import hishel storage = hishel.FileStorage() client = hishel.CacheClient(storage=storage) ``` Or if you are using Transports: ```python import hishel import httpx storage = hishel.FileStorage() transport = hishel.CacheTransport(transport=httpx.HTTPTransport(), storage=storage) ``` Here's how the filesystem storage looks: ``` 📁 root └─╴📁 .cache └─╴📁 hishel ├─╴📄 GET|github.com|a9022e44881123781045f6fadf37a8b1 ├─╴📄 GET|www.google.com|8bfc7fffcfd5f2b8e3485d0cc7450c98 ├─╴📄 GET|www.python-httpx.org|5f004f4f08bd774c4bc4b270a0ca542e └─╴📄 GET|hishel.com|41ebb4dd16761e94e2ee36b71e0d916e ``` !!! note Note that by default, file names are just the hashed value, without the http method or hostname; to have meaningful names, see [custom cache keys](controllers.md#custom-cache-keys). #### Storage directory If the responses are saved in the filesystem, there should be a directory that contains our responses. By default it's `.cache/hishel`. If you want to change the directory, do so as follows. ```python import hishel storage = hishel.FileStorage(base_path="/home/test/my_cache_dir") ``` #### Responses ttl in FileStorage You can explicitly specify the ttl for stored responses in this manner. ```python import hishel storage = hishel.FileStorage(ttl=3600) ``` If you do this, `Hishel` will delete any stored responses whose ttl has expired. In this example, the stored responses were limited to 1 hour. The default `ttl` is `None`, which means that responses will be stored until the [controller](controllers.md) decides to remove them. #### Check ttl every In order to avoid excessive memory utilization, `Hishel` must periodically clean the old responses, or responses that are not being used and should be deleted from the cache. It clears the cache by default every minute, but you may change the interval directly with the `check_ttl_every` argument. Example: ```python import hishel storage = hishel.FileStorage(check_ttl_every=600) # check every 600s (10m) ``` ### :material-memory: In-memory storage `Hishel` has an in-memory cache that can be used when you don't need the cache to be persistent. You should understand that in memory cache means that **all cached responses are stored in RAM**, so you should be cautious and possibly **configure the cache's maximum size** to avoid wasting RAM. Example: ```python import hishel storage = hishel.InMemoryStorage() client = hishel.CacheClient(storage=storage) ``` Or if you are using Transports: ```python import hishel import httpx storage = hishel.InMemoryStorage() client = hishel.CacheTransport(transport=httpx.HTTPTransport(), storage=storage) ``` #### Set the maximum capacity You can also specify the maximum number of requests that the storage can cache. Example: ```python import hishel storage = hishel.InMemoryStorage(capacity=64) client = hishel.CacheClient(storage=storage) ``` !!! note When the number of responses exceeds the cache's capacity, Hishel employs the [LFU algorithm](https://en.wikipedia.org/wiki/Least_frequently_used) to remove some of the responses. ### :simple-redis: Redis storage `Hishel` includes built-in redis support, allowing you to store your responses in redis. Example: ```python import hishel storage = hishel.RedisStorage() client = hishel.CacheClient(storage=storage) ``` Or if you are using Transports: ```python import hishel import httpx storage = hishel.RedisStorage() client = hishel.CacheTransport(transport=httpx.HTTPTransport(), storage=storage) ``` #### Custom redis client If you need to connect somewhere other than localhost, this is how you can do it. ```python import hishel import redis storage = hishel.RedisStorage( client=redis.Redis( host="192.168.0.85", port=8081, ) ) ``` #### Responses ttl in RedisStorage You can explicitly specify the ttl for stored responses in this manner. ```python import hishel storage = hishel.RedisStorage(ttl=3600) ``` If you do this, `Hishel` will delete any stored responses whose ttl has expired. In this example, the stored responses were limited to 1 hour. The default `ttl` is `None`, which means that responses will be stored until the [controller](controllers.md) decides to remove them. ### :simple-sqlite: SQLite storage `Hishel` includes built-in [sqlite](https://www.sqlite.org/index.html) support, allowing you to store your responses in sqlite database. Example: ```python import hishel storage = hishel.SQLiteStorage() client = hishel.CacheClient(storage=storage) ``` Or if you are using Transports: ```python import hishel import httpx storage = hishel.SQLiteStorage() client = hishel.CacheTransport(transport=httpx.HTTPTransport()) ``` !!! note Make sure `Hishel` has the sqlite extension installed if you want to use the `AsyncSQLiteStorage`. ``` shell $ pip install hishel[sqlite] ``` #### Sqlite custom connection If you want more control over the underlying sqlite connection, you can explicitly pass it. ```python import hishel import sqlite3 client = hishel.CacheClient( storage=hishel.SQLiteStorage(connection=sqlite3.connect("my_db_path", timeout=5)) ) ``` #### Responses ttl in SQLiteStorage You can explicitly specify the ttl for stored responses in this manner. ```python import hishel storage = hishel.SQLiteStorage(ttl=3600) ``` If you do this, `Hishel` will delete any stored responses whose ttl has expired. In this example, the stored responses were limited to 1 hour. The default `ttl` is `None`, which means that responses will be stored until the [controller](controllers.md) decides to remove them. ### :material-aws: AWS S3 storage `Hishel` has built-in [AWS S3](https://aws.amazon.com/s3/) support, allowing users to store responses in the cloud. Example: ```python import hishel storage = hishel.S3Storage(bucket_name="cached_responses") client = hishel.CacheClient(storage=storage) ``` Or if you are using Transports ```python import httpx import hishel storage = hishel.S3Storage(bucket_name="cached_responses") transport = hishel.CacheTransport(httpx.HTTPTransport(), storage=storage) ``` #### Custom AWS S3 client If you want to manually configure the client instance, pass it to Hishel. ```python import hishel import boto3 s3_client = boto3.client('s3') storage = hishel.S3Storage(bucket_name="cached_responses", client=s3_client) client = hishel.CacheClient(storage=storage) ``` #### Responses ttl in S3Storage You can explicitly specify the ttl for stored responses in this manner. ```python import hishel storage = hishel.S3Storage(ttl=3600) ``` If you do this, `Hishel` will delete any stored responses whose ttl has expired. In this example, the stored responses were limited to 1 hour. The default `ttl` is `None`, which means that responses will be stored until the [controller](controllers.md) decides to remove them. #### Check ttl every In order to avoid excessive memory utilization, `Hishel` must periodically clean the old responses, or responses that are not being used and should be deleted from the cache. It clears the cache by default every minute, but you may change the interval directly with the `check_ttl_every` argument. Example: ```python import hishel storage = hishel.S3Storage(check_ttl_every=600) # check every 600s (10m) ``` hishel-0.1.2/docs/contributing.md000066400000000000000000000110201477404575600167640ustar00rootroot00000000000000--- icon: material/hand-coin-outline --- Thank you for being interested in contributing to `Hishel`. We appreciate your efforts. You can contribute by reviewing the [pull requests](https://github.com/karpetrosyan/hishel/pulls), [opening an issue](https://github.com/karpetrosyan/hishel/issues/new), or [adding a new feature](https://github.com/karpetrosyan/hishel/compare). Here I will describe the development process and the tricks that we use during the development. ## Setting up First, you should fork the [Hishel](https://github.com/karpetrosyan/hishel/) so you can create your own branch and work on it. Then you should `git clone` your fork and create a new branch for your pull request. ``` bash git clone https://github.com/username/hishel cd hishel git switch -c my-feature-name ``` ## Scripts `Hishel` provides a script directory to simplify the development process. Here is what each command does. - **scripts/install** _Set up the virtual environment and install all the necessary dependencies_ - **scripts/lint** _Runs linter, formatter, and unasync to enforce code style_ - **scripts/check** _Runs all the necessary checks, including linter, formatter, static type analyzer, and unasync checks_ - **scripts/test** _Runs `scripts/check` + `pytest` over the coverage._ Example: ``` bash >>> ./scripts/install >>> source ./venv/bin/activate >>> ./scripts/test + ./scripts/check + ruff format tests hishel --diff 26 files left unchanged + ruff tests hishel + mypy tests hishel Success: no issues found in 38 source files + python unasync.py --check hishel/_async/_client.py -> hishel/_sync/_client.py hishel/_async/_pool.py -> hishel/_sync/_pool.py hishel/_async/_transports.py -> hishel/_sync/_transports.py hishel/_async/_mock.py -> hishel/_sync/_mock.py hishel/_async/_storages.py -> hishel/_sync/_storages.py hishel/_async/__init__.py -> hishel/_sync/__init__.py tests/_async/test_storages.py -> tests/_sync/test_storages.py tests/_async/test_transport.py -> tests/_sync/test_transport.py tests/_async/__init__.py -> tests/_sync/__init__.py tests/_async/test_client.py -> tests/_sync/test_client.py tests/_async/test_pool.py -> tests/_sync/test_pool.py + coverage run -m pytest tests ============================ test session starts ============================= platform linux -- Python 3.10.12, pytest-7.4.3, pluggy-1.3.0 rootdir: /home/test/programs/gitprojects/hishel configfile: pyproject.toml plugins: anyio-4.1.0, asyncio-0.21.1 asyncio: mode=stric`t collected 158 items tests/test_controller.py .......................................... [ 26%] tests/test_headers.py ..................... [ 39%] tests/test_lfu_cache.py ...... [ 43%] tests/test_serializers.py ..... [ 46%] tests/test_utils.py ........ [ 51%] tests/_async/test_client.py .. [ 53%] tests/_async/test_pool.py .................. [ 64%] tests/_async/test_storages.py ........... [ 71%] tests/_async/test_transport.py .................. [ 82%] tests/_sync/test_client.py . [ 83%] tests/_sync/test_pool.py ......... [ 89%] tests/_sync/test_storages.py ........ [ 94%] tests/_sync/test_transport.py ......... [100%] ============================ 158 passed in 2.97s ============================= ``` !!! note Some tests may fail if you don't have all the necessary services. For example, you don't have Redis to pass the integration tests, so there is a Docker compose file in the root directory to start those services. ## Async and Sync Like `HTTP Core`, `Hishel` also uses the unasync strategy to support both async and sync code. The idea behind `unasync` is that you are writing only async code and also using some logic that converts your async code to sync code rather than writing almost the same code twice. In `Hishel`, there is a `unasync.py` script that converts an async directory to a sync one. !!! warning You should not write any code in the `hishel/_sync` directory. It is always generated by the `unasync.py` scripts, and after running CI, all your changes to that directory would be lost. Unasync scripts would automatically be called from `scripts/lint`, so you should just write an async code and then call `scripts/lint`. hishel-0.1.2/docs/examples/000077500000000000000000000000001477404575600155575ustar00rootroot00000000000000hishel-0.1.2/docs/examples/fastapi.md000066400000000000000000000035761477404575600175430ustar00rootroot00000000000000--- icon: simple/fastapi --- Many `FastAPI` users use `HTTPX` as a modern and very fast HTTP client, which also supports **async/await** syntax like FastAPI does. Here is an example of how `HTTPX` can be used in `FastAPI`. ``` python from fastapi import FastAPI from httpx import AsyncClient from httpx import Limits app = FastAPI() client = AsyncClient(limits=Limits(max_connections=1000)) @app.get("/") async def main(): response = await client.get('https://www.encode.io') return response.status_code ``` Now let's do some load testing using the popular load testing tool [Locust](https://locust.io/). [Here are](https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/fastapi_without_cache.png) the test results: pypi Despite the fact that we use **async/await**, we got only **±70 RPS**. Now let's change the `httpx.AsyncClient` to `hishel.AsyncCacheClient` and do the same tests again. ``` python hl_lines="2 6" from fastapi import FastAPI from hishel import AsyncCacheClient from httpx import Limits app = FastAPI() client = AsyncCacheClient(limits=Limits(max_connections=1000)) @app.get("/") async def main(): response = await client.get('https://www.encode.io') return response.status_code ``` [Here are](https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/fastapi_with_cache.png) the test results: pypi Now we have more than **365+ RPS** using the power of HTTP caching. hishel-0.1.2/docs/examples/flask.md000066400000000000000000000033441477404575600172050ustar00rootroot00000000000000--- icon: simple/flask --- As a `Flask` user, you can use the power of `HTTPX` using its synchronous interface. Here is a simple example: ``` python from flask import Flask from httpx import Client from httpx import Limits app = Flask(__name__) client = Client(limits=Limits(max_connections=1000)) @app.route("/") def main(): response = client.get('https://www.encode.io') return str(response.status_code) ``` Now let's do some load testing using the popular load testing tool [Locust](https://locust.io/). [Here are](https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/flask_without_cache.png) the test results: pypi We got only **±20 RPS**, which is not very good. Now let's change the `httpx.Client` to `hishel.CacheClient` and do the same tests again. ``` python hl_lines="2 6" from flask import Flask from hishel import CacheClient from httpx import Limits app = Flask(__name__) client = CacheClient(limits=Limits(max_connections=1000)) @app.route("/") def main(): response = client.get('https://www.encode.io') return str(response.status_code) ``` [Here are](https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/flask_with_cache.png) the test results: pypi Now we have more than **±800 RPS** using the power of HTTP caching. hishel-0.1.2/docs/examples/github.md000066400000000000000000000061021477404575600173620ustar00rootroot00000000000000--- icon: simple/github --- On this page, we'll look at why HTTP caching is important when using [GitHub APIs](https://docs.github.com/en/rest?apiVersion=2022-11-28). Let's create a simple program that takes the name of a `GitHub` repository and displays the stars in real time. To use **GitHub APIs**, we first need an access token. [See how to create a github token here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic). However, if we write a program that makes a large number of HTTP requests to the **GItHub servers**, we will be blocked for a period of time because github has [rate limits](https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api?apiVersion=2022-11-28) in place to prevent overloading their servers. In such cases, APIs frequently provide HTTP caching functionality, which we can use to retrieve the response from the local cache or make a new request if the data on the server has changed. To ensure that cached responses are not counted, we should also display the rate limit. ```python import os from time import sleep import hishel TOKEN = os.getenv("TOKEN") client = hishel.CacheClient( headers={ "Authorization": f"Bearer {TOKEN}", "X-GitHub-Api-Version": "2022-11-28", "Accept": "application/vnd.github+json", }, ) repo = input("Enter repo name: ") # example: "karpetrosyan/hishel" organization, repo = repo.split("/") while True: stars_response = client.get(f"https://api.github.com/repos/{organization}/{repo}") stars = stars_response.json()["stargazers_count"] rate_response = client.get("https://api.github.com/rate_limit") remaining = rate_response.json()["rate"]["remaining"] print(f"\rStars: {stars} Remaining rate limit: {remaining}", end="") sleep(1) ``` Change `hishel.CacheClient` to `httpx.Client` to see how quickly you are **wasting your rate limits**! When `HTTPX` makes a **real request** in each iteration, `Hishel` consumes the rate limit only once and stores the response in the **local cache**. Also, keep in mind that when the stars count is updated, it will not be displayed immediately when using `Hishel` because it uses the cached response; instead, it will wait until the local response is considered stale before re-validating that response. Because `GitHub` sends a header indicating that this response has a maximum lifespan of 60 seconds, you will see the updated stars count after 60 seconds. Here is what that header looks like: ``` Cache-Control: private, max-age=60, s-maxage=60 ``` Anyway, if you want to see the update **without any delay**, you can explicitly tell the `Hishel` that you want to always re-validate the response before using it, which is also free and doesn't have a rate limit! Example: ```python client = hishel.CacheClient( headers={ "Authorization": f"Bearer {TOKEN}", "X-GitHub-Api-Version": "2022-11-28", "Accept": "application/vnd.github+json", }, controller=hishel.Controller(always_revalidate=True) ) ``` hishel-0.1.2/docs/index.md000066400000000000000000000121711477404575600153740ustar00rootroot00000000000000
HTTPX HTTPX

Hishel - An elegant HTTP Cache implementation for httpx and httpcore.

pypi license license Downloads

----- **Hishel (հիշել, remember)** is a library that implements HTTP Caching for [HTTPX](https://github.com/encode/httpx) and [HTTP Core](https://github.com/encode/httpcore) libraries in accordance with [**RFC 9111**](https://www.rfc-editor.org/rfc/rfc9111.html), the most recent caching specification. ## Features - :floppy_disk: **Persistence**: Responses are cached in the [**persistent memory**](https://en.m.wikipedia.org/wiki/Persistent_memory) for later use. - :handshake: **Compatibility**: It is completely compatible with your existing transports or connection pools, *whether they are default, custom, or provided by third-party libraries.* - :hugging: **Easy to use**: You continue to use httpx while also enabling [web cache](https://en.wikipedia.org/wiki/Web_cache). - :brain: **Smart**: Attempts to clearly implement RFC 9111, understands `Vary`, `Etag`, `Last-Modified`, `Cache-Control`, and `Expires` headers, and *handles response re-validation automatically*. - :gear: **Configurable**: You have complete control over how the responses are stored and serialized. - :package: **From the package**: - Built-in support for [File system](https://en.wikipedia.org/wiki/File_system) :file_folder: , [Redis](https://en.wikipedia.org/wiki/Redis) :simple-redis:, [SQLite](https://en.wikipedia.org/wiki/SQLite) :simple-sqlite: , and [AWS S3](https://aws.amazon.com/s3/) :material-aws: backends. - Built-in support for [JSON](https://en.wikipedia.org/wiki/JSON) :simple-json: , [YAML](https://en.wikipedia.org/wiki/YAML) :simple-yaml:, and [pickle](https://docs.python.org/3/library/pickle.html) serializers. - :rocket: **Very fast**: Your requests will be even faster if there are *no IO operations*. ## QuickStart Install `Hishel` using pip: ``` shell $ pip install hishel ``` Let's begin with an example of a httpx client. ```python import hishel with hishel.CacheClient() as client: client.get("https://hishel.com") # 0.4749558370003797s client.get("https://hishel.com") # 0.002873589000046195s (~250x faster!) ``` or in asynchronous context ```python import hishel async with hishel.AsyncCacheClient() as client: await client.get("https://hishel.com") await client.get("https://hishel.com") # takes from the cache ``` ## Configurations Configure when and how you want to store your responses. ```python import hishel # All the specification configs controller = hishel.Controller( # Cache only GET and POST methods cacheable_methods=["GET", "POST"], # Cache only 200 status codes cacheable_status_codes=[200], # Use the stale response if there is a connection issue and the new response cannot be obtained. allow_stale=True, # First, revalidate the response and then utilize it. # If the response has not changed, do not download the # entire response data from the server; instead, # use the one you have because you know it has not been modified. always_revalidate=True, ) # All the storage configs storage = hishel.S3Storage( bucket_name="my_bucket_name", # store my cache files in the `my_bucket_name` bucket ttl=3600, # delete the response if it is in the cache for more than an hour ) client = hishel.CacheClient(controller=controller, storage=storage) # Ignore the fact that the server does not recommend you cache this request! client.post( "https://example.com", extensions={"force_cache": True} ) # Return a regular response if it is in the cache; else, return a 504 status code. DO NOT SEND A REQUEST! client.post( "https://example.com", headers=[("Cache-Control", "only-if-cached")] ) # Ignore cached responses and do not store incoming responses! response = client.post( "https://example.com", extensions={"cache_disabled": True} ) ``` ## Support the project You can support the project by simply leaving a GitHub star ⭐ or by [contributing](https://hishel.com/contributing/). Help us grow and continue developing good software for you ❤️ hishel-0.1.2/docs/static/000077500000000000000000000000001477404575600152305ustar00rootroot00000000000000hishel-0.1.2/docs/static/Shelkopryad_350x250_black.png000066400000000000000000000460641477404575600223770ustar00rootroot00000000000000PNG  IHDRP| pHYs  ~ IDATx}m,Y޳K7Ӏ{p"‘FD$°gCAFd"Pȇs)v!$='8>VHDsAvb3pc8QoM]]]]3sn=鮪za )Mgn{Q,7y~x]7a%^=nHwKx=$nEv kxOF20w {QtDv pbS`TMw&n&FW@+v_?1/lfxt'͛rU~vxt'mg7?]5£<NL·QtF-EEIEgFq3D8zX8.m֓sx9`sOLD.\~IŽܥMc5ȣ?<(Kc]t5F<8woSx=0:g_}1;jNP|>_x  3ߣ>>iG Ǒ sv[U*ieGDFQFQF} QtFJQ4Ca,syz U>AAw!&.(&N C/(>;xW@$$=9%~7- o{oV{ @D/ QɝmgXcIQg)׼s{xoq3@9<8 I?tOӧwy3%M_DʒSʩ ӈ<2y~m 9RT$OF{ͥPk=@=2 TCB8FԐ,!{ xp} ӫN 9l [4rN c0 N $31De9$Ǡ0Hk]9S:GRR?Ց*_8Rǐ1 (\TUU b`o@=F觨br\@m$Z.{>!3ubzxn ߠtA~4~ WzcLMM7y> sfO^e@=FB.&(Hh*Ǩh"l=KBS as@A$Z6( <~wKV8P c0P T؆8/ #{5sAl7|:+xp@6bAoS] u^*pO`Agj|6D"c J`xx2 ԣ7j)B_@ңID32oU{ MaLwI [B"yޣ+i@Ԅp\]b&b.WYXveWa=Bv) 5O8L)#fj'(oQj>c[M~5E^(Viq/=QT[n 1btuY0bɍ\ґLHÌ> ƃɧhNUEg(' sJ1;u]Ʌ-Ln3S} %}VJTC@h;3O@QD|3/?W 獅۠d^PBd4BLQLLuu C+ĵ/e U|kc0V{b'&OvCb|U$D>I,_ {i>g}5pS< )|JstHAݘ(;8Ҧ&5b/#`+EAu6Lus%qoI&O) ;ҳ+*K5t לw^O]__ Nnk2Aڼ20%Ԗ6QHE W~ 孩DL* Dw2 Hx-9kǙJ+$^E=gB:{b%#NC0W>Ispqh7`OP/!AGA3as;-˟kTOC`2[*`8 ͖*m?}rԳTBxgD; &_lCб7`OPH&& &@YР6=&aUʎu5港x{OdUVJe{P_a7Z9hij̾L)FK;o"fPuR[_GtPTW[É`)5dyԗ9R9Y>dC6r5۩^),8'yw,0ۇ JH<_Iz N'x] }YD-4MīOGg;NO%cwURWcÚz4l[7A5yWc{ lzi)SKC@urbD&oQE[(e},hc4.%R%?x~ A҈nR3?@ӕ7ʃ!y:2<wօ}k\GXA[y=n _;]q:4.ig !I0cF!NVW z+]4L&?l91;XV^rʏՌ3 Ø-'aN2n5u:`V"ʒ9d^tL25mXSSSJdu%+W>*.`q [t"#\Y3E}5D" ]7l W7y~[ջ/ mC8ԟ~.Cvo$0RFI:0!ͭKb2cJyLJ7H :nV~Ԣz;4m]jj^A{$1Uy&I.>;o@í# s.3r?Qy.ל;ՌMg}^3tldr 3~vހ\:y@ѭ~n*hN֋|oKd%MĥT 2mNԌAʳLDyP?٘Ќ;cUh84Z'܌eVr>;ە3#J !!kTC%Cam@\@9ť=vƎ/  =DWx}(||7ҵ'Z7oZ+NM;nH?b_tl}MuUխ78FiSCKk?3C9ʨuuj097a^^4a}Ľ튰KD} Vw H$nx^lS 'aϛvԆqw ߄ ǨJ^x eO%5z,||n#i[>e_]ŷMih(.cICsv|B?j/N&W߮9 wP]xe"{H9sU_,%? ǨJ/QUF2ц8"ĥ$gjC&c/Za2sv\YBK>!/NV54j|D7ןEŽ)oҜRm$B i)u,!BX}sؑZ$b~w0J? x=N^ޗ\wBM| ^VONPIĤk [svӄk&?IT5F6y|}]sV(\='f(Q$MQlegN_Ҹ&ϧ(aJ?-Fig cxQ zjM:@WMo( 2ytX=H=n(-`Q{gmH+|Z>YVM,j1v.~[;]~~3mؗ@i 4u/ {;:k(8;[Ɇ䕺2m9jOD 0fJ;_Aϗ^QeL4>@j݃: Ձ`WXTIߖPƭ4U z65Ur8rZձT?vQuDsΠR(ա0AgݎmPPn=;>8R5н[f:ҠŅzϩSM^3< /XnPLdo9u4A?Z= t o+IdI{4ȝ?0WI TU;CZc#}j2k9ܮЄS4^03if(Oo=DsRjimw5܋AXIAS*l>Todem˩9' U0d:M4֜Jdڲ>~8hbgEu~9=l0q:Z FbLYC+\T׹a$,byeކtqmSWSMHe `޺W= Pa8 !As<_Ps7kUpIe=sm6 U7x;P^-ŋ^o wM/) ׅ0AuV,VERocM6ImyٳR79>$͛[+6 L9$MdG5 iAgէ[x݅JoFO:=Du0'[ ᆶs<:(*m6lg҅0 L׫6@=\Eԁ^Kڢf4_sN}4yjni0Ȁmhh(w(5vS^4h | IDATJ+}l8Qwp?NF0б-&?ƻ9:OG iq_ 6/Cy)%qFD(zawzc˨S-Ye+VÔV\A ~o´@N>mXBQC'{dܵm _z e;D;%!D4xR 8Pl:VFєXCO; =Ou<~4ufxpw 䚫'Q?&5HXk{599Tr,Gv0]q})$zsEn9ZQ3>`w@1ʦutUɃ}:hd?^8TuuieeP?u]8`2Yf9-vf㋡37qP Ռ7yR%y@ȠQ>{I:46|7S0f%<˾z59GRF(W/)sg|4ǧҎƾ_H禃{m3HY(uT %'8YXRi/Q>8qY`1PBHڢJ:uPϰς7(:%kS,qE)m=*ʙaxIX-h*C Xv|][!F5kWc%+Nq6dEd:{ z_xJy>7͡!K+뺤 s {cv_q-NBzsܹtD)$z&Ͽ]KM!ȕڸ ML6l Z߭;8Pɘԓ0 9W3fW5m /rAXÈj!^shHMЩVy ?ո/P9_>ݢR+XjCqbRݽ:doxf復|\5, O(4 )Bt?x?xj vR 稆-èNCHJ餮 槅;POhSa7y ) r[Y⹆4QN_?,XZe!K_FъN gݗ{z yCX{3RǙAݝ6OrH :XھnBiƾ3.t\9U8iS& 7!!>d~V*Ax[ lWyץ'(t3xBU?l;߹p?DYߔZoإ1C_MsKm9(#]Sl ΥZ#p܂~-kżJhk=d$)PKux:bR+]mӇ2J̖>}79VQW\9lOQUA,O$K$dauMR!8)XiTg* ]@ t催4^JP8 9d }h-o"DҳYxYQ<5>]S»:lcbӿ7:*KyO(ʎ>] wWv$O}^Kf<.죳sdĊ99`-TA>M ␄+X/ VRB^CdfZ5gƆ3P Z#A+&t;~laE9qBz+7eUIТAgY>Qfz|Ct4(+M )xdBY)+ ҔPp$ڼ8 5:+bW)@3Vv&VpKt# }hmW(H?B]Ԕ(mi+>;5 2xvIOҸ{k,t WOSTsal2yLֳvCuрޕ'P<7+zAI1NqB^BR'$4%'P.JW&a}+;s2ɿ\ wMT gҧO.!L:rUONrJ袬HFuu}}mm?T4m@uR|)Chvn޹_s7y\UbkWH>`[)D6(4޶`/wu ȳV[PcoҥqP (o )A1/.Q2W,>ִ–%(AzXMH)}bDbXDLtK%sԡ l@sO>!xP0)O -𕵁ƮE砺ݸs6Q1Se~&3N;>El3 ksG 4ۖ iO[3pjF wc bƎ*4\[tUK譜nCP"v%.1QK|>%3ƧHWpeKQL) uאZ,yxqʒW ZnWd3Ԕ;GuJsK\j- t"3QsTXDf(V)d&w'Py~q@3X:D'-=uD)C߅Xj7mArj;9%-݂z;ؗ u'q&(Iu/*6$Z"e\MƇsv/>vF@j|$*7D]y0(hr uT&'J4;wr U2|lr+v7B9\ܦ"a_R5]zIݏQ-v|R]Y$?׺0˄?'^ >eWXmlL z=MBP ̒Tdw!D^NZ`ƎMFEWDi(d/)1of崵m=a|!R7lkͯ]8?Zn ]w.@;u66$(} =[(0'{ "RAI4Eygja"[yB5MJs(Ka/sKej}w?4g,0WP>FŽ; eKON[&$uF QAiT힦2(1Dp[voĸ>gR(͇WbY%{k9iN {`IHj(;Nk =L}U&'(;}h$Uە&eˑ6#߬klk/;ZA$M /C\@VOQVϙ)5,R0QK+/urH>(Ku#O@.)@o9PUِN I0MYZdZ*06n.iڲXgJ_7yNؿ}6-A5VY v9Z^YNohѦA7ԌK'&X@n_uFu\}&8Z9+J{=cKԇ\Қ5Ic= %(K]Njg(qǼc1XKtλQKX@9;%m'ʝ Znʾ'ן8,RI\¼=E5{PZ-0g6X2vFDNv$88ƎuxPrb. UM]v-g|8G y]24P 48P &Z-l ")o޻:3cIϲw` :nOQM0cn&L]Ov4ɯBhKekKFєˮ̫jל(>ofk 6yޛ< (n% JȅFȘ)qr!޾}B52v(MRy} :7C<BͶmڠE\(Q>'z= UKDRܞi 4Vbm@Ǝ׺T^nt єM]KErOi1 Xpϔ1YCor[j¯|uY7)RU{e·PNcRynWd8lc@޶K9 Eٹ {*oxL#^yG*-fV@(e_]@v7NukQ}ʪ FH&AAM]u{h9/TiCfnhN>kNC}:M:Ҕ)AO]z.z |O Q[2tF\[T;Xԑgy^#p! |=R0L6`wr$:G5A /+v GRklsTK=Z'!QKjAֶJzoC7̉0m`S-ј0F'̲M'cChP3TITۚYb]ˬd:!Ķэe7za| '[(KJ۬ uRo+/\MPp<\ʾX+vXsI ,B[P3m5(WeJιBU;l8 [ܐAB:O\sVg shV [\FϔXiʎ aM1 ;!˅E-nԑg72`C W88?1\[ ERmkӍ[_U DW%&Θ&uT>3GRo Ch`3o4)06fMjwe 5jN,(rT!Oi?]?vl]WWX\m=*\]Ո64^@Y{%jg5Fć-nN%qAlUMϚ;Zm1&R}F}Tzm2 aZ FuoGa>(btҠ=!1ж-|9nY8kYO@=d5G6dOP J~t5uVK䤅)ye6g+~=jHt0 1[[GۻՀ< J&ϯQYs(QXhUc%Yw֭DBum4Nרoep~|j2e=a?FьBJ^l OH$M{W~e\ i&p7Q~SW]*8&ν+L ti$YYx94~:<}YI]dlW_ &c]M=k3VGKd6gk@mRSU]yWN$:nT6&v|7n-BC@4o*ul 6H'y0gǩV):\/ѐϛ >mjxas[+_Y BJB* ۨʶ=@夽 n׆L9be26Pl ؈}x'Wm?4EÐqP ۺ(k%r* P뾧{LrYG|x}wFV ٦gzdwB݂T# > |>n{D K5cSi%)+037[] 9it-(& %ed PuBp ҡƨ zaruIPs[H귏3Ԝ7GAƗ/U`_=%T;"\q[+c0  B:@޾ƾ̍DDvT !kB]P+36?[s%'m*Xc~Y\ X$}u >I&!k=G9v K3}j.H%>x' e4;b ݂ȄNK%;' Թi "zao8 NޚztMtB׭P b#)>WL^1L( /dk 0H C]y`h%k/? '-%cwRAmjǔ>C9"mٹi OzeTTkm:SIǺhn"%kI֎!$n8+!^KN S0ee] T!TUzzĎ ptS IQ>{ټ웊uRd˺J6Wؿ0tf*gc:K5KDRPni 5EcshVǚxZ0&v֣S up%k3IwiPT.c1Gg]}">\Ouq6 }rlbݞdh$5 (D㷢(R2A}ְN+wXp~BY@YdsFAmqbwL1#cTM^m/^{0̀krLyTx~}WF#P`+:lF'h& yZ/ lw{`eFgT~Z{-ClpDAtM@-QF/!٨ 14js֦}KQڅIU”lF538I:nah%s腵@u?F'P7H+PaiM`fV}f:IUG(eLgR/՛ceC1!ڝ<نބKhti|g 017H} $MK}T)Ib@kꞡ~AEG55VBتmh o¦Ȭ uA셔x5xU7MS-Sb<ۊo=c?T_f_݁HA aMH~(6`1zRXh>j6)Jl ;7HCmA;{j 3"f]Y$^N2El:mADD^]?m.»V5>"AJlS|J"m!–.TD0gZɸ fCzEI]džWVG +)6$6XPrPeUH-ڢ8?LpngUq?.:lA !eQ IDATV ;|3T[jQⵆ0sT;7 s2^l#G) ֌qjoj?;T`NeAفN`~da&m3V& xU屡ȹ\5^3wh ,% Otaeo?0r~N t{!$c@%ZKXHWfP;yi^p@ޮj$BfS!O F]lj)T$ eܺ|C|^V8B.ZLo8HmWȀf)c)y[AyNW/P w3-Yg_1Mgav!E4'QޅAA1O~9îdOkK>Ǥ2gQVm g+e5^b%g8_8j!|j%Ҟ eLkIRN/s/Z?k+iB{EyI-"C}o&(bwQg|`2v~. TŠes~'nEIXl'Ae6>W ZXym|IK_FyIƑz"b9qy.aă'P=fnTIDe*ܳAtNI $:HW;1;pi41")6D%9Vi`I9ĩnܘ8^6%w%;AXާhH[ SBƾe蹊h 4`mGEOп6dhJpz"bm#CyjM% b& c9,!&o֫O6~U`?}{ 1]e/؅M5'@Yxg O57+|-J\BHa26p'l_m-)\sj6D66hH29 g/;h hHt Bf)&DrU%_ :uu2C2@Hs DinR}Sc ";>hK֐kVi=DKf&tjwblUʞBJ$AFf^mo O`Hq Bԗl+|k=ҹ ^9f{>+"m}53Ev8Aȓ^ T eɳAr3[lʶ%%ϳ(.ۥ#6X]P%{+ l4;b ORTaNhjM=^]RhR+"o|;:+>M/j1T>R{u t hVQ-wb 7 hn!M_RCEG@nP-cMι+NJu3Jva1}d/nGP-4#Ф$klON)҆r`&%ֻ(jlόikS"7yX^C<dzAvYC9MD7`Pf4ԶcxPUZ6:IךrW91.ϯAw[S.({x3_*lJʲ("tIuwϚb*$V_Qxvd #E5iLJ_nψt*ʑaX-3'Y?A3T7vTrdܨMPu=Wd~Eq<'!Hm^RC^뭻 x궹[k읃^?0 n I8{|꾆r6y5~aja>dM (ZjcrJu%<^=`b8lOo@! 얻ʥW%})m4< O* SM}-g[Ppx!I9=M8Pl*ڱ@/T {M{=ΠW+xrH;y3_%QQ'O=@=vC< =U'O^&dh {hbpg=yz 걗`̧s7עD?IENDB`hishel-0.1.2/docs/static/Shelkopryad_350x250_yellow.png000066400000000000000000000466541477404575600226430ustar00rootroot00000000000000PNG  IHDRP| pHYs  ~ IDATx}$]g؀ۗ$͋%#Ed2'!۷DIn.s@!zm@̏)z#BxɉX@zB oV oUWuWwWGZmMOwUMwק[,f=;"]w_# GFz{݁7dapp1dpD AtkgpF ЀXI(lx͆l# wem7n欼J 20[@ۃ9+G? Lj 0MtK.i`D*~4@ lgWyD@=.DNQp͆{Pl60.q{> A 1%o}J'ҳ $v<!;6ΉY[@[L@|rBvQif3IʗnRb0^$}u(=nJ.:SVkNdxE -I1;K6!'ӏR*st 1/K6sTK}8BӚ@#0AcvkE` b*2vAS? by"!s+Gk꾽 %BD4|@URFq{POxvݣxw߽tH\>>?͆ ri9+K򔎢@`1[Aj1W Ot xv#lW@(L!)Çtۣ 2! r@~Z&w KzXZ J#w*8 ҴnmhҙB~]7{ C d_BMac/?=/nl ( dz DJR"˕Six9dQ zh]#SCUBb@Z%RgP"hwX-s98"h@gH"Hf} T)$vsxr4SX%9{ti J?h_Rg@ (\U_s?All"h@o XLsM:B.OKL8 @z? !/u޴􉰭q@ Zߧ`s sb) Ae@zBAщl#$? 7 |2/gV}v&N􂗯OxLQd$Zv3ݳ>/l6H4BOHdL!R"]'@Z$2 CU7|AAh@`٤ dݙIݟ $IP"h@#XR"hikKTIP@j(Ui !͵RS:`r6yA^P@jBT 5[L),@4<1ܣsW4i[b 9$P)TKl6hN);5ZťwYL͆{lx͆sǎDB9az)H^*Y5E;,?7\ۗ>͆l8 d#hic3AKrRW%n.ΛkFtީvmW @`8o3r>?TGR[@5 B{"=a m,'_s5HR5~֕ Mr'(6V1=Y DӃBw IRՆrsVD_2-߫t0^"DHlx͆ {8zEA!mYM6IcPH"4YYפIl)E LLK$yIwp,5HW@<&Y^jϝ׸n]449Vd`8c{Py{I~.g'Fe_jd*O8^@<$$rnBP}$N!Œ:ۏ( w,u@S|ϐ;Fty ^Mdz}?v#薠i#'yG9G.EݣI6Z^6(Gj2=D.#n8JGq@#.uAuҤ2CtF'l6L bK^ a!ռNh: )P}qJ*]dڹ)&n7^v 1;fՑ"OyB5 _jEɽfY8NQtݤH.P@AN2e{ssR1@-1Hʻl8 =GKAťΥ>LGGVOgd%8"$"x3/&k"Oi%=Tu/q|֜S֏)uOnE P`W9|+!*Yτ9L6|El6i9+~z8EI3M9bocgu]Sra~u m=@!޳`?lVs&(_N~<\ [8$BIBdxf᱃D 51+֐0]%..sukȽ@~> vt>I&}]Y3$ )>Pr w> j|ptHHFtHp!Cr )H^s 52VR$%@nh%yW$͆SOh .=g V:yV\&ѱ|p]{ ,[w $Hնey`6J>%c u[B/x%7𘎝Ƌt2C'fNro%I f9^E?'F.o{v5|xZa"U UJkw ´| 5pv%9ՁKW"2S|4;) llOa"R~J׍ ¿$<} VԴkG3Xx/{}sAe.Y)q QgL,]qۂ@%pxmXBԙ_C@H&P b:;yH'J**)sBC(j$Ht^U*XB͆B_$A؊/ ܠSvvibПiӋH5 Roֹ\x+TSu9Yerot)}*ABH?lB%FuLV5]JYϑfZMFPIJu! r= %}L 9Dy]^ȦOGhg:R@z6 RLр0Y#oMM?('Tn[A:"c%[eKtdnHi)rR)%sV']7yeXXEP`w|bm~X焺7 ~{ 'l6| is䎚V/Ng[2է`y5;R>iaUxUdǥVUD!'PWې(5]е3XY-`p؁ v %n+aG- }ROO2wS?'Qh^bT3iQG4:P>=g Tg085jÉ" %Q,@G^e@F(:d,j`Z gܧ:<;Vۈ+E1G0@A1H^ N#R֭xZlyN6Q6mENK;kd@E9h(YiN{X屬ҴpeJ@kPz0XU';O'"c([ar u$呖6#Š@;CU# ,%dXP#zgw!IIo`HGeZ~w e>LqQ1ڄ+Ydl+ Wք#د_+шOƋWP2 0p$۠ze]PzC'/%۴6v̑CĄVmLڬʚ=g*ZB$~}6&P'ط/1@~t_xٽDP>WM J<1$6s5z}tO^@,!&QJ|sZ;ʪB*KdĖeM=^I}m/n,^:βhdokj{?QUQt0}n2kPIUN!^tHU<0"YuHSBWL,珠n{ ydyQ9DrfL/BQw@qsCۖ>sf=Z۱u7 |%zjFDr͆qmk}f}ԴPk'P"3%VwCBbIޙVD%7k+! DKvbs.!HAw .U><(`#VN~Aݬʎe"5zHt8ew!Ȭp=O'oird$6/l]|6wI.Xy/-$d}Cn{Ɏlb@΢RINN>V_Y_"ݵ8 {eϰڛw&P%K+!T\'ݔڽu"Abi)tgmi4BPN)y!C} Y<̪&ڦytЉ~D}MMC&&s#2I}&P$(n2vϑhvOSL/&|>\B^4ir)z/\\!bw v}DmA5DvQH֤3]B,^%-6u=G 8~"nJ''&S&n9Vt6o~^k5 Ƌ3л ̡\dTIS,W(k?@|Zf,| ]s3T']-9lB-VCvݻޛN nﴭ,ү߃*u -Zs9/锁hB ,|hդ`gh_.gjp iRi'Pâz>( " KDG?G_@%?ڍ p!#zRשS]v7jc CRVlxX!Qvo)aRйT],d2}P3v辫M"ng(k_wJһ^hdrA\}Pww"D߬F21TR8Ȧ f;]1х8fe1}{_)@*B]"l6fbs>}Vu~/CoHmR(#J<6.ڛ]]r'Sg؅oC4:!NHLwE5$$A3 FI]RWtgUm{/hzrdء>,}Af ]w c~6q ˔kOOK}M..U{)\t *]~Ti##Vvj[$:2_ 4+:Q3*0iO_ )qv[&:OPޛ)$;NFh>}wc{IjR'Ii&y3 vMkB(4jE]xQ~/Zh_݇4:bZ~.wr+Zȳvz5lL"%'CuhPIxҪaHjP\@W(JON\˘h3AG$:<'**"{Cxm۬7i,jjCxoZcicAl@M@&h\359-xLé_{|R9+Don}m'M9X0;ֵbn+m&}QLZ~ DE{#Zţ@"Oœ637qH=bw E#/w՝C͆n23QHy'orpPsb ;l CUIIsV^z6KrЛci/Ym(PޛY챽Jdy"˾aNXFl@ϟYakg簥sA#m)>۲0BSvPC%mOMvhrhԴ}I! = $ڈ@EҒόV3' _q/db3c[z'(uoU|pZ<>*h3e><61NX?5u \RkMPG`QXL=@%ݦm&Pz9Q#;+EGmA/R ^ݭ_,DO [b9#'e%vv-;h\?Mrt 1UA[ Tn`|-eh Q+BKY@,ߧIϘhwe>–,$(}"z7k?jSl6pN[Vk[HEk)| _9d ~+v+~!ֵ;(<{ߋli"o8:!v Ί!I$V"I8&<rKKص:Y]Q[5`M}i #L}?reCd:a 9KrϳsHJd{{lkA*t`S(+ !9}ŵH I3p K;W c+KH rεswnÆB_<1Aq{%8֠K֮P>C{;PtMN(=0}ĴV/?T )3Hyb~ˉ8Y[SDE\"$q={k0uY`P\)QP_|f6JߑSiԇ>U_.@гte$7!al@AGζG<M|./FmCy"4M9|V5w>^b8f\40a><ؑSw.<VN:z׹  oXҥǽgȉj隷\M1j_%d5E«F{ʣ=U]L}kZ݃X+ S"NPb:0wCbP@ݳlhm&8 \`Nbf'}0jּ >iO`hvy@bg;KIP|g`،f1{։O)򱳄;sӹVn{:ȓþ,4ɴA3$ژ-0?9]`O~ER.IIjHd%>O:Sn73y ) 2E\C[sT.͂e$Pl6SI_ rO\O!/1z;"OڲYHkOjg fg](>ae]}҄ڭcN:^V5~.ɮ:uRy]~K"e+}I1̒u5fd1+tm9N=֯ D7+V/rosTY^ *vE3s}S,+f§v +NmGY)ȉ,z s4 `4 Z"p\C%CF3N܅'P𢔒IsR]5!fe/Bવo)S c^1FǪ+ӋxC>CVМK)o| D&I?2/ [7٧l͡AutDKN= Ɵ~$1*}eNi; 7Lv'<UJ`h_B8V@?$=T.s/ggJK>94JU}5v0A~$=YH"Aoh Z~bGBڗIg`1վBuj`}SR<J6<["5 ^p2m=csA"#Q fZ iڶyw;mC&"ƵȊo&-N}l8=hwF#89r ”h-eB"} \rta^vVL]EpbM^ڃx+3nC ,˜'PCM(ڹ _L_WA9#ݲC joKqERip͹mF2տuw0Ryƾh3BmMh lZ}"3=I4fæzϐɗf1Bb͆ǖ1+WM0/- ߆>fbΚ2'!*8D}+Ѱ|fo@# LXl J1$rbMg}J i3BNgau@Ang=G.`k%|5&y r; C^&h\[$6ƽ{]fýh0j'Tyl 1ac\^"q)6X;t +*p= QM!7fOLĥo@EJ̻ 'eDȟY𢆃c`A/-4Q\m47WRQ&Y%D|7Y>W$5&P$?y}]B`䫔VaP!+*|Z㹧\ 8b@=F TAoB,]`zNtQkWvW&C8H#j|ʦ! $HXb 0+ݝu:CO5~_$Hݕ^V$dYfNp}a& g^wYoebsQ mRv=F .H~ |ma~sM&&uژ@u&O GSb ?̛m=YyS4bi%UxAr`!&Um:U>}`o;k +'U'gx|! Gt9DH}cxo(`?wkmf$U=X7dF ZI*M#E7{&S,cNuGHdjZ7MF\d8LBu=k*tJxc&kBg!J yfmGb@On6 +lB$S&C;?qT">'$tqs9eלW4yXر2dԕv%v[.F ڃy7E1Jb(A1BLegǑ zTq>'k5I~]i'+%VACa&3iuyui ^ލ6o&(;"c+$BE/M+&B.Yϛ@%U"̰q>h5&liS{;?wqB~(g74)*;Oa,P'4ZoT <'"@4yn /cݮAӛJ)+3Li M1`yCh* }ίuXl&Y qSUOu>t-3>$p A1qUb-AT#VNTYBᅫ F_q-<Ͷrߧ~ Υ*}T9j< ,Ք_&2ov>su0bME4񴊱5H `Sk6,B ([5jk`mH_苰&!ۋ3A %g"qDٕ?'/p1 Jءs*4m?]M]%m\BD# %Rw{b}" 1}XgC_ZڽLIPDIG1EPOE!Tz>CHdt5#NA.ӪiGFWQ4=/2%G-9yuit5uu%"=4B̪$(X!OKU>0g+NhgNL{M$ts.Q]Nރ7yb!ΩhSB!%Dr[D\j̊9%YyMDԙ]`] M6^@qy`(M` sFzא  itŬ`!RCl֙1+TvdA9{yJ9 _w]_%pɈpuU%xxUS,]]oͱ$!H=ӁS9sG-=$Qj/Zo#҄w' "V603hJd1܃ vO* "/ǭ"+5tuwUm<ȶA p?:GLXB:9iaD3qB]ٹ-Yˣ_{O̦Bjb6f/uj@ph ZJC97^[CPx-dŬ.S.ϜߒDuu)}_iuRˠ~&e:e͆ Iz"q^CiWA@F4kP$R@܌Iv,? q\JLdoQ YdQU?+.c+ߍH+4Lx;|kU fǔ؅!GDZ #Sj>BhZ ™:: y XH:C1lU|3 UF{%"R[PEyBʉNi しHY9r8?aN4(=-8~3)Û&[ tO; U yStK ,k۩ T fUA!P i36ۇzCku’qR ۦbRB.Wl8G.e~tUg{? Ie͆'u|]M;H2Iň2_ʎ~'4&19EI\@LNI_qx X!vЕ笤;ݷ"h6CtgڼH +JU*WS;T <]qYCy1rro~i*\HEv&dBqU/GDrO!L:Xh _wDDWp%Αu6w%_@8 ]= bGH*zI2IGW ;j{D2%3W\7H%> pȇn"{'M pmFbuzW7]A!W/\ 2YЎIN91HFx>׸JuBvN!N8]:^VVkN~^ P'|ktPڱG])YxzŶ]S;j}a[1j t8-ACv$}sXqzSBLg~坖zRmy'P ?P]{W]@/$Vخj!Ox 'iG RAleoj`h\>aTL,Ռa{0B9mއE]8:#PǬUd!رV C\ζ!dKowD:}\-|ߠ5 ::۴-uFĩNL8W4)( maǁT=i^ź=0B+PW\jJ)bIU6ŠޫATt^=>!U@. $^G<.q<&d}`lx&5Nb J>H y* OF<1~@!lCro"GoR`pq\ A-2`r*)Ϡ7^@䒠=ft`V//{ihi=P;k9 15v}yRSۻBSZIHt.$N׆^ (U\@iZ3 i}֪} Q҄I”mAv3xqruܡ`Α'F+߯?z'PJ{F{XS60x;44iox;WgoR/"8 FIDATaeE#j7d[&܍  $Ի46Jm@U$R2zC<>!YzzeG_֎"-i{m7ls(Tf0j"@[uNU䙕A1]涀/(y 05< YyiMۅHA YMȔa&6`2zRhj:X+J ;WH3mA ?r~ِyL7q1b&i@/'QqU "?jy?rO9qrAc u 6@%b =|J"M!B_ [;PFJ:@R&9+E#>,`xD7 $`B6 ƋZɸVhhaS.QauDfNh 'V^T/PU%yޟBeF h"IzM*A/sVŽ@/Zxɢd)al6n-=;~l0^"%|d`?E2("D'02wz}b+T)7/1DvIt \PJ<̪Or_\/TށL f{wIcLL^46"$mcBHHlJ " b!}Aoyñ*no,lGȗ>{ 1x]2``N<"Jg L 1׮&}+[!w8-&4l%rdy}[r ":{Cjdy(Uc`EnAAim(gw tDΈt$7/qD!#Rv,-ٸh"nʑ? r XW ǯx=_ T*Yn-[8ꢏ7t?/\)U#tCJRM"S))n`ViV@R r?*iLy}[ 4Bݰq2|DBV<*!U|d/f6〈Ƌ}gpbZv= &OOAH. |Ucbc3w+-6–0Òm%aRJLjcm.rotfɹ CO?J!+s㎩O*6j%҆ a延Фrksh-I#2uI5f"C~ϯ&p/rjAST$@*M@' P_JT>`m;>VfϪڐ52*UMGX.#I]W*Z@ 918m@WZ oڪI1{5~V`a_ EϽ6;.CRn㴇L=[T%qRݺU؛"h hFr·Ys&cVd)C@p:xMJRY5˳ \W^&ixa/ᅪm bdཀXa O3*٧ ی!} +aP+73RF)d:$gûe꫌@0$hݠ~PNZJ%Ԑo2@xV4/.Z020KI&دgc'z#q uklL jIFI՟NHdAHZ^%RUN@ _T$s$ѴŤq 1e~om\U.!TȴF(f*Jz!}CmINR:vAvrL6&5;8e<96@k"tml_۵*LmH>۲4VaJ|#'6/xz@x%ƕ1{f't\T)Zxk֟xڥn\_jj]J!Ew",&BC:z/5UHNdvSeBj^P5|).|_BSd!AHz#hG0tZVĘoQA@1)!ܮc\w&\w]>9|ڝzgX&}؟s)5dn;rHG 5$YBxr8m/giHg \\8u#iLdzAviE=U yހyE=0馴? 膀g ײAꤾ;guOҐVW!{-i{r=~?G  C> :IYuC.I4"ΑYRf" D&nyݸβ4kLJ_ Ƌ-Pʼ,wKX\'k#zE=33՗:C>xoUHu^ETn1/攏;| YMت&>P!x%O`;0t r@ɆM1/.)`g"Hgl'#4PPTUIKBg >@uZaUd0=IzޙB{&A"XV"992,e:$kGP=@qc7@[MeGƍE[}uJ;HvgÕln,EJZrgZol~o鸙O3}zn-wv3;Rg#ulC)7m O&۩L׭Β5 y@ۄ~%s~^N8\z2ae:w\tQuV9r%ʣcFKNn ʻߞ$K-SxgIoHBBz ɍ\gCC&'&Iɫ&Y9!ϝ@@gfW<(Iu[x{\N2Bv-\Xw#K߸<`?(vhwJS[vlMs&Kܘm5)0FɚOۿ'INWIj$6d=+~߽#2 ; 6.B@@$awL2O)6nCcOsʺO}&̞erpHc<-70^/]b9rI'K6m$7'^;eڴMmF6idA3sܓY?T5SUP`lƎ#uG5kQϹnifwOG)],kۼeG_ٲuk`\?^^|[wDv5mmCB$@@@zq'KvRԛe%ש6ƒ'L-mWtR?qe^WN1ݾ^k+gln,u)׉Ci2mkhffZ)|dkikyBmyv<өetZ dBLu9bwG=*ryq{e[+PYf|4t.+'vJt^?TL|ݶug4DEe֬iS$J9 @@C[3iInFM]WdC$aX+]_$U@oUm[|:z+{V ^ˎ޽M%< ͡j5YI|K5:]<3<;^*)M9H4ݙ.M+>Ibʶ_H ?hfKroMb3kb{g>N?^޼\rwn zNVI:-3!ag5, &U%mz8s5HlJ./vl+4_J@@4cW]u_}CwviuB% uB>@@@ <J RҏF]/)ۜ#;ʚ)O 8bBkmT.`kŒc=Z4G2)3z9ʶ_?0wuNBV7=i+LX-;z5 K&ޭm7ݶ g1J첡+k k5)T[=I[޵g(+HsVC8i{ nm[܌& }YoYךGwjzCu/P re zǶh+)I6ց^V/>q)z? ChxHݻwyp?9߹\yevxG2PRbڿm&m>^D@k_CBJ}L$ ;<<׶ZԹN=.W@@ 궰!fmr|EՖmiWW$y{Hz7ߛ\8@xYH{(-7Ŵ /E9l9ߦ~_''S늜^/$={iEK˸&#g~\w?X|^rgE@Mw j\ Q䔜 $RФb̶\[K,+3+մooL.S_N^30Olȗ[+\    ie40yuEp}Ɩ@N9{J;(^Q8 ZlkF;4oL5ܱk>o|:u̔8y?[7ߝ,_rQ jENpլ)Yٲl tMZ7w>|sZV/U7T3ޮ85:Cԯ/nYl~ 0ˮ:8=g[2t ^L>M>N:#{/ɏ? ܣ&j(iiԬQ]rt._]_h7gL'$k虧&5efyOQX8{εjʮ ZKjZ+/Wyf2}[nlܴ87x\|9ҨaC/P׵5 \NƽhHyHiNow?X֌!N߫K$}FiKZnfw@@@@@WNgʔg ;:ܶלjiԪZ]oJfԁ@τ%xunePB)9iݢ꿝;wڐofO4:V-\V6n$s^;ۯ܄ޗ_|\6/jBrGGm&.)Ћu ~ζ4 ]= #Wse *{soiBY #݄?{?HYnۍlWPP  {y߫m2c +uy@@@@@r(-+a0Qcl DS٠aCVཥxpiؠAĴ4MY?VV== :u5[{? i]mO{&7g 2{}(Ӵw~qWݙ0pK/|_IU P(]F<}N!lhDѮoW_j]Kz &0zQ6a\y՗B ^Sg8 S.D.:L%Kdo`sꚚ*O? {yqݻmrm0=`6<-tQ#e 62o?;>J^yنyݮQNשޫօ!     @ D|wJ䶛oB= Uǀ>m h\Rk*fCG.ɉI2utyie nLç{G&419Lpcswi+w LxgQchu2s]ky ^~ >|,_RrrrM$\_淳mM#mhPh6iZc6(v/{%;;'qd}{ =RVZeYޑ0ˉ}{\K&\zLy7 W#-K/֩][1L͐^ NB@@@@@ rRzZzy韥^sH[nWK߶ݮAN?4wԾR3N=].VT=;Xv_mn%W:t‹i`[T"2NI  B}i<۸XBf7'Ph^Zx%Y?#ϋuAKn@yi"MZYիaaG#UVϜ7BǹgGTt}ʹ`5XǨ>*UzWe\@@@@@O n&9ցy=%z9Fy45=̞tn!Og~A'ަl|K͘kТǧS?N?SG LWbYJ\m?7JAs*jRsz}I?2n/KѴ[^4 % ?j>}u˖7ku߽!gږ~{w^I{is+6aU(     -pHO$=u"E[no֪lٺUb))d2כdd]}0!>A? I|y\#KWx[2_۸iS|zލ[n6W_F=?3Aۿu@s=^WhsC}~IVKrJ>wj]K:P}2aˁS([۶ʀinP@癩' ;j+Șb >2GYCNѐ1u ZG-|iQc#f}mEVb IDATW-7KzJ|1#     D! RT'cy.gζ4{VM>M>,FE2'CNԐyvπ˯t#'!!A&<=hgȣ]N9¶4>>:Vؙp|6YnR^=y~Ei ntB'wgҳKll[pSO*E{0PJs ,yfs~0Ao{q=Zz lڼIZ4k. l~_ZȞ,*}H7Ը4֬]#چZC;j8cbǟ xnjZ;$&$}5 ~B~WJZ     %pH=CcZ- ńwf~4_Vי Hn^UGO[If5kjdegِ׬ bCmHݏ?ǁyƙM9}#8Dܰq9_5i"Gƿ򢘠y}T{ (MLԓOn޲EٳGԮm[e~M5IrR۸q$$&P KCޤV:5޸i .6iXPc %Äy/J;Lg!jY֌mqmVؾ+bQ=@ܷ{M, n׫+كǁ     /p=Cvxva&M5|/g_LW~7la^Hbl1aʩ0E+薯\!o7YӐ_,\ ;7j@.rH*if9{;@$ofϒP/? T\]9WjT.7A >BOY=|IZ9wV{6*Uťabee9TsYҨA} _=r*LA}/KWZx;2lbLty#C1@@@@@r pݿύUr3E-      phsUݮG.g#^ c}L[\nlߓKD*2S@@@@@@J+V^u实dG5Of̚!?]zu4{AnFÁu[Yhvv]4&      =a ҐiSŋdЁeSevejɈ_q2|vs7Yn|՗ѳ@@@@@@ MwJV7^ހ>}e֜9g􆍤=}d܋/ȭ ~hX`[ J21@@@@@@ &}ǝҪe+«"YYYvKL)vuNQN;_2 1rWg?=|:uYlYt$E@@@@@a kݲC{qΑgj޷b[lL>M:wm9hPqJjL @@@@@@ j""3.3)#|x<IYSnVi֤l޶U^ymZ:j"     Tn *7;C@@@@@@ 4М8 @@@@@@  Ыv      @hz9q      "@W!<@@@@@@Bs,@@@@@@*D@By(       Y      T^P@@@@@@B ͉@@@@@@ a      &@g!      P!zC@@@@@@M@/4'B@@@@@@B*"      ^hN      @U;E@@@@@@ 4М8 @@@@@@  Ыv      @hz9q      "@W!<@@@@@@Bs,@@@@@@*D@By(     !uq#)hSU\$ 8D DLn     L}Iz!>&(^8@@@@@pxbcDbq1)Yo߃?ߝ}Nuq?wo]%Vӯ+k g,)rJ~aOpY Ɓ     YYy|W`[LBWpf6=[Pxgf=FrÊq$gzh#eH^x B@@@@ lVV̊Zσ3*luC6}4[(Eeő3w{g%k9~!{ڣ •q!za, @@@@811Xe4 E- Z}m|5[py+, |?+FP_#F    I x2m+יEϳ"=B2.@gnjC0o lYP+ δBPU p+sB7^Y.!B.@     6Ue%e4aσ*:f[,V]^34ڰH,\@F_ P`"WpGr},Q2\o[@pf 8Dz    Dz&s[dF XPD[uf6U33OYJ3u@e/yʴpnӨ c*3{bL{[6N+wY@N7 @x 0*@@@@0Ⱦ$].nUOC3JͩS Js,rC/_Hfô2d&U?`.(0}@ٟf- o ރ,bѷYii={`ߒt=|fJ"Ѧ @ E @@@@FaVbHJ3QKn&d=Ib8Wgz|A70+{MHhXUkܦh 3o}} ]g}ӂ>TLpkh:@*^%YH   D/|$hZҐ~{b|7_zNs3}09LE޿[Sebr ڶYV13c={yye,(3\h`jC0@ Ћug    @ hE ۾M߂>+ qސ·<3G \Ikkvk)/nZ&o^bW}UaŪ|bi&+Mjif8P  D^#E@@@*F_\f8 o@u[lbG_9\mרa#@77?>>hxw{l-yǧF\-㗈s}V9̄[" x@@@@$Pdb@u3\Bkr L/d"a7s~h0g8ʕݵU]b햄敳7G@o    PQݚ=>nqn%zUZXtldshx L竆oH0'Y@@`z7 @@@@@$8| ͷ&E3a^y<Emm;o1h+ ߂]I[y,D@B @@@@ mp3=pBa:[Vdϸ0o5 n1Yh7>pފ~o-h7񐾕ZqO@@*J"   @@Pfݴ-ѿߛ/|!/ j1i+T竚+_G^~oA{Wl7[*~ߦsn·rX*n T*JL@@@0-&-# U™"{ͷ۾p0WvLjśw-&B!]c'  @z! q    * lp}}, oj4{6o0 /gm'c'  @U:D@@@$* #|"  p"&B@@"EwZnLF82+LO|~oA{W,m*M0S lMW:q{d  ^8cA@@@iօ)O4D-P/`){W[ p jp!ŤmQiB@[J9 @]oe~a@@H@ظ@@\]%MHzvKr6*isސ,iͷͿ\oZTVx7·}_9     Q I)R2Uk-(Eagq0%Tif*ڼ-Ϳ~.7;ʠQ#SD@@2 @@@Hp5LVR \ klzO70[}4|3t   *VI'tK/PRʚukdoȦ͛-]J3F~'ysۺW[Ԯ-=u&e[6yulrE   @8 xܥz+lWeW]Wbvz^B>z   '6^zdhA2橱~Fˤi&2']6n2Z?ۛ)}-?&w-.Ǡ!'/K++,@@nO\7ैJl1, 2%NC<΁   P9& LNL뮾Zoɀ>ɌY3~ի[O r7ʰG\p"ʱB@@JU',h3M׸J}by+lȦKIT(L@@J k.BY._&ό'2t`T7]Zii2⁇_*G ,m7w_}U d@@@ tVʻwԸbqn ͌][bR}͐@@@@$>>^lS%_ O#w+yGȫ^%˖E?CD@@,`C;m R]=tczf.nyVf   pap~2juV9IrեKUҳ{y%;;V͘=S-YTN&;ζ4l@@@LXg.xxn^3gbvzq"    MgU;3%))Q6o*o;YXn.3)#|waլ)r4kT6kke՚լ8   PMhhg; t;wb3a lg[h)1qs@@@)V^$fV   Xoxo!8Ŧrw;w>uƏ   za @@K Ty3!nbʹ<5F   P)*22 @@8POxxgŮ; 3s\   @   Zu"O'#SVhL_ yH:G@@P]8  $8mxWTyӴ4-4s@@@@ "h  /ગh3_%{#enq    z2 @@VЊx&3w)Ůqn2w&3-4ws-wZ   %@Yh@@ZwD 43{ߙ6MSY8rn^ޙʻ8s)Z3&   P9*:2 @@*@Vih⹫sK}n t; 8@@@lzmE  (/]km4{Ñ꽺wKh̋2d@@@&@W6/F@@ Pܷxf;ww1;6Ӷ   D^4:sF@@ δʹ-4[iQxnwT@@@@Dx @@@ 41{y+wWb٩w+5UޙO\:n   @e Ы,+<@@OJw;xb#սLL~9Thy$   )@ƨ@@Wd7/sK*6;n82^   @e Ы @@(OӶ73mWU<%T߭+N LL[}1F-@@@^]{f  @!W$63_;sdj*S})ql    Pz˭@@W]A+3ᝯ S%p5ӽ4[wuJ @@@J+@Wi!  OU'޵׸J1VŮn/ϱF@@@` ^  AqH |N+&ӽbWwZ}zA 7D@@@.@   *NK|gCf);[3B@@@@ 4М8 @@wOsK8WgJ4 8@@@@ 2"s5  @kk՝+:uG[xKdFD@@@ U5f  $Pдj[gk箕Pl1r~w+]"$CE@@@2 蕉@@8xXmi;֙8x3_vbir    @EZ3S@@ (hwT&Q3J.V[h    zѻ@@<ɱZq筼3)D:sufLqn.Qqk@@@DH\5ƌ  vI}L_I;V9WwޙlWͅ!   @x z0@@$x+̾w$gǮδ\)q9eE"   ^A@;WD^s5J.6FG~i̸e@@@zT@@*'֡7LOJ\9:gkxm3wZ}g@@@@C@\q:c䇟~7'-n[Ԯ-=u&e[6yul  @9 xc%/֙&~vɴ Lފ\U#    %6^5dgVe^,-1O?)ڴ7vOYsȐd2r5Kq^^/ E@C'jwJ;T1;|wL]f$OB@@@(W Z4k*?pcndj/]>Gƽ4A>ٷO>yd% @@-Iͬ*$g֙ZyF@@@@ MWsHF«/ɀ>ɌY3a_dp~ 7] {n! -ή@@R%j]U RV5J.6]i̕{᝶4@@@@ lwV]/#>.;w˔ϧʼJK<,Ͻ8^R>zd`n]NYEf a/s;wZ |? 1@@@@@r@Ď/Glb'?O_5g#a#wO rx@^2o*  ?wD.{W`g6M)vcOm L==os@@@@(@Ge_"cڽ+7\}d͔?bvBr)/O'G|keɲeQL@*L TiLs϶{y4[{y<6l    aUI"#|H{qٚ^-gؓKvv}̘=S-YTN&;ζ4lXF 'o+uﻂ^ﲵVy;Fӹ5'ʀ@@@@*^ lttfq\T<@233Yg]gSf7G}IYSnVi֤l޶U^ymZe Tؙx@ܤwϹZU    (6^h@@ `Ze'R3p亽w9WVm@@@@|Ǖ" @45Uwʻ|]+ȝ[re75w+.pW.D@@@@\4 &I|޵VޙSfMοېu`*@@@@A@  PqY7kaK^ʰr5J4{ٖk[fƮ6x?;T܄x2    Ex%@@ b){zVlKI$hhi:"`    @EB3M@*@֕%re L5fG{gZg1{+#sB@@@zxq QIج)yǥZh^LVyqג&Iy    D^ G@  k!Mjv>x0s}T@YSD@@@B@/*I" y-S$C-p%nN[K쐂)sJm?!ɟzŋg     n  @VxF/e;$^üya3f    @y 蕷0G@okB<}ܩqBl?{UѶqI -@HHE@t|ػH7QQ^ gAT W '̄l`lz1{<3*ȋ(B<.%@@@@ q @R%uҊY2Bsf)g$h;99     @@@:S3JZ[uhg tB=6@@@@ j@@<HlTJ SA򙺌)jl     pi= @pCkD2BӉi/Z\ @@@@V@k! O~fgo'$_x_Rj>5     =z3@I.0~ 8o&2΄    z^9t @ /^!/΄xA6@@@@p,9 ^'VDxzMf 1;< !@@@@E@]Fv  &i,4JR=r𼚁waMmxn2d4@@@@| 4^bpIS-}j^tx;:s8A@@@@ C! IT9M5OT+SGxzM3bIݢ    x )B@ k԰B&KP3;h<5 O@ @@@@C@=ƁV  g)MK*Kk!    ߘ"@X t9M.^rL!6^x\@@@@V@/o}9: &Rhz9faRhyS$pwaMq&N    W.@w^A IDAT@uWHF;um[/?B$N     ]! oG@ t M]J3YD/)UԳw8    ^rH@\-\UxM$yمx*۪<&^3"էx     Pz<@KBpIlBA'fmR!D@@@@@xp $*fJBpI bu;cfGyb@@@@.O@x .HCpIhB"MXOIr99    x-E"z!Lk!i!ޙD3/fK^@@@@Q= \@b⩙xE2BS*rfť\y     x,Bp FT903~?B5bh    n(@熃B@<[ IhzBijok*^R/)ճ;J@@@@@ _򅙓 x*Cfafcw4ΔL_W_ t@@@@ U ?IPL<ou<=/-!    ^rp@oH P^z9M6f9xN=+Ϩn@@@@(`N iETi6B}i{$i    +@cG@H %t8eυo*l"    z\  (⁒Ll.IJ8xcL͠MgIJ+/@@@@@ _򕛓!@j UN3LZIr!ގ ڢfm=+ @@@@(( "VH5ZDHrbmHK3%4L<?W      @f= @H(ⵌ E3);HD@@@@Fp@J3tY8#KHm:Skmxh     zy˱@\ |QIh>OgR.B<&^V+mX/?QF *l߬ʣ;ʐ%9%Ə>"^!TGx%^x5l!^ bt@@@@ȍ[z+Tʢ :˩'eʕo4hf$.YJ}%%5ռ6~(yDz}8/ @> $ ap<^,j&^h5F> C@@@@7p@j9oil/KedX*{U>EFOgct7ٲm^Q@!\:#8r@Ղ    X<"9d|rٸiiwxXL5N^~mr!e@7HlTJkC%pGmMdIXht)R;yBX;ߥH @ ] -(@T3ZEHR,j=<mx\3     &sU8 kҊOl)IJ8x;!<6@@@@@ ޔ#"/V̒^Nm$W+YԟMgIJ+J@@@@@w s} @> fj&^Ғ\XYՒT)zM<!˧!4      B@|X T*fⵋ Em~Ib6Ee9V     @ ?gG] 5$6 x⥔ SĢj]<$     %,@H(iK)]c]Xo 8t@@@@,=/Z8-RFx>R oGXT=6@@@@@\!@ E^-V$@ 73+k vR(j:     P0zY@ҊY$"%BF痘^JsUCCI@@@@@ ! *⁒2BӌrEmO1%4M%Z,Ϲ@@@@@r$bfRAj&^RpFw>šx*ۢ$뀾!    ,@ΣC@ OR IBp5bTg]o[˓sP@@@@@r#@-EH(, x* l҅5i%A     y-I)B6!^%#ċJT4cIJ#:=;     n+@CC@rRfZTBF9M&IB=     /@\,Rķ3"%-(# 8f%]|f     @ 1g@<H, *ċW!ΠgZUN_dC@@@@@&\5X+mfoGLg<M1     z< /HQKh);BMx[ϊ_,! 7]@@@@@@Rf^BpY(oť]i      s(CHbf%6 s Oۡ/ﲀy     yp<_ AWZ:x{bMg<&_Rw      pz @txNx CC]1]3DR. @@@@@/ ;@b0UN$-м1jMO2ڄ     Pz<oHhni&*Ŵ4Ytl=ݧ_     L@eVf&^r x<     J@/W\?ofEJr/)5c38@@@@@L˄m@ZP$H*1ŧ³MF@@@@pk=9^)VDxm#U9HI)W4#ċKQ4Ţ<]VӲWN!     PzϹpcboi҅3B$3/p[ , xi     y \%Z<ДҌW!^jx!a&9p輫Nq@@@@@@ =.|\ 5$Ȕ3RKexQ]c?!_*t@@@@@ "PzgB▌5S%5u_\A6s#      2@GR6V.;o»x*;#"t@@@@@ q\@UE.S!^PŲ#=O]0oB@@@@@ S fI_FgnStLb~6s!     e x|!R*U('OE-Lކg $W V!^gV!ޅOyQx9´@@@@@<>1hl޲YV|Ro(v(CF_WՋD:x;t@@@@@@x2kTտQ?bcپs=Y%Lb14%G@@@@@<:ЫQt}06.=&[mk~?^.PJnx4+$,G`{r Kթ+}6{\:$|].)Xq<&ȡ3l?R3Zx&#~ x;j Zֈ2n5?ȁ<Ώ&ڒڷ.Zm3 C Zbg/jFMd yp[H(em3 C<:ЫQ<acGl)7of;^m)KBmDBCE//urI r 9 4j$"o~eC^O$-MO^zI3@'W pyw-s犜>-<{yw<:+,LKRRi'ʢwޒve9ժT] w(RDdB""]U{O~H'eh@V";|/qCi f5 n+gOn."eˊ?QNzȌ 7XݷnǦxt2+WH&ML ΡGJJjjxCLૻo1*Գf9!Ӗ˳sr`r+3b7E _tk# y}z?b|q+iCmE;䡈|NO'-_*,ѹg|{x Ng'?FW#*9N鰁r9+gwz|Jv)R;yBX;?ǑqɱuիDȜH 9cIAqtȳwzykT!J )_W3p#n44*V\;| ضQAVAcݑeeY/w~a>!N;i4hUH!d~wgO.7pz;>0/އμ ?ytϼ ;ud䵀?   Լ p; o?! p+ ~@/_N      e ]&oC@@@@@@ ?Cs       pz |Cյ-;boǎ]C军% _~wy%5-~~H-O6,~IHL0UPQ{/ IDATC|Ŀ{[;HjJ?G~gerI1BBBdΔ7+/KSi֓g_``<ٹ\Ӡ}zxp<1U$&$w=<_xe$''٧y˿b}}ͯop"׵m'OzFRRy @kT,];>*e˔QgO?6mR2ڽG={ϯ ]-{U{>t|cԫSG/))%%%mY_ow.5ux%&6V&ϜcI@ ܗ+!SMpt[lO[7YW?`F6D \~ܱsC?Cgc W)껀sn+ >'X6nd0u/_ ;wg|)ޯgoR,|k^_U;ݼefPQkr2rAxzXu~wk^,@{@xXSF5+NoժTU32pPɞHvuYN>)W43Mn&uҬqUjUILL2treŋ$22R{:x!]v3=}4{nCLh=zuuFU}q9I;FӜMQO8bٽgi]xY2;c|ٱk4ؽxzЧT,Kg_.S_ls?7#ձ֠~}ifq7kDIk!zvHsuOYۯ/|i7ք_{O?ܶu=sȋ3gI$"<ܩϯ׵k/UևIJ́9_ʜHU5D?4Vti8}D6z$DN~'~}tw9|,[<Ϟ?8z(#.q@O顇̈́|d=0~KzypqH|Q ߧ=͙{YJCz՚#FK>6Z;sӼqSy~\~C0$W(}N/C:= B Iu/U~˼.3U_f2ȰaMXdIٙiygzrdc޻n=ׁ^S5[n @%M5~S<؉={H@@ @2Ӝ>Uyn@=Dvꢅ|guEn=3ߛ{/F?DUe7Λ//:_ڵj-/Οg'ϿgE]NX,ҶU[d \}KRE},/Θ%Sf0%ݜ,ӟ!;]^I?:~~Tg4y3sz!ro'˒?@ωk]@ w?e3~}RFv=كWdvbɥ?"I*__ߨYНe!Kr9\|y,~E˂@ KӜ]R'AWزmZvh%?{fڢҢi3[K̞2]|yqݝt=r訞"U=]s.En*vR2̔e3&G3O-_j;l_ٔ|%-5^~n=]Nܼe*-뛔(QܔkIX,z.fR{ADkэl2/=,f'sgnhft~ =R}g}z_*Let~}KЯGo9eJݷ {U<6kTJ8pEǹ=iFzD5erZI-C=-sx%>˗+gJudf]@]Fjrw%_Ϧk#j>Yϗyx>{svO~A"Jn`l߹zK>TP^S{#sq~zugݞ~qUz뻋d/UW:i %7C/7]w a>~}N'oZ4aajgO(iZY[@UNG)xf}yMg=TvjJw]'>133qa2Aը~qzN\ꞖH_ >`Jz![=dW_a-{]Hokn|$zt't={9uAyŲՅz,x =kՒgxZ&L"'O;2ײٵjݥPYlaZ+E}@N493.;{rJzt`WE%656զ+F~T腩/k -PFfպˋ+c">m/y(yJxƎ֧GOٸy^S_M=w%~ctUg?7~wkgq5 ^:X16~!OdߝnS\ZKsk6mښuW7u|ժQS?zг?5u/k~ŴeҌirwoL{>Yv钗滋vl7z\:uƬ]b/ZC qZ|qU9:j2ϟ77rz.l8>/ln]u:ŰTl/%%ٔ:{9F*>c5Z)v>+ZTΫ {.7rB*i?zs dU%}۶2zxT_ :Wg]gOwG|HMe/Ur~gkA9i,H@vO?~.[j*M?ѬŬ׼n~R=#N:-3UUٶRHaymk@g+o ^ݞQeuzzq_5kNp@vRaO곧:RY}foȈY;Yv@OעY1t@@@@@@S!@L'@@@@@@1t@@@@@@S!@L'@@@@@@1t@@@@@@S!@L'@@@@@@1t@@@@@@S!VJON"      A'71%A      #@;cMO@@@@@@fg4Ɇ  \@@|T\Sٜ{&%+]19kǴ#+Z̘4El߱]> W.*ʃ'羐oEJLl=gw~zG???yq,[]xq=+*OQ8  ,PBUOLz&   @b0U[c7:dr:7i*m[1ފ."/Μ% #H1~޻W_<׫vSțo%p+I'[HJE|lSapnBTfeV$99E^}%rͷJC$<3:Ⱥ鍝t蟹*9kTֽ|ܽK[%Ƿp}l}rCIɒ%oO\v/xeYb\] U?o.1|׳riӲ,zm5s<޹TZMUч.t>IU&))U?c2EϤY!?Mo` IDAT^Ҩ5rQ 2Uԙgew\ ;#  4^Gy`@@#:g&P{}`;̚!UH֮M[ qGo/~ڶ-[PB?bRote蘑sL>̈́q>Cn߮/Ț_G}j7aXYl&SiDŽ?5?Kʕ_2h0M5V^x%fisM5nbGV%7:&>RYz K2}3Sͤ|EͤzOu}\?׾:[wyWw2i8տ$'YW^+|l GKeĸ16g%%̹/ΑD#jWr97dzv \$TqGʋ_Z|s$R}*BAA2gLyٞ*ȃey\@@5n/٬®]-=&ǍN,jG~b; 6R:"!5xxO:|w8bCt7l`\_s]yJ|m^kp-2Y&șK{  3}5䦌``-9v(ܹ2rp},}r([ĩ};<'-G{)RV,[FiOoyjUPH;Ty=<~t7m,9pAC3-f5>S+W4^Gc+ZL9W__f~1u&үP=~9zidvD@@ a  x@rb=Al#%'mofą 0fߵK(Mh>IaC~ҁR?[߬za6w4^:=QZzvٳ2n$L1U?nf=۽Ea]7z9}:lhBmǎ:Ǿ}9zcV?2>fK˼S:>hvYjfE+;U2f r[Vk@o rB7w_㟿5CmR=7=#Ǐ(,ԳΟ?m-f֠ɗ=Sew\@@RYz\~兀` )%=u7!6{eYY}uz_._.[EW+Wv 3zzͼ'tժI\\чYʅ .;WWזi|#Vf׾K e\<ؔgacvu@@@)&-j7C'փA.U)S^W!od3[9C@@}ILN7rsG=CY 32?O[YH6Lov4;mqcLTzFڃ  )PX-+Cge!sgU(#T}.&[(z\M劕.:YjZʎݻ䫯W6^gȺ ̸)d K-[*V,(O2O:wy~B tuΐYr,Yi  $pSeɉ^)iRrڿbٕ/$w]1UPm(vi_Z<^թ_GY}̩} b'Ri   @ TI?wmqW#j %$%h灞} tidNcr5HoU{h'2k:|DN/)~}N}nN cr^N x*J\lx ^GFm|9ӃLa+F@@@NyfJ0SMN;<;xxy[ *,W׫']dA߽wL>A]6l _F8xP=?IHpϲ8Όidd)',\ \>XZ_֚R@@jT-{c%R◔?c1./o>uCY~([h8nSQ-y=   @(UBSg5M+)*;|긭 ~]ﯷS1QY6偞c3K_$Oa{frmwHժU%0"/^.m'm[W)'_?2mQRR%eo;reɹs~_7}ǝR,q˶;dC] !U+W1I<>TV牊V 2 [7g۪w9]Z{Yg.$ _g;W.Ŋvo)ETVUeeݯMiתpoY5-Uv)K>T9lO[nF IhOHR6E:z-j>\7hmmHLL q9 #     rOu=ҴF=S^sӾJ%B^zY{+oԠүw5_+* kؠvgef.;(Iur=g: :g#5jqU" 6n,W)+.G?W(_A,E 1Yzm}vIKMnŊIL5vD9qɄcU%&&FgBS͙lܔT&zo^isӦp99}&i~iy@@@@@W @/EW(\Ӱt)ܭ}citYr:ؙK&8N:)G0?{e΋RT- p"Y[Lu=w%GTg-h Sgϔj>稡#̽3jj>åZfIӦ/QQ?Wai6z8s.bYZM0^7o_ku=1>u2b5 ҝ:ۏ?${G*J&I7/ 5S/,4]|ǟ}*+l;C5nl~,?N5Ƽ6pP9u[ׄ|%—g`^c^Z+i{nYRDIYɇ.i(x@@@@@r^V4e7N:ԙ:{VtiNYFe/͚c~50m={Ty3!u)̛5x;f 3|%MoiϿ\/mI@3@Q6>ԯ[C,*`Loδ57}麿@O:t}IjF~͐АRz)r]v]} yҫ3ʛ>;9RoZ޻Ws꒙7r0߄iSMf .mӐJڵ      m5!a2lؙre./Ul/XtMիSGmg^^0_6\߮tyQSrѺmk5{*!!mV͚>Ko6m.=f>i4 m e݆w e˶mzut}L@9hdzL[sFJ_-_۴Uu5J\ǚ[' |Vn3v钪zn)g3kgͲz_ǟ4BAAYz-7OXpO|ͺ^}_˩ukն=Jϖ-e+÷o.^D4^l.Uy@oO?JRrE[Jszzt ֤QcYeiݾz|Kv>7dlLkw9m'MkF>"&nx @@@@@3zqSKiMKSmOe5{!g7uwoϜ1vwݷa>`̞2g{Y{K{m_}zT[l(sF>n?gM"ڄw g={:3Cr'̮Mszh@@@@@P =J5+-11QFJT6CZkM=KV*Kbf[N]5et[)H^Mo=ank>u6ZV:ciRd\d2Yԛ)m3}?BCe֤i'߮A[nQ\]_ ?D\X:|?{wfYY UUM7=hZ&EI7FMLb4/Y" 2 1D8( =BC]UϩSMSU]t:o?O=goY6{?w1:6Z'^Yzǽl6z3 vrK\IzŻ}S-  @ @ @"Д@o9o:3z{z#|_\o~DWP^:>蠘7o^|5E¢){ʇmop`6-7s{){͚XB>Wmym$O}I`h~Q-v]wƾ &sx[-mޮt}:+@/_s'e(C|?(~5;8䠃W\;;k?zNx\+맧!@ @ @vz_`𚫋r/g@-f]oWz=.Y֬]W]}U׿7o)>ӟ|E~+b=_J6ny_>/1~t*6rC\fM7O\{S\z?[VJT%ږ?g#sѢ؜[ߊ|뻳kU0^oŇSfm{(^^)|y蒋F @ @ x Zܜî ^]!@ @ @R􆆆ͧ\wmq|v_ϸߏ\x[n%u @ @ @;o IDAT$`,^8g{Q=K/xI'+x;.Cɵ @ @ @t@[z9O~݉_ȯ7)t钺l @ @ @Nh@oN*C^W_NZ#B @ @ mg~rC>){Cw  @ @ @:E|xo_; 8'w+)k: @ @ @XĻ/0.|ϻ7&,^qV[V  @ @ @:E-'cM=###q]wGx_>;btt$k__qy @ @ @r|\> @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ x]: @ @ @@ Z! @ @ @@ MhxQIJ%Ksά/< =+۴iSxI]: @ @ @@' E744oN5]Gv@ύ_;i]\  @ @ @B-Xxq,sx_4!{z^۰ޒ @ @ @-&0/E}qꭱa;CC"Ы1G< лŏ>^ލLz|];C @ @ Z .=x+=_eW+wyv@[z==q^?я{XqYgĺ*v﷊O#@ @ @S Dq~K#Dcxז8?A@])'WW^i  @ @ @Ƃr`CUS>W-/H(߻Gz~(=퀗F<#?%b3o 0Mcǯ~zIo|=$ @ @{4k r@VfS"L+µt[q{ǵ-g q(ytnmH,̋9Boψ;.7C_}Ul6:g t@[z /w_pa~rC<196xqY%WB @ @s&Л?XUVPfeZՂC9LˏqYVV*/c)?ۼ5mãB-\s׶6.?qgl͟S_/_=q+pi~~U\x٭}>@G E~|덑;sώܼK-5'>Qb @ @ @`ΪEǪ*hYm>Ue[-)r[z2 k؆ r8Vf9<++݊a\zϖ\BMUVqgFR ]?:|i<ű5߾gp_-gv @ @>2(K[cZWZm&[5ǭz\VU[UV떞H-*@,dEXVT 1t+Ԫj\͖oII\_T@.VVhWi#@Nz- @ @ eRVieV_XVVq*j'{Юj9Y,?+c\VVJZ%[nYVUm֐ʷB]d]zҬ^IOTN @ @& 񀬪^+ZABUoe6vh[Ce[9|>qmEkzȆʶLbf[-t+*j!gk-'9nU \6-$P(z @ @ 0 Bڌ2(i Unm-UUUngT[-a^3\qVjEVlzrȢzOl[ ٌt A@   @ @t@_U6TU@9nE+Ȫ%dEdYVnU\c'!Y[UVpy>[bVrEk|V,fޛr~[zO?l >Y+gJ @ @-RY"85TCe FՂ"@+·2dTVµq1-hmdno-ϝ4fS@7> @ @@ EZ*5\Phyl*R{ɲZcZüfl۫pVVT堭Jkl+ԊZ[Uz "\+C»f\c @z' @ @LCYG.[h9(+MN2WZNޟd-+ު-z6 ЩN]YE @ `\?^ĕV ꭪lˡ[q;p{rlocBRq ruۦ6eKb[ƍnUoy[ @ fn @ @-&0+vh9?=W l_ۆRkɲ s\V~Pz~Ety~7D}뮌A,oFrжcVU-#Э|\}U\-+ZLV_[n)K9 _#gH @h{ڱl Y\+km#UZHm+[L倮'z{gmGxtMFzlش=޻}|?[\-BlxUZrڦ'3[ۣV,F30-wTf &rRvZJ-q چ ښA @ @ @M}ށÖg7iVd`^5o-eEPVUZn)Y{jjUZpЕ-%t皱![UVǪk|b[R⪪ZWo1+gԪmM @ z @ ex֑'\>N֑JrVq+C-$!\YV65cۚð**LTi[;̏6[[JϏ!\nYV ښA @@w @:X`Ų`Xobe§<,r\<~ެ 䶎9LˡY=(kjm"-%s[ʷudn)?3ncGb: @M5  @ {a].O !^dq ^v-d|md8,ƫr[5rIo#@ @ f] @u}*nEzn< ǭkm)uݖO7b~ο~ϳ @4_@|sG$@ @@1.se]Cu]uM>w-)U] r`wk nKR7Z +7?m  @@) @ [_u b©\*?+ @ @)zSف @b~ݲXYUf }&ۆ ku)uEu]h5;[ @@Ϸ @vkQJr`WUץa4[q[Z31+9vkS]7% @ @F @ @\ЕtCe |?v CW׍ʹVW吮Yf-2Sp+l @ @`wv @ @e-/+n0VEu`V.Owæq[ʀjyku??m-sN @:_mE ˏ:&-Ysf}ewx_+_{O>?5r @@ $.s]3v}󦔹%\&,ZU]m)? @ @Ym śO958'zo9;ߊ|kzlūOz} 7q @MKR.x9Kun^uoru]_Wܚaݪ[a7-!@ @& E788/%{/y^_oo''FF}qyݫW7a @@sfqP勦cpܜ\]|-릻#@ m-m,}G4㰎A"ЫGLv\<(N4F @@﫷\Y̬*»ʺe;IOhxXܴzsܼj-fKsl @c+p .5~U\3%|85㆟h @\9.u2ue+Ta7{?ٸhyK.W =Ov @ @c @OJC}`z>Xqk8F3{Nx>]U&6r->oTMYh[mBVlmMϥE.V8xrx6FV,#+/gENuy^'@A-'cM}}122wug;?o8gxI-4 @ȳarE]nw?ܟc<Ϯ9z]smmg#@ 0dcZcV2WUAI IDAT-&{pq;|dc[=K\3 RhVcJ!t+lZVUBt[Vƕ_UUaܜ4N Ms?4K?z7;{lv!0A-=kF @Sҿܭte`7*r sYc)/EuM7-9,an5l~JC; @L.0֟³`lҖ Arv[}[C[-u!Z.l!Y=-&sVUmžUKZ\Ce@z]. @Ϋ®l59ԛl>6nL0sح*a 7 @j9m Yޯ6jf5uL flżpm|f[-HKjEZC;"ZJVA[ q*lh'^;s%@h)yreC;[aeNv7VE]솋vؔb @S/H+ô2H+}ƪ*XnUP>aYYVVkY̿Vov(-r6x@7 @XǼ"-1W꺲.\-Ic~$h7]V*uwlʟ @`rVL[1V ܪ n>yf[mdU?vy4nַUhe+ȨfFo;nUx{j.[=MM6Kx0  @MX)Z`.ͪ[%aOmشKv7nU_w-EݪuqfuSz `lk)(*ת3٪ o![ߚB2Yr[Lkzp-t-%ȟUnA ^^G^*+ʺ}se]5nɫ^nZaޜ֕0s uioZ[|d=`ۃpU fL 7^VU#m|5ڪ ݊Yo) @* {O 0'+ r~9KnI R]M3&O :ϬT]BuIw6 @@ÿ4'zchUj z[m~36UɢdZHjʶzV&g!\jfUoZHܷ"@ @h9C}Euˇb"vyT۝'T֘9I( @+/WƦg_}વ_LA[UZQVv|嶓j[כT- le me2p++qگlOiX[SA @mzmTN 9 6a6H&ۆuivݍָˁݺu+!@D`,\cO][?o= sުpUȖ_wuE`YUEr&ێ[-Ml @@szv4 @@ ,Hu0 `,_4uuڍ媺ݔ[斘m_ @"0~M]qBх|.?Wyg8k_Y}6ξs?|եH+fW3\5/汕*vR_{ @6b9U @@+Źnj*ZbWꟼhCSԥ.Ϯ˷7Ю_7kRg#@L&0Zt"u.?7PtoE,mIOJv{UPWlh!÷mZH7 @uo @`jfnasIMݳixPWR`j][52  @@w vsr*ʺn 'oϽ3#ѻ1t 춧ShoSqCcn>mSw%@. k: @N`Ra][a>նj֢.u_WükC!c#@ZhYxkβ++r*\61K}uZvobaCї~w}¼ @nFB{/W.+REݾqv zRK"0SPnU5.n#@G`lw<ϝkEW rWV6Z[_,*+劊|+n -nYiIًʺjN]y[u=X`?ݹ~lYu`ebnbnA] r{)fLlm91{֖y @\@练 }=f3ljh7yy*K]-Ek5 9E)omϏw OZ{GlAWTMA;Z[v* @C=tC@R`bEjuKUe>6WMM,񠮘cbv9 @+P,̥%&vRe]nm9E+h֖U-8Gr @@:f)] 0Wy&]1nB+ޥ*IOql,Z .Ǽ5v6iE5Wk @`RU+r%]uEp7nkj6֖@h^+s @ZZ`ԚjZE]mCS^=)TQ"K]1.꺑 @eԥ*.tce-w_ĥ9sfХ`.Ϩڲ%] @` v;$@-g`-q׆]>xe]s'W僮J\rE]-˷Uh *3fU-0g2o)Ϙz@7im΂ ЊV\DL[W:ry} /5.}U3vv]omSSuU`WwW_7]uT^'@"-f効sf-sE\9 .WʍϢrVه @\@.W\r##"qo"F^C=U7Tٗ[`ft~cF4E`Bk[euxe]*m @I@N\  @ }G="bG_Xo/h}>S3  -S\b.UѕsU\CE݌Z[njhmC7rjq¹ @@ :}]`[bx ^+]|Cۺn!]0 @]`bkT!++L[[r΂\O_vʸ~ @@[z=mg%.FzJ==N;̇ @ 0c}+*vKEOϮX R g\M-sA :-w @Z<{q^c? on=B> @@ڲV! nl,2vwkfʺ?n @4IeKgǽv<{y'>5a @ ^#+}/>8F*N-c6MBRqKK>op @h=h¡n?-; ʊb2ȋ4`Ѫe ֖eRk]3 @)ց^^+?t5W#:$.ko;WU @ @A`t@.v(F Ɩ<,b^O|Q{>YϦgMԥm @ 0s/}޼xo:ذaU @m 06[u9+t?v9)HM;z#{wB\ui]nu"U @ @ymqq#g>39x{̣/ƞo{1  @ 0mnfU]W{.w7-)K!ݺ*Ka]rW{`5ȊqK}Pͱ𣿉6MH @h@yTD @ .a/K{Yv Q]>W5teHWVmͦؓ @+ kߵs @̑X]7>_,tܺ<.3ֳ-WUu)++&vf;, @ZL@b t @{E ,|LyE]Y]WαOuݔv @ @B@ @Hs銀UuU;4 uroucEݖ.ͮ+Zd.W @ @EA @@-oRH*F?$UL~=Gݩ.tռm-sN @:_@k  @t@OO̍WؕUuE`؂yS^onٷ&6îgxdϰ @h@YҎC @91+֥ʺ\alT]חA] x~x @h%^+s!@ ;]嶘)[4 e;Mzo#G @uݒ` @#06of1.uU=ƢwT]W][t}\qnvVϧ @ @@+ Zyu @(b~]uܺ%SmmU;.u.Um>g؁ @t@V @؉`onB`a`vcѷ:tuUXbv.c#@ @vM@k^&@ Жi>]QUg*얥.vT[Ϧe`wW u9v}yv}ۦ @ @3[ @XKW,`]骹u6Ϸj]*jvUE]m]bDuT^'@ @! Л UI @`7 tuv⮪0g#l-gץ"Wؕ3l; @ К\gE %cz&T׍.Ѝ.I!޲TinOѻ~\v9khٳmtϰ @ К\gE ! UPlaPogX\o`]noI0m @ @@ :vi] @ 6żjv]qC+.s-,aVrHWue`3<2Gx @`^/K#@xhc{̛u)[a.IR;́26֥`nx`7nևvM @t@ @T'Ϯ(<.u0㩶Mʺ\a4w]֥ͪ2: @ 0@w @@G ϳ.uuUh76$Z+̾TeW<^WͮK @ 0 @"0,t]~`.I0s]4ngh إ_R @ 0^'@xP_sPA]-zO˭1n4F @hu^#@t@QIWv?Fj0bƼ)붏uiv]y[um( @ @nu㪻f @, Ԫ[aviTm)ϯ+3Uu> @h!^ -S!@\Y0n'-1{'j]q[ίˁ]o l @ @@w @@]`l7FU0s[s8ͳjٴ}<+Zbu9[ºu[gxt: @ @@% @"E)+v)oYRwMUA]9lٻaxہ @ @`[ٓ zb$uEXWU劺~z&#eE]KA]+a.̯ @ @' k# @آye`s몰.\c My)U堮>.=^ @ @@ @*ۓZaHjyc ")붏Uu]9v.,6o1M @@}Ι @m$0Ǽ2\ZŬ*Fmjݸ=U5uE]jYvS @ @'޼y}('歛㳟~+  @HEseP 3oz'Ʊ(gZ`V)ٴ}9 @ @>{EoEÇ/MQ/|q b6_f] @}km Ouf0c,%z6 @ @;}/ θ,. Х칽m3^Yni NQ]s( IDATA]1nKV6 @ @3h@o񾷿3?~cm>?53 @ 6>ry\vk\]_`W7 즺#ee]®RuvuSz @ @`fm|-ſ]vY}1>85zslذaf"E'X7OXpPpӍqdK \>UT^]ZfNʾz ޽g߻m{ @ @ u+.z{^7o)N~݉+W]YC @@kl~~KzJ|Wn8>r]OIbV]-Lghk\ @ @ |]{K^SN81_vӷ;O;;7~?^V=>1 F1L.s @ @m$ obhp0>O80|m3{mep @LFV,#+>pXt/fqC @ @%>[04?yя~tlwc|VגjN̎@>qYl*oCs J @ @`>Л#7%@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @M5A @ @ @L@737"@ @ @ ^S @ @ @z3s. @ @ @Mh@?>ĺ _?u6A @ @ @̶@[z===W]G<8coO @ @ @ u?Oz;n N;Xvͬ9 @ @ @h@g޼'Ao~=˷ @ @ @h@7—Gc'?Y\o#@ @ @ 0 [>q/4Vzk|k_ۅ @ @ @@k uȃS3Ϩ+WwΎ @ @ @`m-Z0ַūN:HO] &g-!@i~~6ځ ?;;h1] Mh]5 @ @ @P@7C8o#@ @ @ ^3 @ @ @ z3x~<<(n?},\d3O*c>'ύ{===q 7GuxkW><ǥ篏_vWx>#FG޼ys\k_>όN?5V]S%KĻzaǗݱYgZ?GۇQ ,.pދ.^xԡ_37.ؾ}{9}6 Q{Ǐ~x_S)q+_#*'@c:x^O۰>_ww?qyxկ{﯇?8UI?Fg?_'|c 9!FFmw^#~g{/8x}qֱkht~Ç{u΄\]=O:%:g{I?c_WOziirO>6w ?is쿧/xqK.פ7~_'=ώ{?(6mټTޑ߄.jzL "rNqw}so{;'Lc3t_~ߜ ׻}Oxի'?i\Is`(N>b18@,ϵ-Cקu?0uŷ$0/'\|[Zq^WIy]?^w]-_<,8~?WA|CYr!`=ϴ;-\_G 89lT.I<3myguU[?ؿӄE=*Mmd.k^_ MUqy;䠃S@8S5}ޙgQǿ"&a]&K_*>#Wq=)O#0}U{~ޤxm(bE ^D@ޤ& b+V Uz=שZ{ I&eIyMgs͋_qY׾qi<ñŃtc?/ӛ*.8nV[So!F\5/8>"|{'x^No|S{W6 }NAʄ =z7;?$X4+7tX\rz$hljggL/1Ą zx3>X \yK{>3>oo\C}iK~+HQ`IO紞<;|)W9cGW]Y|(ӥͫv⼋/,!/zBSwjCyq=7Cc"t/oYܙ\X7^ظ8mjȃ/yX=:Z =Il|8?~ORɻ8)}*0zdlwɧJo_=-RWW'z'sZOyKf9F׿}yrq^|o/ `Ir܉qϯ{Ҳ_|i\Koxc\p3qǿ/|Knp6=ްSq'˵/oRE?鏻rYiU,֓ׯe5d+ ?T౸@/W\tyEFn>xꙧbʔ?.N>Sˋ;?򘴖zŒ鏢{%w{oZW]^dԨ2yOhz ./yu5TOh,N}ߐ>@m7d.:ђ>Ŝ>}aT!{[zx@KEoސ?Kz9$PyE|L>XB_,Oy9%<ԓYsyMZ=b&~ih< kFD}y5žyIn{nwkSKnVnX-z޼|Y=5rݔ)S[t zy)CӒ_x@犯_?~ze8#PK}zYwoz~yOȞNS7Ž~r Kn>;(\!7w*Q K@f  ]޲sڃSNZhcyEGܜk(-9rd' .ٓ@/*=O]]TuV4z̑qRZ(@Smi*uD mǹBoqG~3$/v/ۻN[sx/yI|7? ?ρ^^byO|2}챘zw+~@G ܳ&'N:ˋ}Ģ~I.+s􁇼Z /6:wIb'tWt#v~h)JϛMמ2ujW?S^D~.'3 @/yEϻk3s/U^Nح%.LG~7Ooroë~vd<|TWZ^_9$PK}zAEϛ6mZ_ <ow_~ͯ _nyUO}l/3?W=j  71|xڇ\~K; IDATG'Dvnzw f=~U?;6򦝊}6q'Ğz5:%7)gﺛ@ϿJsZK^ U*E>2mjwIxJ?~w}oڛiT9!ғ%-y6ٴXxΜ9Ei9N^iT@`Öo7|ső_>4-~cׇv^KKs]1&-~ɧ_bڋݥ~>lXI |羼1's\p;+7h*oi8cݵU@ye+g)GNY'wzO?ϦD]h^,[|'?9roy#A ց^yrEN?;w^kB{}xƺ_iYC%_ջ2Ҟ|M+ @T=-K.e qxzS;/ /q>p_ÇŗR /\θ7v~ɪXz3i%+>lxweSw+;uqg<ܗ :t䗧)m{ Gc!ok==[gb\Z6b߻zy#r#+XsZO"|u-Kp);]3gűgNZ gN|scsu:{@7W]i}.)\)z)~yGbO,`ǒ?{7v}绣WKv;ǎm_T53-M*.x8]#&2:moykMoҒ7n V媕8;InΘ1C7 Ҟzܗ7t\{mOwL?:un|[mU^޺m+,¶=7wv7^~G?hZY'@L\GˬN6窌AEv^ϟcƌx0o{@k~Ԟ{6[m!n;}gZ=M;ۛ@/?ģ-K" ʼkE~'_[k7IaZ^)bi=sya޼ݒz=I;-~'@L=//*֋RAķN=5w􁈆2mß(Zv ~w/sUJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @?H @ @ PJ=}'@ @ @]^[:h  @ @ @@ *tt @ @ @*zU1I @ @ PJ9&@ @ @ ^ULA @ @ @T@RgN  @ @ @B@Wl @ @ @* Ыԙo @ @ @U4$ @ @ @@ *u @ @ @*zU1I @ @ PJ9&@ @ @ ^ULA @ @ @T@RgN  @ @ @B@Wl @ @ @* Ыԙo @ @ @U4$ @ @ @@ *u @ @ @*zU1I @ @ PJ9&@ @ @ ^ULA @ @ @T@RgN  @ @ @B@Wl @ @ @* Ыԙo @ @ @U4$ @ @ @@ *u @ @ @*zU1I @ @ PJ9&@ @ @ ^ULA @ @ @T@RgN  @ @ @B@Wl @ @ @* Ыԙo @ @ @mU1R$@ @ @ PA~@&MW  @ @ @G@W=sm @ @ @( ЫIe @ @ @U\) @ @ @@ *pt @h=(}b4n36ZV yp9=@'@ @Rzrj @K?b~zҰ>C{ה?檅{Ωg7x{ݦ @ @9 @Dq1{4aW>Cl<ښhmkUL @Akl^x$^y @z/6>mhGtW?6${権i3f7xiK1qb>GyxGbu֍}>xgc1q΅g>阴Qy$Ӊ @F6,Μ)Ы7f @*C`kƜӫK1/@矏3N:%9Ș7o^oA/!N:s[zq~C=2V_ub{Q}7nqcR{Q?x({ @X@A @~.0-y㑽eͬwm=~̲Ǟx2.8?[-fj?n\vIߗֶ{ul+cYqľ}mo\ǽ_ @ @e m  @T#{݇q_5jf7qc?ʅgE߸4yXgubw+|x'R01bxw1eԮ <8s/<xp|ú~޶[cu։[o-~xO1 @ @%  @+"{;UTНE]#>?|1u+]}]]ob5JNnkFr#aC7O3{ @X @XXrs qq~12Ue8kkko 6ww׿b7vvax?!nWv[em1|7.Ƽ'>>\\/  @ Zg\hK VJ6"x%Lg5X} @.0w5cUR^=fIb(-v̛??ӫ9FMmM|^]ƴ;^ 䞍'bqVT] h^kZo} @ @C6:BT{zO\+ZoR]K蹳 @ چ3 =f|' @ @@2hpM1=E r%ګLS7}̥v@[Zqc}wYWG%Ы+&aym*fN~^{|,vnhIRʟ=Kns߽q͵}#iuRsޯͯBx56`h}6~wyLzEy챴󖶤ju|TJɜ;gNЃtyhsEg\M jkc5vzb*DmJ_|饸/u͵zoi @ @ @~)w_ׅ&mQ,y- NuSz^"/-_@o=^k>JL>=ƌXp߿2G,sz/[︽xч9{bӴ?_M:/8okvua1lhllLas/Ő!C=H=vlvI%7SjO|tb|݇yoK}}}_\S| ^_) @ @ @@/-׿ǟ|"tcOruqƉCG1/:9t[g̎;d{M?>9?/C _Rz_=3fL[sYgⰃT]viv]me9suҎ'`0~[N^އܷ{￿Otuvy[xQyʼ[n5~?c6+Ţ'Z* @ @ @Xlz~n=Hܕ{w_|u TYޅ_$E{~db{7q^i\ɗ+qYǐBxbo%-*%BRYr3*Uv9<]8cc b񈎊|N]&o3{ώ7ٴ4e]bA @ @ @zWpb=WT|-X׹?mw_쒢g9T:3cl rZg5]Y_NKkW#G^?a\ Kgz7.==lXp//uxeA{Cco_7cƌW]e4'G}]];⢯_>xҤxꙧcམqo9wRH[o)B_z)6XzO%~/OK^k8bؐuϿ|>q&9\{g&zA!j%^?>~rNŚkӦOOfi=~ jS`һ?p} \-{yO#FTwĵu4>&˚N'Z2o.s T^1cF䗧ĭw˟by]/Eo) @ @ @z`  @ @ @(_@W  @ @ @& +RC @ @ @oE @ @ @ J @ @ @zj @ @ @@i(5D @ @ @|^Z$@ @ @ P@4J  @ @ @(_@W  @ @ @& +RC @ @ @oE @ @ @ J @ @ @zj @ @ @@i(5D @ @ @|^Z$@ @ @ P@4J  @ @ @(_@W  @ @ @& +RC @ @ @oE @ @ @ J @ @ @zj @ @ @@i(5D @ @ @|^Z$@ @ @ P@4J  @ @ @(_@W  @ @ @& +RC @ @ @oE @ @ @ J @ @ @zj @ @ @@i(5D @ @ @|^Z$@ @ @ P@4J  @ @ @(_@W  @ @ @& +RC @ @ @@.>'blsύk~ǿn*_J @ @ @V@zk7>'yF455N$@ @ @ P@z~Vs^(WFk @ @ @@EzÆ >7~zg"Ͽ. @ @ @X~&grjk]lqG6mh @ @ @J@/W]rܹ ÿtH\ qdZ'@ @ @ _|WSO^\hq!ٲoh @ @ @T|=>C+~XoҒő3f@ @ @ @`*>6dhcM73fOJuM @ @ Џ*>GB @ @ @t^$@ @ @ P@;bݵM0mΜ9s⋇R:  @ @ @ 8Kg]~I @ @ @O*>лsN)Ӧ)  @ 0چEŐj_5s[}&:fP6$&/? lsP k9~3XH/$sݱF3㪟_.L @Khq|<`~:cЭ/3#@}󾵺ϛAKhYsXhYshI^{c^ښO|*nθc-q Ŕ*z;x @ 0yp|Խ0/j1 @U 䪼{o#֏<3[k6= 0ۍ5+^{9j1(M)q!q?7|siH"@ @@E Fj%{-k Oi[6?k=.{X@[zOOxs|gޏ]WLPu?#➹OOᣝF@Ez Z+~Ouaq%@ @hѐ¹֍kr`Wuc;ҏYk8~5-QeP(C =ڼظǾx/)7Essvzc@ġ=~Ī 箍gc5jZ @_Tt7l8k]w{Ol>#?.fΜ^$@ @@ ,.wmއֶ2?^nڗw|5F]^mOPCT K9 @`yѼiQyQl 쵧ۅ@*::훷?cǍ/M_}e @寮KuSXJP-J5m>qýcMW]/0]kDc1)¼ʟR# @`4m=6]01Sc_\Wu K5FC @@E Q]hUtE`QQTq7e^̳4QE':O @^ z t @W"ˁ] ꊿO]vu]΀ @ @:z9FM @Wu_׵]{`W3= @蕀@W\N&@ @RJӼ0Ӓ*o @k^#@ @GTEҗ{-g]]5sUh>D @ J @}!PJu:Muu}1w$@ @- @X2k;*EE]$%@ @) [M @ ʩkX#^vs @ @* 3k\ @V@YuSCX3v{=?T׭t  @:3E @Z]g]ڗ|e9~0T] @ @V@oF @@uEPlkˮ1j4@4O @@o`ϯ @T@iuA]ݔ0ۃ0  @ @v  @ U]75Wpn0Uו>s$@ @ @@)uZ;u]A].]^e~6] @,@o< @@Jkuuʮ}WìmOG @ @' sG @.PJu\]|eEûhS]W᷊ @ @ z$ @U׵/}YT-fQm7?T׹ @ @N{ @T׵,e[x9yi: @ @ z, @ PVuݴ.Uj"묶K4 @'~2A T5Ίuu^ڻngK  @ @@Oz=r @(n͋+wN @XD@ @T׵v-}+nhQ]/&\' @ @ zLD @W-PVuLvNuݫ!$@ @@O @*E꺦T]+|y^EJ' @(O@W @S꺦0+^Y3v)ȳw @Xnrj PUu,YT*ʺʾQ @XiF @ PVu݌T]r9먬ˡ̦0 @ @T@:ݨ  @"Pju]k׾jv @ @# [9JX@Yuz/sXJPWTݥ; @ @@߹3 @` R]ܣҾuv-dx @ @@d XU5v)ˁ]f,f^nϬ @ @V Fgpr\koX.HT@uط+|{Ume @ @ @oOnI  @ T]W39UԵWjW»*O&@ @  o]wg}6N_*5H P9,ێA7O!7}&ůRn  @5bƻGoM|OoΧ¹0;%14,g @ @XM\m8`IgpøX-DQѴhxT:!8ƹ6jDݔXﵪV46 @ @@/*:{=v{{C N'@@Ë)}5uTb8np8luΣ1e~9?Q̜6!@ @ *:[tĖ|U @!P)]wxm 5MQhxpF?Mw5OŰkc  @ @%J@ {a]]m8[#3RCg/BGQbZVsw# @ @ccg]#@U/К"*&v3{a^*f} @ @]@Ww @>hYsh9ߥ@oѣYKg/x7%@ @ Pʝ;='@J /ٔ»bMFE^Rs-K_E^ilWc @ @Q@gE @@?hkm6禣fNsGp=2J @ @O^"@JudCG+2 ֿڗwU5<4=ꞙۯƠ3 @ @*U@W3 @ZV]}+Z&vDCy ͙ϴ]  @ @/ +T @iQ PT7=SxKj: @ @{^w]{MJm96v.4Fi E[[ @ @^5̲1 @@U k_:sҨhJ_y EMQ@ RpWC'fW @ @O4BJh3(v)yZ{~^ރ3y%\Y @ @@/TIV@ġ]5m6:Z vgnzG73jg6 @ @# [=%@+Ay^nL ֋Ҳ!}?<3j[WBO] @ @2ze(j@]Mҙw)bt+m%4gFCY} @ @ z+%  @F4x9K5o0۩/Ϗr^fFݳs @ @ 0zxr @E]s 6-麧x/-Y;L  @ @(M@W @Zֱh:7S: IDAT:+]ZRA @ @@ wV@F#R7:UmvbMs[4ܛKh(< @ @ @@Imoş3ʻ\W쒮 @ @@oάq @@ nIim1:Zu/̋S^ rW;y^ @ @X5FC}(вڐ T}kjN^Z>3-ِjf6a4M @ @@5a^@û*Q۽]|CFA @ @2zejj*W&&򙛦 GwKܖ(TyWج @ @ P1*%@2چ֥Fx[uwkvJc /w){~n] @ @z$  @ZJ^ێq itfofNka? @ @@@7& @@:ѸhT5<0=K_5[Q @ @ @ ݔ F/Yzbt"+gx  @ @V@o ,@[CmwF=jf6Ǡ{rL=3gy/ @ @X) 4iTQѲn{q^4T<7p. @ @@N @zZ' )ktT4n^ poG*jf7W/ @ @ 0zzz !вw)[p_?/93-9=2Ʀ @ @X^_oG?OSM^su. @@ 4OJw/Eynj f%47&@ @ @@@&≠g?n9j98 @JچFEט[sXN_yUc @ @X@Ez oM]#^;Jh3(xn3[G7tF3s:>QDh @ @#PсނP__;aeO6+ @-oT4nGtoóR7-xiTW9_O @ @ ЯDA}>N]vI< Yg 0i߻Ƽ-eu w yGf  @ @VZ]mml&<-& tA 8H{ߍN^.-ټnC^^NA @ @ Tt7rr㦛o9ˇ77vKyJZ"@@ O^wۧ6E=){ 73j4Va @ @ @`Tt7|83/}WZqaigO?uUT@o1iq6[럘]WTu @ @/Pс^z˭@?>fϚog -kIi۱Ѹ͘^Ty+w/'$@ @ @ T|Ӂ:(мQ4*Uߍ Gv#Rw /wOήF&c&@ @ @@9\6򙛏NhYmHNmLg @ @ пz{~KhPx[3w뻝[윎/U=8=jg5%@ @ @ U؄.+вפoq8{+QWZ`FN @ @"  i & >GDӖc3 5w+uOϩ>$#&@ @ @W% {UlD@U 2w۶/:~p7i]KgN_dO @ @^@<*h 6涇w9Kgj$@ @ @ 8 iѸhzb_Ȭޥ%4K hiq0 @ @T@2I/ CEVm8+Mgao4M @ @#:FDkڗlYmHin_>3=0=ꞟWuFL @ @G@BOAuѼ騘_7.چwJEޠE3vzcD @ @ @ ^G OZG7Dym7~jvW^j[ht @ @DT@^2iE]&;fЬdfEW  @ @ @z7FLMmݚC+koZ=3ǫ @ @ @@ @ DS.WуvzSG7-/3 @ @ *zCuѴbyn{~nwy ih @ @^? PE6.xM^,A^YB3mmHe @ @ P*|C'Zֱ|{-(*xӢ+G @ @@M8ۏwwXܖ|fc ͺ   @ @(I@WfT@[Cm4m>-kRۍg G̦jg3~ @ @ L2@FG)6cTnP`z4$@ @ @z) % T@˪Sx׾|fF#b\Wj2n @ @ P@TN8(»Lm`5MgWݳs @ @#^? ]!2olh֕M]Mک+M @ @F@W5Sm^hRi{-{~^Wxp_nn B @ @ @`%VKX3Sxøhdb/_].wߌE#@ @ @ PsX{cӟ]w&@U 43XϪj/'@ @ @YS;1n79lŁ}9՟o<}+WiQ]gԭ\fݝu//Z#@ @w`VÀ4 "$"*vc5hػADQ `K[؍-F"j5vDE7snvW,{ߓ'ϓp;09) @ 0:תeXwuGSH8A647&0:&wQjrhbzZwW hI>'1G lʲ @ @ @Yz mXl8[:els{M+0qYko WŸw+7m5fc^Lv܍ @ @p~}quW/ÉeR`UcO~]^+wmF֯~- @ @ @r^ǟ/vWp]g] b9OҤvs<Ͼ+wi型 @ @uXC+4F]=5%0M{pqO߹%~1ţ* C @ @̚@qd'KM zMzj{+15 @ @ } u@/wɧƤI]pxꙧOGEIZwbig9z. @ @ @:W5 @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U! Wլ @ @ @* W5' @ @ @U!P_F۞qO?U @ @ @!PM78Yjo/nhJI @ @-ڱc5fLyD*( @ @ @:>WSM}-WmV)  @ @ @U% WUխ @ @ @& Wn5& @ @ @U% WUխ @ @ @& Wn5& @ @ @U%PѪe~};-}A\:xϴ5i$ @ $!jT~' ({}gW  4z߶^{|rfsv7,U6fsv7*ԚU.f@s?fw pT@sG VX*ߛ{CT*SQ^{Y1 @ @ @R@@[ @ @ @C@@P @ @ @޷X{͵bۭynoy3.ÕFKrb /Zj=Dkb)Ѣeyb_-Z>F\}U|5"ݢC|d\u5~mbM7){„ knO|Ͷ>|a=ڵkg24nθUJf/M9bUVZx۟F3O^Kį&ƽ-n_t[lQL4?~[n/<5?#qw#6XW_blvs*V`=w-~Oo=7[*JX1vi|/q5z ^ɵ߹V+,\yX=H}xч}wAɧ غT0_ƌJسÂNWV-[ŰƓ|ޟX|~ؿ]S9ϱSiikD>3د PT:t7:w]ueoxѥkzom7//_NhЧzvFP, 7dP-E1GC<=~qbE;ŐΨGP*.^4=yqX<^믻^y9K^/bc%:-ƼvL?d@oyKp^Q;z2(qm7plME@ZZr\*v&:-h?Bc߻O =;2mb—Ks/:?>F5#-^eՊ a㎉/ ~nl\}qt0(柿]tymRN*3N7rK\k@ԗ_FxW\&7[ P3znاJ?;ȸum]oZL0鿓f6~m_p| ֘g/d+CJ^mR)g. 7s#)T` RPٴ*.__"*fM|طAQ*ݞ}aq]=JM7ڸWuK,'~]Õ-6,~ӟB -{9tV ʪ[q'*z%ox|m~|)W] 4@>})iWѯVnv/2tq3⼋/W^}5Z2zT'z2{y}x6|(lб-ݚ^<3,x_ӽҊ+:i]wk6vQ Y#)?hm;,p|^3Lg>κq9/1׏猳O;=\Q ~~,^LQ]4 7\qYg;4HO򤱅4dp|:V;oC],`{jlL`6z&;,سy Ǟ>R%iR}(qÀ^NN; "'CɾW^{<'~ulhGnIf<}ϤO:5zʻ;L۵f`co(&ּ;-5ͻZ8s9k&irdzUٽ[2OO@@O 2dםw.ny@(_vgҲ0]fK[lHO~z/F5XЃifK͋n,U&oif^^Yu.qɕǛo^tú ZkD[Yhا5d؅qxf4.t\x5KЭxA\wk}s?ϧm7 ν +~>39-7gIHM@#Zn뭽^f A'۵Ok.o>oڪ믭>ҭ1<,!󊻼Rt<5^^rig+Dy[ǟX]8tqo^#ڂ$̚@qߌƞ ӕ{.ީSzo{>={ P)zĸ!m?ݝVA[$Ўi, ZLOkL.mg:b'Ba⅗^J=>ݱg/X3~uڀ^ IAž1~zv׌ LD`4x4xWצwM/1}[m3`1YӻwLKbKΫ&N~[-7GBںڀ^>dyp3Ɣhݪ 4@>5LתE/U<4bbtݵN3W ~~!sis^\w䆮i;G 8&P[4^ zy[sآ>z7\ R;[lVrۺhjj~a/M5̿W}ǵg5,g݀^萷">2myEˣ^)z72 }B,ϔq*(<=[>vH[q^͵^{Ƥ\~Cvz'w1vlX,!ȳ{7Tů4B P3֊N:u0]?tn^:|蝶(j۶m.ʓk6&geO{yEZzh 03ީaRG7y2$onqO?UT^P4ה:zMG>7 zy}3^{"ϫR\rňqpYoALr߳2A'9%>~aV_3@sȰ4!O3.R::+Z#{>I_Ǩo^~O> 凜xr:+}o(i50.hgTqgU>JA]9`xږc3]ϼ|ڧ+R\}u1O>:+mh9IJ=Oוinر_ ;~@mCugGzQ<.\KfF^=yHz9moOg>VcͮkWWZ+{}RK>QW7jzyУy9if˭M @ӧ+.oyRW^.wr㱟g\aWkI_}Ult6ӼB1}Yx*i;N&k6nDĆ_,|<Z;ȴ}M<3N+.m>t)YLk[j:\s:9WVpǜ3OoͺXB/{M/h&]"_opԱ^շxN) 2HNq)'Ͱrn⚀޿|+N;锴ϝ7k6DJ=OםV*ͷ޽jo*bg\yσ= =^yv& z {3GN+my>z;2h3G>Х-+zИ0b?{z;n}tZc ~an-ќs6wMgQm3w}&(է=ǤT%pޚmB .}K5Os=:^x㭷sGzx =| CAqb|^67#Ps5wy4(j~>}ǘߩKҶm/Ǯ;?qe;un-ک-qAiۤs^HDzK/#Wխ] 4@>kϮ/.oo9gsf4fk4!F\} ؀^~ϢZzɥsΎy[@izR}Zcc<*uz g||Pb{]w}뀞go WիpU`tvFM 8mSA\ ,`׌6prͣUVŊnز׿ V_KZM ]n+ȳamsR p!      @D f5EԹ@@@@@@      ,@@@@@@@ @@@@@@@ "pX  9OC /k,ŧgF=H푤9nq~-䬚+`VysFl]5B@@W@/\)އ  (nWKj%!כ~$9m[tq\ٻW\8_>o#[x<Ͼ\~{JrvrR%>.^_V]nNNI3lWj?(n+yCK. ʴH>ǟ~o}_Zj%ryr3(G@@"^@/⏈"  @%'הۆ䏲%彊z:z[2rx;қ4WTo@ !Vzt|x;nˌސbiڤ ?P^|eY4+7HIO?[$&3r=K>ʾ!^ùABB8v)((pg|~ϵZSr!  (6;p]NvGWa2>  *{R{qO {y'.KwP!=w9[9z|){s2|P|Ï[vmٷ ½nIR~oa+ap^UyHII (ߔW,Pz/>=E&{1kNTD2қɯYKdR=,clo钓O 2VffljSdŪ=N?\9|9sn79 2 -/\d^u߷W $>>A\NYeXǪf@i2t`0l]n^*wkܰLO~'v2ixyTErխ<>pg]sMBՎ;T?.G3v9J>ߧ>={)d,4^2G/$6p&@@8b-[6pP^zJ5Y.wf59  T@E $ΖvIڿ[n?+Zs[o2_rÍ6]>5nw_s;ԢέYG>hvo\_/"τګ:;uϓ7ߝ)j/AN:ʍ^/;)ee|dX,[s>GO{{n9sՌ6EއHRRjRh\vTKM5|Ga7"  G**gpdAz-6SE+ȶ]!@HO#  @^\^OɿKa?v;5îN4le>F@@@?/hB?WJz w/@oyjY{W_qlڼY~p?x'}p젟OLL|:t[]%K?= =Z#dݲΗZj 2e˗Gs攛{9ڦMy^tC=G@,PrrM;mX[H([R{#M-RRϖI]ެ-rҟ>ހ   PyÄzEez[j)&+*)>Ftצuk3`k5_W^>f~ÑzeKhȽ{VxVZG j뒋*>y/[Gj]{s8̃ <$$$+䙩ϛa_.XnY/}*g@Ϩ@FOC /k,ŧgF٢=H푤9nTm-䬚`VysFl玨+A@@"]NZM٭Zh\銼Fjl"y@N{~}z$Dz[pjy '.\Zh! k.ubpGo~~yٗ_'ȕ].gv眫qFteU*%lGI\tRkMHWY-KOTjeWՀLg܍%̆+;/?O1k*5Oq\tuV_X ƍlTyK%NSꩰIC}T@/sƌ7ij*mX/efOL,[,oYK~̾uX'+VOhEߒVc'="\p=l8n~5πzqqy_O<^|eT p\/WA^o6֞:կO=Yyuע'N9E7g N 9>Ww@.K@/ @@@@@@@/G\äNdZ '}:>hSUdJƍhYN-=E}OJv퉉PdڌWo6?@[vii +>*t{dvW{;[NL}>Uy+*R7M|CM/D|ȼ? i]{:>z pY=6n _H65lϲgOyFY& rSve`yz rŪŒ}{wẦ+^4a^ۻȅwUs@@@@@@ X>;sg׻#?fB5rR۶`jJDƍΔd=w|y[ECnwOH&:<їnt/o{L05a$Ukgpユ5Iaaa|д__ڝ| u~ʞo*{6}^[c6.~YU,a㼳Uq7s̉eϵk֒t5;R_y9K{y^?H45VU_|@^}MUk֘֯WO;a= @@@@@J8Ҡf w-UKC~昶,[- "u _{Gu .:BWʮs*+{s;*0[CĽ_.P 6C+_zFIS~& u+Dz-?wk׵o T2`~w?xO>rgt罽K㿧kj֬)ǫ{Ƿ9^:vT^||7*pjS笫]}{-Xu+V5aTXZg\Uć"     @d DLwwiأooq{zO?]ԭ5S3L>=zIRRIł*H[51x j_~5_JTЃg,b\m~U6ԁz:KWi˼Y^p_W 73 u>dRmK* o@M?Pǝ,En}NHtH㎓<a25?O-C޽{~`{|6\yUU8@@@@@"[zu݈r󡁃md2dp`!O~a 2ɧLu$^6]dccdZokC<@F_p[m z-77l$F1t/5P_z% q7zuM`B"!+`T<򋁷 8Dڴn-;rwoP@Ngə:V5e̤gӕnz3#Qo~<2ueܣe-J;oo⫯SU[ъ<ze-7=WAL@@@@@,%pLj#Lxi<Ӧ7죹sd1c7MW,IOřقR3 ;^RU/O>󴹟tuwf^ i]{:85al&Z3v__7=lpԿ*{=UU!^V7|C=n㦍uR&O(]~"y%oX٫]\uk Ĥ =zeE?O_Y v.     XKzFZA *hҨAظG'IQqQSI6[/7!6l *,j7|kBo^^}#w\ݩY3M觯!▭[ԚKEFx }=^9vϹ;wGOIq v9{so '32#㝭j̇GJJrۺu$&%PMs?gPֿk؟779T93޺-̂s Vh.CT?.]7'MZuccgx\nӦU/LItU*\I n5l gq!     yNj{iu" e;ǟ~1jؠӽ7iŠ-R/RUl޲~*3QsgjϦ".&c:2OM`Eס߯./Ҵq#zd݆:2_"ղ䵷ޒR-FkɜΕwޛe      @ S%W6;oE@@@@@ޱq.-zU"      Ы#Ы"x@@@@@^^      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-rx IDAT      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ b-      ,v`,@@@@@@ ,t rFtV]-_M~zҫ[wHo&;seڌW)[@@@@@@DwF˯gS[6o, 4Teɜys)ΝeȈrE1@@@@@@Dw͕WI4dz="}/..swdŪs@@@@@@ j,ߺtv* _-[ȝwcFO^lrY¨=46      ;quǝrYg 6cO>!:$sM6eg˼?Sd      Q+`@ .N??E+7,IPUPްQ#t_>8+ }d      ['e- 2Ӻe+{3q<>a8@JJ&fi3^WI[@@@@@@Dw5KF ٩ϋV6n"ONyFPwd9ұCӂsr      @l X"KLH.&Ƿi#Gm&ߘ!%ujז!-2KNyi˲~:Ev      V!      p=@@@@@@"X@/!      @3      @ E4@@@@@@x@@@@@@`>            ,@@@@@@@ @@@@@@@ "pX      z<      D^KC@@@@@@@g@@@@@@ Ћai             z|8, @@@@@@=@@@@@@"X@/!      @3      @ E4@@@@@@,2em99QR^=խd7ܝ2mƫzN@@@@@@DwuW]#n[> 4Teɜys)ΝeȈrE @@@@@@\WV-2rXIJJ 9ayc;b>Yv       zv&֬_/֭ZɝwcFO^lr{.@@@@@@.`@z2fH4biyRk̉M2Ϭ~6@@@@@@TweWH dګѵnJzoبwYE7      @TX* oKj RRRlmbX6YzuT@@@@@@mKzS ~x޳'pj?»U2{ءi9tqݱ}@@@@@@ *,$ȳO<)#nOiXWvm",7na      X&@@@@@@EX.B2@ L0x     !NVZc6Th}j]NiPwpLa]e @ b"hX     U< 3Nv:Kw-2mEj f]q\;5Nɴ0ΪFHT#    ĬkvNϸk^$PtOx:c]>KlC |      p3Nv&wz]=n TuCWة;\;8^żx7     @ }-2S}ᝪSvj!wk_bZd:\v̵-!PEzU"    [3.?Wm*ܵBvjncnYZi9TLeX>}    Dnԭ1syf [Si~v*ӯzv5.Jl $@gb     F]7ITAm*'Ea8@JJ'fi3^Ws     @ %j;SiC;6}%bוvN!c]?P @X"ЫQ)x\n"t%;wf͔T*=wt:rܔoWŗ"    1p׈WS]#L;jyvEnL;]m;q;.@D:u(p$$$Ȳd3PԮmfh.9;/"G      `کδl:=NͶ?!/5ȄuNw*sWU¡p @ X&;F|      Pu6i ԏ ܾj;O=΄wξCvf] \U@R*      کJ;{LyŇmNw̵3tݞw"XN@rGƂ@@@@@  E 욪vNsjb]on{ڎvV8uֈ]@@@@@ <9UhvNTP5 LLk[d:=1 @*"@W-ދ    '`gf ;عxI<\ݾv=3.vT^8r@@@@j;?ik4L?We D^ A@@@@-kܙN͵u LSa;3hG8@@@@@@J;Nv1u`g^)5;mWQ0.6XM@j'z@@@@@@Ħ*sv t`}Ձ^˾Gϵ3thB;j+t! @D E䱰(@@@@&vm^\ !l._ξ-_Uj;g`!XP@ςƒ@@@@&\;]ai_%ަ[T L;|;Su[$E^@=@@@@8f]SUig;MB~]t;ۮvG@H Ћa=    XU.li;_~ą7NvUک\ ĺ^?@@@@ yv?K:3e+r{[dvn"v<ގ kzv@@@@0ܵtpPV?!/Weki*1. uނ @p= @@@@bT̵kgdmZe&'^ q(63rUL܉l@'@wl3    U/ఉj tXmU>23JskW @bL@/"   D@5f_ŝiڇkWxgڙު;WP @ YX2    @l v vn_`筶K61at`g»|w&$F@bz;0    8_KL_kLyItܼ}a:k C 8f6    Qj Lt럴C̵w;xgWᝮ"j,@*Oީ',/.wR֬ٗ_HzWLrwʴk*O;!    (>8'-nj[96zސ&]71lNakifye99 @AKzgv$۷g^x 4Teɜys)ΝeȈ⿑3[F@@@mDz˱@ҦU]\:_u0Ez9E*βVy+EGm@)`@/U+(qHzm~9ayc;bJkF@@@8a'Ʌim`2_؊I{Lh D>Y D%+/B:^$Z5Y4K^{MIoD3:ۧg/Y|,zad*@@@@pYq5KVi"vzydnyCϮ;NW;=׮.R,@ X"xҪe+yQx{m99k̉=vM2,@@@@RvNtYWX'I_l-  >6[K.diCxF }dqVz1E@@@@"Z _y IDAT'cۘ/qky< =," D׸a#/,ݻw =[e?'L~HII̱fի+@@@@C$4Kx۠C;_xޅjYtv]&cgĭ/y @"] ^'͛eOQlҷ=&dpUxJfϝ#;t0-8!.՚ @@@@     `tx_ML23)HLTy+sm%AB@%@    '%N:w&keoxf_= @@@=@@@@rNUl;oh筸!Dž  P9z]@@@@ vᝮ+wy8+ =K?,@@@ 5|s\ C]m+6֙Zf3~@@XD'훑g٤Zj5ٷ{T<7ps9VxEV^K^@@@e/Sz[ ̌;o֝{K,W  @eX"+ѲzPwd9ұCӂs=@@@@8[UiTxI1vފ;;w[ =3+.'@@M^ڵzH撓C^߸!Ή    x߼;ohg5N foxf_GX  U)`@*n@@@q5H*Sy `m w:ijx\   ^V@@@PsMS?[fS423v  p4*D@@@bԸ;๚ois*Ԭ;]6-3-v,@@Bz:,   @e&z+T߹$=mͻې'E   a  @@@-3K[gzAԜ;;[ӊf  D^T'A@@Uҙ.;Υ<*"Be3 ̫Dž  '@ygŠ@@@8v/+ \~ƾ7o;-3y@@N"  Ĝi8U՝muZ|P|enx}̌ # D^)B@@'h({'=3m%nߌYweV<{֌  '@@@dw_ŝwΝ{je枒@xgm"n  ,@ɧ@@@,/jZfNz kiZgnb@@82#   OM\Uf ͽ qj] ͽ2 @@*_@M#   @ xRJ;ܻ&!Zft] @@@c%@w@@@*pMUgwĠk,2w=J͗"  ~=@@@ι$oVU 6-5l@@G@/zΒ   1#C`]0[+0뮴uj.6   г|@@@ ܵʄwOUK mboL]y!ϴt23ڟ  @ E ?@@@Bj]7xuZ|8w߬;Xh,@@On[Q=lldR )*.In%#̕i3^kׄ{ޅ   P%DG?.U^~96Ty+sm%̬K@@@ X"лϧ)'˖.X*Y˲dμvr{2dCt9&_   P^]STm2U3޹h鶙-"  1-`@U˖R\\"7ú+qF֬2y#wr{7f#+Ve   @U%{;.޹hfyC;oŝ <.@@@,]r:u{zlUk..2r[쥪˂r   GIov:ϽK.L]ig+eQ:n  Q&`@o԰Ңysb|rmk̉ҳk7Uɗ->,ʎ   'IvN}g B-ve+L v򜾊;5_}M;9  (`@OPZZqsgٟ'/AzoبwYE^,>@@(>=R~if;=NwZ Ak-v|6p @@@r,z)#wlݶm;&&I;gabX6YzuJq7@@@ J>U&mb+/=Ite Sssl(a   @d X"л$Y<39ܕHηKrr<;yAޭsHL Ρ#G,~X  KwqJgjRpYcȢǚ%tZPm*j   @X"ЋO;oMN;?1W]#xMv-ujז!-2K{i˲~ㆪ[@@@ᝳj2M5˷|4V!>n823%   X"@@@B $٪WWzJ7=v1ze/=?OB@@<;V  pWr̟=rwlS2uFx$njYʿ$qH;QG@@P=  ݦ*-3KTEiY7mįQ3֩xŖ@@@ J   X_ ):Rx/m;_c{7@@@B p   U I3sLӭ3SʭDu;W+@@@P@ j@@g3tw~]K ̽S3uMN @@@ b=  wDڕ;≽ŭWstL;Ƕn   `u= G@@*$8|3Uj5ʭɾHL՝JΗ#  XC@*@@@ B\MRLh篾s5M)2[[uNQx*uf!@@@,'@g#c   pt{7U芼u tHjp   \F {QQL>Ej׬%OVǍy%ٻoved  x@j݅껴 AOTRv س֙}e   / xMhLS)Y:5IHLUJkm6]wM[PV @@5@?IRwTKjG4nyNĹv^F>C@@@o*[# xni:3IT4&Zuii     s8C@p@rBK)]0wL 8qr@@@@@ +=  >%Z]0IRA9TYgIM)N@@@|G@w%g \qiA* 3UwZK.g=+iy=Y8a@@@X@ϋ/CG@4[gO)_(A:x}9 8w1q    cz>vA9@@WR e ?xa;U*$֙<@@@@"@3@@ wI.XøS/wmiy2    ,@@@ @Jɐ9wufL+3?Y@@@@+Q@J3  Fw9@WŞ]ǂ    @M  y,\I,XRJtn$ۼݷOV|K \qiNw-UxitXC;@@@@/@zZxFȻ lBϋ @\v9,sjq)X&Z*Na;UId?]YKh     @F=  .lB7Kقͺ؇s1{΍&}4OzzH&^z}G&Z&B%\퍕@@@@D=O* @Kmi*}vr\f .ޮ*NopuaTO'US$RKUP@\ZE֝)qRFW7Y@@@@=_ 45?[{I{[[-(MЦ8݂2}{;Sa^,~:d9j.=K9%몺[⚔Hjh };{B@@@@2 ] @}y99@smyܲͺNmj8.5G-&t&ZHZysͱ-+Q}T,_AwL濵Hv/%P_vUĩJ6[fWWÉ'[I~l- >5^ǂ     ^ Ol"V,uMJA$9yYJ!f8'}r+O.Ze>iTh* &--&ӷBXTIi#o遜 eX'=3ce ;/%om쉏L@<Xf_D=uBoXGbw T(*o+ y,?zwN3AX`hZҥBUn8}R_:mW*Uh*uiA6mBo?o}ʕ?EhQ]E~]emd߱@;_NskiRd=@ gYAۥe3,g pq^{^܈5@4E xu&njNzHRR9qGޔ;vd+Qr:YE )QBE U;/œƣzrxM74m*"?eh z64x==00p)]Zd"#Gv oo<:Ӫ}T],]L5lhZp$)j~WvA9pOQs !>t19p?@ϭ      z.±       s2@@@@@@@E=7<"aeϾ=ErL'mk'‘w>ޛr!ӏ=)5lh#v,|mILL̴0iת,"F =>a)Z~sǓM<()??я?`c3hmH:f|b|gWƅ,@ _ǟoQd_{[^qia;$00P/"IHHBeR\y4aOYB ҹ]{u-ֹa#u{\ݛ Kx}{Ys$99>--?cٲ/>78l[o_T˽ʕ*IKRɟ7eeef/Hyu|DO?ާ[Ou\(YpqGʙgeŶ MR߻;ڳ`um.ujQdN7|f֭l^Soeo̿E>U,]/ZLQ#,_jԬQCe˯ʴߜ}͞%!)Qқlδ^ޘ9\o?Ϝ5w>x~ɗ\HN#)zS&'J*VDp )_cGH*Uz5h(m >ж?_> }Ȑ~M9zl@>ɒe[vt`Q8B}{Guݻ̇AoIq|R۶}찚4 Olе}'پ#F4oLU0HhHA=|(ms˾%>.*&#}ݷ׼I9ޠ,z,|x$4T2HΩgO!ϜÆzFJ"Ҿ[+9# ǎ>D~mԯ[O:i+{{=\~\|÷RV餾ХOO 1Cѣ豣rwK dN~uu^Tjh]@sOQ 7z[4_r>[% fzy$bw\yמ/<ץ%Z5u{_J~^b>]o-M7@/h~ci:t+7]uNhߪğw>|_} ?Pf̙%cbݽO69zpYl]N*//z{//ٽ~-2LY=޻y]16b2*ۤ*>Rr,3ߚ>,ie"JzC:LƏo>`λF[&"E~ԇ!D0b_gש?Z&^5ce&!_*Ud ?ևcǏŁm>d44A)S]8WbESAkBI7۸<R e';DrJ}Yoq [nIoH:los7 XC/U6ɜʺ?u֛Uխ+7*n]ݶS=~JsvE (k(k~v3^Æz+ty3]k IDATkt^ץ-ԗ%˕-#6a_}]1Ӄ-*"zt*DiTɒ2r8SIg|J捔bŢ @Nw=מX?{c]ot?ur_~3Ϛ`=eŸEc6Ɨ˗Ӛ3D- i wș3gzv&?יzf&22sT ٽ~ھ$݉-O z^p!͞}|@_P:j_U~iRz SvCv>tQ']VqMGY?QZna7vHyeL[;Ofꗇ@Fc=]YچƢg^)^tQ6Q.S s^treuﳞtN]eß{5V_(ByRHUSfΐH:WټntCl^TѢ2yxأٶM~$s_-3^-_ٶ]V~厴g-' [oU:rvYHupֹ}uQ2y+浞n^VԯK}j=^u z)rZ* v{oL #GO?{ﺛ@?>'{N=߻S -Lot=iS6ˬYz%QPӏ/zl5~Z^}yBr*X@瞹W@XM2tj޼>_&s׷{/Y|_?mZ ,fvڿ~Re>5ћN)x~y#sŬ\ X}WUpSm܂eʯ-bQQ2*zXz};V?تD2)T_nh*d^x$՜{:kץ ue@Z?>w,=/~2t_hhBVdjn^7 z )Wi=?-c3/nYjU_צg cB{.{n_{jXu%64g[gΙmЏ/cyXCWié$ǟ ާ(#G#!fL&)sKFWedu+_鰣/͞7|5%W= Ncרjv{w|0 @w9 9A 5abjp#dCMyk3!2,rDxB&ERSRͯŝS07>2y@o?ˠjNʫ3o$%#F+#/zddx~_ۤwT2/ ս/((H-?B|m٬z1'd^fu3}<@Ny ݽ PU@/_yٹ7 D 2/סC??IӦ:)R.' ufO9Fl.M~X}ɭ?d\L@/(ّ/ <λλ!?DGޓ؝|ۭj^V_:y5j9je!}mwu M40/U7}G7=pd=%7<:nRm7O>MO^ (P>=usھJnl*}Rx^2݈܃#O,3_-CT+oSUQٗ~[+U Tzw|zz|0HzơA]GmΩ@NWef9Sĉ2qLs2~clcj~ӭe{?4s29f9. =ym@]N -{wמ9w+kmڙרۍS9B}`mmUߛ{mNp{zj~A#]rr?8>%hTymnzy͗WW_zq3Pjj|JN}7/ׯ_nS@{,R_޻sq]@u;D@@@@@@        vl      @ ;1@@@@@@@u=@@@@@@| wb      z۱%      .@@@@@@@\cK@@@@@@]@/߉9       nǖ      ^s@@@@@@\ sݎ-@@@@@@w|'      .@["      zN@@@@@@p]@u;D@@@@@@        vl      @ ;1@@@@@@@u=@@@@@@| wb      z۱%      .` ԒG       Knl      [A@@@@@@pM@57B@@@@@@-zna       &@[!      =0s@@@@@@\ s͍@@@@@@p[9       V      E@-@@@@@@\sc+@@@@@@"@f      kz      n s 3A@@@@@@5= @@@@@@蹅       knl      [A@@@@@@pM@57B@@@@@@-zna       &@[!      =0s@@@@@@\ s͍@@@@@@p[9       V      E@-@@@@@@\sc+@@@@@@"@f      kz      n s 3A@@@@@@5= @@@@@@Eթ#q@@@@@@pZؖX.d      g͑@@@@@@ȵ^@@@@@@ Ϛ#!      k\  $(, "$hx\BVT<\R`i}winT/]Ќ6`|R}{PvI3`x   #@w\k@@<{?X6};/ac%S>^tR|E]6..Ne^3M%"2Bv쌕9 ސӧOK\uڵRlե7Ǿ؟GCo@{l227lo/)S_z,V1 ^5Ĥ$ye݆f?]_s͜4UFL+G/xeƜ3eh :ɂ  "@   p kVI[g&g.㆏sf -*Fo.۶o{B-*Α +ӏ?!Os['i1Whh9{6=g<9j K2o2x09~dw}]]^^_G???ye$[.gϝeQ  ^,P|9(Y嚌 @@23k8}{IMNW|e$>f?ya6[ońvz )(L$=H!~ܵK]tE~\ZnSȂEo9RҮe+V\ߕTXGl,7pn?{} )ڴe}H~}X(i‹*\,"G3!I% -$pa9 KZ7Ij%2"RVՏ:v?( ,n~|gW751W7'+ufmIﲼ!!!>ad}rwJDDkbLLYl\]\%+]5ޱ@nF"US^zTRU]_yfRrUIIIo^YS_?D/V\~R7=;ʵd 3V\]VF@@hP8s@F@@N Zkjasb$cNo3w,sY:}Ft&`+±w>x϶/4A*[nU S."wz|M2z81T(_^I* a>2QǙ0mT̜cC%KdMW몫8F oW V$+Un;IM7bP6slkr]w]נ'*,v6ngvo֙*)gJJk׺K>^ڟއ֮MV׻d2j0أ$o/M}',L2~h8lj3kRXUM|e$%% >'if|U*UݺKU+./Ta+g}{_|٧*H,ytU &J0jd_Y@@R@+/F@@{r=5G%NmVlQauʀarwHlҕ`_-%w w1V;u^9Az{ݻotSy)9t,]yqS& g36VEE@Ϻ=@sȍ6Œږsf.sqgePҡG],|w[vsT/]Щu+6/=\  1ՊK9 !oN׷j*k'`=:e =T?/:7yٻz`~w+|]/w]˗m#> RLTϯޘ*?czM|JٗK&@кu|b^Nu@@@+1h@@G R!95^DڒK~TqŊDI3ooCdե׬W.ۺꪹAm{}=j4{TUZ:O/ri6f rFN+1wuN IDAT mCec?zcvm-ѩ2T/No^P*jSN#X̸XorRr@or>ݜz:2rKzv*]\smPzU`'C3...NL5A]ɗq<ǫ/_Y@@R@+/F@@{Vs̕j>92@Udvu<<+N9ƴ95zz{Ur];Ue3gK^M\ld@A=dWv/00̧p>AF nܳIƀ17g_,v>eK]U_Uޟ^_m{v5CzL|߬Pz&Sm]WzBo%v.R%KPi8٪ oo:mvu:  xw^7F  fhPU4AfΙ-%ukבmJj$!bIJjyfλPFM'e˔ζ[W 8Xf GWt eЈfM?lZl?ç[OFETth(}:2j^7oBz5ŞPhjM"|\]~?[JU5kn?1Us:LfUev0c%0(H }9`7{Ǵd}Z`Rٍ﬚qiҡ[WIHLfgŮ#  w*_ERe%@tR84L|0t~Qu*yD@@!pzՒT#Cr79\l'Of rʗ)+-[(ŊFqzR%K-RT._薙9Uٟٯfe_g/*IJ;Yo%NoS4th΄l[$&2k]jn/.oIBC ʗ˗w~f y-UJ||E!!!Ҽi3j/n$-9OR|2sK 0ӞU9idVD@@/XHn;vXk UWT9[۶Y3G_r  $63j8==$b&w犺B ^cU*,78(XE4l8yaBO#A@@+S DM+COɦ B@OW/^J֪0Թ39BYTP1RR왳}e wgs=60ƌ5}3Yl{>|U~`)K/ş}  7 kVIDWJDKwr +..q^RTũX+o:Ժc% Vq1@@@"&ԋW-75ZK:KHJez#ЭI|֖9+dm ˲+*vZL? CGT_S[ϓ2h9|[A@@o{?X6;/ac%S{JWr/3y<%5E<-3״DROf͝#6wf.N.K~){ﳭ:@RǏ*yg:SԄߛ c;s{jY+sr3(̝&~ ;vȼ7H``|EV:yJ>S?a(t}ǝʷdK1ӏe\[Z)Z$R'$}䫯f5|NK'_N޲3ϛccHLL}{K] G@@@@@/֮r4KuL{ͻc:˪Ϳe@ٳҹw3zun3ݽKN~zPWeqYyf&;N~g/_ GBC ࡃ"MGWiЫԬ^CoS wXT b_AX==s_7աijՕi)WwŋS!J Lul<`cIKMMmBz&:j=vTdĠ!Rl99sw Mu0:{lڜ})EWZ,x{9=7c*UL&TBbל@@@@@@s Gz]&[:=ox*9L5G:ӡرIfM}EBĀsΗ5Z*֮#=PA%5xӏԏ[C,*`L/Ό57~z:^<ǎRuO7AFUkT[[oonQGU6m_i^MPCKkTзs.9NT ߬7bSi]+cv%!Y@@@@@h[4Q2Yteyr39]薛zS3Ǔ9g}uMECuTTBB5xۢg4Rᥗ5^:ik>j8)P HtXo̕eie?+tuitw KXssڗ}KKy-yZwQ-QϤ_CM_֮9 ްi\\[Tk*~\:]ԯ[OzVz髂YBպX=WƮ1R:H9~@@@@@p@z{5% @-,2dINI1هZn$^m[gΪnFY{cbdԉf^6kd6˛vm_je nzi}}Y=Y|y~bc]*[̹@%ɒeKoYz-^D4^mnUU?HRrr-Q9uF:kxmQ._|T>,O>ַC55se`S/m7"#tx @@@@@7<vWԋWM>-sٷ?|wDC.z;nMjbik ގ8amsGHׄ:nj7j˺<ȣjGl}6г?>֎2݁}%߀ar!g/Ws>ē&U/wTe<`NcBKˊ      KjD8|jQiwmckMϑ6v$IJJkQ9},Ts5"k̤Vz^7=^2zwϟgns 0T ozVUT'OyN<)E~ԱHLNs@OΎՙsSwzQEʤQO;ywu]4Wsʺ8~+~9==n]Tt*+ݫک~%uA#4ժKpWUyU?$k~v}:Gk5?Ui].%pvLT 9iXsYs瘐@@@@@y]gn۽ooq%@@@@@@,xbx[qQ'ߘ+kׯs8ǩc ΟTyU+       p.<ǽ@Vo0E.)&/V[1UF Gn      KllȈѕr!!!eŷa ˸#Hd5'rY~L     $S@@@@@@_ +      O]NN@@@@@@|r>      >%@SA@@@@@5=_      O d@@@@@@|M@׮(      Sz>u99@@@@@@_ +    @/I( B(A}6D}PT C B5TRD 'aDHBB!!Ν&Mv]f!@ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ j @ @ @@S pj  @ @ @@ 4LՖ;\cڕ__y%Y{1z c3뮍>lI{ @ @ @@CzZuꔸ={#et섘vﴸbm#xb,hmT  @ @ @I!f+.`?|8GFk[[ޔNn11 @ @ @Sԧc:릟uR3o^[ow}biV߸ƽw}W?=M @ @ L Ŧ//RrA‹/_'ve219xYLI[ @ @ @@CzBoˆo0mfߞ߸6J 'U{MSOOh&@ @ @4@Czbѣ,zo{3qޗ. :'?f|̟R7uqu3Xi @ @ @@?h@o׈3'OspA<3<MqQGnFrۭ1E&Mֶ~x85 @ @ @"oc|CƽW_]̝j9SoS+*~_v @ @ @ 4LOf @ @ @s^??4 @ @ @z}| @ @ @@?@  @ @ @[@W @ @ @s^??4 @ @ @z}| @Il*ź?>79}[VH @. [vCk @ @@C r⭛ϿR|i ;K @" /GZ;  @VV_۬ rb3cˆg_Y- b1-;[Z<SZ%-[iϓn+/[NvK4 @ @ Ad @~a IDATG`UR@X\=K!ݰV2?7.ey@/*K9rX +b%$/y*e)H,3׾",)/S 9+Bʶ~ @ zKz @ + )ܡz.w O/ϼ4?f yRK~)~zktX~6_@2d@ 4T7$=yJ Mwv=y˖Sz|ݳZ̙TXE)jPY)UQ**+-K۪jre[wk%@ |zZ @ʕsTEW2º#S@PŹ){1q)+^`.ӕ.b ^^Zںt$sFqsq wi9ӀUJX KacvJhY K!d%ﵯ4yTC<+5o;E0.6.Hݲa K!]Ο_Z\4H,B̼",*=m+D @ ^ @(00%F9x+?W۽e[)+EWTI7h`^ Uϕ\-Wˁ\z\aRTYS `6+S +Չՠ4S,UTWZU <Ք*ʁfcnQKsd{W,uZmMkmeo0s /,V]~c0Vf-9v4nd`{{VTZVӺ]<*N麫ij _8Y}|l'pa[DG]`؜k=&z+ߦ  @wSuN+ueYҲe1ܐL\~ZOT^H\{\=ty1rg"@XJWڰCשK,N]Zc%䬝gq ۻ`fVƐ u@4~ck KՌ+7&_覀@`f'@ ЛK˵aZE]rh *so)+*Vڽ..[,L݂BJ7E]){&GW+Uѽ:7k @ @@z]@2  @}-s`R9+F Kun!ݫV=W r\~Maxt#z& @ @zu- @ 2(tq.Ɲ\n-SXV}-ƠCmi-t..a3*bL Yg"@ @[@W @"R J MΕ\\2W֥*ugze^[Ε*9ˁԵem7m @ ^8IylXio36Xw}biVܸƽw}W;ZLmyT<+Ɠk2pn.k O] Vf@2a R]9vsY+ޒơ[Ϧ*: @ @$ގozSlɦq S%ޡ'z*rb]v)SϬGg͊vj  @-k6rĪGy(ոW.s 72WUBǭ-P.uuYT..ߞ;6/ @ @. 4D׹%[lyqWA);a,G:.6M^ @@c b#)bFZ /;Ut.>;m3 @ @C!^ژx |} ϋ :'?f|̟_/gӋϜGH+ @% |kc7#M|ǟz=+wwD @ Ѐ }bb GE]lVѸߋ::w3nnj)0ib͖hn-_5E~U AOok @ @@h@oȐߞ{nmm%%FpPl?H@9@ @X6"4&"hNPPP82_9ȪU)*YStU^8uW~dR@9Ҷ= ٟլ{aM[mW١ y_Ja"ݲ^_zzSDwcE[NW:sOG fivuzNl @?֌V*vly1o'Q t:8jJ@S*Ъ&䓩4_)8jk*NT2JF5Ȫ J-9jjCeWŶ*U)*X] rXy*ޑkE׶DЖ_SOӿseO/FkI;^~i`f˔[Z_ f y\g ?7^a5Sl}5m)^ig66²E»wd3_C+?</ܷch&WmRmVvk neN @,] $j.KUN??]U뮭~LmQʪJW~UJiW?^ ޢ=_{1]^۾Yq:W.t'0/J}}NN2?հ&N%5oJʦJT !P*dՆ_u/9jjU괞E*A\U5ZG\`&âe5c}/*Ώ#@~:8~hR ԫs@@M @,Pp$9x0vQ rvIy<&RQEY]_m5R۾1jk֯8MWB|K:7U_vu*f[jѡskiᛯ/v^\OmeO{j|9)/ ^QuHX `{TSuT JP¡-R}竩堪V!D @@PQ @`qoX#Z8"XOjDTk8TC ChTNv!U*aSu[beU5JK0SelSTCpaõbcGuZ~B)][Tj+*^0ҽbŸjpS *tWRPM}ծjǟJ{ѶƐȷ47qL>++?cIk*Y3UchUj\JOOc#@ @zu<  +t`*'9 YLT0*/kq0TIeyT5\ڪ6ભ*H+JV5-RmձjrׁCկՍco vlL尩RU>=lj\T[Tf̨mw뗶_ jHw{Qja]:W<%dmuhw{{{xtt7ۢ6cҧ @ - v;GLu^;``Økct  @@c ixMk1{G'o[_k zbn x5TYzy4KN6u r5O5)W~//S jZU* r7}Gm,C7i j_RZv,N]pWnwQT֒հ' VJ㘬Qz}/zc3I @A׬n|ŪZ N:vBLwZzm^{O<1~\L @@3 z1S|8`8'o&w "TIPS^uޢբ˕+[aWy:D_Ъ&Ԫh+CVZvy]ES9˛ @ @+Pބ/~ug3Ύ)4M9r @Zƌ>e6hx<΋ΈO-*Ts®JTjrG#@ @o V[>SnOL:Ԫƽw}W}; @`1XPi<@Z){^ |P}@n @ @}(^y&pZɦqPzɓdG:.6M^D6E @ @ { ]to)N9=>sfY3 @ @ @>h@֢B/N݌[c1c.8'Lm @ @ @@4|7j8䀃bS+*~_e @ @ @Z= @ @ @+R@"m @ @ @9E @ @ @Ա@]#@ @ @ s @ @ @c^F @ @ @@ @ @ @ P:>8v @ @ @@9@ @ @ @zu|p @ @ @s @ @ @@ 5 @ @ @= @ @ @:k @ @ @z @ @ @u, Ыc @ @ @ @ @ @X@WǮ @ @ @9 @ @ @Ա@]#@ @ @ s @ @ @c^F @ @ @@ @ @ @ P:>8v @ @ @@9@ @ @ @zu|p @ @ @s @ @ @@ 5 @ @ @= @ @ @:k @ @ @"0p@|z0`@.̋u^;``Økc8 @ @ @B!w].˱`q}oqt섘vﴸbm#xb,hmm @ @ @[!M7$ZZǣ-G>Xﵯo}8GFk[[ޔNn11Y'@ @ @h jVCTž{N;:˸Ǧλj @ @ @['L7(~~/o[oUn1eգx~JYqGV  @ @ @B,>|S{˯0yR`q踸g4zMqzj @ @ @@Cz۽axzēOGlͷ'<sc-{SW^wML9%@ @ @  }rO 7^~i >{J\|eqQGnFrۭ1E&Mֶ?8@ @ @ @!!Cƾ3޸1 >Wxcȑqƣ7RW_?/G @ @ @@S4DA @ @ @Y @ @ @@_ Jv @ @ @@@4 @ @ @+^_I @ @ @zf @ @ @}% +i!@ @ @ ^,B @ @ @z}%m; @ @ @z E @ @ @@m @ @ @@z=@ @ @ @ @ @ @聀@h!@ @ @ W @ @ @=" @ @ @J@WҶC @ @ @Y @ @ @@_ Jv @ @ @@@4 @ @ @+^_I @ @ @zf @ @ @}% +i!@ @ @ ^,B @ @ @z}%m; @ @ @z E @ @ @@m @ @ @@z=@ @ @ @ @ @ @聀@h!@ @ @ W @ @ @=h@o|4޳1x?W^wm̛7/Y{1z c3|PX @ @ @@ 4DvO[ bܹq䡇3[~';!;-n~c=' Z[O @ @ @@Cz ʻct7o1?8lV4}I' ϘM  @ @ @?:}%w}biVgwظλ?m{D @ @ @ -6<[+)SϬ6Gg͊vG7)N @ @ @&0`@=bu։^vikoi&Oq踸g4zw# @ @ @4Lk_uE,hm-:|ذsc-ߦN9=>sf8,B @ @ @"r-bqZu<Sx7#nq̘ &.2_} @ @ @]h@ohkm{|1j8䀃bS+*~_]k @ @ @Թ@Czunh @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@h @ @ @  @ @ @@zÇ C<8F9"&6 klW^wm|^b @ @ @})+L81{b7l!; 1iqn{G?XڗE @ @ @W"[ib5ֈ5W_=sj7|8GFk[[4夓㆛ngL0+%@ @ @ З U@lnOL:Ԫٸƽw}W_: @ @ @^h@ow-L=s~ǣf?W @ @ @@_ 4t&A);a{MSחgm @ @ @@CzÇ :'?f|̟R Mrz\y51}^Cb @ @ @}%Ё^F:Sx7#nq̘ &Fk[[_ @ @ @^h@owqMbРO8v @ @ @@9@ @ @ @zu|p @ @ @s @ @ @@ 5 @ @ @= @ @ @:k @ @ @z @ @ @u, Ыc @ @ @ @ @ @X@WǮ @ @ @9 @ @ @Ա@]#@ @ @ s @ @ @c^F @ @ @@ @ @ @ P:>8v @ @ @@9@ @ @ @zu|p @ @ @Y{1z c3뮍> @ @ @h N:vBLwZzm^{O<16 @ @ @-Ё3ΎmmőrqM73# @ @ @M!ЁnOL:wظλj @ @ @[m:ve2Q!H Ч~췁^6F @ @ @Y @ @ @@_B6 @ @ @P@C8m>Vz$ƵSO-7m?kf<ج45ēO}rOěnj)=q߈E1|ذ8c#bi{Y1rĈ_Ʒae?mmΝ_ZE~cH7xe\sYƏo5{ǁJzU`vwb#]+E]>xN;G\yݵ1o޼Xm} _A…˻[l;дiqݷn1;knڼz򫮌9Azŗł /\V\ƴ{ׯòOv IDAT>mk"Fž{}k矋o;߳*{z{‹/kw/s_o׏O 48}S"X%}.]6[m9>Z[[?ϟ x聸6]?ӵ7SN^~9 iލϸyz_,n{q'f)͓-1qޗ.L @`g/bhwO~zv˟GsX}1o~KܔK3cI_󽀯_tI;y^0ϼ:we>V$&9򍔉Ns.Pq3(z{WwƏ~rKuC@ܕnyd?^CÏ{gfGd7Pn|!#=<ϜQǞ10p}*qڤSMzgYO<s̭zkZ+N4X-\{7/n/ c~?ebn8_ůċ/'zJa=X>wLjkơG!'6X >'o~bcsnj^{*𘔾_|׊ϰ]wG, ?ׯ:ߋ@oQq/>>±ŃӒ>qʩqıڼJ> zvo nB{RG6x}69iBfɁ>7"Q/)7)>"f|,=2zb͋LrEJM7WZiXc5bTEoV68?qկ]C@/ϰS<rȷ&IS+z1>;l@s l3-- -Ob׾6.׾G.*Hf{;`na5+f{!_[_5(t{{~E7ov!q~b|}=-}>k[zZk!>b3⥗^*wGw޻̮\|W\|iu1E y/Yaaʽ{I_GCbKj}rJH /Cuѱ뷪vmT,cͷ(^jO׿ĝcD *7:l%z '_jw]\_>iS*\Yׯ*yX{qş4h@oşj@ k=4n" [ܵ?25>Pz)"VK݀* /J<6o(_~啪W6.׎ozF :'T̙;ܒow˿zI|KbtK|X,alVxŻx;O}yOspC>NrF\/rw9Ү~~~iK< Wλ\lMN?>\0{qz+TNw}Ϟ! /^?DrqZz%uUwc.ޞ ǜx|FAzxx׽.?5ݙAV@Y*d8|0YW}cz6?|??=8cPJsINk?n2ztt}2_o2&y#W9b# ЫcaOT`3Y}vwԏ)]l>,o1T~_>B~k׳'.-Vs>]nNޢJ7Y=u 7yF[Q!#Ыnh`'L,ɕv}X>Hf)*|+ ])8>Upq[)nK:zkgL:T;/uq\J$Sޟ3&L1rw`7[I7b^v@= qs/<_ty/q7sǤnڮ՛M [̏<ݒ}~v7ˁ~<^zE9ˁM?n1.`sM7-zm?)>r@Dq{w?{ڃ֕kxmtȽ]vI1F~7>wE7yLn5)j ԥ@~+~1Z}M[Ҵk뿮a']rïK:~cwz{ctکV{]>vjVM6@޹N9SWZ_oyKc+՛&y>/Hq{|Sk^,nߦnVJw㣵X$3u3?QsUܽ9T#*#ksh~wgX"dgΛWz6iI׾\aǿcϨoywI_L>:r9']_ie$uyōw>Gpl MkT^mS&[1];c׾&.Fu^]wvg |cuѵNN]xS1~Jztj /]?7 zE>UVY^rзgSSsv:9U/IU瞢Ү+c譞|섘SEݜ8뼩i,㇖;SC^ĔψR79Tw!qT!28݄ guG=8^Ŗf9q@,c/>{֎#ov;?]kgj7}wآ[ xzup$0Sj}ëwftڗ0b,z0r8wz"7Կ0ޒ>nq~ϼq]>6q9gL',7rfP;t#zo3f<@QB~)ElJ~*y-FvY[cB=U sy1?zĘ7P|POuRn`oӧ}}/J֗4u'˕_z^|zxj(7]"~L_[7vmo}kˏ:#u񚴎bFM58?s/vII]IU#=`X+WE#͛+U>>C N7ק: !)`t*['MhB\Gݬ\_]r{3^dLK~yN]0wb9͛n,d-7bK+9,j^^fI'c5ṫIV~\wa [gqR|Fͽ1H7+ev_ڥ]k}ӟLN[OoxcjW}ۧϯ[1P[[k>I mY -{A踍3nΌ6XvAA6QƑ]AeW]Ph)Rtɞ7ihIK&}߼;L›{=|:9痿u6g3;7Ǘ,/xpCqM&7C3 oI @ @ @. pb @ @ @迀@v$@ @ @ 0' @ @ @ oI @ @ @. pb @ @ @迀@v$@ @ @ 0' @ @ @ oI @ @ @. pb @ @ @迀@v$@ @ @ 0' @ @ @ oI @ @ @. pb @ @ @迀@v$@ @ @ 0' @ @ @ oI @ @ @. pb @ @ @迀@v$@ @ @ 0' @ @ @ oI @ @ @. pb @ @ @迀@v$@ @ @ 0=^g7 @ @ @ /^ L @ @ @'  @F@ @@5)]݇co׽o@6::;4&7 @ @@gDSKK<ģ}& @@ȺXy9~wQ{W/Ukė+tҘnw+ƍMo+<0sx߻>8&LeozO̜oԤ +_Tزt# @Qj\|s&@ @26fjӞ}?-Wm6 _$.:si/xECC}w7y;+~};N8qҙN3O;=[n-:x _{Q7s;cA \ @ u  IDAT@e.Cm֘>fmkL<~f[C->wɥ nXoĉs΋c>r|tvt柝}k֯OX|忛~q>Wkqg @ @`m @;U`珊1u}5ڶ빷w7]X=5xerAТS0wM=*:XbesÆ kxOvr*%{m%"@ @  @(s^O+.=G/,>zƩrS]]{1}}#?8#8m1ĉw#G{=_W @ sz;_ @ئ`l9y8o}3myGIw6yȷϯWU=yg}qgO˸[sS.'m%/QF?q>ݱ6 @ @j8phmomn-}.1vчʳlj&OA . @(\`k7٧vii>=3kx9m9=?߹{iC[gj׮I☲n񞷿3&Ny 7O¾i龎xpѢ5WGsE @cF0;]D{+ˮ-zL=3nXrVz, @ @ȺXy9~{wQߍ @ Pފ5㯋zzUMeJܒ¼qzYs_fѯ|u,|sNgΊ{/.t4?vX~mIg^j ,b?:n—`{1 @@ 4ɱ8Y#~xٍ݀ @ @ШIޚ*n'E[{{:nl@gqHzuR;zvȡq>7sC?&˵B(< @rM/-Mi#ᕞlcoD6d!@ @4f|J[hw<eyO5:R'{̚Ϯ卫Cނ}TxYU:RwEĄƛ~W=<#sl{̏vF{͍7ĭwx[ҡS%KfxogqDm)t??=6nyϼ{oM[ܐ~O~Ҭ׾51cƌ/ţǏ~@٧},{|L z(.}ϴn{q2mao#=7q̧?.Zzx^/|c.DmJ\4ntvv}k;, @ @ PKGn@/goy{q^3ww߶eUy;깑m?>kw4 ӦŲebښX/C:(jI>ߔ,m]b1~ܸ'ͮM}!q,zpC:{y9W})n3N:5{|5ϏK?-O6=N?9|D04byzg? .׽8b3 ?@4U?Ŵ-o]|\'?y|a}:"@ @ @A`k֚)X#}(;sbq ~ ν;pk+.dES߼/t^y_sbT5riwA v}w\έ>¯,γ;9}G?Iֶ9,~,:'P19g啈[]q_uC:8^k?+b{ß @ @ @RB,;c)1kT9ܕ_,,>vz@Y8d뮍?O)Xmʕ1献.~eWn]]).:Pc=基,9 ݲmc֯NgM4)>uE?->m:,,\lz?~|,|Y`V5e ؞6? @ @ @*Tԧ?}=[}-7@oƙwO]<$It܃/?zóys~. ˮ/~?{|{:_/yY^uwitMϸ{s{ο6!N>ψ/]ugOٶYu3]ٙـu]~kωc5llw߀zY`w)'_|ƍWVw~hnn< @ @ Peslyqw"}t]vkKۖ>˶ʼ;^w_[ wo:㮫.J/>qɅ1|ذ/w5Xdn5?[ok!ާg 6 -BBg.$b4ӧ_qf{"U,]ݓ/xbYGV1ܛ`A\x٥qf:;*&] @ @ @ h7e)s΍ښڸg޼k ~]/], >s1!TYje˗5*+;/;/8z߈nqL_I'Ƨ?eY%kӳ?z)m ڗ@ﴏP9?o͚5=m.i >uR;r=jT>uo'Ϗ MbK}e. @ @ @z huw+^iw 'Xa}zǢqx.]@/kʯ-/o5Fe.4wr|Y_VU]OEkkK?sVilvMOaW_߳N9=1#V@-:WZ'L̬gv%{q>=o{GC?]71ˮ˾K̶*}ޙWeٕUe~ٕzYȶ}P @ @ @OQ 544ome*-[,&O{oyw|*nG+ΘsJ̚93y5xOkb[eWSH?>ه |0jjkɥKc>{|kczlqI'#3<$v{nYc&jkc?=tWo_wml^~f9}Xn],zxQ{Y\dfۏf`E_ @ @ @zYozW*}@/^/xɋ^ǎMeo|{AE/ >~1c}Oӟ-o瘺XzU׿_tLǽU}RU,7Dggyo}˞ɪ^Fvިѣ1Uͽ'?˖/^P_x^:oZkBeo~wc5UmzeAi~Jnq۝/ذK_ @ @ @l.PX @ @ @zśj @ @ @@a(5D @ @ @x^Z$@ @ @ P@0J  @ @ @(^@W  @ @ @& +RC @ @ @oE @ @ @   @ @ @zśj @ @ @@a(5D @ @ @x^Z$@ @ @ P@0J  @ @ @(^@W  @ @ @& +RC @ @ @oE @ @ @   @ @ @zśj @ @ @@a(5D @ @ @x^Z$@ @ @ P@0J  @ @ @(^@W  @ @ @& +RC @ @ @oE @ @ @   @ @ @zśj @ @ @@a(5D @ @ @x^Z$@ @ @ P@0J  @ @ @(^@W  @ @ @& +RC @ @ @oE @ @ @   @ @ @zśj @ @ @@a(5D @ @ @xJ񮷽3z֑yC\⥴H @ @ @`'T|_S_W=8⋢e'p @ @ @@}K~:;]/K5㮿 h  @ @ @ Tt7y _?bĜ$8jժ @ @ @,Pс^VwecO<>6lh)OȜ;ɴ'@ @ @ eg>\p~,],89ƶ;nh @ @ @ *>{Ç ~Miⴳϊ5k֔! @ @ @1F{{5뾧:o O @ @ @@zedi( @ @ @ NA @ @ @  @ @ @zj @ @ @@q,D @ @ @p^$@ @ @ P@8K- @ @ @(\@W8  @ @ @' +RK @ @ @ NA @ @ @  @ @ @zj @ @ @@q,D @ @ @p^$@ @ @ P@8K- @ @ @(\@W8  @ @ @' +RK @ @ @ NA @ @ @  @ @ @zj @ @ @@q,D @ @ @p^$@ @ @ P@8K- @ @ @(\@W8  @ @ @' +RK @ @ @ NA @ @ @  @ @ @zj @ @ @@q,D @ @ @p^$@ @ @ P@8K- @ @ @(\N==Wq֯_>eNP$@ @ @ 3*>лO_2[xg @ @ @ T|K>~XjBi @CIsD):& ˧T9j6 Lc|C6ud4;+ڧ=1.Pс^MmM;uW̽gnr!q쿿?N?XR^_O @ P ϙm{Yçӝߴ$JO4Ũ.#%U{14<Oܱhlo?VM @qh9bb0!vw{(#*:ے©'Ή[ C @*J6}ؐo˾|ZϹk\ Țx俏އW/ lzO㐹=qg5n;v kӧM|"=}~y%@ @]¹M .&tx3_YpWLk~)|+jZڣCˤ P@vh^奿o9Jo-Et  I)#";=֏ >4.5Cr&E` *:9bd\v%/]w{Ozq}V4666  @ 0ۨ»agCGgԮh]Ymycknƈ>#Ӷq.غ@vhvƫ؆s(=lE`K7ׅ@*:˦;;7KL8!-]ߺ1.  @ Wץںum){~yP IDAT*ޭvH4.qca @@ lx:{|ݷ&ݾBWKj @ rȸ|?axrz%PZ!@ @Kg4eUty`]QUuxnES4ٚ'@ @@z}s; @+PXu&A]e] WnzW  @ P\w&@ @Wu_s]W`WΞ @I@'.7 @ PT׭o{0Ӗ*0n @@ @.B{69ԽEfu۵n"@ @Bzrj @(n&uզsJ=?;n N @ 7 @;"PDu]꿶}+꺾o T"y @v@og @@S]޽fKwp״Yv] @ @P Օ5/ @!PTu݊P.˷L]럛Cu`,> @ @\z2E @  ˂tf]umve0UC @ @e+ +ۥ10 @,Ptu]e[avUue5x"'@ @C[@7 @Xbv]A0M @ @ouA @pVfUuY8]Y0Ur$@ @} @^꺦/eeguUE_M= @ @혟  @]꺮P.;ή{j;̚uήx @@Ĉ @*\欺.E U  @l@oD @[꺮/M̫Cu7 @( . @DͶ̪6)] @ }sr @P(nUw@*j!nYo\MBSu]e +ꞾfMK6 @ @@g~f\q՗^R޶V  @¼cgPoDϬn_#u5D{sgJTȫϭ,۸*uDt @ @@!5M( y-4B*C`+o+^58yʫ%? 纶2mWu}VҾvA @ @`*:Лnq#λ&F @`g Om3F16kl:<Μ)OM{7K~5QZح 0U]3Q @ @}@8U~ @@ 5*ZW[ :oqJ⨑3ƺ_r @ @@EzO-7x@mn\mOnrմvDƨ&t}orԤFy1' @ @ P@DM @}Y޾cz5R+[,[nuL퓆Eɴ @ @X`Hzelh @:Rw3Sq>mD/MnMk̿g @ @T@'@$>uDY_u:3x)ī]2@, @ @U9(+lg?6-57:W९ @ @(G^91 @2謯 f~^#Yux |VG @ @<z.FEJcL}wUm4k|˛{*cjC @ @RzrMP}aqwY^z+=>d[h6 @ @(^@W  @@ 9-4wc{͡ouԥ.;/RE @ @ X @zξο;dBDCi툺w+Re7"@ @ P jXes$@Q:shM_Oj״F)K]]=L @ @@9 i5 1!v)>m^4]_' Y @ @ @o TIQ}ʈZz^h] VwxQ:# @ @;" = @ ж/;n|t6z~^63Uէc'T @ @! +BQ @`J5[gKߥqzV-4>;nځv  @ @ K 5]^ޥsfuk樿?(-^ @ @!, ‹kj Pwm)k=h|:נK fʖ @ @ P@0J  @m OٽhοY롍wu V/t @ @ P]{3'@Ahot ƥ 1r؄^=ִuFwkE @ @Mz P@gC)3Uu*JGԻf @ @E.1>fqѾ^}hR^ vYӀK @ @ -ZO!@hmxw^Ku[ݢu)Kg-4WMcH @ @T@V  @_m{k9rRDmf3t}t& @ @E T@mMLgeg*k.5#R]~Ck+wFN @ @@*f QJgߍ m{|튖J^ RWZ!h @ @% .&7 @@ tLhH^V7.ZzMt]ufv^cԮji? @ @C@@7 @@#1rl() _९ @ @(;^- @@fBT7{h:t @ @ @ouH;*Y_u~k¼_5mpOe[h[z @ @;E@SuJ}S3xϚxɦ/;nYs_p/ @ @V@WKc`^Z-GF¨{x] um f @ @ iސ^^#@@eOU}wP:.x[ʻ-4#:*cnFI @ @@ow)bu_뎺xPmf*wY#U߭L^cB3}-Z;4 @ @[ǵ~|-qء?>4hmm-oy#@@t%;n^3]ٜW?M^WzJtL @ @@!sgW]8㜳ce۞; @p )Kwvm1Wm j0 _  @ @ 0d*:tJ0^ٟ8/::4dR!@ڧ Oh9$xGN(okS*xiTWeG @ @@Y @ه |0V^xhѢB6 %GE[:%;qyk>;naP @ @ @`Djkx⼋?˖/tL @` F:n\ wi̶FbhHguxv. @ @(N1c!7rKi=)n馸[SDsd] Ͽ;27iX6E=)?;1jWTi @ @ @`Tt7j̅Ru1}8Sⓗ^>x$vywtV5ߘt݆  @ @ @*:grh&MukƏ~Ӹ1 @:s\>!ZKwY^:.&  @ @ @`{*>މ(ж_96UM}"inûy)K]U#9 @ @ @ze<G謯l̃ƥoR6õ+[Oxyx. @ @(o^y @`cev>3;nT]{Kwxn]F @ @*L@Wa fT@R#&nt]Vw ֎3s @ @ YH @` 9*ο{3. C @ @ @ z^@MḎzpwSGUͺޭ{V3r1 @ @ @ Xؒ@RZgO+:vYSߛm&jW$@ @ @*Uᢛ2/1!76L9)Jvm]RЬij @ @N@WvKb@ GDwm4t߿&  ´́ @ @@@7$@ϷlkT/ޭoM]_}HfL @ @CT@.3;3;& Q+*˚  @ @ @y*],Vk7twYϬYZ%:I @ @- ha PqEٓe-nޥ-4sR^wV<  @ @*C@Wd @rhwSggWQhF @ @ @`s7hotsBdgm+fxu=D @ @ @ݒ#6kL49)ZHMkb5-4KO6 ɛ  @ @(H@WfT@g}mο;2mο^ۋvysY޼QZlO @ @) &ؒ@瘺h9@ :,w]g7f  Je @ @ @P^#0tfû,k2jZ;οK_̈́ @ @@PL9&D^C]!ު]ٲ3o @ @ P5Yj%@hyVW]vޖҒ~^:nC;B @ @ @`'v. @Ć}f =1ZKgߥ3! @ @؊@z{GmӦ5;߿6˟-8hsSMEuV @ @ @r@΍ՍC IDAT<8>c?--*؊h=plz5mxs3VFb5 @ @ @`*:+ w?1;:r+>8OēK @ȺheX4Giz7j'kNgE]M[Vtx+!y5m5d @ @ @,PсmfO|ccFgGi@-GMf4ZzlCr~Ԭi NK Yx/vuF @ @v v49|#o#7o@?V]hmx9jxuU,nYm![ИwsWEc @ @ @@% @oiq17wc$@۞bb!O*>]j{ t]s @ @ @#P.wO81+`Y93RbʘI_Os?ݼ,Fy @ @ @}@ﴏ76sw(3^?}Qb~4ܶFj8 @ @ @` TtwF[[ffWUqǟLG}(DFvEs4M[l5"@ @ @Z^9'@ @ @ ^U,I @ @ @T@RWθ  @ @ @B@Wl @ @ @* Ыԕ3n @ @ @U2$ @ @ @@ *u匛 @ @ @*zU&I @ @ PJ]9&@ @ @ ^U,I @ @ @T@RWθ  @ @ @B@Wl @ @ @* Ыԕ3n @ @ @U2$ @ @ @@ *u匛 @ @ @*zU&I @ @ PJ]9&@ @ @ ^U,I @ @ @T@RWθ  @ @ @B@Wl @ @ @* Ыԕ3n @ @ @U2$ @ @ @@ *u匛 @ @ @*zU&I @ @ PJ]9&@ @ @ ^U,I @ @ @T@RWθ  @ @ @B@Wl @ @ @* Ыԕ3n @ @ @U2$ @ @ @@ *u匛 @ @ @*D<'wW;見3I @ @ @@7n\엿U{k @ @ @@oGOhpӍyuM @ @ Pm\S>2GWY @ @ @J@WUm @ @ @& Ы3^ @ @ @Ur, @ @ @@ *mŌ @ @ @*>;3cST[ΎWq{U- @ @ @ M沘 @ @ @.7 @ @ @@ xq  @ @ @@;@ @ @ @ze8F @ @ @@ @ @ @ P2^C#@ @ @  @ @ @(c^/ @ @ @y @ @ @@ @ @ @ @ @ @X@WƋch @ @ @z @ @w'p6Wm[6,Q) mJ)-Zdi![ٍ}cBv$JIiDvJyi 39;cfk~~>|Μyy   pk      У       pk      У       pk      У       pk      У       pk      У       pk      У       pk      У       pk      У       !л SͤxbdYu &@@@@@@H@t)7EKKk+HGr,!!       @ t@/O<6$$&:>di %@@@@@@H@H(UJ4l,Qx8EKw|W`O@@@@@@T z宺ZUGb x)ٵ{,yis[      _ zW,%5+:DȺ뙡2      A,"]{v~3姟v=VR%n*3 '#OҪY )}D)XgʜY-P^bs$ʟ%G?.E޶gٵm,.1@x{}}K6m,],^{NޯrbnFhRF捼}@׬RL?/K yUY{ %orh,ou9_/ vgN|a"@@/L^$q#o2|Ӳ租䱺ubeQI.V@pqceRf-)]! ~zNdϏ{o%9G٥vT|^'ݵi$?Y._|>z;w ֛Ȳ[=w'kG 3_8#U \<]FogǤ]m3 m[tqY3ΑsS͛^m'ȡ بhw]w2k )~tuGB%/ۿGv8O~f{@>ꎷKN蝋gF QC˯*}TDh)Fz%EG|,}\{U9i:t"9s!1=xWRbE:jdۯmZIǝsuzR%f`kޤfϞatѿ 6.DsTL|٨'O.͗7Dwg]6u-_.ܽ1 ޱ<7{+TL~nlٶV;E/ 蝣eFL iR>(s濤 J^}dIy˖$JcEoVJEwҹW77PwKC ExgpP jo=k3J(ҵOO7jzv4h o˪5k~Iѹ]{?0#Tx;.)S 7fXov6/_>ɯHcwۈrCP륶~?8,WdI @cw/o>zgtq![3G] Ͷ_\r͘+QAe `P\ӤgdW7˯Vڷ 7:(a܈Q2qU.B]G5۱VyfvKbe=q;>GBϛ_^}MTY'SM_׮W]_{ܪmvv»Htlu?^/nD3 : hӑm(St=Ka74:VNM{ݔz6jaC\@fT<7ya.1^dɒ^5C6.Ry.:'ȇxPU 4ɥi@l ǟ~qoгK7W+T2\FvzKFeVnM,L۵}VM}붤 R!YfU]ժIt@It)}v8k& :ˮma1dg/.MoiKmz?r\TI񀦣%%0, ^{U:/ja'={ZmOvᓖ߰t>?y-S}S xMU7_S]}knCb]Ņ®@d9։ Zq3L޺G箲J[k?l\z'Y4fj}גKhi[cK\*u)Y'G|gl#@@/xwuq ]eVב?#NRk5~ Y~ҴQ~rhLоIfY#IRJгчrs .}'g :yK!C@/H @ Dv͕j?adW!C~̥5I_{ yN]z[DsRB$heYwm)}E/eر}\6yZږjkYƝ>4WK3]Hr ݃pS @@/йd ؚ wQdaj|㍚޲4c*0??.gH!0:E?}o ZGfyz+>T%5e r- b#Htt7n 4uĩSHpβyVnOi/;Z7}lɷ-`WBgWY%n+*+/s$Q{{wN@/T #@ wIo2-Rݗ-[6][~̜|m,{n2dԈ~M:nt[:o*Pj@uΕ5m&NȞ=,~] =̘$Y>(q IDAT?ȝ㊒UGO4uHAٳ")m)гO۪ʠäC5 ra2\?lM5^ڡ|S~{~Sn\}|ާե,#b$Ofiӻwtil;m6Rh܄)&Ioa컥~쓞#*vgc%⎪U%jP/Z^Un`8m{ڷy]dU޲Rr]2ryE.oJ@/ @ X*aluͻQ?~ͯ=?ai-׹ǿٿ…[?('' 'K?d銆<=uLvKy#ҩW79Okx@ʙKbGБ#\iZu%ˤڂji>hKy*kYL+A#ZЫvRKכZkZS SYxj?"ŋqS ƚj-G.,2@g> [?IzyGG+g]uֹvYC/~SyZgviӖMIs*Lu8Ki]e|ykS:\ΩNޚzxaHCOk$_jXb?5M"N!U;~L.Fkwm;vjJIre¥@Z ]ַ{ZmOu;.a1s$O_u m޺U"ZtiG+~C@/@8 D-6m|Sj9K:xu[6#=u3l[KtێbX]–$k~-wɧ;B]/zHCvƳ1zgvwޥѯ2wދe67|ptKVU׵kIo!t4^;l2nMuʌ;o&7TFI*ԙrݹ,otTl;W=nzj۩la#?c@˫D>[C$V7m-[|rMTv{"z Mq\PF~Wq#FɄg'urXW=۶Հ>_|m4u4}y~߲u&Jb] }1m>Sl^GVY֬Psw[Pi3f7^+~$8e}~'}3Zй]O{)#  !/@K@@@ P#ji[8q\g뮼FwRMzل\ N^b4I>w /iܹk o5i/ל4v{R*U10>a׼vn=BYs qqqRffVЁ^'j1gs.>BرC@@E)*wҵ^d\  *e̱ojN;diFYYډ|pɡ-ڨ_Ӗ{.ߟ1{o :;fK<7f[ 1[7^#cO=k{(mZMR'a#T.]5ߏ=֮[k#L֯/׳/hNoX&oA`k '!  64KRt\UMiF] MƟ'n7q_`h|or^+^Z<3m畟eYXEo?2MmkyB3~y&LQ7Uײy =s1nV#۶mz`C^.:|w1fL[Wh[0vE'v¹[SLRͿOՆ'Xҁ˖߽e-/(u櫪a8Ɲ8x5:Z-pfm 7{s   @8n<7M6=+JŹ0ٳM޷rnti!9PkOGEz]y6dԮU[1R5ic~ѻ })w-[*&&Zo4PBEC5n9=N׬1!e8ʄ7A觑УWl.qM 4OoX@@(+гosZ9Ij|X5g z~ -}nK¾67<ůTkyS۷[Z]o͊TrFNyZ9czS-}7,; ʸjջ]r@/:Z n[A7&M(9ބl׹֗U'&KkxN9kW^Ѯ.}\%Z^ /DtUMԚ ;ezK   @ TT!WC[}4hP~sw^[cY~O>*׭;n鳾֗Ӧ_Q-7 4N֠~o@o^ngwM}nwU{IIzq6OfwHƫdz;=EZlfvgz~Ըg\pg݆OC߯AwG{o5y-hƿ z8 @@  Z\1Rg+<;-'&Vzׂq7)kW]Oٛטޡ&]z7ajc~hs8e>2i=칙Wh/->rsP*6X*Zx܄gי`7kFg(B/*.TvR[hWW#~xo&+eviikF]kU@ke+;ߨ%&<=} (7PwMۛg-`7,hrr~K֬(uy^:UJ5-\eTαa_OÛ@k@@@<3jǵfU)rw lk o@Ϟߠ~}պek׮r٣n_k%v7kՇv xeW_S\}7aZ+#cx]r?ܣ/u<.RmܴQ͛gs4ᑮbѿBnN')T^x%mٲEt:wkÙ?P jPּY3]wժVݷ-xFD@@BP`wz+&GU1pZѷ>DUSBX@U6jpnq5 ;-}Wg6dzM/m{6)ݩE/1Y'9E c;ąg;\Zqkt}@϶x5=ʝ_UΆuuMe`')&64Q\b|j+kk{=uʹw*sޒZ%OE'V7c}cOaZ&,/ VOUꏕA= '5ёZi¹Jۼ\!+/@@@ 9V vGP{A9@'aWAwh_,9@@BS@Zj-|VjsiQ+&>&7z/q7*ƄRMႭ 9U Oz[RzB/kJmc [yM\ c=:Gvv|6tGfǘkv lE^=SM`jvj=zG\$',[Wtr{@wZbҳlܵ|?Z]VNn={~B\ 6m۪9[l%_J$}o¼;/CUC^f,z /~V^%ˊ{'}Iag#>>^l[[6iIkAǾZms)YӦZi 5Ovg#粧8vlݽ{# ,{D-m6)$N\* P1Wҽ.AsymEھ7Tƪog,: e ;õ޸0pxA2H|sstw4PߞrhYed%KzǶhjU\/֭Z 6K<=N o٣~Fn۶-qjs^D@)Ӿv\B5c}LV) |;Z =ٶ^ܹN0{6[0@so@@@%6lB8 -Um]m(jhV^Z^vzAֹIiM/h8-}=m1OL5[`/iK3W siVYJuSw9'JK\3]ۦ_tg =L+Pj3}{g{nG%W<֫&[oLcoxʻuWڽx NTlZܦe}hԊwbz(..N -Sύs]tT:q?%}&~Zv<@ooԸ@*71ZY]stm5![*aZ,MTӾIs]ؠNUx^K+jL@@ukiiis1a+mlq{U%+ kY^vm.th3歛9hDmOOWllӻǫo/rQԜ׵Np֩L-/f낭ЛgzwJ/6.+Ћ3}ҵ״/i ] #W{e7]qM콭 e'=g@w*Ə-ӏjZjy///Ow+|nrn^v z./_      p {pz6L1{']Pa!}n*^3xyp5NN.r^ݺ-=mSMof*38e͘~_k4m >b*lSfnE{\xlsϸ 4O;w* @$E#=A.P "hN?|E\_K׮#r!wg MmO0u jiϿ‹uώVR.\HΝ {#Pի?|{nyN8 Dž>\?j֤QhPn|w]ϼ z\u8UYg      p>;IÏ=B1]:m[Wt=w ym0XPz᪒I'snl< lhb{8ou.=F=:T-qnhܛ@/?F.]]Zu0חeLvwq m|7o].g֘ɻӯ?ʽ xt:ټ=ܽ˖?שU[)fH{Daƾ\XmW.5c^yGFksԯL̀^ NB@@@@@` rjXnP]n~\{ThMjwp-2}G+~֛M_тr3O;Cw*J@w6>*eJ>:~."wڝ֡/L>F5jpOe <.g]\wYg[Wmj[Q74T{^ù      !]:S]nE޲oi֚=to襄Ki&H۶5:p |o 3)״ -}|4cu>,\=mg2=Se}ڜkZ: R^g+;X' |O'=Ӷz^_ wpmjRVow mwv>f[~O[Ԏ;GWm[j>w׶m|Vzm.:߫@,8@@@@@B[BO^=z:-7?PjuaC9eLz{L֯ 5'\/\q t- m c{piie')<ϿfrR#zA/A>{ڽ԰FĹ>Aկ[O2egg澷ke {|)C֭߭Zix= NxNѴ¬njݛ%=x-:ń?ZR^==b_(V4=@oY{Uɇ  l IDAT     VUZU=<ps~'~ҵ{ UM{SJp1-={D=Uyif>c$׸ǟt|c|He+W#Gvv6x=v>ۅ8+WԛォFIIz~IO?7΅G7]S'} 콞{iĸ1Wh`{;XuTet nyK;(:z}68CJ[ս;e{)`@7*{_FL5c5ƭLMݛnϬm^y(}l=BaX٫n]mk;PB|BP{{55gK<])k] @@@@@KB=Kc[- ņwvm&hܨAClxk]%=V\ioB.{,_\Y&,jluo&:مT|^y}32]kǛB?{kRט1yӦx5ge{y:6nܜՕ }޺v)ܵ֩۷oW]>h)[-jS*.55M .Tϧ<_~5MJjY'Ʃik^p11.! t>@BZ0lhx1Ey{]8[qM l}٪D{|6O<0mD|v/>@@@@@ ,m۹V͛63C6nڨ~IOr#anyR71aobL04s_z7u9&C%i? (] 3L͛mwic!:Nx6LԱGmizT>/^bb+t̑G+g &v64S;7i *jTL㛶nϜY}({e=m z}Y**[qi[v;j׬kV V^'@Ϸ~'p;T{6QժUoč6X_|*=VsVg5IndW._[``oj{LzB1@@@@@ pݳOuUrޞ 3 kv@@@@@q)z "     a&@w@ X@@@@@@ ҂$x     ^-E@@@@@,Zof      fza` @@@@@@ "k-      @ ق1\@@@@@@ Ћf      a&@f p@@@@@@"K@/֛"     ^-E@@@@@,Zof      fza` @@@@@@ "k-      @ ق1\@@@@@@ Ћf      a&@f p@@@@@@"K@/֛"     ^-E@@@@@,Zof      fza` @@@@@@ "k-      @ ق1\@@@@@@ Ћf      a&@f p@@@@@@"K@/֛"     ^-E@@@@@,Zof      fza` @@@@@@ "k-      @ ق1\@@@@@@ Ћf      a&@f p@@@@@@"K u\mڼI_}Ek׭s+r~ڹ?7^W@ W=4mظA/NxEK3V"     TZ jժQ# uΙcc]6yMڑ~w?WӧiȀA7>2YlBwGyvј      @LwXֺWi;&5c]ʫqM<}wQssC~}_P>^ZxQ"3E@@@@@!%&&h׊+uAԸQy9 {Θ-DR$ zګѰ(^7 4뙕vј      @LgO:z^{23Zl3{ {&~:Isu+Sn]=X]~%>zdx]\JS"rV"     TZ Ro[M6lܠue&aߧf̚;yHiD諧}F7o9wqKo͙7 J21@@@@@@ B&lڬƾ\ <3{=:K7HOtߝСN?T}~5F#℗hɒZIf      P)B&;aCڞ۶UnR߻š枽cRffSSgN7U{_-YI?Qs-8 J`L @@@@@@ B&гuO:Ux7y[ /r+b+vc5YzwzUNt j޴nX^e+G*2[@@@@@@J+R^Ufb      ^q      !@W<@@@@@@ K8.C@@@@@@"*Bg       z{ e      T^E( @@@@@"TՕ_/^q(jCvJ0m^@o@@@@@v#߸nƉgUz~؈!@"     1yc#UswNg=J'S^gNJPtS02QP     *P:(+v~ 2cO}=&ۏÆt悇4m1w.ܶz{}     9e%J\a2_YYan23]*gʵ-\wr"K@/֛"    @( olgeeb%+JVk+㬬_TQVM @aqW%VtUl«AW銲p bMۮ$[%ZiYyc8 "z! @@@@*oXNPf+`jX{bş*Jݿ8Llk`%(eE\[m=V+Ji!ZqЬ(ۯ!7C~6    _KNT,ŬP?3t-8o2yT*+jXVJd, gZ.O߅a vI~a9fr-B5oYx^ %a @ 0.@@@@{@@wDT_W Je*RY+N+FtVhWuV-\2H- +JbU {j,lx( Q   ; @@@@ LUP=V1iWKW6th'^UXq cjVaZ-A+]af1~e{^-}-Smg+H_V8@,ze%    <( j[-v sqzez`{+^X*U1 KTEm}e~*_Zq_Vg @@@@@@0o6ٰn:ӆu&Bt{'#OQT٥B/~/03Җd~m d  S@@@@@ $d5[=W*s}[e/&۞+ OgI1~߹Պ^H@@P UaL    @%p3s&+(|m.}t;{Nbtг.*r6B:~g>TO 1G1K:#1s  ^b   D7>h9޲Dhg3u AtΆsEJ9[Agøʹ]C; s]J a&@f p@@@@%`+l՜+l¶s& 0ake髜+ Cҡ]Pd@@ B"d&   T~o ÷E{͹~{ٶE{㢂C1ɹ=L [}lЮ<@@} wC   oU_V :y}3{sx&t+lg9tBtr.  $A@@@ *reMsswUvR'(SOnAa+ \0g/Ee  A ;H<@@@\dm.s\Q{K\~t.󫞳+:Ȁ.'h9`U@(?}g9*G@(K@@@@OQKK݇esewM0'TW%Y=WXMg_;]0 TZJL @@@0p*:[1g]`Άv m9j9p}sE. [_ȓ]@@F@ooԸ@@@*\U?WEl%i?}{;HSU t63Ams髪QgC:@@-@w?   ):[Akoi8[=W? LQ\Z\=|Ϊ:O.ts>   ;<@@@G:΅t5Lo9[U^tbns;]^tu63Uut1I@@ Uf~   쥀71hε+lo:?YҞUQ8W9ֲD]QxgLK噋8@@0[p   .s抂9 *|-.;+l+sݗ.T]侑@@ P@8@@@V΀U*:gz9/>*ۀbζt t.sI'/]pȜ  l   T@a+KS-W\EWƾsm.MX\KiWY,Q=ysp9,/]Tees Ϣ}lK$ @@@ t  l*M 7ƠM03+ LX9Άx`׺W9 Uљ/ Gp>   z!D @@ؿ(&%nZUJ0+3aGgue0t,\Q+ڏn[a2bE@@)@W9וY!   lsk|9/'¶ Ѽޢ η+joiø6˥.HeNG@@DK  DO7ڣGͼ;%C L wn9_p.^  E@P@@@rVVFmW=Y|}sYzŬءΕM_L@@@ `6J IDAT0   kZy'xQ[rTP+ă?Hq?l   ,@τ@@M.,oG|%|D=Ż I0*@@@@@@*@9%Ig3AEάeN   @BR//C@@HPykUM=-*obo4   T ֩A͚6պ/iيյsEGG~Ыooԯ^=zi!ڰq^gY &  "ע )c]wz̊f\9   !,R=9sW_贓OQ˖-/]6yMڑ~w?WӧiȀA7>2YlBwGy!@@@#P`Amim"/4yr C    A@~& [+M4idGWSύӣ=[Q~Ar^m-\p@@@ D{ pN=d 1\CNҜsMu5cuEhf7^C+WҔ/HG  pܶ5]E^ѵݳbl7A^~x@@@@zB}#G8 OG=1V-7ҌY7)}Գ X֜y;xOF@@,h$e!y 'q;@@@P@φtwv=zOO~#]|<}wB:S5qzlfqߍ>B/NxY, EoƄ  ,Mvyv # #  e dhKV^j6%!   ^P\  ^ &3[ǝx ~   {%@Wl\  o*j辈ZӔ`Zlr    /zǵ   hWgV'@ TEmˍx@@@]@o   D@iTq֛ /U1+vDF@@8zB{"  @= )Zno55>ymfr   G@T@@C[5Y٧w^DZ3~ƺ0 CF@@pY)Ɖ  M[-T%)[Qd /յdq`@@@ Ћuf   Yg6To05yiN;r       !9Q'nql6*"4@@@BE@/TVq   @H5悼,a*>yn1@@@ Ћ5g   P@AxZ3If<{Dmq{ٽT @@@&@wy0  G"/k#ԎwCyQCb @@@ "{=  -ӱ2$)Eu7{KS̟#څ#  ^hA@@ m]UW=-v6WӦ x:@@@@ 88@@X 'K:'YD6A%|Ƴb   ]@0C@@yce,oyr yQ<@@@i^  *}rTqzk2c_o   T^0@@hk9GrE)v C@@@`@@@ '*y٧5pC^aT\jCe<   $@'!  @ xĘ 5=<鹮"/5d    r   @ d쓗 n. S׺ /:-+@@@ @@@9'rtsy Rp[Ή#   PZ@w@@N iUWݩ{tWư F@@@`Oz{{@@Zq"/I1EmqySR%7d@@@@؟zS{!  : vT5A^MU 8@@@zyu  @ 4嵬fF^3J0;   l   pZUwA^N q?o:   <gϓ@@@ z 욤of   @D E3i@@BOQVde,oy]E^+ϓzfD   T^A@@(_ fKNz*zUl   D^Ŀ   pr4Uyf bn1y '#   bz! @@Ho肼3FpA@@@@z   Paheu-'/6Jy&[c3W8x   $@NX@@c*{&Y$|fj4 0H'S&uW(/??8 F@@2rTV7O^9[LU1@@@ .>y<u88=X]{ڸi&MH>}zn}aگ wÇܫy[ />  @OJTV$e&bk?k}43C@@@ B*KjP[G=daŁwiꌩ_aR$ zګѰ(}c/_@ӿ @@&D)K27F)j[ ?¿Hǁ   'R }$pF78z`Mt̝dխ>྿K4|b롕Viʗ_T"OB@@d@6V~RQi.ȋ^UIf4@@@? NtkZ/Sf{o=R7Q;g &|b;n9Q~"#F@8GVfd嶫F&%}bo?    `B&s˭jղE{UVMz:Jߑ>Vu)jب1?F'EK   fOiΌs R7{#v    "!kݲnK?/effi@;5utSz,֤ɟqǹ U~Q0" @@(,SgɳGԆl%|Rd0    Bz w];󻝫hf|y^խSG7]7m녗kي-  #9I5VA8)Zk&|-]/8@@@@ BB/   /sl]eHy-^ SR|GO    P*25@@@ޡՔ-Y9u 󷙊5m3@    zaH @@(.fFL}ݛq    $c@@8`2My~cyȓoZk1{奺?s    @x z1Z@@v+}B]eXy)Uy ך4E@@@@T@/La#  /{X eu7䵯>   a.@ @@"[ a>yg5t1]E^7##   P*b2@@E /I47.JQ[s]E^gkw&9(@@@ @@@->RK#4aY'oˡ    @zy  N vIyE*O& Y!@@@@ 0Z F@(.eCƬצkR8E+컝ť@@@X@ϋ;! |˫+άV*X. jSO~   _t3D@(VӭW;4|N6m/hQ   qq0  @qHnP:y-ʻch9PܚJ{@@@@G|&  H&OHŷN)ya?ĜS:   IDAT    seuy:F)-$P{]17ZJKoZ   ^!@@%@@N@yPS)]xvEu$pN@@@@ G=. @@HjR֭gnaKcܨ~gA@@@@!  @! TJ+1d䅮SHg@@@@ ߔ@@L -"H65乺YЎxmS7;T@@@8R@@@X _\U5Z>T )nD^Y+/ 6X!   @ +>}IK@@2 $6/F%/h"nS8@@@@   y $*)OJlUj:@@@@ nR  H-fzhkY7nQ5nzgu ѺGꬕW3B+@3+cZ%]mwA^Юx/!@@@@[3No:^IO~7ݢCOj/P_xNNýz())}6~(+Z~wS;@@ $+%OJlQ޵4=fm ^{!   +5^%5j0{lvF {׸īw3jo[_ou=sղߐAJIe½T( @ȟ@jPyT| 17Z?_    Ek ;_qRRRT{>Օ^mRPp[z]b z{Uv٥_E7# @`   x7 uB@P * yfd/-UB@@@^KZ f\ܠ+?|mWжr> E@@@*@  @JV硿uk1@@@@" +BlN @Z`yEӦn_\x/T:"   N@u) B@] fzH Q$ٵӎPD@@@8.`@@x$YA5\kPhE0/pWBh @@@@|X@χ;# +\,5v 1A6}ӇE@@@@NމPL@@( UQ\(VUdu) >7@@@@ @@H Y9%7(> Q 8     / bQg@)%LV\Cc[      %@w\| .Z&y5ߺYs@_E+=NC@@@@ I@ )@OsU B#ތۦov-F@@@8z'@@8 $Sy)"N^\ڄ    pNrpz@F:JIM˺J}KfT^8i5E@@@9=2* E-Z)Lq&K;uȪ}&ȋV}E]·    z~4@)(4kFH<;"ώcC@@@@J  O $\TEq&K*6 ¿cC@@@@(Rs! $5+gJnP5&ȋVЎx;D@@@(zų_i P F E*U%wd=. Ys%;    +@W >&j1yaKc|%T@@@@ מ]  @wuJ(pOEKiy    ^Qs"@ĖڬWfhݮ} RE    @ @rnz3ʻ6 74@@@@|>ЫRՎ1zYZq5F@&Z:y5i5w? oBsI    z{Wsti붛oVARXF >,*҅yi F-    &Ӂ^ҥ5i8uU)F 7}Gk֭  I J.KTmskJ @@@@@at2rxz󩎠4[YOo0NO!zoACG6kEDMIަw*iYwdw;MMA[=X"+pJҚ|ijX˺Cf: Q:Vew~Mܼ3@<^lRg{g0=^'W @Mj1;λeVz^)+y+`hJL)Kc?[zr+@/? VG԰uφmWoT 89[-KU?9 Ԋ(>Z9*"IT=Z~t`\$Ո">5W_!>Y+Wfo^~[5H*H]J?,}VJbօ#y ,wHg!}4s. vrˏ@4G#<7?f @}1 uxOJ{%oO2 OJ*c'^=<~(+Z~}֓ of T:u_ג:wx*Ih ;Of魷ŋļq./Dx_Q#>́Վ!)wVZ!^(pmRҬYΝ=2/!Es}EZlSti»u=sղߐAJIM=*z.7w^1T |rsЄzI4~6Sn0t .q Yn&Gھ >;#bϢtN93;D}{~⢩gA ? nE"P#,\+Ͽs2}{gԁ QbRTuu͏r{/~\y xfWukю]z_ywL `b'РneMmByB78UB=zo];io p+Ƽ\IąhUDM٬K.W}JҚҸ(Z>.V=\5V4_鋽VܱJuPMKqF:s]~YKUʅwOw_k?}>; ;Xq y"u)ܻ NK P\wמ] pIwGKQ(f?Y(2EF͉@ p,FIS@H鷁^^ @@@@@@c ;F8C@@@@@@(Bs       pza%и)߳ ۏh|P`wujsڧfs)u筷\ٲZa:+5;Ek^nƪBs_^{,e\kV)G+~Eod?ж ]c2-W~?ȿ:" ~O~ҥJ{Sr5h,_yjweiђ%zwk}znV-2kULO͘=YyNiiھc}7hb,u‹tozˆ@)so?|c_mEӪ7⧌}*VWuj]+/ke)#88X/Lqxw{szAMO=UPJJSRR6lڠYo;w~ާ[OS=,?b< E @vw˯TZ~#ƍіn:tm^۫OZyD [~tۿ_9r~^N3x`9:{{b~z(T쿧^+y!m^5ҷ`{ӭw3Zʓ}XrD݆QܳϼoBn a=<^4_,C tܸ)un^J(G3[UZUݻ<%2 G;k֭ ʖ)^}s͛ g -Ч_8K/S[;@h)΂uO 7<8Y,}fLRRrᆳM7wˆ^k+iԐadm| IDATEq5ײg-8w 2?<`^jX'~nEM;("[sr B4l_?\yE_ ^Rs=qzw#Zt{sm`wyxw@PE 4bn>|Nà?^0#+ '==2/nv~ՀM7A}T\_1_@U3J}v V@b;sZaCysh+/5M3(*쿧a۴5y~yz{Zy<޷G= d~bsO>e+_ղ]vϹR?ܙ@x]*`<>j>B/~ܲug?OWRe G=3GhZjH׌0.svoμ_@~f_J{fQ3OKcQ7]?s/J*+#Prwwnݏݳ}{?.u媌0.]1$$2˨O=Çe!=гƛ܀7 ls 1)u~+f>9^|i'M Ӷ2}UT~3 3Л2n{igɼevI1v{6v陵ID8 yYP ~h8$#г:4>U?2o=<8(XUT5b\Wr'^A߸[vJ1CGhܰQzbG eF\e9~ L0k C觟W(((@ϻ.?jO vO~ﳍldFdl](a>ę4o̍>N;2)>>^g%lw 9@NA7yuMq ~2B|~3nxzzvKOLkg=`+#]vʢa//u@/ּow2zeZ;f6u~gmVӣu1^7^1#3?ޜY+@>z)nr̴WihϞ楋KrGѻ~[_J]ArwOݏݳcg{dG{YdJ5n.0/}j̛_xޅj}%6jR͔=Ro~}m5{q~2#kլi|jx@Nm 3k|Uze'fA:|Ng<~]Z4ʹīW)?K] 9yz'Ϟ3]b³k/ِ-}^nUNd?u[ľoe)>#GN:HN}@c?̴O=+o֣Snw}<ӌ@ϮoRLi/[^R]H$UDGsOܔO3Le3EGh@ Ysm S'Q^5zܪ^_r1J$决շt dBB x [a@~n\TզiOuͷf r~@Nt筷iʴ'5ʥ5dԈ,0hu43:8_A=\f%{O }a\/wS􁋒*"#==;ʬQo-T%3-fn=̺oiyIn-n=q́ TĽ͔Ttvi :o|7 ˮG#%Dr=[ճzÏ?g_~fhegr(R,k矵ڬ̼k0|.&XdzEF͉|M.b{qˮe6Öۃ6kָ9Jp/@Ͼ òeZȅemH6mrCqp?7s#G-ZXWA5Ҭ/s |x@~i=;eH?3"ώ24טّ!wrH/mߴ':Zo֚Qs)ᆪ6nڤC5+/ggyH%;}5Gƺi֎%<ݺm}x2ѿw9{sO>>h 'ݵRRR\uiGٻ7#гS0M9ƄOq)fn=_8+^.Pӧ[?wϜ L3_񱽧6>=x}ny75fϿovK*No\wyI~OԌFOng#[r/b,\HK.ͲKNKv jpN |UafYIř3YS)-=Тc/5loŬi72&Muu;Wd)YnR.$$Dv(Όiizvġhb]|=q=_R7'=-{szhCʕnnwDf]w2[v+$8M9ncYNc y,mڎ2zvZd;wj3']_hs`1JI>,U_;_}A$*?z5=zdYL^dINu+#OgK;d`uKgkf/PCFPzzRoP0~继m7n/Ҭ~̊ӬI,mE^˗eQi;zEOMqg]@ UBG {:9h5|BmP7_gY(,4LQf.;d˳3;[ĺ ^j緻^hT @z]C~3lv(z60yy;=b tF!v3[$л䢋<0|}eIW\Zڅ~Ozgq?4vZ7}۽P:RG)SF1vCz)h9YP }-S;zӮBw1j@q>B/?kYQf[WL?nփMt!bcv{Ա}{ FMY^ԯGOmشL)7:@^ /=kgul7 >Qe3\}km+͚P}xN'ҷzkׯW7!bCMy m\_T*}ƛg-n3kN:fn%$$IS]gڛ:~Zxy14!sժR{~+vT@bTVuv 6V vS$٭y3nʷACE[N>ͥk䉹$+eȞjLiH緗5 G delq{Ha&Qn6SZO7\s.3Z%٭Sbהۓ'5MC[ vW1͛[o(G=hFUSN% [^{߈Y#<RyNte~]wrn ᙳ^ѶբJ]q u#g.Fyݛ;Ď={];?UM(̿3{n=;r{ݷwRZܬ s3/~ּin;;UX7nhtA=& f}A#rW4gA|u5ӹz7U¬g{6nXtl}9>-_bvŘe.1s^W%mI@@@@@@@/ Ρj      q       z^9T @@@@@@=@@@@@@X@ϋ;!      @5       yqP5@@@@@@@@@@@@b=/            ^,@ŝC@@@@@@ @@@@@@@s      z\      xwUC@@@@@@@k@@@@@@/ Ρj      q       z^9T @@@@@@=@@@@@@X@ϋ;!      @5       dzifzR5@@@@@@Z @ϯ#     xwC@@@@@o=Z      z^AT@@@@@@i=      yyQ=@@@@@@       ^.@D@@@@@@[@Ͽ#     xwC@@@@@o=Z      z^AT@@@@@@i=      yyQ=@@@@@@       ^.@D@@@@@@[@Ͽ#     xwC@@@@@o=Z      z^AT@@@@@@i=      yyQ=@@@@@@       ^.@D@@@@@@[@Ͽ#     xwC@@@@@o=Z      z^AT@@@@@@i=      yyQ=@@@@@@       ^.Pi4/#C@@@@@@bVotm&󻮧       @ D@@@@@@V@o#     /uD@@@@@[=z  $5*(qYVuoU= c/#DN0{iu]vZ95ι.ڣyůygBԃ   -@{   @&k62WxziB_`!vTms\;6..N]rܼiƛU\Y߰Q|APP`Z},]5ѿo}܎k}Y^zBWԻ/oԫ vJT[oWS+1)Ioܕ[fnoڤ9qvY.8|-ZsONӀCTt)=@gןl      n˫4{L{1JO=7]lۖ%4qXMyYF:UPAxNjk'/@/ 0@iC2\%JB楷XB{SaaKY#k޽ʭ3ݞǀ=1aRF]tiUTdMA@@NDmڱ +0   gW.찕I1vۛV-҅_B;MR4rP4׆Mƻo{GE^ѪR^Zhn:o?MXhmsQ~JNNѳ?~hR;MX^vŸkUZUwIe˔Q|B^M۸AQ5]wU+ըAC+[Nюw?|/z_d; gwwYVhIcinڼnz͛M>X^_-[VsRz?4}l5kD˕ 4yG]sK2=7PzM_o溰t˭_RRo]L}B|>LUTYߙ֯C]t--:Zz\L@}-Pǰ3   Nރ |0  >$@3%7*]zn~13zF^S4e_Wr[eo줉eB.0cCx]rE:4f,uh mxcCAr|5j.{ǚ5&Z&?Ew2L}~e Nmc!zZ;խSGzXwAYtY֮~g2M[6l6:|E>FXf*ly̌|ƌLt<ʭ3ݖakVCKJ4ofޞyI}G&- hGm҇_55.g6aW5`KlF۽{eّ`O>;]U+W 3@ug:5_:@>ҽ򅧟U>=uu;4{}mRLrAg҆UbE襗a,#]ҧ6`[KP\=_^dz?͵5xjX="_.ڣscEDъի5 _+ok֯[/퉬c&y򸆏F?fl7~$o災osGMm9s1Qz%K4tHf~7_?&>qhcgG|##  OvF@@wa \ Uv~@ЌT*5^~㵌l2r#Fzzi@57Fy?[oYH-;:nA:p킜QiΝn#t>",={а02u>Wg{̭W/㘙k;U{If[o7SUs;ܻf[z&'%4;r[SzCGT̿A_χ_֙-ȵmvl1숰Ў3mBBݨA;/{}Wwr̎   z>mT@@(@/,4̬1WSzr3ήf71^q⨱n32B/^}H`3iHn?۶ؾ:29vޘ#mgG4z{ 0$Or-7WRQz㑮f(_s_{Ls s@oH}kWs ̴ ?zx^7orZj. kN{~A'гmn@@|S@7Z#  3E1fftDM{nVNkTwb:Kc&yfz 7:>ҋnͻW+F\Geƶ#&ȱkuڵiA#:k،w5->KC닯t]2֬[Co)s?.SR:m/6~s_vԩUK{P} ԕ^nUì J&;0 (ܼLM>HNm{`ڿ C$[{7jΆcZ{֬a[f:rCB4WhZ۽cлNaﯼ.1 s_Yqչ[W%$&䲆MW9  Q/UPYeJҚ_4iY}x9MD@@!p`@3%5*]Szn~Ⱥ hnͻTByћfݹ?9Ʋ볕+WVk֬󯼤X-SVCf O=@+UTizqZmg rkwDU#RwwS*Ttu:oժVuuP8gWSfm^v<9o:_Q=!on1ʕWp!5,wenvԂzΞ9O%JD9zvͼ褆起j(뎛o5o`@j|mV~}PZ2IKQҥ6l չ9g/N3|h |O>}((Gvo)tf ?{XX&s+WNѻ @@_8tk_^)i*;7?v;bddj]u/oԫ vkߓ @o   @P` eNc# 5Hܹj>%=LMƌPJjjߞӞۮ2hPعHI@@_ {>7ZIE2rv{/N+#\A=j^:k<s$   P4K>3j7;"z*J5A?wfLi?m}VC]tf3\xe|ܨA]}U{իWO!Aںm>bVs>AfWw*U@bܵK /wm؀[/AKQuZ5СC~3o6ؾb5*%O\\VL[ٚUN]WBvMcγo~|}ߺ.{gr3$$T3zϪiwکdZ~^|euwAzڿo>2.R;_^_[݌dKպk˶2e8 U(_N ڲurVE揶|+-k@?wF['{n[Du[q񞼺@@@@@@]snk}tA@ϲհ^suY*)js+@/66V*Լi3u{Q3,P7o~޼vhGe'7£$u60O9ث5lf&ZjEVoi'qGՌҀ}T"<…6ۣ͈n6{;p݆ n蔆p1wRŊ. [dk~۞u_@ώ>9yW M6hhfdUTɄ&X}%]`:pPcs}kԡm;ioTdI5W߅^FҮ]  ACYSԦ6ӟU%8r(fo R+1G@@@@@@{r rWzaajqz{]pe W &l3\pd;QߞJgHӚ456ĥOM~Lkh;{{{B=h/[~Tn=x)SFC+0ǎ42r;Wr_xe-Z ƏWiʴ'ݾk~۞X@ϖ>j/ÇQpdݾn^~UEՈԨ >c}lFUP]vw>x_vv{t-b`Ft^i=mجI&z^+eiwپ^oS鉉TLYKyu#     ˍzG'ꌛ<эpV[,{hc&Mu ʕ+ 7-tSCکo3BAOhg4fO?1AէɎ@s@ю6K/7s h?Olh1姮i{^z6]V7\{Fu2~*+ߛr%^n㰏eWjybZZN 6lڤF 7rX7z3}KSԤq,mg@@@@@k *௝۴|]˕ήƻNi>z1Ӟ?P.\LllŒJHHpNV&=yxsV+u~ϣ'WhhlXc/rwfkGّ['S_ރPkAڞWoOg_pY7G͔QCi3,]f1eTlͭ}6w _k\=TvkY;jKm (mPiQOɆ     @z[nq-((H),+j[6 =R))̡֯VjǮm~X,ctV3js)NqýiܺlkԄ3,7=`&,44@V=?[-+\ِ/}K_//sg?˫MNifG(']o;z]ztyi3T9z~#ӎԳ}dgFQۧ_?<γ3[vlYڞvzZ)=Rn-([ ]&a J7CwNJ)d# m;K[%˒Ȓ3[u54߹\y 쫮xjZw*/=/?;w=ozn| @ @ 0k3m{\J+|j5e>>ɏ?|2smr??M3֞C??J||Z#s|3~O+P=og\jO>YϜxm>WuԻסᡉvx&W]yUݶXfOt_yկTjw=Лz! @ @ 0{e%*mdd$xթEeeiڙiZ;.0pϊifeK}~H]o;'ZAny[>?\n3ME`T9G)=裱rŊWkzo-tRN^);1ׁު+o.J>*n'=ቑ+4~,}xyx|/%,Vc:3N%krK\Iz]wol+| @ @ 0+smpVMWGG#r|瓾#^_+¡ٻ7;Ϧ/SXvm qEm۷{D~a`n9fn*x{U)/O4-䕯}]b׭Ov9<̭1@{ڧs{Oe##Gܳ.-7x׾~ᄑ'6{^̧ޑS8yOf׿;/[ӽ~t:5 @ @ @@///ďS~ nƭǹrY?,/~aeJg,/}n)󟍡\-?Ƴ6#O3EH8ȳgť_S%@ 랻4.Pc^~tuk?َG?#CБTYy?ŝɯv OF\plJUy<_+[o /}'RvN1{Zwe! @ @ 03o Cc?ا*^vbtt=61yIN @ @+ Xy;}kJP|}^+8Z~8ju @ @ @K@7_{R_~K9}W? {7~E/-Ϳ3u @ @ @) N7ͅ˗G퍿Ƨ?YlOwO\Ċ3V~~M @ @ @|; l5 @ @ @VڎZ @ @ @@K Zj;- @ @ @zC @ @ RN!@ @ @h5^ @ @ @@b @ @ @ZM@j;j= @ @ @-% k @ @ @VڎZ @ @ @@K Zj;- @ @ @zC @ @ RN!@ @ @h5^ @ @ @@b @ @ @ZM@j;j= @ @ @-% k @ @ @VڎZ @ @ @@K Zj;- @ @ @zC @ @ RN!@ @ @h5^ @ @ @@b @ @ @ZM@j;j= @ @ @-% k @ @ @VڎZ @ @ @@K Zj;- @ @ @zC @ @ RN!@ @ @h5^ @ @ @@b @ @ @ZMM7 XtY9:㎻l} @ @ @6h@mum_mWx^Gi-l @ @ @Vh@T*Ň]~ol} @ @ @6h@/Z|, @ @`>֞G\M'н#6ꎥ=}xhӭ o r׽x5W|> @ @@ \Mqų7NꆛK[ @`f.~Ņq񆾉矝g Lsǧ>+  @ @LC [_I=E/bl8}8 @=~3ʈΈ/{h8Ī @Kz]{yo\kb  @ @N&Q58sϭM">Ɠ]  @&p/E^13rq|@Kz]pao沽 @ @z;RH7DP]|-?V-j`GyjG<ǞG? @@\/\qO e1H_!m`3 ޏ5kO?3@ @ @ I`Y_grVEW ֤_¹Fƾؓ)c|Fjqpeuݱ.͢˭.T_kuو @4@ @ @XU*rP7MrDžt\|ȨVN@ ВV"@ @v(]-+^.,>=v]=rV ͤ)>'W I{^#@ @= @ =qf5ˁ\UJPZ`VU⮑Pj_ylHjmYAWs)+>V嶘 @V@7ޮF @&uNs^.MmxPe\-r9Za7C#ck  @c @@-'ZV+r5],6X. @O@7L @$әBT-W j\~Ο̤TӭX@z @'L  @ @)TZ[pn"=ou?UJ)3upԍ; @fRxsO#CO]|߾:FE @ ;WNUtE W\9W\ZaVSkGʕp.͚ˁ\-+\W]9 @ 0oM3'Ngŋw^G  @ @@}ҔvpVI¼FGRZ;ɡ\u]-4[%@ @`~>w\{bCͯ @ @3bw;S%i]u\-;~6ݚe]E-p,z!ݤ@A ^z]:wD @橕|궭 ]y9:>\M/t{T+FcO9 @@ b<ϟG"~aY+~.GB3(ԁ^{{7^qoprū//Ÿ[fɩ @ |}ݝq馾tC\?1=G+'|"rRn_ r C: @Z_`<QN9}Ƣ(z_7).IJE.*sMWwv%\lk @ z @,ΪvV9 Um^W]folRX%ʼg`ܓ_49 @M'Й\%t)3jm.s@:ne j-/s07R̡+Z`RMq@-"  @,,u+h»䝿7/w$nOݖ\q2󾇴\Xn @ʩ@j\ &ϥU喗̣++r`nR`W1λ. kt @@_wg *] .ٰ;+ze@mV喙#G xnh'\1MAWwZ].np]cR\;Wt*j]1n$8vzY+ @ @`fOsr]{| 6ΎR ܝZe޾-wR\yeMr @TZ]m.tEZ`Ẉu垎n÷¹LJPv(ϺKʜXqBё5UnΡC ]Ӌ  @sy\M̤~.~iz|Tzre-2.rr.s)+.U9 @LfB9 @@wEإE]_.3ϹV]Hr˽qYw~ՂoK"@s.P:VIW 沨t ])<+juE]~Vy @`zs-z @yk{ҍVYw=)ԫw|q}V[Rh=* @Fysr`.La^#GihREWrytvW\F @`>z @I`Ւ]frkSpfݭN-/nPܕ] v}.K@gGWsn"3szz.'_9tlإ@AZI@Ji- @/.I]/.H2OUxG#)e~c`1nK20o* 4.W3jaݱVEˢ;~q}.  @4].w)$UmN2su+_9-S]1n`.2m/@),\V+犹tǪ9tՖ[]¹s@e6Ĭ\E7B: 0M]w[?3 @C#LvE @&iuO=>wC"[_Վ}Gn`lIޮG]Xocn:`o. IDATE[DM"(OTU"UU_Lu֖UCŘVJ 0M}uW\tq@|>}˺ @VŜ]TLظl[S\q\u @G q7LJ"ˁ]u] jm.J4t4ͣ;>[I*R%h]J鯔^ͷw}Wōw9sS*FR@>7%K]s @h@'6mމu_y_|t-Zx= @K/M*ssjy8gMv#i&ͭJ@UT Ŗ܊lVIrȕtհhmU/M.GuL\r0<3wēϋo#}ߊ7E@Sz}뮏?{'^xK_ohJ @rxwYiܭ>o.L Vtǥi)`]o1|fzJTq[fޕe}ͱXwIQ(,\V<.rjn"U~)o3&ϣ{LPV+zb̞$:ΞwK @h@j  @ȕun+»Smcq9tLjH7_I𤑣4<^HܔJIUuϧ @% Л/y%@,M{ Mfu:s}O?>|hq77Px]GGj4@7ͣ\;eQUWuyF]zm#GP9rP+usk%x @̣@o] 0:.*s9]s ΣK?;Vùj5]Gn|\Mw↛ùͩUUf~޼?}\ijiWel7]12ҞN_`lCzy1!ǒ?E XD@W|VI dt2~kJ)ṭ;w3 @ LM='@}ӿke+/zYϗ4[)ygxRTUK0v @ 1z)Kq8?c +csZ[涗p2uvF @@Sz֮_}ě[7 @x[{Nʈ{/=fAwݟR`Wi9Rߡcgg>:wŒ?Z| @@Kz  @ @1J`/U܍ZMW;cidX`ûhcj @X=n @V_},!^ 訿\EWKE`tnU"@ @I=o @̢@&sSoWke.{ՎG] vЮ64Zi @ @ߞ[1 @$+*]zN]~_]]fѵ=Uڥ.ɬͺˁ @Ly? @ @Aݕ.Ϻ]~^Eev>4\ Sxen2dr @@m  @8@3v}vEpƗ-躿2߮+U+?٥| @P@A @$0.wڮκ[{Bm*]1T  @m+0&ˬVUwiX ~\aW9X̺xX̶}Y8 @@@l @@yqWe]Y1»F1pX] rp+ @ @zK @`:R<ۮލnHݺ3ˌrtw(O]jٕZfv8!2g{ @C@.  @,H=S»*sl}o;oC)KvG v,ȵ) @ Pz e' @,P,Ze-3#[gt׽7Uݥ&s]fy/ҭ @ @* [ @̗@1߮Xj97=Nx;]Rmr9 @ @͌ @h:ǂU\Ww$]gs׵c(: 7ݺ0 @h6^%@ Р@h9)Wܥn};JGRTuw]ef  @ @ @Xl";ʻ,ս-ښTq?γJ.Ÿ  @ @h@sΉbu|cqw^ @--0+fs*K]w##u_jRq׹s0:ii##@ @"ԁ^TO|"z;xK_~eq*n7u @YcRXû\u6}7Wzh9U D}efvTcLv~Y; @4@Szu|wW_c}ͽ3 @*Uwi⹼ECѕ»w)m;7 &@ @.ԁ-oys[}﬏ @IƗ-*b]yW 9+mMf+Zf>4ܤw @ @@-}瓾#^_G<}۶5 @f\ W.ϸݰ8S̢efuWH]1nTu]CExW:f|s @4@Kzٻ#.4ηǾh* @@ 9۸Ywi{m?Tۑe4 @̆@SzK.'\zi|k_yk^__ /$@hs%]v)wR'Wԕ) v9ˏ]sP+Z> @ Ј@Szo}0شac\+.vz%@ 0U.Wݝu.WEGVUw]vZ @ @ 4u'<1~'*VZ?ƍ_S @m%0&vrpdVZe⮺T#LGGb  @ @h@o\ @r@7+rx6wefz_}v#cnk˳rl'@ @& ks @'H-1G7Юh)[\^Qs]j]ss0JG<9W @ @- Лma'@U\]Ww[e抻Ty7tQ֘Epu |Ȭޣ @ @NG@w:z @`}Eѳ2k-3c]unK-3S].2l\ @1ތQ: @L VŬ"s<ʬr e>8T7Uݥ\uWq @ @   @-0n]~.=]40wsnH{#Z= @ b $@̿@#rYyݱ.xQ*ս9wnGA* IDAT @hG^;5 @fQ66f՞}]uڹHUfwyue;tj @ @@s k- @`莱 _SL+2Y0q# @ @* [; @ Dcr] ֦<.?֙uvS]FgxWj9^^ +r @ @5~[ @ r=)ϕev< @|؍*r ;T]zNA^i@y;X @ BLK!@C`|IWm2wuh(wS=eǻ*  @ @V.Z КM]Ef1ZyW;ACѵZqS][Ǫ @ @m" kL @`a I1קnmoMz_Q]fXup"UwG" @ @) N7 @NM8́ܭMm3TS̸̎Kyw<5}E @hN^s&@@G):"*slm_ zGǾX"Kݎ(x @ @[< 0rVyꞺ/ UZe抻|_jy`d&n9 @ @ZP@ׂjI @:+2SXWTܥjuf/4.Wݗ»{óssJ @ вM蝵blڸ)<me7 @je@##UJ#cssB @ MkƯ|9>/y|/<~umZkq @`Wtvw.ЬwkY̻K2=2Cw4 @ @+ԁ^gGGJ{^\Ğ{7 cctS1î끡iU{:R& /.sб9w91H @ :;s΍_+7>f~ @a+. }g΁^Oa]oʻꌻލ]:.s@+, @ @ 0-Y:./#q- @3K6S^?__L(?){EfW 劻cvsU.A @ p-mڰ1~ۿ48|+ L1)> ?x\'%t.b]zަ]f3{%@ @8&ޚku,>^{K[~T-]E @ @ M5/7~X Ѐf-}8 @ @XMyz;btttoܺ 0#9=wI=}3r^'!@ @ P:[( @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l fKy  @ @ @̀@o @ @ @l D}= It-e @ @ @\ .˗?zsrA @ @ @h@q6Ŏ;uzM|/ f @ @ @s.^MW_.Л  @ @ @̶@o @ @ @iNϷ @ @ @ml ;? @ @ @o%@ @ @ 0M]7ņ6DgGg#?p|g  @ @ @̺@z. @ @ @Q@7.M @ @ @d : @ @ @y#K @ @ @8@dBN @ @ @`z @ @ @N& ; @ @ @G<4 @ @ @ N& @ @ @Q@7.M @ @ @d : @ @ @y#K @ @ @8@dBN @ @ @`z @ @ @N& ; @ @ @G<4 @ @ @ N& @ @ @Q@7.M @ @ @d : @ @ @y#K @ @ @8@zgY/ٛ??z @ @ @B7q=w?~SOx|\WXSl$@ @ @ :[ti_16>^7)G6߲ @ @ @^ 6o}w-8_oE>mAô}Ǹ1Zζ~'@4< ߚss{fy  @I8OY px; :'>c٭lC G<G1{Μ}_s'İUViݿ*?gv>hlh,~?o޼_\yE,X ;|r8 Z;t8ㄓ}\sݯ{։5Z@״4p`bUcl?mlODMmM򫯌9NϘ93х>z;kgc߇];^ϻ 755/W\{M<~['{7sl{}7_{m(8c\7zKM]]Kt׽3|ذ}oǘѣŗ^ri<ԓڨs~k_~0o< ^ﵹM6(ghhh(~UWWS]x#?~]^>x|)'KhXKs6>xz3Θ1Wo92^j\qtv;:g3ƛonsU?{w]緿yzu  PR;6izײզ|mx̋k~?oԷOѣF7͝;FK[ٽ[c, Ы+F 刃-O?@o=O>T|zcǷz::\(x,vWɧ@m~׿5߄ƵÓiP ?O'/>HB~#O$K7Zz+r=E^^_'Ŀҍ-6ݼ] N?5b:ǟxKZUc''x[;#/Xg`C*n~۶,?bN|ώf/unXnc߽z=6N+H`oxC^.Ծq1$?ß./7X.n>#دӈ^ﵹ9FysC_ݻŸ֏'_zesܲAo ftA9 ",ݗ7ӗu~vjtTO6nbcݾ_ǖ'5nůF5*^?@ 6< &Oz|ac⢟XCi$:~xܱ;sScyXs5K.Ni8IoafξK>q]@WgHV@Aߜ#} /nJo=[oQ⩧A@{qE4 [=NN7[_n;}]zy;Ϲ_:zy-1Gl7믻nzk`8xc+z+jK״}})nFM3M3v[ȣ-Y'Vgn| qE/޵;s*ɯۇpp| ZvGĵ&T?.8ܸ޻_wy.oYMo|oO^tJA^~ov<~Zf}=~b\~eު#VC80?4{_G>2>a\ks:z83oX7>__86ԙ1sF/ uh1\R4's>{=w=&OwEHh Z{Vx{Y)}nzԑVw /~ !1K^{^|JtL׾V[-fyR@v'\+ݴ:;=S/Hv{m}]"\f2;!S^gqq[|c 7Dm^~S^[SzY--99~ސϛ7?F#ٓk];;zUUqƙ,wY&J3"3Kr0.)|g={7jjjzu XkZ׾wfE_:OEGN8X[o.ڗ{~^ 򗹶6bF7~/矋_[g{ҍoT?IFLWm O9ze?{:^y?_;_]n^y]~zCٽy?{uekehLn5}kV*z8c1-v*_]f\}Ϗ>;Υ;O8X~:*p^{R[^E_:O<{ǟveN9X,]V׹6K;;>3fJӲ>K4gnf V#W@]k/吭eCA^.)/;4Srmz&?vMy 6*7hwN譒Q%7|衸w׷zzMX u@,kZۡtu' 2q Hov yɹm8>Ԧ/7#}z/-xX {U[fIt~<(QCB.L$P;n˦c|]U;zyo|q9gy8XHlcn5ҍ oKUL> 3wNZ y-3~mqAǤRg{N^^]K5m7C//y2#>,K3,^~ECCCu3{W[IB3񉧞i/,ЫS_ @)>,z=b_V䧗>_S7`5uMbʹqK*@oxl?x!E}\'UW7<0ײz^,op8E[o{~v}ig ts ;T>iMG)h;wn{I;Tj8C/ߴ;6vWSMc=S([MYŒrz~sll_KgNTn1CRT&P~oֱ]4{=_fIS]+[Θ1qо[^ѫW牧0y~;qȑh٩mE~=G*U6vH57d}MaYctquv97LFV{{TC/oUq9>krhZ}E:9A[% rM<˾ˁgӗ+*jx1zh^^M J}I{ǟ|l[*Φo!E5vj]-v@ɧS;!% zex X-Cꋶ8ZO]>ĨdY^-bG}ig tj 5G~wA}FǷ 'rroJ|fӍMؓNl,e7rq~ǔǟHKʝ@]/\"Pm?/i {ٶT^7xURs`\q'Ź&TńJR/OlK=裱nI!83g U7KRC/[|=훖^ϯcFIKn_N nT{N.~ }1vs[{Qo4gz V=V퇂Yuy \+ꇓgK$m3gK}_޽{C{(.O#OC.n>o-ݺ Iȶqi'/ig>On> zem@״}oM479.m˵Zj=}q?4[SkJO+jm@:',"͆ny|(fOS[y-KX^׾=<\ctc&җ m^rqZn~7wUW]XB~>\}Q?cHջ|/~orB %ku={U8#{o}q=m4yjZwZzmu]bk6ӗdo*5Rsf.fwz&S}Î9h s 9 (t׾~K}8-^V-5~v/{n1kbC:_NGWJL{iZ*sqe*?zqK۞{Ui] \ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X@W'G @ @ @\ @ @ @X5kJ[S @ @ @h*^>O @ @ P2?AG @ @ гz== @ @ @@  @ @ @@o @ @ @e. +{ @ @ @=[@׳Ͽ @ @ @@O @ @ @l^>FO @ @ P2?AG @ @ гz== @ @ @@  @ @ @@o @ @ @e. +{ @ @ @=[@׳Ͽ @ @ @@O @ @ @l^>FO @ @ P2?AG @ @ гz== @ @ @@  @ @ @@o @ @ @e. +{ @ @ @=[@׳Ͽ @ @ @@O @ @ @l^>FO @ @ P2?AG @ @ гz== @ @ @@  @ @ @@o @ @ @e. +{ @ @ @=[@׳Ͽ @ @ @@Mi*> @ @ @'0NJ1 zܩ7` @ @ @JUYG @ @ @+ 뱧  @ @ @*A@W gI  @ @ @z@Ǟz'@ @`e 4 X%_t/GԹ+kK @e( +ÓK @t_a|-kjS^xGMO @ @/ [@ @! 9>{_@~/Ze2({c1CݻW寷1Mҟb1cb=~Heq!Ǘ$xoPEUė?_ @ @`=W @\`ƄM~ܠeK7 8S_*=ya℣=~w456;rqWƜC~ظ׏=w=.O㡇W @ @  @XhT}jN=_j466/?tJ}1bQkdlO`8p@Lg}dkw}wcaa#@ @ \ @(smO38V٧zp:&>1j͵Rp8cIq5׈{ ;N?Cns#@ @\G'@ @ %7G M<2./ҒĠ4./Y]]]|뭻n|cGR{]xgqYq;NiߏUG omĀ;#}|C; @ @'l4jݨkG}peت1x3NJǪoNs @\`kϯTxѣKqcƗ?樘`A⊫EUuU|_wmhlj3f%?,rWo~}ʰhjj*f|-q^vƮk'/4c @ @ bP 7g}9[W:#׊ƌ;~ ubz, @IohCy jۑ @Co>Eq|Vw0oƜYzGYƬ=8X.ǛOgv>]Ly8WcC_Vs\h~1gj^7tjSHE\p~{-3gΌ}f9 @@`;Ǭ-QK|2%N @ P~55E7oaZ=чz[Q Lusnv~zgώ?XҥehÚkqti/O_x!~xc7 @X1{ۯW<nxC @ `JkK3fCG}CCKr ?]ڑ'aY-6,>E3Gx\<䓋m\gbY-= @@ 4 X%_t/GԹe!@ @%>hhLOKh64yoZ4 i/.ÙY{z<c[?'ӧ7@I'*CW[nk\v7ܰGrh;f閛{T}d<+~Q^;}-,mwW^{mQ=o-Kn5:/%BNKY+ _߶COm뮻ng}6-ۑSi㏧_({%=W_m;iys&?Iz3E-Kdv[E5 yXW=ܴfLWVǝ<屢y,n~}MRX,:"@ @ @,5R_x'{}wGų)*U{9g>E`]Ko8ܜxXout&X9x8"~D !~vjL<樢-6i/-@NCƔnSYycm꫗bI @ @ PdX}/>wObS%7@ou׋:8 4դ.՚{,=KSC%<{8՛;SYtyYνZz?R̺.ٶ{[)9IѧO8p_ Ώﻷͼ,gz[ k]w}g~ǻbݚ\{qb~زyw}W &mT%9y @ @ Ѓ[Mcx[ɏM]z]wײuuTɋX_M|K_m.Ty]L9t{ lpɓ3N7ذdot6 @ @ @58ᨣ:3kv}9W,JxRN8.͟\,:Vh#@ @ @,\|]w%8 IDAT>Z6;pRȷq̝77xX̚=;zۻ|yz/NK^ug ? :XuĪRw|y_gxکQW06;fΚ® q'4![wx%->}z [e`0LӲy[@o|7Sǟ|2Wk 5㋟\1.ogsV;͔K~k;c-feY~y_rCeXc @ @ @r(B{.6keM#F~1S- CƎ>x K}ˡ^.I!_o|wEUuUK:n/+~>E6jQ1~E_x>2-M5sg&&Ы}~lf= c\~y\GO8<v̞3'xcF<{.yѼ Z,ͱ J @ @ Y`z'>UmC1t4LJC[wY#94Nvn| k7v:qvN S`kōMMޖv^~N-v\So1+{um4>,M5z⽩&Zif^>)$La]n-qvAi|oKСCn˯]ˍ1oݗbi_R @ @ @%  @ @ @ Pz^MH @ @ @dQj @ @ @@z7" @ @ @ JF! @ @ @T @ @ @J& + @ @ @^@WzS- @ @ @(@d"@ @ @ Pz^MH @ @ @dQj @ @ @@z7" @ @ @ JF! @ @ @T @ @ @J& + @ @ @^@WzS- @ @ @(@d"@ @ @ Pz^MH @ @ @dQj @ @ @@z7" @ @ @ JF! @ @ @T @ @ @J& + @ @ @^@WzS- @ @ @(@d"@ @ @ Pz^MH @ @ @dQj @ @ @@z7" @ @ @ JF! @ @ @T @ @ @J& + @ @ @^@WzS- @ @ @(@d"@ @ @ Pz^MH @ @ @dQj @ @ @@*>Ы]smoy 5U㟷^J @ @ @V@zӱkƅ\\#.qInJtH @ @ @@OxZ<  @ @ @2@q)\~xXP_^*Z] @ @ @ >"N>uoXw:1~ġGӧO_v- @ @ @Xzqf1oo%L @ @ @]Лx=jti*pΝ4P$@ @ @ 2*>Лtqg]~I @ @ @` T|wɧ'LuBi @I_M4S Q5; X Fʹ맍XWuz;DK7~=h'PEg~`cYqկNL @X1kqt# @qTZ{i#@ 4?f9.׺K/Pс^UuU|{otMco'&51^yՌ< @ 0ͣ~wd1G?5/̏L 7F)<+oήŠ=sY uDxSmzz%n9,Jиg{ϡoJ&Pс^W 7>nqw IC @x7(];qzCW @` 4ZKnq[4L&po^m>xpg319mC` TtקwZc}tƛw{rC0 @]ms~rQ_-~wӿ Qr @` ڣW7v_u/_=j3~90]adG\sZqs^{Uu}#P6?ΘtrsC6$;qȑc֬Ye# @X?$zFI˱͏^̊uv]맣Z6FGsҖft#Q3u.6X@WtXzp7Ou/*Vi/M_^}e-g @ @(zw}sQ;yV fF YQ9}nVAU @0^ vAsYw)˳__a] @ bz+Q @tku6/9vpg^[X>sVTͮ&G @J% +v @ڪ.վ[P5n7\.ϼ3ьz%b @ Pb^A5G @ 4MݐK]l[̺v^ux @ @` V# @(Kwx^ǭ#3YQwey2u @@[F @ GH^Z>3Ͼ ݩ'.YѬݛL @ t7 @j]:.xE>5ǔj5׾3fA^g#@ @V@o;* @"ԿM7$\jv]sp79xvʬ7!@ @L@dN"@ P \z<;"Ʀ @ @ z7n @n!аVKuxqXҙ3_]-n @ @zʙ6N @n!P»w9kס]SֿMurW5[  @ @@O6cQ5.nz> @ 4Y4.[?y)a]Qhv#C!@ @@ vƮ wA&@Hơ_ 2k4W. gԿȓ @ @`)E!>l<~[+ @+M-YwA8oLѲ|f4V s` @ *>Ы#']tA|tmz+BrX @7_o`s\/xM{uzRY3RWԿS ۃ @ j_+_u @#ԫy],EMxU ̻^46U  @X!\}k1'r8 @@W{-Z:3K?c7}k䴌s` @ @PO|tSEC}C1о}FCCctqu׾@ @ 4wQ7$ #tff>2#j^Tz{ @uzG#@ @,N@ @^UBTnQxWԯ-]ZBjACr0 @ @ͩ4 @@a]s7^Gz=23jXFM @T? IDAT@"ON @z@ދ!ş kuW=mA<+-9+9#js#'@ @/ Ыsh @n-аfܥxi^QnDw5]%4 WԿ @ @& a'p  @.Pޠxy 5 4tfyߕy? @ @ ޼g @,@S4}UZߐ<.-hsMx`O'@ @T@N @*]qhfT=}w)uS @ @2 ϓ  @^OadtffՏ uQyŬ3gEP  @ @\ @%_g`qPnjv]Ɏ! @ @@wudzjL @ T[»lf)fय़^\U<.x TApz @Fnt2  <lC.-̳:n3Z=<'&@ @z@gg$@,@w6:n5/_͈GgE-5' @ @@@!@(Gh^>sxC{w}rN fKg%4< @ @-__ @SjҙߥOMZwEp7]rM< @ @XoG#@xz:jv] f5xvʬW%@ @h/ sE @@>Q7y̺Ca盛oOj %  @ P?F@/ ^.=3gU @ @@*$" @@}}?ui]sݻbTj~H @ @ UIe @g 4IKgwi joP5!ͼK^ Zf,%%@ @t_^=FFT@>͵rF 4W jQBOn @ @P@Dv @,_5Z5x#v:`yx4Y: @ @|zs.! -E^F^;%վKr\jN}1L @ @= @`9 4^|fKմ/W!վk /c4M @ PIJ:[J@^mi] mW䜲 @ @<ʳwd @4޷9;3sǭy^WȌyQnp  @ z+ځ @ gէwuyg7xQ=Kp @ z+Kq  @Hnl(5nUuM)Kf3xw6 @ @@o1cb|=2rd2ո⚫_* @=Hi` j,v}mgOg#@ @ <*:ЫN<)ڸ;b6=wN|~QWW<ݴMt}^FNrݻw"ó @ @X;?y{EgudX @@ԏJrx(kڧSkӼ|^02]$@ @=15m:3«Z @ @@Y t@ﭛoxu8'(+d!@· YlЩT׮a/Z:sQ7eV  @ @P[z٣:6`1'^N̷ @VqXQ};\oҔWhs͌u  @ @XAŦoڍC? nָ;i34 ui.-8wM\0'. _Ksgzޕb @ @'PсހOs/ Z1S&3S~gˈ @@_oP R?v}Թv^N_e{Ua^\  @ Э*:gfM7/~s1|3{v\ǭ{>iG@SO]s`pDUUaVkHf3iZ=#adyzn}5b< @ @,P^O>yN*UqH5ϼ_?i(y]{W^^l @ @z@'uc&@+XadER 5޷Sj+bYQ=m  @ @(O^y"@-P?f@ D]Z>3x{uO4./Bg՜ @ @X^%] SjKhM? n? Qp^:3x-zq @ @X&2y2yM{5xi]i]!ޓsz @ @(@D!@UqDE3r߭ѿPkZϬyi~w0. @ @+\@@]]qhN}lkR YߕY; @ @JUw PwwU[o^L-Կ+& @ @,@oD@SEgYxYuQpK=]8FA @ P}UkC=tB{(xի^ " !BT\ X#("UI=4!J2!m]kOf9{|d9{wgg~^v @h3X6X>shNiv<$տkS @ @& 7j"@}/мΪm3rŨȁ^WqWe5Du}c7 @ @xL @`63xیֺꮍJfe3go-Ɩo @ @ F 09kJgv׽EU nKjyq`m @ @n& @oZFֵ-^{lzߵ;yvq4Ʒ @ @ 0`a UyQѸe7wwS»UkH @ @@ *|uJWi:l8&Zt^7s~^\2 @ @D@'tX:3kzt%-Q7c^=E @ @}@j/x[ W뱕7tW>h$@ @ @z>O%!в*8adQaёE)o߽$ڮ @ @@G @D[5QѰ}Jwm]wQDN"@ @ P#@Rh޵x{ט=0m݃iw2tA @ @ze?:@+[uhMy72-BRDƶ/-g<G @ @@@Ϟ@^e*E<1;k^chg/ݠq @ @'^?A @nd506Zh\iݬt%4W:#% @ @@op @`P %3oѰݘ-uk}fJ*&@ @ @z%8(D]uHu4mlmFG&#z|YՋM1$x]m} @ @'@>hQRFԿ{nI=0mCQw}:( @ @* [Z vMi;Uk{lQ7#xi^^FjaS_7 @ @ @z}F  @`e4m0<&%4 ;R.m$Ͼէw "Z[ @ @8hdhL]0qL7K樻?x]y|!; @ @T@W Pվ߭ݳ]ܥm3rx7>jSGU @ @Xeu+Ư7>ϏQ޿l'G+AeUq|Nc#z_sFi̇O@@+ @ @(cN25nⷷnu|eɇECCC @2[-ft48NJ݌|f5W @ @,jo~Kq0ƉN~eXAMSxmm6ǧտc߭  @ @ 0:>noQ|ub1GFkxuAuhu4M(f5aXVVn4oAsI?& @ @ PY1ntkg̨Q@˘!QjT,i\YEm^}h& @ @ @*"zq?6}="ZO3Rca> UC^ @ @ >[c5b!E_~x& 2x;Mhr}7s^1 @ @ @``>;#h(CֺTn|f uVыi] f,2& @ @ @:uΚvZ455u /(=?:z@$В5m6*Iގed]oyvI[Ym^u_C @ @Gq%@@% 4JZ>sT4l7]uU%3-md}#@ @ @@ v4/ 4mZgߍ-Fimm   @ @ @@d4i]Tn4/׿[gMm^I^Ӌ @ @ @@ t4#кjm4nmǤoLңsĐG =DO  @ @t^ Pv-V). yQ73-9>Wv @ @XA ByJyaѸy^>s\ZFsdlY)S @ @ @z8*D@E 4m:<KwLRnIsS7+տ{J!t @ @ г @ZW|\nlݿ»){pATkiW @ @ @@ mĴh=-K%4G{󌻺9K?i̪EM%'#@ @ @  Pkw]QnQ&ϺKhάO3#Z˼ӚO @ @* Wn#@6J],ٴj=SВ»4.-Y;>j,,.k? @ @ @o (a\ffiݎel/wY%%!M#@ @ @ri3}":67o[>3kg/-YP> _J @ @=;VyѸͨ4nlstw)͏h\:N @ @$  x%@rͻƉm8gRXWwय़>]- @ @ @@w}Fw]ZBe*=Y).-Y;k~<2- @ @Z@Wç -컝s SܒwsW/g"@ @ @i! nL4?{{>}WxG4$@ @ @e' +!`SyaѰشhڨw-k rE @ @*A@W *PiˆX㸴hYw_}7#տ{vI* @ @ p\qQ컆Em}7$«9?@ @ @C@ʶA@QCq"knt:sտެjlH @ @@oPN_浇w))տx9Rh^@[#@ @ @%, +4*дɈ]׼F/R%4O3˵M @ @\@6@jaۼ|wjztz^C}7mތE @ @@oŜea%4ӫKS̻eTni3? @ @ @5 ^#,kޥ%4RjZPfVnI @ @# g[!P6M O޲wk "kB<]  @ @ @@Y r4h9K5ZWmrfUM @ @ @zl*FuXͲwic{WͳK3Gb*f @ @(K^YFX1ՇҝǤwiԿ{eR7s}w @ @ @&PKq_ﹻl@ 4mߥ3Oz6-6!@ @ @t IDAT }ǻw'lF_&]|P 4NLvKwzjaS!i*%@ @ @ Pǜ'ɇ~@A܇;gGi):4ZWYVn4/xjOo}Z45u @ @T@zp& *cXCu*w<-k͖S),īHKjz @ @ @% Ыԛ2hyㆍK7r#1#R:etu3^}We[&@ @ @^@hy/>h^w՘wvj][_1gs%4kk  @ @ @Rh۠?mhZgXl=lxq~<ߴ X *%@ @ @ JDbu1E%B @ @ @B뮳nTDskK7߽w mx]FZCjqsT]Ui"@ @ @@zzt @ @ @z?:H @ @ Pr=m'@ @ @x^ @ @ @@GO  @ @ @*^@WC @ @ @, +v @ @ @U  @ @ @@9 y @ @ @z?:H @ @ Pr=m'@ @ @x^ @ @ @@GO  @ @ @*^@WC @ @ @, +v @ @ @U  @ @ @@9 y @ @ @z?:H @ @ Pr=m'@ @ @x^ @ @ @@GO  @ @ @*^@WC @ @ @, +v @ @ @U  @ @ @@9 y @ @ @z?:H @ @ Pr=m'@ @ @x^ @ @ @@GO  @ @ @*^@WC @ @ @,Pޚk_b͍K2zrm'@ @ @ !P17tclv>G{t457f @ @ @e/Pցވ#맜zp41 f~G @ @ @u7aMb=?SN:c$K̙;`t  @ @ @@Yz[oU|CO}?q-7 @ @ @&lI|1xG0c$ot3 @ @ @!PցވSψL:49}qUWă=d#/E @ @ @ xoK:Rx7;~u N;Kp9hniyY=޽{IL1Fpïׁ jA=:Okp|p>F(gzƎblϹ%_=+x>0o @8vM P1:A8~M Pe?:@ :>0_>GpK]M@% 8~V})ٗJpԑ/Zԏ|?*HQ j]!@?+l@u~p7j"@;+h0u~( u/1 @ @ @Q@| @ @ @@C6 @ @ @Fk%fQG8%y}bw'|X,xŎlG~({…>zqy1v̘Wm{}w|Chin)~qϽk~p],mX|cGѣG7N=3~y tp  ݏi78b cؓN{{DMmM?ŵ?A.8=>.{vitH92f̚]vi,Z MMM߷ijg\wcGӝqulr4{ 2c'?o+s]b=.M;6狱s?\qy<]6.>Žc_e>o{csmqFsssWMǕ^=\O>Ծ1]ԓig2|:^ϵ{~<_ht7i1'c 7?ga^~c'޻Q>,~7o[ƛ[wt>cw8[3fuTݿ|W?q*ﵿjޟlqb$Ѷ)G }۱oѢEŽW:/m"+u}Y x4?,yLȝvYzx|@2yRGڪũ'NM7/J78>x'w)Zkj:lՎ@49O7wf~˯r`7j䨸+/@oqSN3yvyrN|᳟%KkuDq%N;X㏍v/OunX7;-l3ftAS'~sD)n5˅7YJ秿M]|ws-oM74=PvIS?1:=zcs7sS)zȐ!CmiL=}Z}q}s|ozC A"zWa9O?vkժ83G_cm'~):Ў>5~1W]E7ntN6`øKzdM%zv'lI|{Ɠ<-}뮳n|Kҟ;N:tk5oxW]`;. +Ҿ7I{aqS"?q7턩\o9s %{ߒnfl^rQ~3{;řfsC'^t^mswuv~p vr7nq3Y0uL ަ*KcZc*R܌fgp{s<8Wr\q-[eǝ-ޓ~d|;_]{㰯u;{Jߊ=(6NOCc᎓MKѱz8ڽu_}K7a\b]ݐ:N]f\OBN~@/g>blC^A_]l"_OJ}>~y } v=طk48O ;;3{yίNucӊi;^\.b%& +ќ7 ?8ctzFV[lKKu~uIymMmZgʹ*/^$VOGƤٳi{.|O%?GWU]pr|z%[se_iN?̒L^|eEo|q^i~ZCwL~˝,@w n_?/L&nM1}}?4kyv,Y$vNJ@)lϝ%>8CbE˻(8eO~e7/:^!o]x~|瞿--Y{(-d GW,}]%~-?ka0|Wvn@Y ,ڽs/wQZxq::8u%-/Ro{]'|R%;z C i6Ҳ7׹tǤ( [/ުJKǾK689ҴO_E߾ >=6O,Ċv/FkN@7p\yvI~ /^![ 5EA^.)/;t7k[P3O'_}ݵ]"3GNz[|&z|@oLGܜ_#MFQckFK: `De""Ǵ]ק2~<6tX=ttQK姱Ŭ@oq)'.[r|4vuQ;fvQPn9=]'?GN96@o,urF!/J+S3 ?ߩ_g?.^o>=΍J7Vv-1]>N3)PZa)'W6zi|L΁_~526$mivɯsSKnND^T!nX=-y!]!~vq_KK]r楈OKn˥51o-3E'+R3 򮧗^~hSO~o~]0)ώI*͛5ySYjm5A>qJ Mt^ڽ v&M7^QP %Khk :/cy7F03f*½$/q{ߝoEKs -LKݝQ13}wqT~QqR/K$+T{ :7y33_8租?ظ ˳L:zy̐}zvY˿{aS<u`>;yXpaY31./iI .9p/#ˬ@rE^uxGL;^;:^X Ҋz|h477_ِȞy߿_x#K0qҴ2~{ iV%.2ۗ[scJ]ueǯ1u7/oqLǼίmnzklW>fۛT~#qWژ2s*+R+]O֍f/W#˯-#?qwğ.o4{X>'r88-C4a H[@Z -Zn^J7-6Ls\t70!ħSM|X#zWn,H/Cl^-}^q/ӝ񎷾-N9bvz座j7xcZvw_?o7KV]uTWh C&utuQW[W,yguL^._Ruq]~yvsE r8zirݻ2FMmСCSXvkgڭ7LƯ^|+ŤTC/]O:9jV˯|C'z_;bRG"^^.)ڶz9Hz)񎷽-256@\wT&P+}E=s/ͯ||?MlV]nLuiYwZoKnv~C{sSN_bo@w@M"Pz=h{9ߌQ/4u-]2d ~hY١}xWCx^ڽLw4^N +U O0Xvg.n&+=wأ^[o3\gѝ_MJ7nmSW{~xǴ;˲guZ3/K|+譎Tiz#Gg .JPOK͊_xcWsQ^4/PziJ>CoEje Czx=zw?#NL;nVŴBĩg;>2Z;'Xcb ˮ"~|Qv!uCW^suZnnұ}@ ?ۜkzvq#ڽ+]/}K3JK]*L};6XbՆn9=${Kѱl[ԸKe.|bfG_Mk}<'=1C![!0_Ow>}㟈δ{uZY,r?uߏUS |[Ă$u?cƎMe.~,w^y\ƽR{/ @ @ @@G @ @ @ @ @ @JX@Wƒi @ @ @z @ @ @%, +n3IDAT4 @ @ @= @ @ @h @ @ @} @ @ @@ Jxp4 @ @ @@>@ @ @ @z%<8F @ @ @@g @ @ @ PM#@ @ @ г @ @ @(a^  @ @ @ @ @ @@G @ @ @ @ @ @JX@Wƒi @ @ @z @ @ @%, +4 @ @ @= @ @ @Zӫ۩i @ @ @@@oP @ @ @-@bgIENDB`hishel-0.1.2/docs/static/flask_without_cache.png000066400000000000000000003146031477404575600217530ustar00rootroot00000000000000PNG  IHDRVUKsRGBsBIT|d IDATx^xe3Zh HEPDquT,(" "RkkwA,**(":;P' $0 d&}W$~g{TK!      @X uf@@@@@@z      ^/            a,@Ƌí!  @I1}Nc=7fOLϑ;%rONGڴ\HNH[6w۶]s[    +~   jޛ[IA7y:IsTrrܵK~GgC-Z;ww?O~=9)Y*9'ݏ?Sؾ]VNܰF/Ws>)p9ǟ~w{9ҸaCysrO;ٳeƬ_K}ЦU+s^*  ^/7   uH]{ҔuYB=t k4kT}!ל@i!Vd`{oɼo)?J&{oOy ҂wSjűݥABAtm/p5KLSK9`i䂋d޽򭆧l   @8 ovd\y]o@d  *clgq&}{5Z$q vI!m77_}_gN~UWۂ?P?H7Y֭SGvc`;u3<4M^_*lBZri@,ZXJ{q2`sN{߻wfi0+m|:?ryKNf /?P]ȣ˯Xw}bCR׷ޛh oKRj~_=ó#   P[ݻ S"  @d@xx6~nxvq[x_ƽO Z˕>T9,9ؐ)mٸiS#LܽTǭʔ.3z?mB')ݎ*^p<8lh'<6flM*v:FR9vd]ݮA[y7'T>yi2a; = lhh$uSOy$3;+ggG@@YHRV6ndo['~#;(N\w,{羐\'ݔC;y;J@r36ɦ˞5wc駉gѪ>gg$i*8M%;eٲyRS&2q  T%IuJHu{|7g6;CmwuOCʳɒeK>t$'C8ζNꫠMiuo`OqY}ٽ\iŋ+?޳{jX}@矵׹ƞifLN>GSv-ue֭2w;xy7lu_#?DVY%6(vB@@ L}4.1Rّ$}TvB}>߃Iޞ6xi|zY6qvK+;SVMz\rz+[f/9;7K|TipuT A   Pv>F\JH)Α͞`۾Uk&lkGږf3^-=ZVշȏ9iӪ&ytuxCg4v7C\EyV3oVnYZfZv-?O=ˋಙ{[-[+of/~JLuٜz]vɣGJ5^ {˒>P,Z(Yf2S:ܳϖ װR[q  *Zr؅dɛwKAnjM81ZYC6c7llOY6"^'\.ZufmY!~`p=w/IH<8mٻnݯ^ gIAٹ7I9b+RR~E?{8׬/Mκ]b&uIcJ7eȞUIߐ?+5*ov@KSn>osp}.cZ@,j  UJ2=8x缹2UC;V oè_gҼrsϕô= ny`nvJmyb(6jlشQO=>态ޫϿdϵcN^'(BUeuzp߰ACoQz"w_F }TzYپ#1 RSg_~Ic  D@q#z,k(ZMeŇCϵ;.58[0i{Gm8v#d eY@i}ÓlU#mn?YmPf6ݥqOGhh+M+y{H+=ﶹZmKj˴2nDZ^9Lg$5l_X],{obAJNА&rlub3Ȇ^WnJ Z]7VY=_,|f vHg\lSBJ   @ TT}C6wAC Xǎ%gΐt_}{_6ߏ?_Q-7K 6i,W\yerg˭U{67'-S~=[fU>6Jy][$'j\^=~@л llmGz?NՏf /wmGda2>G}gʼi4N*MxCV& b'@@()ku(:Kd<;'G * OAK[ _r26hU+M+3NVO+[WY#n/+sVf}ms<->Qx?DL| l%]۞ixvgHd?ѩ=s'j M%Icl^:<*p|~-7JV,6:F[EhoS޲\-krPfh=4<UFK6O<|Ú".)kTL.K3q=%?   $}Z{!5]1h+Zs>`gO__ iۺmW[g<r"U{)uerA߷qUVt}ǔ@]Wc;e9z]"W\rl۾M榥ix |X 3J'JVyzoرCNCgӶɕ>, ҪgoeruKjլ_gʤ/ @@CUInrV5ߎ}ߐ,ɍIbfwb֨z5i+k ;Q[g6&dz/$͔Yfox85Hsh{z~$>s_昦ܵgyvT7hI&yv̻3a]] DM)鳧ȶY%_@nndo f%@?KΎ@ak%&P򊡲5, H&'Q֥oEk=Aي Z6l*Gh#̗ME%i  Dŕm|j-(샠/RN;~IgM .M׭ zvD@@(.Ыެ4=nYrI>}GlV!X { n\"Aklstٳf~PN_+~aa*R]wH~&,-;{>k:Ga+Lh-ժ LE^=[cZ9)O^Pg6zޭ \ïzL6kZggsʲ}[럒߼p@`CvʼUK) L%_ 7 v}.$ޣ5?蛽JgAnf@E/kׯ?az3'_y>ds/7%cv`L ٩Uu;Tpv1-{N;diѼT^][쿞^xLl7^L;G@"Y C>GHN?[ԾSjűݥABAot?v@@@ Zg¼g->wտhL Ζywfk}ˑ՟_c-s ji݊O׹S["Wՠ2NZ }~ӻ635~m˙m'pt8Eĩ;:ZCtqh rs z[I6=ց^6m_toO8áz 4ܵkW#8Tp}9O2{{v@Ik?ۺ-]3Z8"eO@(ONeͭV\IN&Gn{הwk*Y'̆'9F@@ 0^RV6LRnҙٺܶ4*b>Ra;Y,^V~6LM1m1O\j^ۤWMκM~=k序SV~2\im5 ;Guo=k+PmgV}hW!OeݒV 2zθ1wY[V+,.ӠKN]gdT+޵zx{QYK}zͤÚ55kAUo˯s:(2յϔO>~7_)b0aZW!-N 襧xYtx8rƩuW]mt$u\e6^Y8@;)Fi,Ԗ#:s$nNIqĮS֦-FrBJsM޲Q߸F۶J=+  @)C[h\̷F:6t\m lv\@o_8_ ΒZlyϳϕ[xM=3vf__N{H\\ǻ/ԜιҭRNʒ+V'>ӁǕBo>uZVS/U/XP|l'CӤqF_ ˖/SYa&]}U5>>NY.o=2pԪ%S~%Nyq&1B`_CCIculcNk׭;U/\pP_h7=L'$k駜"efHѿ-rXK5kӺ]R^z(OO>H~m뙵ۣۚuke`;I9g%M4|u]k[0p;n5k/xW_ԫSWgϷ š{jɲe2Q߫1#F%|     @ \t۴"b6GW-+rjQȴxc虰ne,&´?(ր;Z۱c 6Pc.^Հ`̸2|ٻ>p?)/g}ŗȅ:ϴq`$kV%F۷w ڟm..M^]R_&\gM)u\dž<"͚4i+WSuxx,\DizŗkOxU&,%KʑG`Q:ֹzp&4ۜwz1ʳ/f=^-Yۖ!jZ֭[vSCVhr2@W@@@@@]@ ]g¤=ֶ@4aϽ *L6AB!j{ZgQ &iYn]1-=MSϴUYJg6;ҙvϼ Lhb8f3sk63F?5VÖ۟hXo{Ͳz^?:&>srdggY|~ٶm':mF4O&&_|~kwo_z0O     )6(:;nw^cnj֚suI>(}?G R&sC/-a`O?J w/SO"@Ϝz&+6,Se6˺tM9Twlg;ЄZm_ʧڶl%z_ dmSMw-D5l(?:?g˯ѭ7u=sמYbcclR43}qt>[+2幗^3 nuwwh]{}&86zlڼIZ<\et_VО^hU]3FܨmohfӭYFѵ ֵ{jq:?f+ox>=ojZX=@/5E.|     %P1Mb;3CoM5u&XͱwVmڵ̬͐LrmՒaQ/|!Ռ~w=suߍ:l| "nظA9OZ6o.{U1A.9c'smQ]1a/37.'8y)pݒZm9)噭M5JrR۸q$$&PS%YϙWۓUNf7nlg քh.1nÄy/JLS?gCG*mZM`[oJ4[i`=sޢU~L n5l gf!     @ TxgȎjmuزy dm7_@η5lPn[5ia^LgMg1aVrz\wKF eEB܇ ?4Tk̶4-6gd[4aiH[hj0dB/~-.? ӮZ {D/],jEߘ#l%%%ʵW^#QC8=O &k;R}>;7mX.<|[VF RM[6˴ΰي$/2Eg-k mh*/`4ozt;Q+NjϦRZ4Lܶ=CbL[[9X0jN?C6n[V^%_izW ,w?@>-_w|٧P<     Hz\*@GE "[@@@@@@b*yUdeeI˗/Ș#d Uvx0@@@@@@@e+VmW3Δ٫o!= =JȈU@@@@@@@Dzwg?=wLgy텗enZkZv-}~]0 @@@@@@ ".л䂋m6gWn?̑i>УCe{{*      @@puW]# RSHNnN+2i/2ߪT      Q%15W\%SR oHe)!>A5m*˽g (ZH@@@@@@j DD׮M[ёI̘'drGȃÆݻT      Q%^{nR* oX/F:7˯ujKtydђQ<,      @@d      X@7@@@@@@0 @@@@@@ @@@@@@@ xq5@@@@@@x@@@@@@c0^n @@@@@@=@@@@@@X@/[C@@@@@@@w@@@@@@0 @@@@@@ @@@@@@@ xq5@@@@@@x@@@@@@c0^n @@@@@@=@@@@@@X@/[C@@@@@@@w@@@@@@0 @@@@@@ @@@@@@@ xq5@@@@@@x@@@@@@c0^n @@@@@@=@@@@@@X@/[C@@@@@@@w@@@@@p9nbŹ-G")eF@/%A@@@@@ \Mew߶j俫$i0KnB%@*I΃     @E8dKɩ84K{W+׾qR"k Pz͵@@@@@Vt'ň;YM=wSw,>K{gIA k&Zc*@WUWB@@@@;QD l 4p+n! Lpg~u^@đ/s'ȰƗIj\Mql͐H̦\ @Rp#     !phU$|V7SgTٯXghȑW l󕯁Kn>y֟5o>7|se^T.iVbgI\ ^ZO@@@@lg*< e֔* |sA\ѶiYm&`17tnpwiU}(߱&+hd+cdK%f}fH@ <c @@@@@ |8l8 J_e`[̋3)BTE*TøJv+m #@U ;E@@@@Cm4_M5[fΊg>0/$g-*MJy7[1*MdžT*<    DSR6Ӟ2` \p2$34sgrҩ-+ߢR2 .$kI@J Ua@@@@@ bR`8S |HH筜3a]`5,bsy50.T78ﱡ @U Ы*+s     @ u>~UpUqpZ 1P.ѳo6OemeL+JL8Ty[Tz[RLe>p73?7oB:Å!T ."iۺ<ɑڊsQ}SOYn|    !~p*.yf9_\8۞Rt\H6 N>TRfNq.pﻷ=e(s  @ DLwWIymЮQ<=z9 !oN|[._!    @ ~4a qp2΋ f[S Tę̋r5< D@Dzڴ믽N > ɗS.g[p:deAyN@@@@Rlq[`Ir׮o&3!\1⴪.T[a L gam]Yd-G[Rݡ%΃ J ">={InݤU߰^)uԑ[oEZ6o!ӷoMUkV @@@@h+Lk}ff p&t+R8/i#gUtf?g) ɚp@4*M #   %* 6x\f"   IzZ+  0WAxq'Ŋs[8w啋iMY$t+2_޲եeYnV8ovrեs   T}  ,ӽ g.x&c.^%/G@@@@t-7$̙#i ҤQ-}G *"-F@@Uˆxv.^s♋We֙A@@@@8AO3~V\ @RWd A5m*WGzh@?g+'G@YF r\ê)b6d\tIjvm-5g\    Pl  `\<>%nNKg.     @X ps  PVWD5!W7U{$wm9c8w    @ U5B@p'v9'ճUy͙s&s{ 8?    @9*D@\Wrzݷ92u.Q7@@@@^19 T@~kףdPQxݸ޹xIn+!    PNzi@@ D9䜒*)s\m =9gD@@@0Xn@Jp'\<❔*-wْ#qs3@@@@ U%@L.u${}W:os"sEk@@@@ U@@ mt.މ%1xq 45]ҙruN    zB U\ūW++HU\C@@@@d jT.-uRjːdž(~}ӳ4ovoK7'#W͚# @ \< N/-mΖ2$aV]7[C@@@@""KLLG=$͟':t, -L*$y`CrU"WB@r+9=t.^u.^sd'@@@@@/!!Ajժ%)5kMjԨ!O|\O\v~XGdҨ\P@pkWSrדS%nxi5s<6@@@@(V "=ߝmݺHצU+_7?\}d2헟Yr@$W$mYOrNm 5wn-5WI+e@@@@"Q )W\| 3ov:Ezp D;9#UMY:Rx8    @% DtVrx'2/- J|4 @t عx'Jn;wI\NmU.@@@@ DtWzuyzXs@˵4c7'-K/1C@sb}99;%~V:sxU@@@@@Dza=ixLt98ۂs!*(!B@W#3d*s?iKtq6@@@@@DzYO퐘\.ٸy 1\֩#|lB6o7ޚ ֬gB@ t.^wOj었٘%swH @@@@<}  ب/-s)NxlI]sR@@@@G@/  &\]QQ YX!tB|7B@@@@s ;7?F@#Rx+(bawxv5Ϧ.6yd @@@@r(@C0NG@۔5u+Qx Q\@@@@ ۩g $`Vŷ)M]:EǙx1 ]lsNb@@@@@   f.^%.ʩe⭦.^(9@@@@)@M(NC_.^yS@]<}9    #@W0@x-5Ro=f<_!    /,2@/6+l]mORs%urV@@@@@ 'z9\@Hhv.^tu%)Ԭ ͬeOA!    wy@@@@7|F y-TD]KIM[uX.^^O#    @ :) "@JiSiu񊇸o bx9 @@@@r_@/Mi@ RઋW=]]ݦ.^Yg<@@@@@|a '^|Jhq$' .< @@@@ )g w $.v߶OB#Mgk#5(z    @@/X ?)ewi9.^j uL򀂶ϟp@@@@@ x= @@jhOKZ+6m%o    ~)@Π@HhYZqWPbtu'39'     @:D] Nqŵ7u̶#)Uf% B.?$@@@@^E j::~z#x@JBk{.^`w7q.yش@@@@&sfkΝI@ 8u.!^%%W)~)hxk̊1!    A'O uA&@CZxWVTbJ0R3FaRCn     x}7w,u*=H/ o 8u.7ů<x6 ]F]<ߚqF    %Ձ^@`:w_+VV(5mT;wѐQ:p{q_r߶/p"#i[j6uSF    !Ձ~^_. @ l]J.شxk*duxp@@@@@@/,4LժV͛C ТU+sˈv@@.^%%6*pEmx<(     @^xuWpM4Y3V亵jָ=EGFȑ#yE _ $5u񮨨6eHumK+    ䷀WzwmwTR{W3 3M]. #dﷃf      ^y2}D<] gB<[B!wwW]i.O#C@@@@ щeX @v.(x ;`♚x& ]?;p     @ !.M#(TūʸlӌI Lxx8w @@@@S@?Q# m4WpV㥆G l]m]X?Sa     wy@]    3D/Hd]QWQ LWniWE.%D@@@@rK@/$i8KԢ.^{x)Bݭ8uj<6g:!    x G%.Cо/*F絓K@@@@@\ ELBHl|.^tu'NlAe     ل3\Ex鏐'VQ/';"    ^$@EEW@{RxⅧ娳Q{&"    (@W|O Ҵ/fQںx&3BWSf!    y+@~ $ Zr6hBN ~3u9@@@@@8Q{䪅/}xJP-5v     .@w~"Z"@RZ]JJR}~J<'[C]!9@@@@@=uzsZᯬ )eÜ4!fGȟv+M_!},L     WzQ{;*b(t/7gQ!Pk Mʐ8=K.acx6@@ ^lzZ/aZfQݕ[ Y TQW StBVQ稕Y^  rSl4joE'w+?|ڶ}~ȳ^(PDamn})=$NZ  55xlSM@Y^>ab=t@7JhSkǑ6i?|c@lSDUծ&<2xZ \}Lh 4Fu+HKK={JWKe`@_nZyF:p@JM @?\9>'F(p /AQGx䮀WzŋӴ O~}L=V^{EmܘTZe@ O̘BRRN.orIj@/g p+U$w97G X g_ @FC-|r,`9&.@" ׄw/-8E SrJ jy}ȉ@barէ\2>M|iN\0 pvuWSU{NN:F ?ȏ'@`mnw)?&Ėg ˅~+{N=?G># #Aߏ>+c`gժQS{ŗ_Ar@ thS_}TPNֲX=nw ^G V~o]"\CCQ=F}׬@ ~c2.@|%QWuӁ8|HVg!{Yq 'lģ@ xzg;U>1g;.Cދg) 26@;x/y x?@c<  G@rU\1@,и@ <<\'Lg TP@ ~TX1u}a/acF{UZu=ֵ"F鵷K>y3E@9|X_xgZj{Ux '&hkMTi۸ ཝ+/i9zal%%%5wIm8kT;{sukz}/?;?_V{<䔴q @ /#ozĠ /YRr@WG7׀~@V:YIIޥvܡ>7n|0}@cǏOu2}{kXX}5|(8x6/QK(y[9%]~XO}W^믾V?^=Uȹ@~*Tȼ okԢiswWvu^mߵCc^ @[ u{bwV1xf̙6l೑mw/G'_|nիUӐ{pǟԳ뮺Z WI/Zq#F}8W`w@8t o=t, >r, D%~{&Rխz ˎ j||~UXQGCqF`϶m7{IMn4)U_u9ڹm{oSӜxD k¬ g[nu^pWmشQZ틭kM8bX'г[wŅg^EB H~+WG}~~ s>(a/_^111f%U{əVx/Ǖ# {QFYk6mБ#Gᾏr{ɏ?g#} QY=| -54+]+ދ;ww3NEwӜqm۷k@߻*c=50b''c:yO|r3j٪ݻq5oLWG?I@/ y)PB=Hw0NF=!qFZmԧ?2z?VU}v'}g荋w?|OkͶsVۋ/ӳu>Hg8> 0Ȅjv]Z2zdlmٶ 8sMdڞe˔Խf.\rRg _6XHdZ>[nF[?%?K(lik*EAz>2]z?r V{I G IDAT> OSދ*O>dʮϸ: d vӧ9^N'p@ZiB _hʕ*;;؝vP&3y-S)/ҷ6 fX'xMSΘE5= \bg_}il+ڵW+>c)kVu iމ6]}Ы~'gn{\ή+V%жۜG?8ihCkS/%9XrzyzK~ 1#3ui6zgp ~ʹzA @x{2ͮѵ>1ɗZgx.% A0g+:lSn^yuErΠ>5adݻe<wsÇ蠩:\=RRRc.;NW l9;v-[t̬ 2Fݥ92}>?/UKtd|{v~䗟{1L-6\og#S@2N "h OjӖͲ؈QNyLknM2YM7ܠhOz6đc֛))z~<3܂ +9@vIeʽ&}(88]AWǻQJT޽|{58xPmlZg߾*|~+55UM-^ʕu!ګڰy)cIgnlylI !y*Gj]Y}y.fvJl]u+]-Y llW[g Tދ1q@ Or(gދ"ƎVTFusײ+ֻ:L(O@\s)yMoqJ⤤$o;czf4|~ڸQ7b R}1{뮾V\yBCBG䒓@^xgV;a =x d3T@@@@@@ 9      ~$@GP@@@@@@O@#     M6CE@@@@@>=3z      Gz~4 @@@@@@o1       d3T@@@@@@ 9      ~$@GP@@@@@@O@#     M6CE@@@@@>=3z      Gz~4 @@@@@@o1       d3T@@@@@@ 9      ~$@GP@@@@@@O@#     M6CE@@@@@>=3z      Gz~4 @@@@@@܁^9@@@@@@ f      )@F@@@@@@D@O&a"     xwF@@@@@=?h      z9o@@@@@@Od&      w yk@@@@@@? f      )@F@@@@@@D@O&a"     xwF@@@@@=?h      z9o@@@@@@Od&      w yk@@@@@@? f      )@F@@@@@@D@O&a"     xwF@@@@@=?h      z9o@@@@@@Od&      w yk@@@@@@? f      )@F@@@@@@D@O&a"     xwF@@@@@=?h      z9o@@@@@@Od&      w iR+;N@@@@@@]|w       yu@@@@@@ 9f      ^,@œG@@@@@@|_@"  K(]y%6,ҡN[*tZK)~O]JYU*4aWEҋֽ{CZC@@8kB@@SUWU2|`t۬?br 1hjTaKu=~zyfRڸi>@]xeRURY>}AqԪQC~D#+Vʕ5WrUj>/okŊU{S {WVpl~ӏݞ7stc]r,YR J.~yz>s5mXEg=;ui6f&=p_Gխ]Uxm5υWQZuδO?-[\rD+[|6t1fXY/QjTnN7fH=5s֬Z_w#-73k׆9]Sݯ tj"ښsJJW׹՛ᆪ7m6[b^7]|1J=T&kcShѷO>2hI=1vj39|*aVMyztߝw+%5_횵ԿWo߬Evܗ橨FtԐazz,m۾wK~}hRfգoOj)zN7Ow_f@@@+r4  #ZL]l2<իT8[Nڰ+Vmk .VT4~MF[VmY)&>S5~1aIӦm8?vSo19|}`s9rQHQ3O4<_Ͻ0׬wm7A}ӿ&t]]Uwv̉   z^9mt@@HYT1#丨_`P~+[ʘy/-4z>:Ubz=a=̳7Z]cVjy Ç5j8';y물{kSB.bP8ﶡaaaz}מ]''} v|lG&[e<샿d:hv>Uemg5[u-*3MJL:%cg{dVSz#ƎQ􉠯=ju^Lz&0'a}葧vuv)y!!ΪA/c\5g蝩l#s"  xWNF@@{# 35j':UtZ)fwWj)Kv{c'8l~I+ 7ЬlVֽ0sYcNSS&(}cW1Zj;/88ȩqFWo$c@O?ɴ>IeKzO5~t~5mS3 \6Л4m=oZBByszf[?zvެk-+VgUxov=fnv@@@;s5  ^#[n)\Ĭnsf+rZ5kD##LDM1As^ uwy/:5nz2IU*Wt\zlBom /Rװ1#npf\\S֨[O'M1J_~5W\fM:5F0?_|+i&+iҏf#1T^yWeSLۍ?0*?ܼlM>HM5V4s8\oush֜zK~Y5g֞5eVXf6Nw߫+fgu G 05nU+olz z1 3QSqԽWO'gRCLjx@@CQJLNƝi\Gf^TH1ߞEQi4 @@ BJ_[xxI_e=%Jj26lzvk~yVz՗aS.isb:t Z*z*[sgUPkR }-eV3=mYٝwTk*et_R[[}MRޥBZ^{UNl~ӏn@?%]"E _ߟq3zfރ;^:M((*THy{5'g=r:  $.#v9#d<Ѯ{zG jӊ'Zֽi=[n5R[H@@@? 6;p8Fk d Jj*j b9#Tzvkr=rTmڨϾ]?GsƖ60&u^{[eq7X:_}('=y~On x{j*lJNUIkH jNwbTA۱]Mvnzml['@o ?D@@,P/lɿj㺓Vu)=aüĄ,-3m۱}CUf-LswϡSp;n_{Vk^f :>s*n};h"kFK փݯuj+P>2?/央]r[ɬdKц4my^-Ztp'h4Etukc7dF߳'{oۇ_b ^G@@@@@ts+'zu&[7TDԨő3@ѣzC͛4UG7+yŘEβC*ky  7l̆a6xJ_lh_-jͺ(UXI5N{㝷̶:WZMCPBf]fEV^]ea&!AZjUL' GdkvǞs@Ϯ>{iN64Wq?U*VlYDLiuӡG9tnRSRfm-z8qc/z4fURUGі: FugК̗N3^v%?_u{}Iʕ))V @@@@@@s2 2Gzaa:E u}-]L_ Ur6ؙ>kΆb5`Pg?"0ܗ_V5kD7?4AkKDWf_8m֛v uV4^͈ACTVmxI_D 0?/k,b(^}ᕗ䗟0oҘqN&*RO|97}سzs\Q#*ò[w^~5U\EcG|ج+StS}}aVѮtAz# 8A`ƍ0q7N;b& 9)5;vKq.}ir ^qգ#:j7}_o'=?oӖ-jHja;獙4Y:\=}ػ7lxR`e9@@@@@Z&T!L ޻S+6d^,׷̬wۛߕrM5Ҁ^}8sfkUm;u^gEaCe+Kz*>>+x2٦>i}?ɓ"c stŲӞ;iZv?ut2}]{'?,-N_s2f\>D}f2S7͖GN؈ζ,]9/nL[yu$hΊ~Zi-5W=`n:\ݶ>ـ6H;9mM@@@@@@ r=۶}SAAA>naYF;wԈqc켖>=r7q!l{uV]jҋڨA>W׆ 8}SO=1}s]rnL.PJ l-W6ܳ!pKײk afv~\>H||KL1-ѧv6nUy@WbR)liW9-[~*K闟?:?i5+;ۛOu_m/Tw#4      @z[[&mi[WnMi\K+o!e5ڷmoJ"8xЩfkoMnpw@'ԙ6 v]:nڶrގ2ch߁^|CFEhٝ63 6'䳫*i 2L}b^@@@@@@ <г}ԧUi :z٢2dScfkM6U jP GeSx*eVwM{+H[wG@y/:lMC;m7[f`ڵuANۡCTT)E bUJ6ox==ِ2}؏/#2KkI  }7yW^Mɮ|tyt5zٙJIMqλ‹N-nӶxdz9֮$]'g>T:      'URYcT`@ӟ -ԫۣN8dvZ "n"hUVq[kk֨)͆v=˘ootʘH"5^}Y?mH?@E vV޳[*V2B&mxhZGv5;cΌwgtmugUF gf\=<±OvVX0թUۙ_/tv^gNK~e͜SUDIׯL_gg:8@@@@@u| ln]uf<7[+[ٮ;@Zz?,Qνef"Eu9 K:QZjvbc㜶\?_:SUt0.  YwĬ ZVp6r kvǞՌDgtEvgCmv_*x]ի[O%WY{-G=]+^2+5]ǹz $v9NHɁ     @z,xч e*@2@II9k϶}_o      ix0%@S`@@@@@@|M@f      OM'A@@@@@5=_Qƃ      Sz>5 @@@@@@|mF      O t2@@@@@@_ e<      >%@S`@@@@@@|M@f      OM'A@@@@@5=_Qƃ      Sz>5 @@@@@@|mF      O t2@@@@@@_ e<      >%@S`@@@@@@|M@f      OM'A@@@@@5=_Qƃ      Sz>5 @@@@@@|mF      O t2@@@@@@_ e<      >%@S`@@@@@@|M@f      OM'A@@@@@5 Z8OwzJ׶hjޤ6mݬ[oIukӧ?/TVϝ2MCRԺ(}pZ4k{K QR៟-F@@@@@ ԪW;͊ޏ>}v]Y}Iŋqգ]6zp9]LC@@@@@@w 2NNիVsõ׫uV UbŴfm^-URE1ܗw/#     Gz7Q{u!T_-TY׭s޳GoE'wO?m۷k7|`z      x\wх7ggh޽hxGF{[wb?ٌ@@@@@ Z8Ot>5]1cĕ+VXg=5h[iУ(11ymN6n)b      ,1^"E5vHM2YG4'|jVsf+ @=_Z砈aJ6[sr       ]v%zJNN>ɴOܣZ(%%Y~[ޛ)]ZޝIVVy?ooԖU`"8LOndFYKE+. 2hmcKø =TeTVPYEmFfF9{oDdfTV~o>O   3!pY+3gsEwo_鞉3@@J\@7C@@@8aefE*jD.'={@}]{7(]zvi}_3> @@,b9I@@@@ZYFV-Ջ:jQ4T'Dzi:Nd-"y>/ea_:5_xrA` 9{}ҭ?HPX"@@ z18    -f֋v5ю~WU{HAOzdK[4g$_R)8}8ahCBü7EP8G" c @@@@,Uu-k \x-]5{=W^zSrek̷}m :0ۚ44ts ÖaQ0 B^n C@I Л$H>@@@'I[cYvvaݢգ~nmu/[evVi#;Ʒ 5lPh^mE} QޒAõ+j((pAȊB}0ҚtXKR_Uh!*MK  @1z(@@@@[uVVuרahǫ5fS[[Yv>y֘Z8oUlf Ъs-IMێcVmXS=5Ao ®>4>P]z5Lt}h  PZz    3"Pjmټ87*cz7怵tvA]ֹ; h9#[&JW6?$t!*av* t/ۊ \,h5axhe$@@ Лt~$   qtAe̵Jshff؅v;Ń IDAT5#0)ak00tAa؂VDWI8rP%L NeP10=h`X1  Gݵ~ A|@ +ݽ@@@ƴ:kͲs5iX0>kiUu6f17 j6l?jՂ.$CgEs 3 5HԀQ+>s* \;Ѱ0*CUUUv{̪9@(RE@@@"rg\ 4Z[c&uaŸ yvYKLJ_?9/rxbUV=Sߎ40 CÑ.\ GJ3 u60sCCڠPoC@S@o:Y   LJYN ;J;^r֘{-fvVi h9 Ǐ@<j4(s ð//0tY@ha. [Eޯ* -tU>AEtGfCC ]Ua0|=3CY#k@@@-hek̴;bk)iYv/ZeB>i*;S_2 00lC:hc^`` + G$p*#gF5tmEfZ2`1 نYAaʪ%ETlL޴r@@@(NڹʺE~V-[cj9?ηô·wz=U *j' ڍx.pEFD*;7~=kSkvE3]S**'~IgҲhB yJ!~ﻲj@@@"/0I[cUuaeW+KV~k:gauUwtEY  Hʤ0P;lklQ3쳦]Q -sA`RԂlP>p2 ڗ=z@*@or7W$3 G#_/Ʌ] w|..B?ʯt LlQZoFfP R1Wɟ6nt+W#}C~7$y!_7}A. Lp-r?_:h@@@r֘% bcvEr֘:0=7.k*heV."@R 5/7ao5C׶4B é {U ]ѠJUv 2 ۍa0f assrLo?H{&DiL7\W\wo͆}K/FG=OņoK瞓~4#@@@A`^cU0NlhZ9j1ikL7N+,;mivz5f9|{8G@ XЅa* ]b0Zs)J E6yU֢s.< @X0ki{erϥkeݲK|pӶz@$_Uyw)Q~?M7;go7yl,_ m;w#e Y"   @TV̷*vU#lߟΰۮu>Ik̨1@@ [QT :0Ax,aϳJũ8R+$4pzYsy"{~%2''^yS'|&3)@?v|ݻw;뮼J~ѹyvX\ĕo~].n~ʦ*f[F@@"(Pm1:ki1;Ie4 f؅v/3͒@N.*}@0U' ٚ4lG}Xq"ys"3آ7V^TwIoyw+*~-]{쭧&Loȗ?E?垻cg}Gn]`   "ZcjU]unj䨹5Nkaͮ۾ϰsθk蝔!  Xs!7h`/:}qEfIj>-7]KXy-qLP ot1n s%/)==rGL~mSNq-8ofL@@@(ܵaje]sFfWi5V,;,;_yPW@@8-YO앋uOF)Lg ;_'oN:;;]] y~/?ddy^$WI{oS#  L@ueBꂠ.hZe.;8|)vvmvZbZףg8@@Rx8nHʎ=)?-)<@@$2   P>s*uҮytwtnUiP/3hY>@@r +|@@@iX>Za3\wE#}@[c ;sn_ig1;h9M;ȏA@@Ee'X   3kiu4[~]vƔ vnwӂnZcr@@@`*RF@@b.`15s34΅wzVvt l]Yvq   '@WB@@JVUͲ\ F;vZi*|ݞ%kƉ!  )@7,@@@`+U׹YvعYv]pϲʹJ8#~$   P^zߜ-  Jf,;uݑ[cikLWUC;"S[cJTB@@#@b   PK5V ; L;7ۮVlh4sm1m{֘e@@@ z"  "P1]PguYv};RkV[v/[]i]Vq   S@/ƪ@@@ ֘naV-_3Yټ:[g0[r0.oS   p="   $ ,1ÖZYgnn1;5VYv~ 'y|   @K@@"%P Zc.Ѱ.gTڭ^1׵JvaT܅2zif1   @" ,@@'ZcYvvaݑZc5ͯs-1}kV \L 2/+B@@*)Ւ>Ij(MW @@8j*;Yvu>FCͲ˵ƴ΂REt^  ˡK಺?S~6 Yn C@@ T&Ňt<; VgCZѿhhЊvsAwUUuNty@@q t}xDVϕȿw>k'$If/ە7"U B@@fUh[:e++V,3zRi 4k 41^  0yjI˜*{մz_8e!' [zvϵH勇&oA|DB@/"@@@`4:nkUv FQ.kvi7@@`l]zrݶ.~ `抄ܶ}jڃjj̹~Tq @i ~r6  R"fv"Ӈwv]#5Gniu fm[zh/F@".nh]Y]ƪTJ.їġ$HRg6'3szm%{'J׶F\!xƣ{@@@`u6Núծ=f>V#+_[cj[L?N: ¹v۵.ֿN@@@`F T i{ia, ù~uj wPC: 19-\I0[k깃R׊}+C lX.  QXlviP}xgvGlޮ15{ήw3@@H dE{ȹtC;I.e]!t"[1sN \]PM}`烻tdq 0cz3FF@@ ~βv> +횵n>^{_m1v"ݦ5f @9teͦ!X*, 92Ϊ< vto? +=@@UI[c뚵f؅J=a1}Pge;iO3҃  @ap.N|X[: ZIw , +|( Yu2Dh Es_X  S*X1ΰ[3f٭^\'unh^dE>k_jJ͇# K iFTYqtAVY!tn^VUYX- P@@  %(L&Yv1JՋkZcjP7N+j+"@@^97N+fiu6o.b.9 {<ϥt @ +m$@@QZcjXVٵ t]QI5f%t11{0_1wsF@2mhkK27N[;LxB:2o. 笪NsDh^_@N +F   0C5έk^A Zc&(bhxPZeki9C[ʏE@G@;6 Cg.us} 41TuUҹʹ\0Gש0w߉M~S@J~9A@@( $w'4v6n:mu]uŨokm1A_]l5f! HtG_+|:_E*8=gAz_ˆtZZfUJVu/D=/eTY]PyPY3ڱO"ϯNvmv}b@@P \kK ꬭ͢s.f0.K7J:7IJ: *;_UGH7G#e+@W[ω# 8vYsZY.{w>Si[]uvZdk[1k*F=񝯧f0UJVm痆U# @25.d+\%Τ+.C!0ks [_&zc W@/{@@ëӗT#sy~hƴYvͲ=n=Fk̲Zq  0ZUsVEq9t3X;Vep;ͥUٜ:& -EAG# Y*NXQ/'ˊ92^/YDzv kgiP]6 @@LtͥUiP]E7t.XV[\Btm.5KXK |4E@ z& yղ~ez zθ+x,}VyA>n?n;*kǁ  09[Cgaͦs3:ad,:ֹ*N ,ȳ}S@*@םc  @|pW< ϪU+6-m]R_]!˥C^{墯mG@O So!].luEg-/i .39 \Kw;hyUiU]"EH7};OB^w# .lxiL$F׵蒖6 ܵ^4˞qٲr~~ljiB/:ֆ S"Puvnz{_\;b+_s  3-L΂: p.;.[IՏBlUͥ: tkyA7 @,Kٿ[qǮ厯|I/\%Owlu66@(Y:WufZvYT$OԇMۭҮ+GS%É! @r.tc J9 ΅v:.E%! GEΖU+q$T]7VMuW^ SO9ŵ֛eP[sr   樺Nnꂧ֮j]OP}%/S  DQB+vA,_U7$Jt.9kqWUgm/]%ͥ;@(uzww|yWVVoAzzz}HN:$Iɧ6~R)?o|UޱG۲}Gk %(P_S̹wإuX[Qll]k Ł LD2? Z^sgҹʹm=u @@ D&cS@@rX6v'^/oc$efYgA +ù" 0!LUR2sjnx\ts. *Ξ?:JtQI7M  Pze< L4k[N/ m}nΝ-[Zw:@@ n?.\p OH為as$MH E@ tX6 DO`nCva4[Njκsv*sVm{wb@ 9qk$ѣ,7m)9#S]1r\^U]~h9L[Bkp.!-.&@)aC@@V/Ͷ nOKYxgw-j W} T#MwoWF9 ^6Uke]=*T0KL:?\: GCr@@@  Tk+v6nκXe拻zeӎNqם vKa %$0Uyz;SVk>|S*umzvʺt.m-%&]2lyiׄt%T@@  9rι[N[gꬻ fiTC].6G.a[qa CB A#޿X,j](|0o@@(@wD"^ QvvrxVuhvᖙm}tΪEX ΒYqAJ7OΪ㬵e^͓v\ʂ0.:WUsB. ;I8@@) ЛR^>@&*Jη?V@I s¶L fǹ W -s+{Yra-AoaI  c /E@ ,W-Uܹ;k]UEbćkwʦlxݥq@fR STM8 Β:s.5K]Ÿmm(ðUr6O.7K?|u݀0Wn@@H Er[X ] -[fHiLmʹy@ *[8;¸Z.%W9ICN:s¶Ds.fi>s"E  ^92 $ _lҷ\kʻYBxvg۲[KK@,Pi-,7ƅA>s|0g\P9 8W r}eq9^z#y    ˀ  ^\eZpwҠeQu7(_[}hgUw[4@@`j- %*.Κ* w*n8 \K`\0O. @@@`z7@( d]Uޭ=VhpxNukӪ;mک]kTIXp  |,**JWY\xPU2~s[cZ=?[΅v. *   @k@ XHwι[N_֠w2pӭ]:N;Wug]h!6m @$2C¶@.%gu҂9I4W͏+0Kε CvP=gUv   @ ێs e'p: tUչՋj :_ZpVYmSvf0 ; |`VkY\kK\x߇qnUi7 %mSiaU+-sZ-昣:^sއ  )@WY#,m{^C; _fw7_Y[xqWMZ}g6kP pJ @|+ðMlTŅ!ݐvoxDzhvYrVYgasO}   c E@ " j5VeYݒ:LXa`wn f#d Pb| |_%YrA n3^kEe0+εs{.87[Ϊ4@@@^!@vA e b~MA)m%6΂;k+sEN@F@"g)sa\vsR+]A}ei オ8iuv_[_r   ^-g 3MU.ӪX2_f/ -3ê;  ݘ:E)HP0.N{$,l˟-_%orA Bxy   Pz% @T,nU-u2myѭvOm9wVu%ꩱ.@`2:sڮ6w۪쾵 *:;=WW"Y`b oR|\̅smUYˠbN@@@ ЛLM> @aZasnͺ[bm3keVmӂ;ڟ@*CF 4Ώ6K͞ : Ir2>s[Yr+r   DE@/*;:@b/l^_ 'j].; 6:E[gc @ h%m~ΜX *lWY7Ņ*Wȹ;j@@@@J`9@V-;?ŵOܨwVqg{D@ 8ao8 Ͽ_;r .Ӱ-%faV[J|y@@@[@G8\˴yw٪u;_e>?G^tL̆ma `͐jl5>Zv\s\͖sv8s.hwIy#   z|@Vk\Wܭ˚ꥺr 6j ā-ϒ 춫ྻ\8Mfr!^/s\^   $ M"& j˴kuƝv\PSeλoν-;dY%LΕVq,*. |u\j MøYr6CNA{_@@@%@b cX[Tz+V ܭ[ZeN/ /uwm/9@`2Zk Tus,G~8ʅvr^b`dz9tZ\Vu?m}ǁ   ^(DLnk jF̴֘mdw[vvGX4 GW stMՒ,:nBCzA 82C jf%;z@@@@JU R^E2)8}Sj9ٙugȻNTT$O~ljiB/.@։S*PN\UwK·wvyw}]*SeۡxU S]+粕tym/CuG_+hK˃Ӟ~t vڹ;mr{ [jVV iyTϹ rL97Z^IK3, 1}0/F@@@@l9]^ؼlp̼[6B:\pQg=!]ӷ @(den]0N,yts\RC:tnE/B@@@@"x  P .wzi9x[%KZ4@@*)*l>+4 *2=F̣t̅ t^ͥu     0zoOD Th]}^pW ھWmg]oo멘+ d4u\ :?N+謒\6V}VZyWaq҂8=l&]8N.@@@@1#@5ZkiUwn杆wz{-3_*|-3 ${=ktA\XIg,sUtYEݤΣ.Φʺ!Z]N^    DI@/JZ@`F;k^X]Ul_?25{e_y]??| *l]Y%rWM<:ȹtA8T%zhu9G"    l EP)ǺjTZTSGk[B:s-hTY%]Y{K{lw&luiUtC+|U]x~X5    /@7DF |̠nk7_Bdz;swYL㈶@FCZ |ίu8;WIYufD@@@@^sɜkyqW]wIo?m8MDm ,ӹsnyt-D% H6E/D@@@@: h(BЀfޔU&Egδͼ-3mw]ŝZϘWoT%%t馰jNk`]V7CҰzsy™tA%]`XU^    [@otr˳'+=r[eI\Uv"ӅvAՋk BkEx{wWFێZ%kytGruV=UiL<ܜ5     Qn B 4lzNKkX-ҿ_$Ovi8TUZ Vxvgwf%.UY%]7j\<ͥOːtGIm/;;(X     0hZ\c[N ("eU}L Iݺ*tokg9Y@!V<:U 5rl@@@@@+@Ǘ8Zg 'T9L;(O-m1 25ɼnڑ./s}{NCa]_E^    ^Ym7'( ;&w>sb>4XkG*q?hi܊= /m>^<:K@@@@@!@74ނ@9 ^>eZ93^xgz<-g.!dϣytaK [AUu1;     0z3D SɠzN :#Y57UمuZu7feхUYpVYH8@@@@@^Twu!QC[Y]ϫ~b{ ᆴyt>!謢Z]2_!     0F1rY F}PQ#_x'?xKr_h oLupytAP *ڎ@@@@@r +]@mMɣn|FG|ZgЅtLd[^,q      z|C@hjHV}`[ewAySR6+@@@@@&O@o,$Bs֊V#MwoW9I@@@@@[@oyɽ}RAItYq      DS@/ª@@@@@@pz|@@@@@@^7!      @w@@@@@@ ExsX      z|@@@@@@^7!      @w@@@@@@ ExsX      z|@@@@@@^7!      @O.BӃ?= |ײhB yJ!~ﻲY@@@@@@JB [O=MN}AZiyXN:$9s?%$v@@@@@@(KXzo.G7 ٤&s_ˮ꽴{nMHakYn('      PZ3ԓ,(b2ȹɭtvW.y=^Z;      @,SO>Y9y="EȮvٸigˆ;nn_ m;w#eYn('      PZ􆓯]F.>}Exۭٗ|ʦ*J{      e+@o钣G6cɇ?rW$_k{ unVʉ#     @,uZ,%Hel'rݕWkxSNq-8of֜      ] ^UUI'$ryrg>=K/Dy9ywy@@@@@@ :[3ϖ wܞ mNyW<> D@@@@@@H}\ލݚ=O|RBޑv@@@@@@b!@Q/\%)~džʽlݶpur       `ug]yw/~HN=ׂ[otzTwޘ~cX oѤQA ߣ V@IߢIaC@&(b͟7O>ыdu*i#mپ9 @ oQ췐@JBJb9 @ [c @DߣzD}c{^@ >[b @) Q).CO(u{DW@@ Q±0@IߣI@!E@- .6Л@@@@@@)@#@@@@@@,ɒs@@@@@@)@#)ukV>[]F,">y)W\w̾;SNqMߗT*53{)IOp3iilh|r$/?g{}yĞ3#)ٳ#9s~NGz3EE(B`:=jjl]x̛3WnmU\R>.~xoQK@ Eoԅ/Ǯ]+_˿;@<(w7 pˊ++fI_J?ɦ??v|܏?|ē^SYYٻ09rlI#)4iR˱ 9VTP{WQ9*bW,RTi*J 7ddvs^{myv/+a9rD\vWcڔi1CSNF!z_׋?w{?^Wĵ7\W]}oM?Q7h*'"@@hԨQ=wة+j^`<#dg zjE`me>o,Y8Ƅ G]zq}=7} @ z+c@9)mCgG˖-뱳7qfc_]^~N?3;9ERo ƢT pg/ %><Ԙ0a|wЁ*^,A/(y P*/|Q|˗Ɠ''CW/G~g\>[nU^kϞ_T=5`SnEwg9Ds߽kߴ/=Sx^-"9yEE&ǟ*jiԭ@&ڑ#G߰_w[{uzna̛7/Ux/y }p@uhuEE/%X`A!|ͩ{oke @`eZ#_m}?_/wa~Y9[#:4ru]&ǡL8g9+z>GW4~1r-׿N;ݾ= 0HAv)0yS?N>8Mv>?nO%ݏI'ńC{?[nX.Q~C83ԉJar IDAT>3?~+uIJr^yuF^"`[o-l X|?|MҢK[:߁5J!$0Eݿ7z牢Rl}=H7B/[!@@Gߩ2{i;޾wUg߽?Gp\NPtZhQ׮zK?w@iVL25~zr|W?R˾xI\K5xe|/+~vOf_;#L=@劅_K;+Vwi+zq{éˋٹ-繟GcgUݓ&NS;)?}GUx8,_SxyߴNHr;+]^Kj6y^9ڣI7 ^.+0XG  פ+#@{јQ >m\1ru^37ק+&@>/j/đ%E8;9Xf}ſ_vxY!" "76Y'#>ZG swb…]kfzfbdNc<3Z j/g^zyǻbƓ.-zqoڹ/v9ؾ?@Ή%K98jwxI;:Wx?ޮym#A_^N-z^j ߫:{Q+ T3qtƏ1q|!~wu72%@@ \חzcF :'qGN=b<Ҙ۾Gsω~:ޱvǩg'˳aJѮ @ @ @6zy @ @ @Nzur]& @ @ @@m j5 @ @ @@FL @ @ @}k @ @ @:ɍv @ @ @) Ыf @ @ @u" Ы2  @ @ @jS@Wͮ  @ @ @D@W'7e @ @ @Ԧ@6] @ @ @ԉ@Nn$@ @ @M^m7&@ @ @^hI @ @ PڼovM @ @ P':. @ @ @6zy @ @ @Nzur]& @ @ @@m j5 @ @ @@FL @ @ @}k @ @ @:ɍv @ @ @) Ыf @ @ @u" Ы2  @ @ @jS@Wͮ  @ @ @D@W'7e @ @ @Ԧ@6] @ @ @ԉ@Nn$@ @ @M^m7&@ @ @^hI @ @ Po^ͭ5 @ @ @+0 { @ @ @jX@W7  @ @ @@ocWH @ @ PyN @ @ 0zB @@z#bncmژbwO,;Ũ͊G @Mwn @N`Mm={3c5f} ZZZ?AwYo}^ @ @` = @D`K&ƂՎ1鱝@~Ƀ  @ @"[7@d@ @ 4Ŝ_}~zM,y7_(5b~%dLhC]7.[wM74GG y0>V[[جYq)aK.W1bDscߦfA}*?xqQGWzt]^E=q_o{R @ @ : @ 0Cn=_,-ly@;7vK,\XqI~r>.{cqQ'to[oe'o~#} @xv޳y @֪Ͽ$5{(-j>G{{[|Ϟun\KMo-v3L1v:qQOzȑ#o3{8C࣎\]_lI|-79 @X@ϫ @C\`0-E݅_Kc:E^sd̙"kjlwGL:-wߊN:983(N:%;8ۘc}?w7w @ vzk  @ rsIq'vjykKw gCCC=o-Mo*/{y/ 8~濤獍j &M7:댉~#c5x @AӷָzA;flV3+M~ʠ @T]`Sc7׺#o|2^voV[{Zsz,],n+.RC)ph/yk߸"r6~pXQ.ko.Va>;}}Zz\{?sf|cY: @"ƍ^'^NU|m>yZV^-@+ @ Ps~A6lrG4=Ϗ@ @F,Bϋϼʁ^䛾)y-xƍW-;)glMm ~Ǟ[=pzyaG6[mw_qg?y֍EaiEkk*K} q.d?hO w~A̟??>3!@ @ _21uVc/c= @@cCc-Y{W@o=?Ʀ9[ֲY7? … cq-]:޳ދ:eJ~f?5;f=x|zch @CN`Mm1džm @/mvR_K[K[mmb+QF%kjl,nš;c˜|ivɡZW-5| @CM}tɱ| 6mL'FbfE#ږ @(0qZhȋrE7!=]m8xj՞=[=ohIGl̝[9yoޅgOn>.769bm󄣎-6<~{ݵ[o}ihxl֬]^zŻm-7)r+~xO?E&bU?Y2c-ƦxG_(yxԱf{p{mac=bmR)ejd=wOzb+ߵ}< r3KկxEL`hHO>7zryԢ?< @ @ 0$|ꛮ卾xw̼Ǿ';>tW{=UrU_-#k߸8zgM6-fϞnI4b^(x;l]js^oussцybzŘԟ4qЧD?󁘗N;_vi|ۭs9aw#MJ8s >}8#b̨ѱ|F>Q5@q};޶GαiL»7bjKs z|w|-MMMkתas  @ @ @ VZ ~ctQw'ţ)V//|>~=^_muFJ8bT+ξ;/E_N:0U͙;'=Ȣ2SNOAۆ; ܺ~`*ϳ;"_\3~^ku-7?x[\ufyu׍'GO9XcwCRXJ%л_?O;n}ۋ+./V _P @ @ @@六Cc[oUk|/V-ˁ]ܠ,qͯYx/'x"fpl9sc*]>xۊ#g|Zj?c^tޅşs÷5-^q3Λ4qbwY|OSϫ^d'g?9g:7Ə\_E̕ݏmTlj @ @ P~h~'[jS?Y%rq3*N|ygv;toKS>zfugN<.W_RW+z E!G({3^җ~۷xi#G#:p%qurg:L<0eq-z>= Hf^ymG-7/~XlYlWŢ/zFr+Φ[q ׭5ӟ@ogT¶\+;=zF䶠 :6r IDAT˖/+͟?k 7 ݃S1nmqї.zƮN{O:XtɀXv.oR2*J_Te|*\헏mM-s.%@ @ @XU`)4ʡЈ#w̡ٳcҤIS}q[ c[oU<ña Hio9RHw_;(5'-6߼͵o~;3ȦOG~D5Q7Y jpG-OiW{k~,=_GL$.Z3Y|}M7\=~4\SA  @ @ @V@/{3x[wr{ͷxk_]7U'k_xkw5 N8bƛx߻SL/4?o}[lBYz7뮍r2ﭿz9ZnϷ=LuƎλWg'П@/?)5կxReވ)$La]~{uTA^+»O=7vsw%Kv=|M-s.'%@ @ @)P@, @ @ @UԊ @ @ @& Ы @ @ @T_@W}S+ @ @ @@j"@ @ @ P}^MH @ @ @jQZ @ @ @@z7" @ @ @ Fi! @ @ @UԊ @ @ @& Ы @ @ @T_@W}S+ @ @ @@j"@ @ @ P}^MH @ @ @jQZ @ @ @@z7" @ @ @ Fi! @ @ @UԊ @ @ @& Ы @ @ @T_@W}S+ @ @ @@j"@ @ @ P}^MH @ @ @jQZ @ @ @@z7" @ @ @ Fi! @ @ @UԊ @ @ @& Ы @ @ @T_@W}S+ @ @ @@j"@ @ @ P}^MH @ @ @jQZ @ @ @@z7" @ @ @ Fi! @ @ @UԊ @ @ @& Ы @ @ @T_EdْO~/eE @ @ @kAw:ej|k_I>8쳢eZtJ @ @ @@3ωs>{~zX @ @ @@o1sϏue-~C @ @ @@Mz&NsN;=~|U_"l831's]s+ @ @ @X5 /\ȁd҂rŵ7\7vZuz @ @ @k.PӁ^/^8ɧfGrXwvs_V @ @ @5}Q#G߼"6tr8cC @ @ @@zcF}>wl`:o^M @ @ 0j>BB @ @ @Z @ @ @@zճ @ @ @ NjA @ @ @UJ @ @ @. Ы:  @ @ @TO@W=K+ @ @ @@$@ @ @ P=^,D @ @ @Z @ @ @@zճ @ @ @ NjA @ @ @UJ @ @ @. Ы:  @ @ @TO@W=K+ @ @ @@$@ @ @ P=^,D @ @ @Z @ @ @@zճ @ @ @ NjA @ @ @UJ @ @ @. Ы:  @ @ @TO@W=K+ @ @ @@$@ @ @ P=^,D @ @ @Z @ @ @@zճ @ @ @ NjA @ @ @UJ @ @ @. Ы:  @ @ @TO@W=K+ @ @ @@zytl:}S.p/^qXա,H @ @ @`m|wɧE^xz8 @ @@>qdFSˣ @!^, @ﲋ.ygl1oLc @;%{Nuݍ,q,; LkGmX6ut\pCkgCJ tWj(Hrq?!qIsT + @ @Nk9}qq/Zgxen<|NTe @` ,]֏)#&E[p-0O`#05s! 7!tMmg} @ @` 9+?7rn9 MƌuN|K⎻;n}Ͼqԉǂ~ah @TA#[QUjXWnn -/ڔϭ#}R|.ϟ+^bK&XwwLj<@~Zp=CA@/ߎܼcb;W~? ; @ @@.WTu쪢,gҒJ0UBw6m׍ň;EyOH!вӄXz>qd4k~Yp(PPD' @ @`@yTC%ڲs6]eV݊(FZ Vu=޷5= @ c[ @ @`H 効\U<>s݊<-/v 4nE] fiQ9 @USZ @ @r+A3TՍGee/A]]Vh/׸ @@V$@ @CJP1n6ǖ)x--;gѕWf6T! @-a @ P V̦묮֕G78Wu R\EU݂> @@o< @TU os螭'.wkm¸n9ˏu @ 0zC~  @ GRvE`JUJU]nףa~ ີ2.W; @+ Ы{g @ Vl_2gU]<;--mڲe :*:( @ PڽwvN @ P[^.UuΨ[uV].ZLXvp3UՕ UY @Zݳw @@C)먦[uV]ط5ӣJ <4.sN]Z^  @XE@EA @5+PZ^s`eK{̞tYt+BmZ^  @Ԑ@n @ @ egH>tu%fg΀nEJP=+-ϨH @I @ @}JHW V_UzP[+̾q @jvG @5$PΕ;ՅubƲLeg07gU])t @ @` v} @@c[]oUu]Uv}^`E0V+ՕyM$@ @@=.F @$5m/;;gukף,l1nYu. @ C@A @r99+Hw}:[^n~g\<[OKz @9t=ƧǤ]-+.Ut+_F{z @ s0. @Q-/;]v+V̪4Ȯ/G *s0תem}Yc @ @@ @-Xs:u͡eV]ydcZh{[b 3 @ @s='6O"@ @`cs\[y6]{t򲥽ua]G0Ҽhr  @ @o9y @U(77D1.͞TUZ\VU}lyЭhwb]D*FK @ @  @ GΖEXWTԭEk)ݏ)|̦Kt[\|mnkJh @ @@@k @^rjP{U] ]VBUuYz  @|w] @J]!ݳU}=J *깎jynaK_8 @ @C@@ @`k̦+]b6]Ǭu|ejwYu=[^v{ }^  @ @  @M<Vran]%RoJ[\b>]eg`}V])Ͱs @ @k_@ @ԓ@ *Utg+GZ^UuaJsR;L @ P[&;vluҩß^p]m%@yrjeYiur֗.x|rG0eWP7?vm/sKRK{@ @ @MGm&~kz[ @(77rUuǖjtΥluٽnżҢ!kcc @ @ @/y{G떖r\ty6]3K-/i @ Gģ/4޸z}FU/ غGT}-/S0n% r̮YuGS=[utsWxeZ^#  @ @|oO[}{, @SPpʼnS/E|KʲREWڪZ^ փ  @ @@oF|2N9hmm KĢ @#жhthdh? +}Ƌ:.n]@ZQJs].WURZ^ @ @s@o-xۢQ}]W^9 @-PJ&i^ .}Q %ccqaz%p-0?ͧs @ @@Mz+ -7٫ @NGc  @ @u+ Ы[  @ȕq3fMrvna Giy{O @ @:w @gh׼"4ͺxi~]oGiQkڮsCeË @ @TA@WDK @j]mÑ*Rx#zu͹+fޥaZ' @ @ [cc @a݊vʻ̻KUvMڮ)ͬ̽K-3S5 @ @`zgL @A(nLa]:eSD4VG=ͺT5Yw99 @ @7 @`O"k`TW0%wjJ>f- . @ 0<z* @` MS,*rx7mL7>*3xi]sXǥ @ @@oSWD rS)R`W͜9,j f.媻Ιwezf @ @X@W7ߥ @CG<)ZshWyqw긻(-mO] GW9 @ @@oxWWE a<ۮ- R]aβUZeϹa!|uF @ Pm^EG&P쬸˟\h :û*^ia O @ @:  @(jTuκdt𮹗ywhz \*:fޥ?Gk:  @ @ b @h?۬MKѽ5fvw5>d0 @ @D@7Ln @Fmњݵ껢}fk0ד5<.WU»<A @ @`Mzk @h*]eݘ>)͸k]yݥmx @ @!# 2F @(kwi][<-UMI-3KlM/ S; @ @  @kE}Ҩ4ﮣ⮣efۆ#{Khz wEJ'\+7I  @ @z^  @誸K^ZfYKywE̎aA˰p! @ @G@7|+!@u#P1cJxWثA̕wv.t @ @^-%{$@u,>aD r]Gx7%w"zwwYRz. @ @`8]t  @`Mݭm)k޵OZͼ9Ӽ)[6De @ @X! j @_Tm92ލku/VmfR]}WZ:vF @ @kA@Нԓ@yls2GR^ݥvE9K2̼r=V @ @=z^ @@7Ŭ.sZ 簾.(-jw+r]K  @ @.r']d4.ϻ+»)xt?]4^Z{pa̻-c#@ @ PڼovMM<1ʬ^Gy2TuW9}^ @ @@9yGsZQ̻*\ji/Zf] 튶,0 @ @ZE|&@kS ϶+»Q̺˕wuK=f%K-3Z\x ;7 @ @ͭv P .sl:3Wܵm<*r5^oG<.wi]nQZZ|. @ @@o69^k  @ZsZshfݵNR^SwT/Wޅqwk9) @ @ tW*3ώ+7;o|C]'@Z}J]uWwef ywK*Uw)ˡ]g->. @ @p@yDE/?O]㲋ǜtb4S]sh8r%Cl @ @# gg!@@] v9wy]k˟zuȕvyw)K]y2s @ @X@kHm"3wy]+V>J+s:>rx5:' @ @ @` v}@i׺hC9<34[R]fʻ;7 @ @Z@wJCmJRhW|.hX2.,E0{i}@J @ @xFPTi]ڥ)GrmU-R @ @ @ . hû\mT}>VkzpaDkgP @ @w^\?h ϸhޥe]:gS]cK @ @ @51m:ywSr7:ZS]v+%y]%w7gp`p  @ @ 0zC'P.v!^{Ѩ^OԲ-Zॖ9s @ @ @  0Gt̹K]/R73Uߥ @ @X}M^q1.UޕG7~[[ϋUهE @ @ @@z+fڵsR]nY|OM-3K̻[i])Uw)kX2{: @ @@"@FWB<.yzwDw5ݟB^MY @ @ @zS6b1oޏAft:T}nw3rx7ץLsRhWw/Ϳs @ @ @(Piǟ7;NCST5b{o,WMԨxobDPQ@ (1&1%6l1"V5*Vݽ3wY\pyΞ}39g{$;FϮݢ׀QVVV'E!mYef.6eT5)+,KV5}waQ8 @ @@fMƾ{>XW,VoŗĨc>VBMHW5Y8[m-Ӏ.&3{ݒ.$h~Yuh%^WB @ @G ׁ^2lnݣAQYQYk @ @ @@=#@ @ @ Pb @ @ @Z@i< @ @ @@ G @ @ k^˧ @ @ @. +  @ @ @@z. @ @ @@+ @ @ @|O @ @ Pb @ @ @Z@i< @ @ @@ G @ @ k^˧ @ @ @. +  @ @ @@z. @ @ @@+ @ @ @|O @ @ Pb @ @ @Z@i< @ @ @@ G @ @ k^˧ @ @ @. +  @ @ @@z. @ @ @@+ @ @ @|O @ @ Pb @ @ @Z@i< @ @ @@ G @ @ k^˧ @ @ @. +  @ @ @@z. @ @ @@Qzs8뮍iOO/ @ @ @@ >;cm:F۶m?++ @ @ @R}iܹ1wxz0j @ @ PBj5~J @ @(^TZ?  @ @ @r) e4 @ @ @TzRi$@ @ @ȥ@/eh @ @ @R}7zظѬi(+:TP?  @ @ @X ^F @ @ @@ @^IDAT @ @ P. @ @ @ @ @ @ X@W4 @ @ @=c @ @ @@  8F @ @ @@g  @ @ @(`^G @ @ @ @ @ @, +h @ @ @1@ @ @ @z\M#@ @ @ 3 @ @ @@i @ @ @z @ @ @pq4 @ @ @@ @ @ @ P. @ @ @ @ @ @ X@W4 @ @ @=c @ @ @@  8F @ @ @@g  @ @ @(`^G @ @ @ @ @ @, +h @ @ @1@ @ @ @z\M#@ @ @ 3 @ @ @@ _?xrlaxwYsf0 @ @ @@Ōg]kaCbqyy| @ @ @@ :kݺu\p9ѣQ^Q:I @ @ PZj;/zep]}:DדN-65iv ;VmaYnf \gkE( @ _\Q'wBl۱c-,>nF9SOH}1+_:l!N1ѦuX,~}o/yogtquƴgi޼y\uXxqwo&]9{WOs=7z+nM~ߌU?/gyxqq8-+s иuvL Gڶs~~~^=_;{@@w#mcѽKxwRa ܟ_88=xx`Cꫯ&_$͛9қm۴kn.?H~ߜ,{ǃO?}gw98C⑿<&[e/"@ e˖ɜ7$}u]m*;k`zo @ /_\u;K,t~4X:1rи ̙2 ^đG|/f2@oy/cG;3w۴Cw`1,\pz6mr??[;gYnO{N_=7p$Y 0KÏ>LEC~QcbE޽z 0^@}zQ9H@hҤINqٕ@o 7g ߯Lb@Ki]}o};:nMη; of+}{ˮj_?u?k̜=+{^?}ǑI8r,Kz-c_q^NDJVt^I6I/jW_}삵]6SUzlA|J򃲹dy+jz梒:N@ 4\ThYsQJi3㣏>ʄӒKy1FE:tZ%lخZWfY/'r{w]pQK7f^5+ܹ1w a1pvW1G#Oɧg{z7]v9ݾ.U$ [E~ k_uΎCGTzEm$KkIt ?^z>S}$[wX;i:s8x0ztK=.&$TzbTl+͚GzH!Yi4kr֏ӻb͒yl[/UE_Jh̹Eϛ4m[oe-ƞRHڨ @N=G2fA@뚺_/JB.'CFˎ;e;]}'խN?_3*zu\?867ƥ//+;{oɶ.rb>񋉗gI'gvZ[n6q:@`P+^#]_BoY]襟;㓭.N<-vag`>’=[w3|Tt+}XeF~S&A䏅Gֵw[nx%[i:ۆ3}^eTDfzE0^uXUQ`CzzF@ \f5ҟ_Ub꼴2Jo|1!>8? ]jzu uxH -'^=) Жuo>۹$9$쌵ۮň85ځ^A=Uw1hİ$\z]{ ǝCGe;36K'JcXe A`ekl쫯beo@L^,7Yd;qgjۀ}N4KWZklK W羚O>_^|r?I\zLle^ G< g83~qe^{ VvY`UGU]2 @+((~eZbGnqG~iOOwm RyfeXV&kѢE~⺛nr+u.ovݩP]qɄ0|HKޕWuTzQQQF[w?Ou>'w|y$Y.UMɿ$[*y^y8=Y>MdM=ϟLs>uW3B<ӗv=d_zc|t.McޯW_N[Uum+Si8o@<dzqsl LWzbE@) \xo<<pŒ#v/]](W.˲4ob?oUL{ |@z}Q70&L"ۋ;p$_;{"%d|,Xi?xɪG?:*]q '3g{>0%@@ ^Ww譹ƚqѹ_i87fĨ8N^Swݛ;1Ɲ?>{|;ƘY+=1+7+*+z%6u}5 *P5]?ٲVʽ]$PͫߏwX8ƶ;&+*7L!ޛ7/U8,y'}~N.ɻ6n>cٟۊz6%mla@A @Qz>~jײܝv=%idv(O(I _V4IIHd;oW+}\Ԩ  Ш =ռ6Z\4bGoivWSd7>kNNռ9:yͲ_vi8dĩ(?_<ԬO0.rb&_7ZIENDB`hishel-0.1.2/docs/userguide.md000066400000000000000000000210051477404575600162550ustar00rootroot00000000000000`Hishel` provides powerful tools for improving your transports. It analyzes your responses and saves them so they can be reused in the future. It is very simple to integrate with your existing application; simply change the `Client` or `Transport` class that you are using. ## Clients and Transports There are three ways to make the httpx library cacheable when working with it. - Use the class provided by `Hishel` to completely replace `HTTPX's Client`. - Simply use your existing httpx client along with `Hishel's transports`. - Mock the httpx classes using the `hishel.install_cache` function. It is always advised to use the second option because it is more reliable and adaptable. !!! warning Use the `hishel.install_cache` function only for experiments, and do not rely on the functionality provided by `hishel.install_cache`. ### Using the Clients `Hishel` offers two classes for the first choice. - `hishel.AsyncCacheClient` for `httpx.AsyncClient` - `hishel.CacheClient` for `httpx.Client` This implies that you can enable HTTP caching in your existing application by simply switching to the proper Client. Examples: ```python >>> import hishel >>> >>> with hishel.CacheClient() as client: >>> client.get("https://example.com/cachable-endpoint") >>> response = client.get("https://example.com/cachable-endpoint") # from the cache! ``` Asynchronous Example: ```python >>> with hishel.AsyncCacheClient() as client: >>> await client.get("https://example.com/cachable-endpoint") >>> response = await client.get("https://example.com/cachable-endpoint") # from the cache! ``` !!! warning The client classes that `Hishel` offers hide the constructor signature **in order to support all possible httpx versions.** This means that all httpx client fields are fully valid for those clients, but because they are hidden, **your IDE cannot suggest** which arguments you can pass. In other words, these classes merely use **\*args** and **\*\*kwargs** and add a few arguments for cache configuration. This example also functions as long as the cache clients are fully compatible with the standard clients. Example: ```python client = hishel.CacheClient( proxies={ "all://": "https://myproxy.com" }, auth=("login", "password"), follow_redirects=True, http1=False, http2=True ) client.get("https://example.com") ``` ### Specifying the Client storage Sometimes you may need to select storage rather than filesystem, and this is how you do it. ```python import hishel storage = hishel.RedisStorage() with hishel.CacheClient(storage=storage) as client: client.get("https://example.com") ``` The responses are now saved in the [redis](https://redis.io/) database. By default it will use... - host: **localhost** - port: **6379**. Of course, you can explicitly set each configuration. Example: ```python import hishel import redis storage = hishel.RedisStorage( client=redis.Redis( host="192.168.0.85", port=8081, ) ) with hishel.CacheClient(storage=storage) as client: client.get("https://example.com") ``` !!! note Make sure `Hishel` has the redis extension installed if you want to use the Redis database. ``` shell $ pip install hishel[redis] ``` ### Using the Transports It is always preferable to use transports that `Hishel` offers for more dependable and predictable behavior. We advise you to read the [transports documentation](https://www.python-httpx.org/advanced/#custom-transports) if you have never used `HTTPX's transports` before continuing. We can divide the httpx library into two parts: the transports and the rest of the httpx library. Transports are the objects that are **actually making the request**. For synchronous and asynchronous requests, `Hishel` offers two different transports. - CacheTransport - AsyncCacheTransport `Hishel` always needs a transport to work on top of it, as long as he **respects the custom or third-party transports that are offered.** Example: ```python import hishel import httpx with httpx.HTTPTransport() as transport: with hishel.CacheTransport(transport=transport) as cache_transport: request = httpx.Request("GET", "https://example.com/cachable-endpoint") cache_transport.handle_request(request) response = cache_transport.handle_request(request) # from the cache! ``` #### Using the Transports with the Clients If you have a transport, you can provide it to clients who will use it for underlying requests. ```python import hishel import httpx cache_transport = hishel.CacheTransport(transport=httpx.HTTPTransport()) with httpx.Client(transport=cache_transport) as client: client.get("https://example.com/cachable-endpoint") response = client.get("https://example.com/cachable-endpoint") # from the cache ``` #### Specifying the Transport storage In the same way that we can choose the storage for our clients, we can do the same for our transport. ```python import hishel storage = hishel.RedisStorage() with httpx.HTTPTransport() as transport: with hishel.CacheTransport(transport=transport, storage=storage) as cache_transport: request = httpx.Request("GET", "https://example.com/cachable-endpoint") cache_transport.handle_request(request) ``` #### Combining with the existing Transports Assume you already have a custom transport adapted to your business logic that you use for all requests; this is how you can add the caching layer on top of it. ```python import hishel import httpx from my_custom_transports import MyLovelyTransport cache_transport = hishel.CacheTransport(transport=MyLovelyTransport()) with httpx.Client(transport=cache_transport) as client: client.get("https://example.com/cachable-endpoint") response = client.get("https://example.com/cachable-endpoint") # from the cache ``` ### Using the Connection Pool `Hishel` also provides caching support for the httpcore library, which handles all of the low-level network staff for httpx. You may skip this section if you do not use [HTTP Core](https://github.com/encode/httpcore). Example: ```python import hishel import httpcore with httpcore.ConnectionPool() as pool: with hishel.CacheConnectionPool(pool=pool) as cache_pool: cache_pool.get("https://example.com/cachable-endpoint") response = cache_pool.get("https://example.com/cachable-endpoint") # from the cache ``` #### Specifying the Connection Pool storage In the same way that we can choose the storage for our clients and transports, we can do the same for our connection pools. ```python import hishel import httpcore storage = hishel.RedisStorage() with httpcore.ConnectionPool() as pool: with hishel.CacheConnectionPool(pool=pool, storage=storage) as cache_pool: cache_pool.get("https://example.com/cachable-endpoint") response = cache_pool.get("https://example.com/cachable-endpoint") # from the cache ``` ### Temporarily Disabling the Cache `Hishel` allows you to temporarily disable the cache for specific requests using the `cache_disabled` extension. Per RFC9111, the cache can effectively be disabled using the `Cache-Control` headers `no-store` (which requests that the response not be added to the cache), and `max-age=0` (which demands that any response in the cache must have 0 age - i.e. be a new request). `Hishel` respects this behavior, which can be used in two ways. First, you can specify the headers directly: ```python import hishel import httpx # With the clients client = hishel.CacheClient() client.get( "https://example.com/cacheable-endpoint", headers=[("Cache-Control", "no-store"), ("Cache-Control", "max-age=0")] ) # Ignores the cache # With the transport cache_transport = hishel.CacheTransport(transport=httpx.HTTPTransport()) client = httpx.Client(transport=cache_transport) client.get( "https://example.com/cacheable-endpoint", headers=[("Cache-Control", "no-store"), ("Cache-Control", "max-age=0")] ) # Ignores the cache ``` Since this can be cumbersome, `Hishel` also provides some "syntactic sugar" to accomplish the same result using `HTTPX` extensions: ```python import hishel import httpx # With the clients client = hishel.CacheClient() client.get("https://example.com/cacheable-endpoint", extensions={"cache_disabled": True}) # Ignores the cache # With the transport cache_transport = hishel.CacheTransport(transport=httpx.HTTPTransport()) client = httpx.Client(transport=cache_transport) client.get("https://example.com/cacheable-endpoint", extensions={"cache_disabled": True}) # Ignores the cache ``` Both of these are entirely equivalent to specifying the headers directly. hishel-0.1.2/hishel/000077500000000000000000000000001477404575600142655ustar00rootroot00000000000000hishel-0.1.2/hishel/__init__.py000066400000000000000000000005601477404575600163770ustar00rootroot00000000000000import httpx from ._async import * from ._controller import * from ._exceptions import * from ._headers import * from ._serializers import * from ._sync import * from ._lfu_cache import * def install_cache() -> None: # pragma: no cover httpx.AsyncClient = AsyncCacheClient # type: ignore httpx.Client = CacheClient # type: ignore __version__ = "0.1.2" hishel-0.1.2/hishel/_async/000077500000000000000000000000001477404575600155415ustar00rootroot00000000000000hishel-0.1.2/hishel/_async/__init__.py000066400000000000000000000002731477404575600176540ustar00rootroot00000000000000from ._client import * # noqa: F403 from ._mock import * # noqa: F403 from ._pool import * # noqa: F403 from ._storages import * # noqa: F403 from ._transports import * # noqa: F403 hishel-0.1.2/hishel/_async/_client.py000066400000000000000000000021151477404575600175270ustar00rootroot00000000000000import typing as tp import httpx from hishel._async._transports import AsyncCacheTransport __all__ = ("AsyncCacheClient",) class AsyncCacheClient(httpx.AsyncClient): def __init__(self, *args: tp.Any, **kwargs: tp.Any): self._storage = kwargs.pop("storage") if "storage" in kwargs else None self._controller = kwargs.pop("controller") if "controller" in kwargs else None super().__init__(*args, **kwargs) def _init_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore _transport = super()._init_transport(*args, **kwargs) return AsyncCacheTransport( transport=_transport, storage=self._storage, controller=self._controller, ) def _init_proxy_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore _transport = super()._init_proxy_transport(*args, **kwargs) # pragma: no cover return AsyncCacheTransport( # pragma: no cover transport=_transport, storage=self._storage, controller=self._controller, ) hishel-0.1.2/hishel/_async/_mock.py000066400000000000000000000027531477404575600172120ustar00rootroot00000000000000import typing as tp from types import TracebackType import httpcore import httpx from httpcore._async.interfaces import AsyncRequestInterface if tp.TYPE_CHECKING: # pragma: no cover from typing_extensions import Self __all__ = ("MockAsyncConnectionPool", "MockAsyncTransport") class MockAsyncConnectionPool(AsyncRequestInterface): async def handle_async_request(self, request: httpcore.Request) -> httpcore.Response: assert isinstance(request.stream, tp.AsyncIterable) data = b"".join([chunk async for chunk in request.stream]) # noqa: F841 return self.mocked_responses.pop(0) def add_responses(self, responses: tp.List[httpcore.Response]) -> None: if not hasattr(self, "mocked_responses"): self.mocked_responses = [] self.mocked_responses.extend(responses) async def __aenter__(self) -> "Self": return self async def __aexit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[TracebackType] = None, ) -> None: ... class MockAsyncTransport(httpx.AsyncBaseTransport): async def handle_async_request(self, request: httpx.Request) -> httpx.Response: return self.mocked_responses.pop(0) def add_responses(self, responses: tp.List[httpx.Response]) -> None: if not hasattr(self, "mocked_responses"): self.mocked_responses = [] self.mocked_responses.extend(responses) hishel-0.1.2/hishel/_async/_pool.py000066400000000000000000000200031477404575600172160ustar00rootroot00000000000000from __future__ import annotations import types import typing as tp from httpcore._async.interfaces import AsyncRequestInterface from httpcore._exceptions import ConnectError from httpcore._models import Request, Response from .._controller import Controller, allowed_stale from .._headers import parse_cache_control from .._serializers import JSONSerializer, Metadata from .._utils import extract_header_values_decoded from ._storages import AsyncBaseStorage, AsyncFileStorage T = tp.TypeVar("T") __all__ = ("AsyncCacheConnectionPool",) async def fake_stream(content: bytes) -> tp.AsyncIterable[bytes]: yield content def generate_504() -> Response: return Response(status=504) class AsyncCacheConnectionPool(AsyncRequestInterface): """An HTTP Core Connection Pool that supports HTTP caching. :param pool: `Connection Pool` that our class wraps in order to add an HTTP Cache layer on top of :type pool: AsyncRequestInterface :param storage: Storage that handles how the responses should be saved., defaults to None :type storage: tp.Optional[AsyncBaseStorage], optional :param controller: Controller that manages the cache behavior at the specification level, defaults to None :type controller: tp.Optional[Controller], optional """ def __init__( self, pool: AsyncRequestInterface, storage: tp.Optional[AsyncBaseStorage] = None, controller: tp.Optional[Controller] = None, ) -> None: self._pool = pool self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer()) if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`") self._controller = controller if controller is not None else Controller() async def handle_async_request(self, request: Request) -> Response: """ Handles HTTP requests while also implementing HTTP caching. :param request: An HTTP request :type request: httpcore.Request :return: An HTTP response :rtype: httpcore.Response """ if request.extensions.get("cache_disabled", False): request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")]) if request.method.upper() not in [b"GET", b"HEAD"]: # If the HTTP method is, for example, POST, # we must also use the request data to generate the hash. assert isinstance(request.stream, tp.AsyncIterable) body_for_key = b"".join([chunk async for chunk in request.stream]) request.stream = fake_stream(body_for_key) else: body_for_key = b"" key = self._controller._key_generator(request, body_for_key) stored_data = await self._storage.retrieve(key) request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control")) if request_cache_control.only_if_cached and not stored_data: return generate_504() if stored_data: # Try using the stored response if it was discovered. stored_response, stored_request, metadata = stored_data # Immediately read the stored response to avoid issues when trying to access the response body. stored_response.read() res = self._controller.construct_response_from_cache( request=request, response=stored_response, original_request=stored_request, ) if isinstance(res, Response): # Simply use the response if the controller determines it is ready for use. return await self._create_hishel_response( key=key, response=stored_response, request=request, metadata=metadata, cached=True, revalidated=False, ) if request_cache_control.only_if_cached: return generate_504() if isinstance(res, Request): # Controller has determined that the response needs to be re-validated. try: revalidation_response = await self._pool.handle_async_request(res) except ConnectError: # If there is a connection error, we can use the stale response if allowed. if self._controller._allow_stale and allowed_stale(response=stored_response): return await self._create_hishel_response( key=key, response=stored_response, request=request, metadata=metadata, cached=True, revalidated=False, ) raise # pragma: no cover # Merge headers with the stale response. final_response = self._controller.handle_validation_response( old_response=stored_response, new_response=revalidation_response ) await final_response.aread() # RFC 9111: 4.3.3. Handling a Validation Response # A 304 (Not Modified) response status code indicates that the stored response can be updated and # reused. A full response (i.e., one containing content) indicates that none of the stored responses # nominated in the conditional request are suitable. Instead, the cache MUST use the full response to # satisfy the request. The cache MAY store such a full response, subject to its constraints. if revalidation_response.status != 304 and self._controller.is_cachable( request=request, response=final_response ): await self._storage.store(key, response=final_response, request=request) return await self._create_hishel_response( key=key, response=final_response, request=request, cached=revalidation_response.status == 304, revalidated=True, metadata=metadata, ) regular_response = await self._pool.handle_async_request(request) await regular_response.aread() if self._controller.is_cachable(request=request, response=regular_response): await self._storage.store(key, response=regular_response, request=request) return await self._create_hishel_response( key=key, response=regular_response, request=request, cached=False, revalidated=False ) async def _create_hishel_response( self, key: str, response: Response, request: Request, cached: bool, revalidated: bool, metadata: Metadata | None = None, ) -> Response: if cached: assert metadata metadata["number_of_uses"] += 1 await self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata) response.extensions["from_cache"] = True # type: ignore[index] response.extensions["cache_metadata"] = metadata # type: ignore[index] else: response.extensions["from_cache"] = False # type: ignore[index] response.extensions["revalidated"] = revalidated # type: ignore[index] return response async def aclose(self) -> None: await self._storage.aclose() if hasattr(self._pool, "aclose"): # pragma: no cover await self._pool.aclose() async def __aenter__(self: T) -> T: return self async def __aexit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[types.TracebackType] = None, ) -> None: await self.aclose() hishel-0.1.2/hishel/_async/_storages.py000066400000000000000000000701101477404575600201000ustar00rootroot00000000000000from __future__ import annotations import datetime import logging import os import time import typing as t import typing as tp import warnings from copy import deepcopy from pathlib import Path try: import boto3 from .._s3 import AsyncS3Manager except ImportError: # pragma: no cover boto3 = None # type: ignore try: import anysqlite except ImportError: # pragma: no cover anysqlite = None # type: ignore from httpcore import Request, Response if t.TYPE_CHECKING: # pragma: no cover from typing_extensions import TypeAlias from hishel._serializers import BaseSerializer, clone_model from .._files import AsyncFileManager from .._serializers import JSONSerializer, Metadata from .._synchronization import AsyncLock from .._utils import float_seconds_to_int_milliseconds logger = logging.getLogger("hishel.storages") __all__ = ( "AsyncBaseStorage", "AsyncFileStorage", "AsyncRedisStorage", "AsyncSQLiteStorage", "AsyncInMemoryStorage", "AsyncS3Storage", ) StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata] RemoveTypes = tp.Union[str, Response] try: import redis.asyncio as redis except ImportError: # pragma: no cover redis = None # type: ignore class AsyncBaseStorage: def __init__( self, serializer: tp.Optional[BaseSerializer] = None, ttl: tp.Optional[tp.Union[int, float]] = None, ) -> None: self._serializer = serializer or JSONSerializer() self._ttl = ttl async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: raise NotImplementedError() async def remove(self, key: RemoveTypes) -> None: raise NotImplementedError() async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: raise NotImplementedError() async def retrieve(self, key: str) -> tp.Optional[StoredResponse]: raise NotImplementedError() async def aclose(self) -> None: raise NotImplementedError() class AsyncFileStorage(AsyncBaseStorage): """ A simple file storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param base_path: A storage base path where the responses should be saved, defaults to None :type base_path: tp.Optional[Path], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional :param check_ttl_every: How often in seconds to check staleness of **all** cache files. Makes sense only with set `ttl`, defaults to 60 :type check_ttl_every: tp.Union[int, float] """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, base_path: tp.Optional[Path] = None, ttl: tp.Optional[tp.Union[int, float]] = None, check_ttl_every: tp.Union[int, float] = 60, ) -> None: super().__init__(serializer, ttl) self._base_path = Path(base_path) if base_path is not None else Path(".cache/hishel") self._gitignore_file = self._base_path / ".gitignore" if not self._base_path.is_dir(): self._base_path.mkdir(parents=True) if not self._gitignore_file.is_file(): with open(self._gitignore_file, "w", encoding="utf-8") as f: f.write("# Automatically created by Hishel\n*") self._file_manager = AsyncFileManager(is_binary=self._serializer.is_binary) self._lock = AsyncLock() self._check_ttl_every = check_ttl_every self._last_cleaned = time.monotonic() async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Optional[Metadata] """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) response_path = self._base_path / key async with self._lock: await self._file_manager.write_to( str(response_path), self._serializer.dumps(response=response, request=request, metadata=metadata), ) await self._remove_expired_caches(response_path) async def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) response_path = self._base_path / key async with self._lock: if response_path.exists(): response_path.unlink() async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ response_path = self._base_path / key async with self._lock: if response_path.exists(): atime = response_path.stat().st_atime old_mtime = response_path.stat().st_mtime await self._file_manager.write_to( str(response_path), self._serializer.dumps(response=response, request=request, metadata=metadata), ) # Restore the old atime and mtime (we use mtime to check the cache expiration time) os.utime(response_path, (atime, old_mtime)) return return await self.store(key, response, request, metadata) # pragma: no cover async def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and his HTTP request. :rtype: tp.Optional[StoredResponse] """ response_path = self._base_path / key await self._remove_expired_caches(response_path) async with self._lock: if response_path.exists(): read_data = await self._file_manager.read_from(str(response_path)) if len(read_data) != 0: return self._serializer.loads(read_data) return None async def aclose(self) -> None: # pragma: no cover return async def _remove_expired_caches(self, response_path: Path) -> None: if self._ttl is None: return if time.monotonic() - self._last_cleaned < self._check_ttl_every: if response_path.is_file(): age = time.time() - response_path.stat().st_mtime if age > self._ttl: response_path.unlink() return self._last_cleaned = time.monotonic() async with self._lock: with os.scandir(self._base_path) as entries: for entry in entries: try: if entry.is_file(): age = time.time() - entry.stat().st_mtime if age > self._ttl: os.unlink(entry.path) except FileNotFoundError: # pragma: no cover pass class AsyncSQLiteStorage(AsyncBaseStorage): """ A simple sqlite3 storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param connection: A connection for sqlite, defaults to None :type connection: tp.Optional[anysqlite.Connection], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, connection: tp.Optional[anysqlite.Connection] = None, ttl: tp.Optional[tp.Union[int, float]] = None, ) -> None: if anysqlite is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `sqlite` extension as shown.\n" "```pip install hishel[sqlite]```" ) super().__init__(serializer, ttl) self._connection: tp.Optional[anysqlite.Connection] = connection or None self._setup_lock = AsyncLock() self._setup_completed: bool = False self._lock = AsyncLock() async def _setup(self) -> None: async with self._setup_lock: if not self._setup_completed: if not self._connection: # pragma: no cover self._connection = await anysqlite.connect(".hishel.sqlite", check_same_thread=False) await self._connection.execute( "CREATE TABLE IF NOT EXISTS cache(key TEXT, data BLOB, date_created REAL)" ) await self._connection.commit() self._setup_completed = True async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata] """ await self._setup() assert self._connection metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) async with self._lock: await self._connection.execute("DELETE FROM cache WHERE key = ?", [key]) serialized_response = self._serializer.dumps(response=response, request=request, metadata=metadata) await self._connection.execute( "INSERT INTO cache(key, data, date_created) VALUES(?, ?, ?)", [key, serialized_response, time.time()] ) await self._connection.commit() await self._remove_expired_caches() async def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ await self._setup() assert self._connection if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) async with self._lock: await self._connection.execute("DELETE FROM cache WHERE key = ?", [key]) await self._connection.commit() async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ await self._setup() assert self._connection async with self._lock: cursor = await self._connection.execute("SELECT data FROM cache WHERE key = ?", [key]) row = await cursor.fetchone() if row is not None: serialized_response = self._serializer.dumps(response=response, request=request, metadata=metadata) await self._connection.execute("UPDATE cache SET data = ? WHERE key = ?", [serialized_response, key]) await self._connection.commit() return return await self.store(key, response, request, metadata) # pragma: no cover async def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ await self._setup() assert self._connection await self._remove_expired_caches() async with self._lock: cursor = await self._connection.execute("SELECT data FROM cache WHERE key = ?", [key]) row = await cursor.fetchone() if row is None: return None cached_response = row[0] return self._serializer.loads(cached_response) async def aclose(self) -> None: # pragma: no cover if self._connection is not None: await self._connection.close() async def _remove_expired_caches(self) -> None: assert self._connection if self._ttl is None: return async with self._lock: await self._connection.execute("DELETE FROM cache WHERE date_created + ? < ?", [self._ttl, time.time()]) await self._connection.commit() class AsyncRedisStorage(AsyncBaseStorage): """ A simple redis storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param client: A client for redis, defaults to None :type client: tp.Optional["redis.Redis"], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, client: tp.Optional[redis.Redis] = None, # type: ignore ttl: tp.Optional[tp.Union[int, float]] = None, ) -> None: if redis is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `redis` extension as shown.\n" "```pip install hishel[redis]```" ) super().__init__(serializer, ttl) if client is None: self._client = redis.Redis() # type: ignore else: # pragma: no cover self._client = client async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata] """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) if self._ttl is not None: px = float_seconds_to_int_milliseconds(self._ttl) else: px = None await self._client.set( key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px ) async def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) await self._client.delete(key) async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ ttl_in_milliseconds = await self._client.pttl(key) # -2: if the key does not exist in Redis # -1: if the key exists in Redis but has no expiration if ttl_in_milliseconds == -2 or ttl_in_milliseconds == -1: # pragma: no cover await self.store(key, response, request, metadata) else: await self._client.set( key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=ttl_in_milliseconds, ) async def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ cached_response = await self._client.get(key) if cached_response is None: return None return self._serializer.loads(cached_response) async def aclose(self) -> None: # pragma: no cover await self._client.close() class AsyncInMemoryStorage(AsyncBaseStorage): """ A simple in-memory storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional :param capacity: The maximum number of responses that can be cached, defaults to 128 :type capacity: int, optional """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, ttl: tp.Optional[tp.Union[int, float]] = None, capacity: int = 128, ) -> None: super().__init__(serializer, ttl) if serializer is not None: # pragma: no cover warnings.warn("The serializer is not used in the in-memory storage.", RuntimeWarning) from hishel import LFUCache self._cache: LFUCache[str, tp.Tuple[StoredResponse, float]] = LFUCache(capacity=capacity) self._lock = AsyncLock() async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata] """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) async with self._lock: response_clone = clone_model(response) request_clone = clone_model(request) stored_response: StoredResponse = (deepcopy(response_clone), deepcopy(request_clone), metadata) self._cache.put(key, (stored_response, time.monotonic())) await self._remove_expired_caches() async def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) async with self._lock: self._cache.remove_key(key) async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ async with self._lock: try: stored_response, created_at = self._cache.get(key) stored_response = (stored_response[0], stored_response[1], metadata) self._cache.put(key, (stored_response, created_at)) return except KeyError: # pragma: no cover pass await self.store(key, response, request, metadata) # pragma: no cover async def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ await self._remove_expired_caches() async with self._lock: try: stored_response, _ = self._cache.get(key) except KeyError: return None return stored_response async def aclose(self) -> None: # pragma: no cover return async def _remove_expired_caches(self) -> None: if self._ttl is None: return async with self._lock: keys_to_remove = set() for key in self._cache: created_at = self._cache.get(key)[1] if time.monotonic() - created_at > self._ttl: keys_to_remove.add(key) for key in keys_to_remove: self._cache.remove_key(key) class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover """ AWS S3 storage. :param bucket_name: The name of the bucket to store the responses in :type bucket_name: str :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional :param check_ttl_every: How often in seconds to check staleness of **all** cache files. Makes sense only with set `ttl`, defaults to 60 :type check_ttl_every: tp.Union[int, float] :param client: A client for S3, defaults to None :type client: tp.Optional[tp.Any], optional """ def __init__( self, bucket_name: str, serializer: tp.Optional[BaseSerializer] = None, ttl: tp.Optional[tp.Union[int, float]] = None, check_ttl_every: tp.Union[int, float] = 60, client: tp.Optional[tp.Any] = None, ) -> None: super().__init__(serializer, ttl) if boto3 is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `s3` extension as shown.\n" "```pip install hishel[s3]```" ) self._bucket_name = bucket_name client = client or boto3.client("s3") self._s3_manager = AsyncS3Manager( client=client, bucket_name=bucket_name, is_binary=self._serializer.is_binary, check_ttl_every=check_ttl_every, ) self._lock = AsyncLock() async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata]` """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) async with self._lock: serialized = self._serializer.dumps(response=response, request=request, metadata=metadata) await self._s3_manager.write_to(path=key, data=serialized) await self._remove_expired_caches(key) async def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) async with self._lock: await self._s3_manager.remove_entry(key) async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ async with self._lock: serialized = self._serializer.dumps(response=response, request=request, metadata=metadata) await self._s3_manager.write_to(path=key, data=serialized, only_metadata=True) async def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ await self._remove_expired_caches(key) async with self._lock: try: return self._serializer.loads(await self._s3_manager.read_from(path=key)) except Exception: return None async def aclose(self) -> None: # pragma: no cover return async def _remove_expired_caches(self, key: str) -> None: if self._ttl is None: return async with self._lock: converted_ttl = float_seconds_to_int_milliseconds(self._ttl) await self._s3_manager.remove_expired(ttl=converted_ttl, key=key) hishel-0.1.2/hishel/_async/_transports.py000066400000000000000000000257031477404575600205000ustar00rootroot00000000000000from __future__ import annotations import types import typing as tp import httpcore import httpx from httpx import AsyncByteStream, Request, Response from httpx._exceptions import ConnectError from hishel._utils import extract_header_values_decoded, normalized_url from .._controller import Controller, allowed_stale from .._headers import parse_cache_control from .._serializers import JSONSerializer, Metadata from ._storages import AsyncBaseStorage, AsyncFileStorage if tp.TYPE_CHECKING: # pragma: no cover from typing_extensions import Self __all__ = ("AsyncCacheTransport",) async def fake_stream(content: bytes) -> tp.AsyncIterable[bytes]: yield content def generate_504() -> Response: return Response(status_code=504) class AsyncCacheStream(AsyncByteStream): def __init__(self, httpcore_stream: tp.AsyncIterable[bytes]): self._httpcore_stream = httpcore_stream async def __aiter__(self) -> tp.AsyncIterator[bytes]: async for part in self._httpcore_stream: yield part async def aclose(self) -> None: if hasattr(self._httpcore_stream, "aclose"): await self._httpcore_stream.aclose() class AsyncCacheTransport(httpx.AsyncBaseTransport): """ An HTTPX Transport that supports HTTP caching. :param transport: `Transport` that our class wraps in order to add an HTTP Cache layer on top of :type transport: httpx.AsyncBaseTransport :param storage: Storage that handles how the responses should be saved., defaults to None :type storage: tp.Optional[AsyncBaseStorage], optional :param controller: Controller that manages the cache behavior at the specification level, defaults to None :type controller: tp.Optional[Controller], optional """ def __init__( self, transport: httpx.AsyncBaseTransport, storage: tp.Optional[AsyncBaseStorage] = None, controller: tp.Optional[Controller] = None, ) -> None: self._transport = transport self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer()) if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`") self._controller = controller if controller is not None else Controller() async def handle_async_request(self, request: Request) -> Response: """ Handles HTTP requests while also implementing HTTP caching. :param request: An HTTP request :type request: httpx.Request :return: An HTTP response :rtype: httpx.Response """ if request.extensions.get("cache_disabled", False): request.headers.update( [ ("Cache-Control", "no-store"), ("Cache-Control", "no-cache"), *[("cache-control", value) for value in request.headers.get_list("cache-control")], ] ) if request.method not in ["GET", "HEAD"]: # If the HTTP method is, for example, POST, # we must also use the request data to generate the hash. body_for_key = await request.aread() request.stream = AsyncCacheStream(fake_stream(body_for_key)) else: body_for_key = b"" # Construct the HTTPCore request because Controllers and Storages work with HTTPCore requests. httpcore_request = httpcore.Request( method=request.method, url=httpcore.URL( scheme=request.url.raw_scheme, host=request.url.raw_host, port=request.url.port, target=request.url.raw_path, ), headers=request.headers.raw, content=request.stream, extensions=request.extensions, ) key = self._controller._key_generator(httpcore_request, body_for_key) stored_data = await self._storage.retrieve(key) request_cache_control = parse_cache_control( extract_header_values_decoded(request.headers.raw, b"Cache-Control") ) if request_cache_control.only_if_cached and not stored_data: return generate_504() if stored_data: # Try using the stored response if it was discovered. stored_response, stored_request, metadata = stored_data # Immediately read the stored response to avoid issues when trying to access the response body. stored_response.read() res = self._controller.construct_response_from_cache( request=httpcore_request, response=stored_response, original_request=stored_request, ) if isinstance(res, httpcore.Response): # Simply use the response if the controller determines it is ready for use. return await self._create_hishel_response( key=key, response=res, request=httpcore_request, cached=True, revalidated=False, metadata=metadata, ) if request_cache_control.only_if_cached: return generate_504() if isinstance(res, httpcore.Request): # Controller has determined that the response needs to be re-validated. assert isinstance(res.stream, tp.AsyncIterable) revalidation_request = Request( method=res.method.decode(), url=normalized_url(res.url), headers=res.headers, stream=AsyncCacheStream(res.stream), extensions=res.extensions, ) try: revalidation_response = await self._transport.handle_async_request(revalidation_request) except ConnectError: # If there is a connection error, we can use the stale response if allowed. if self._controller._allow_stale and allowed_stale(response=stored_response): return await self._create_hishel_response( key=key, response=stored_response, request=httpcore_request, cached=True, revalidated=False, metadata=metadata, ) raise # pragma: no cover assert isinstance(revalidation_response.stream, tp.AsyncIterable) httpcore_revalidation_response = httpcore.Response( status=revalidation_response.status_code, headers=revalidation_response.headers.raw, content=AsyncCacheStream(revalidation_response.stream), extensions=revalidation_response.extensions, ) # Merge headers with the stale response. final_httpcore_response = self._controller.handle_validation_response( old_response=stored_response, new_response=httpcore_revalidation_response, ) await final_httpcore_response.aread() await revalidation_response.aclose() assert isinstance(final_httpcore_response.stream, tp.AsyncIterable) # RFC 9111: 4.3.3. Handling a Validation Response # A 304 (Not Modified) response status code indicates that the stored response can be updated and # reused. A full response (i.e., one containing content) indicates that none of the stored responses # nominated in the conditional request are suitable. Instead, the cache MUST use the full response to # satisfy the request. The cache MAY store such a full response, subject to its constraints. if revalidation_response.status_code != 304 and self._controller.is_cachable( request=httpcore_request, response=final_httpcore_response ): await self._storage.store(key, response=final_httpcore_response, request=httpcore_request) return await self._create_hishel_response( key=key, response=final_httpcore_response, request=httpcore_request, cached=revalidation_response.status_code == 304, revalidated=True, metadata=metadata, ) regular_response = await self._transport.handle_async_request(request) assert isinstance(regular_response.stream, tp.AsyncIterable) httpcore_regular_response = httpcore.Response( status=regular_response.status_code, headers=regular_response.headers.raw, content=AsyncCacheStream(regular_response.stream), extensions=regular_response.extensions, ) await httpcore_regular_response.aread() await httpcore_regular_response.aclose() if self._controller.is_cachable(request=httpcore_request, response=httpcore_regular_response): await self._storage.store( key, response=httpcore_regular_response, request=httpcore_request, ) return await self._create_hishel_response( key=key, response=httpcore_regular_response, request=httpcore_request, cached=False, revalidated=False, ) async def _create_hishel_response( self, key: str, response: httpcore.Response, request: httpcore.Request, cached: bool, revalidated: bool, metadata: Metadata | None = None, ) -> Response: if cached: assert metadata metadata["number_of_uses"] += 1 await self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata) response.extensions["from_cache"] = True # type: ignore[index] response.extensions["cache_metadata"] = metadata # type: ignore[index] else: response.extensions["from_cache"] = False # type: ignore[index] response.extensions["revalidated"] = revalidated # type: ignore[index] return Response( status_code=response.status, headers=response.headers, stream=AsyncCacheStream(fake_stream(response.content)), extensions=response.extensions, ) async def aclose(self) -> None: await self._storage.aclose() await self._transport.aclose() async def __aenter__(self) -> Self: return self async def __aexit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[types.TracebackType] = None, ) -> None: await self.aclose() hishel-0.1.2/hishel/_controller.py000066400000000000000000000600741477404575600171700ustar00rootroot00000000000000import logging import typing as tp from httpcore import Request, Response from hishel._headers import Vary, parse_cache_control from ._utils import ( BaseClock, Clock, extract_header_values, extract_header_values_decoded, generate_key, get_safe_url, header_presents, parse_date, ) logger = logging.getLogger("hishel.controller") HEURISTICALLY_CACHEABLE_STATUS_CODES = (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501) HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"] __all__ = ("Controller", "HEURISTICALLY_CACHEABLE_STATUS_CODES") def get_updated_headers( stored_response_headers: tp.List[tp.Tuple[bytes, bytes]], new_response_headers: tp.List[tp.Tuple[bytes, bytes]], ) -> tp.List[tp.Tuple[bytes, bytes]]: updated_headers = [] checked = set() for key, value in stored_response_headers: if key not in checked and key.lower() != b"content-length": checked.add(key) values = extract_header_values(new_response_headers, key) if values: updated_headers.extend([(key, value) for value in values]) else: values = extract_header_values(stored_response_headers, key) updated_headers.extend([(key, value) for value in values]) for key, value in new_response_headers: if key not in checked and key.lower() != b"content-length": values = extract_header_values(new_response_headers, key) updated_headers.extend([(key, value) for value in values]) return updated_headers def get_freshness_lifetime(response: Response) -> tp.Optional[int]: response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control")) if response_cache_control.max_age is not None: return response_cache_control.max_age if header_presents(response.headers, b"expires"): expires = extract_header_values_decoded(response.headers, b"expires", single=True)[0] expires_timestamp = parse_date(expires) if expires_timestamp is None: return None date = extract_header_values_decoded(response.headers, b"date", single=True)[0] date_timestamp = parse_date(date) if date_timestamp is None: return None return expires_timestamp - date_timestamp return None def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int: last_modified = extract_header_values_decoded(response.headers, b"last-modified", single=True) if last_modified: last_modified_timestamp = parse_date(last_modified[0]) if last_modified_timestamp is not None: now = clock.now() ONE_WEEK = 604_800 return min(ONE_WEEK, int((now - last_modified_timestamp) * 0.1)) ONE_DAY = 86_400 return ONE_DAY def get_age(response: Response, clock: "BaseClock") -> int: if not header_presents(response.headers, b"date"): # If the response does not have a date header, then it is impossible to calculate the age. # Instead of raising an exception, we return infinity to be sure that the response is not considered fresh. return float("inf") # type: ignore date = parse_date(extract_header_values_decoded(response.headers, b"date")[0]) if date is None: return float("inf") # type: ignore now = clock.now() apparent_age = max(0, now - date) return int(apparent_age) def allowed_stale(response: Response) -> bool: response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control")) if response_cache_control.no_cache: return False if response_cache_control.must_revalidate: return False return True class Controller: def __init__( self, cacheable_methods: tp.Optional[tp.List[str]] = None, cacheable_status_codes: tp.Optional[tp.List[int]] = None, cache_private: bool = True, allow_heuristics: bool = False, clock: tp.Optional[BaseClock] = None, allow_stale: bool = False, always_revalidate: bool = False, force_cache: bool = False, key_generator: tp.Optional[tp.Callable[[Request, tp.Optional[bytes]], str]] = None, ): self._cacheable_methods = [] if cacheable_methods is None: self._cacheable_methods.append("GET") else: for method in cacheable_methods: if method.upper() not in HTTP_METHODS: raise RuntimeError( f"Hishel does not support the HTTP method `{method}`.\n" f"Please use the methods from this list: {HTTP_METHODS}" ) self._cacheable_methods.append(method.upper()) self._cacheable_status_codes = cacheable_status_codes if cacheable_status_codes else [200, 301, 308] self._cache_private = cache_private self._clock = clock if clock else Clock() self._allow_heuristics = allow_heuristics self._allow_stale = allow_stale self._always_revalidate = always_revalidate self._force_cache = force_cache self._key_generator = key_generator or generate_key def is_cachable(self, request: Request, response: Response) -> bool: """ Determines whether the response may be cached. The only thing this method does is determine whether the response associated with this request can be cached for later use. `https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-in-caches` lists the steps that this method simply follows. """ method = request.method.decode("ascii") force_cache = request.extensions.get("force_cache", None) if response.status not in self._cacheable_status_codes: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " f"as not cachable since its status code ({response.status})" " is not in the list of cacheable status codes." ) ) return False if response.status in (301, 308): logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as cachable since its status code is a permanent redirect." ) ) return True # the request method is understood by the cache if method not in self._cacheable_methods: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " f"as not cachable since the request method ({method}) is not in the list of cacheable methods." ) ) return False if force_cache if force_cache is not None else self._force_cache: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as cachable since the request is forced to use the cache." ) ) return True response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"cache-control")) request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"cache-control")) # the response status code is final if response.status // 100 == 1: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as not cachable since its status code is informational." ) ) return False # the no-store cache directive is not present (see Section 5.2.2.5) if request_cache_control.no_store: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as not cachable since the request contains the no-store directive." ) ) return False # note that the must-understand cache directive overrides # no-store in certain circumstances; see Section 5.2.2.3. if response_cache_control.no_store: if response_cache_control.must_understand: logger.debug( ( f"Skipping the no-store directive for the resource located at {get_safe_url(request.url)} " "since the response contains the must-understand directive." ) ) else: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as not cachable since the response contains the no-store directive." ) ) return False # a shared cache must not store a response with private directive # Note that we do not implement special handling for the qualified form, # which would only forbid storing specified headers. if not self._cache_private and response_cache_control.private: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as not cachable since the response contains the private directive." ) ) return False expires_presents = header_presents(response.headers, b"expires") # the response contains at least one of the following: # - a public response directive (see Section 5.2.2.9); # - a private response directive, if the cache is not shared (see Section 5.2.2.7); # - an Expires header field (see Section 5.3); # - a max-age response directive (see Section 5.2.2.1); # - if the cache is shared: an s-maxage response directive (see Section 5.2.2.10); # - a cache extension that allows it to be cached (see Section 5.2.3); or # - a status code that is defined as heuristically cacheable (see Section 4.2.2). if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as cachable since its status code is heuristically cacheable." ) ) return True if not any( [ response_cache_control.public, response_cache_control.private, expires_presents, response_cache_control.max_age is not None, ] ): logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as not cachable since it does not contain any of the required cache directives." ) ) return False logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as cachable since it meets the criteria for being stored in the cache." ) ) # response is a cachable! return True def _make_request_conditional(self, request: Request, response: Response) -> None: """ Adds the precondition headers needed for response validation. This method will use the "Last-Modified" or "Etag" headers if they are provided in order to create precondition headers. See also (https://www.rfc-editor.org/rfc/rfc9111.html#name-sending-a-validation-reques) """ if header_presents(response.headers, b"last-modified"): last_modified = extract_header_values(response.headers, b"last-modified", single=True)[0] logger.debug( ( f"Adding the 'If-Modified-Since' header with the value of '{last_modified.decode('ascii')}' " f"to the request for the resource located at {get_safe_url(request.url)}." ) ) else: last_modified = None if header_presents(response.headers, b"etag"): etag = extract_header_values(response.headers, b"etag", single=True)[0] logger.debug( ( f"Adding the 'If-None-Match' header with the value of '{etag.decode('ascii')}' " f"to the request for the resource located at {get_safe_url(request.url)}." ) ) else: etag = None precondition_headers: tp.List[tp.Tuple[bytes, bytes]] = [] if last_modified: precondition_headers.append((b"If-Modified-Since", last_modified)) if etag: precondition_headers.append((b"If-None-Match", etag)) request.headers.extend(precondition_headers) def _validate_vary(self, request: Request, response: Response, original_request: Request) -> bool: """ Determines whether the "vary" headers in the request and response headers are identical. See also (https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-cache-keys-with). """ vary_headers = extract_header_values_decoded(response.headers, b"vary") vary = Vary.from_value(vary_values=vary_headers) for vary_header in vary._values: if vary_header == "*": return False # pragma: no cover if extract_header_values(request.headers, vary_header) != extract_header_values( original_request.headers, vary_header ): return False return True def construct_response_from_cache( self, request: Request, response: Response, original_request: Request ) -> tp.Union[Response, Request, None]: """ Specifies whether the response should be used, skipped, or validated by the cache. This method makes a decision regarding what to do with the stored response when it is retrieved from storage. It might be ready for use or it might need to be revalidated. This method mirrors the relevant section from RFC 9111, see (https://www.rfc-editor.org/rfc/rfc9111.html#name-constructing-responses-from). Returns: Response: This response is applicable to the request. Request: This response can be used for this request, but it must first be revalidated. None: It is not possible to use this response for this request. """ # Use of responses with status codes 301 and 308 is always # legal as long as they don't adhere to any caching rules. if response.status in (301, 308): logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as valid for cache use since its status code is a permanent redirect." ) ) return response response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control")) request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control")) # request header fields nominated by the stored # response (if any) match those presented (see Section 4.1) if not self._validate_vary(request=request, response=response, original_request=original_request): # If the vary headers does not match, then do not use the response logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as invalid for cache use since the vary headers do not match." ) ) return None # pragma: no cover # !!! this should be after the "vary" header validation. force_cache = request.extensions.get("force_cache", None) if force_cache if force_cache is not None else self._force_cache: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as valid for cache use since the request is forced to use the cache." ) ) return response # the stored response does not contain the # no-cache directive (Section 5.2.2.4), unless # it is successfully validated (Section 4.3) if ( self._always_revalidate or response_cache_control.no_cache or response_cache_control.must_revalidate or request_cache_control.no_cache ): if self._always_revalidate: log_text = ( f"Considering the resource located at {get_safe_url(request.url)} " "as needing revalidation since the cache is set to always revalidate." ) elif response_cache_control.no_cache: log_text = ( f"Considering the resource located at {get_safe_url(request.url)} " "as needing revalidation since the response contains the no-cache directive." ) elif response_cache_control.must_revalidate: log_text = ( f"Considering the resource located at {get_safe_url(request.url)} " "as needing revalidation since the response contains the must-revalidate directive." ) elif request_cache_control.no_cache: log_text = ( f"Considering the resource located at {get_safe_url(request.url)} " "as needing revalidation since the request contains the no-cache directive." ) else: assert False, "Unreachable code " # pragma: no cover logger.debug(log_text) self._make_request_conditional(request=request, response=response) return request freshness_lifetime = get_freshness_lifetime(response) if freshness_lifetime is None: logger.debug( ( "Could not determine the freshness lifetime of " f"the resource located at {get_safe_url(request.url)}, " "trying to use heuristics to calculate it." ) ) if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES: freshness_lifetime = get_heuristic_freshness(response=response, clock=self._clock) logger.debug( ( f"Successfully calculated the freshness lifetime of the resource located at " f"{get_safe_url(request.url)} using heuristics." ) ) else: logger.debug( ( "Could not calculate the freshness lifetime of " f"the resource located at {get_safe_url(request.url)}. " "Making a conditional request to revalidate the response." ) ) # If Freshness cannot be calculated, then send the request self._make_request_conditional(request=request, response=response) return request age = get_age(response, self._clock) is_fresh = freshness_lifetime > age # The min-fresh request directive indicates that the client # prefers a response whose freshness lifetime is no less than # its current age plus the specified time in seconds. # That is, the client wants a response that will still # be fresh for at least the specified number of seconds. if request_cache_control.min_fresh is not None: if freshness_lifetime < (age + request_cache_control.min_fresh): logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as invalid for cache use since the time left for " "freshness is less than the min-fresh directive." ) ) return None # The max-stale request directive indicates that the # client will accept a response that has exceeded its freshness lifetime. # If a value is present, then the client is willing to accept a response # that has exceeded its freshness lifetime by no more than the specified # number of seconds. If no value is assigned to max-stale, then # the client will accept a stale response of any age. if not is_fresh and request_cache_control.max_stale is not None: exceeded_freshness_lifetime = age - freshness_lifetime if request_cache_control.max_stale < exceeded_freshness_lifetime: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as invalid for cache use since the freshness lifetime has been exceeded more than max-stale." ) ) return None else: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as valid for cache use since the freshness lifetime has been exceeded less than max-stale." ) ) return response # The max-age request directive indicates that # the client prefers a response whose age is # less than or equal to the specified number of seconds. # Unless the max-stale request directive is also present, # the client does not wish to receive a stale response. if request_cache_control.max_age is not None: if request_cache_control.max_age < age: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as invalid for cache use since the age of the response exceeds the max-age directive." ) ) return None # the stored response is one of the following: # fresh (see Section 4.2), or # allowed to be served stale (see Section 4.2.4), or # successfully validated (see Section 4.3). if is_fresh: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as valid for cache use since it is fresh." ) ) return response else: logger.debug( ( f"Considering the resource located at {get_safe_url(request.url)} " "as needing revalidation since it is not fresh." ) ) # Otherwise, make a conditional request self._make_request_conditional(request=request, response=response) return request def handle_validation_response(self, old_response: Response, new_response: Response) -> Response: """ Handles incoming validation response. This method takes care of what to do with the incoming validation response; if it is a 304 response, it updates the headers with the new response and returns it. This method mirrors the relevant section from RFC 9111, see (https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo). """ if new_response.status == 304: headers = get_updated_headers( stored_response_headers=old_response.headers, new_response_headers=new_response.headers, ) old_response.headers = headers return old_response else: return new_response hishel-0.1.2/hishel/_exceptions.py000066400000000000000000000003061477404575600171560ustar00rootroot00000000000000__all__ = ("CacheControlError", "ParseError", "ValidationError") class CacheControlError(Exception): ... class ParseError(CacheControlError): ... class ValidationError(CacheControlError): ... hishel-0.1.2/hishel/_files.py000066400000000000000000000042641477404575600161060ustar00rootroot00000000000000import typing as tp import anyio class AsyncBaseFileManager: def __init__(self, is_binary: bool) -> None: self.is_binary = is_binary async def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None: raise NotImplementedError() async def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]: raise NotImplementedError() class AsyncFileManager(AsyncBaseFileManager): async def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None: is_binary = self.is_binary if is_binary is None else is_binary mode = "wb" if is_binary else "wt" async with await anyio.open_file(path, mode) as f: # type: ignore[call-overload] await f.write(data) async def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]: is_binary = self.is_binary if is_binary is None else is_binary mode = "rb" if is_binary else "rt" async with await anyio.open_file(path, mode) as f: # type: ignore[call-overload] return tp.cast(tp.Union[bytes, str], await f.read()) class BaseFileManager: def __init__(self, is_binary: bool) -> None: self.is_binary = is_binary def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None: raise NotImplementedError() def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]: raise NotImplementedError() class FileManager(BaseFileManager): def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None: is_binary = self.is_binary if is_binary is None else is_binary mode = "wb" if is_binary else "wt" with open(path, mode) as f: f.write(data) def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]: is_binary = self.is_binary if is_binary is None else is_binary mode = "rb" if is_binary else "rt" with open(path, mode) as f: return tp.cast(tp.Union[bytes, str], f.read()) hishel-0.1.2/hishel/_headers.py000066400000000000000000000162331477404575600164160ustar00rootroot00000000000000import string from typing import Any, Dict, List, Optional, Union from ._exceptions import ParseError, ValidationError ## Grammar HTAB = "\t" SP = " " obs_text = "".join(chr(i) for i in range(0x80, 0xFF + 1)) # 0x80-0xFF tchar = "!#$%&'*+-.^_`|~0123456789" + string.ascii_letters qdtext = "".join( [ HTAB, SP, "\x21", "".join(chr(i) for i in range(0x23, 0x5B + 1)), # 0x23-0x5b "".join(chr(i) for i in range(0x5D, 0x7E + 1)), # 0x5D-0x7E obs_text, ] ) TIME_FIELDS = [ "max_age", "max_stale", "min_fresh", "s_maxage", ] BOOLEAN_FIELDS = [ "immutable", "must_revalidate", "must_understand", "no_store", "no_transform", "only_if_cached", "public", "proxy_revalidate", ] LIST_FIELDS = ["no_cache", "private"] __all__ = ( "CacheControl", "Vary", ) def strip_ows_around(text: str) -> str: return text.strip(" ").strip("\t") def normalize_directive(text: str) -> str: return text.replace("-", "_") def parse_cache_control(cache_control_values: List[str]) -> "CacheControl": directives = {} for cache_control_value in cache_control_values: if "no-cache=" in cache_control_value or "private=" in cache_control_value: cache_control_splited = [cache_control_value] else: cache_control_splited = cache_control_value.split(",") for directive in cache_control_splited: key: str = "" value: Optional[str] = None dquote = False if not directive: raise ParseError("The directive should not be left blank.") directive = strip_ows_around(directive) if not directive: raise ParseError("The directive should not contain only whitespaces.") for i, key_char in enumerate(directive): if key_char == "=": value = directive[i + 1 :] if not value: raise ParseError("The directive value cannot be left blank.") if value[0] == '"': dquote = True if dquote and value[-1] != '"': raise ParseError("Invalid quotes around the value.") if not dquote: for value_char in value: if value_char not in tchar: raise ParseError( f"The character '{value_char!r}' is not permitted for the unquoted values." ) else: for value_char in value[1:-1]: if value_char not in qdtext: raise ParseError( f"The character '{value_char!r}' is not permitted for the quoted values." ) break if key_char not in tchar: raise ParseError(f"The character '{key_char!r}' is not permitted in the directive name.") key += key_char directives[key] = value validated_data = CacheControl.validate(directives) return CacheControl(**validated_data) class Vary: def __init__(self, values: List[str]) -> None: self._values = values @classmethod def from_value(cls, vary_values: List[str]) -> "Vary": values = [] for vary_value in vary_values: for field_name in vary_value.split(","): field_name = field_name.strip() values.append(field_name) return Vary(values) class CacheControl: def __init__( self, immutable: bool = False, # [RFC8246] max_age: Optional[int] = None, # [RFC9111, Section 5.2.1.1, 5.2.2.1] max_stale: Optional[int] = None, # [RFC9111, Section 5.2.1.2] min_fresh: Optional[int] = None, # [RFC9111, Section 5.2.1.3] must_revalidate: bool = False, # [RFC9111, Section 5.2.2.2] must_understand: bool = False, # [RFC9111, Section 5.2.2.3] no_cache: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.1.4, 5.2.2.4] no_store: bool = False, # [RFC9111, Section 5.2.1.5, 5.2.2.5] no_transform: bool = False, # [RFC9111, Section 5.2.1.6, 5.2.2.6] only_if_cached: bool = False, # [RFC9111, Section 5.2.1.7] private: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.2.7] proxy_revalidate: bool = False, # [RFC9111, Section 5.2.2.8] public: bool = False, # [RFC9111, Section 5.2.2.9] s_maxage: Optional[int] = None, # [RFC9111, Section 5.2.2.10] ) -> None: self.immutable = immutable self.max_age = max_age self.max_stale = max_stale self.min_fresh = min_fresh self.must_revalidate = must_revalidate self.must_understand = must_understand self.no_cache = no_cache self.no_store = no_store self.no_transform = no_transform self.only_if_cached = only_if_cached self.private = private self.proxy_revalidate = proxy_revalidate self.public = public self.s_maxage = s_maxage @classmethod def validate(cls, directives: Dict[str, Any]) -> Dict[str, Any]: validated_data: Dict[str, Any] = {} for key, value in directives.items(): key = normalize_directive(key) if key in TIME_FIELDS: if value is None: raise ValidationError(f"The directive '{key}' necessitates a value.") if value[0] == '"' or value[-1] == '"': raise ValidationError(f"The argument '{key}' should be an integer, but a quote was found.") try: validated_data[key] = int(value) except Exception: raise ValidationError(f"The argument '{key}' should be an integer, but got '{value!r}'.") elif key in BOOLEAN_FIELDS: if value is not None: raise ValidationError(f"The directive '{key}' should have no value, but it does.") validated_data[key] = True elif key in LIST_FIELDS: if value is None: validated_data[key] = True else: values = [] for list_value in value[1:-1].split(","): if not list_value: raise ValidationError("The list value must not be empty.") list_value = strip_ows_around(list_value) values.append(list_value) validated_data[key] = values return validated_data def __repr__(self) -> str: fields = "" for key in TIME_FIELDS: key = key.replace("-", "_") value = getattr(self, key) if value: fields += f"{key}={value}, " for key in BOOLEAN_FIELDS: key = key.replace("-", "_") value = getattr(self, key) if value: fields += f"{key}, " fields = fields[:-2] return f"<{type(self).__name__} {fields}>" hishel-0.1.2/hishel/_lfu_cache.py000066400000000000000000000052231477404575600167110ustar00rootroot00000000000000from collections import OrderedDict from typing import DefaultDict, Dict, Generic, Iterator, Tuple, TypeVar K = TypeVar("K") V = TypeVar("V") __all__ = ["LFUCache"] class LFUCache(Generic[K, V]): def __init__(self, capacity: int): if capacity <= 0: raise ValueError("Capacity must be positive") self.capacity = capacity self.cache: Dict[K, Tuple[V, int]] = {} # To store key-value pairs self.freq_count: DefaultDict[int, OrderedDict[K, V]] = DefaultDict( OrderedDict ) # To store frequency of each key self.min_freq = 0 # To keep track of the minimum frequency def get(self, key: K) -> V: if key in self.cache: value, freq = self.cache[key] # Update frequency and move the key to the next frequency bucket self.freq_count[freq].pop(key) if not self.freq_count[freq]: # If the current frequency has no keys, update min_freq del self.freq_count[freq] if freq == self.min_freq: self.min_freq += 1 freq += 1 self.freq_count[freq][key] = value self.cache[key] = (value, freq) return value raise KeyError(f"Key {key} not found") def put(self, key: K, value: V) -> None: if key in self.cache: _, freq = self.cache[key] # Update frequency and move the key to the next frequency bucket self.freq_count[freq].pop(key) if not self.freq_count[freq]: del self.freq_count[freq] if freq == self.min_freq: self.min_freq += 1 freq += 1 self.freq_count[freq][key] = value self.cache[key] = (value, freq) else: # Check if cache is full, evict the least frequently used item if len(self.cache) == self.capacity: evicted_key, _ = self.freq_count[self.min_freq].popitem(last=False) del self.cache[evicted_key] # Add the new key-value pair with frequency 1 self.cache[key] = (value, 1) self.freq_count[1][key] = value self.min_freq = 1 def remove_key(self, key: K) -> None: if key in self.cache: _, freq = self.cache[key] self.freq_count[freq].pop(key) if not self.freq_count[freq]: # If the current frequency has no keys, update min_freq del self.freq_count[freq] if freq == self.min_freq: self.min_freq += 1 del self.cache[key] def __iter__(self) -> Iterator[K]: yield from self.cache hishel-0.1.2/hishel/_s3.py000066400000000000000000000077671477404575600153440ustar00rootroot00000000000000import time import typing as tp from anyio import to_thread from botocore.exceptions import ClientError def get_timestamp_in_ms() -> float: return time.time() * 1000 class S3Manager: def __init__( self, client: tp.Any, bucket_name: str, check_ttl_every: tp.Union[int, float], is_binary: bool = False ): self._client = client self._bucket_name = bucket_name self._is_binary = is_binary self._last_cleaned = time.monotonic() self._check_ttl_every = check_ttl_every def write_to(self, path: str, data: tp.Union[bytes, str], only_metadata: bool = False) -> None: path = "hishel-" + path if isinstance(data, str): data = data.encode("utf-8") created_at = None if only_metadata: try: response = self._client.get_object( Bucket=self._bucket_name, Key=path, ) created_at = response["Metadata"]["created_at"] except Exception: pass self._client.put_object( Bucket=self._bucket_name, Key=path, Body=data, Metadata={"created_at": created_at or str(get_timestamp_in_ms())}, ) def read_from(self, path: str) -> tp.Union[bytes, str]: path = "hishel-" + path response = self._client.get_object( Bucket=self._bucket_name, Key=path, ) content = response["Body"].read() if self._is_binary: # pragma: no cover return tp.cast(bytes, content) return tp.cast(str, content.decode("utf-8")) def remove_expired(self, ttl: int, key: str) -> None: path = "hishel-" + key if time.monotonic() - self._last_cleaned < self._check_ttl_every: try: response = self._client.get_object(Bucket=self._bucket_name, Key=path) if get_timestamp_in_ms() - float(response["Metadata"]["created_at"]) > ttl: self._client.delete_object(Bucket=self._bucket_name, Key=path) return except ClientError as e: if e.response["Error"]["Code"] == "NoSuchKey": return raise e self._last_cleaned = time.monotonic() for obj in self._client.list_objects(Bucket=self._bucket_name).get("Contents", []): if not obj["Key"].startswith("hishel-"): # pragma: no cover continue try: metadata_obj = self._client.head_object(Bucket=self._bucket_name, Key=obj["Key"]).get("Metadata", {}) except ClientError as e: if e.response["Error"]["Code"] == "404": continue if not metadata_obj or "created_at" not in metadata_obj: continue if get_timestamp_in_ms() - float(metadata_obj["created_at"]) > ttl: self._client.delete_object(Bucket=self._bucket_name, Key=obj["Key"]) def remove_entry(self, key: str) -> None: path = "hishel-" + key self._client.delete_object(Bucket=self._bucket_name, Key=path) class AsyncS3Manager: # pragma: no cover def __init__( self, client: tp.Any, bucket_name: str, check_ttl_every: tp.Union[int, float], is_binary: bool = False ): self._sync_manager = S3Manager(client, bucket_name, check_ttl_every, is_binary) async def write_to(self, path: str, data: tp.Union[bytes, str], only_metadata: bool = False) -> None: return await to_thread.run_sync(self._sync_manager.write_to, path, data, only_metadata) async def read_from(self, path: str) -> tp.Union[bytes, str]: return await to_thread.run_sync(self._sync_manager.read_from, path) async def remove_expired(self, ttl: int, key: str) -> None: return await to_thread.run_sync(self._sync_manager.remove_expired, ttl, key) async def remove_entry(self, key: str) -> None: return await to_thread.run_sync(self._sync_manager.remove_entry, key) hishel-0.1.2/hishel/_serializers.py000066400000000000000000000265701477404575600173440ustar00rootroot00000000000000import base64 import json import pickle import typing as tp from datetime import datetime from httpcore import Request, Response from hishel._utils import normalized_url try: import yaml except ImportError: # pragma: no cover yaml = None # type: ignore HEADERS_ENCODING = "iso-8859-1" KNOWN_RESPONSE_EXTENSIONS = ("http_version", "reason_phrase") KNOWN_REQUEST_EXTENSIONS = ("timeout", "sni_hostname") __all__ = ("PickleSerializer", "JSONSerializer", "YAMLSerializer", "BaseSerializer", "clone_model") T = tp.TypeVar("T", Request, Response) def clone_model(model: T) -> T: if isinstance(model, Response): return Response( status=model.status, headers=model.headers, content=model.content, extensions={key: value for key, value in model.extensions.items() if key in KNOWN_RESPONSE_EXTENSIONS}, ) # type: ignore else: return Request( method=model.method, url=normalized_url(model.url), headers=model.headers, extensions={key: value for key, value in model.extensions.items() if key in KNOWN_REQUEST_EXTENSIONS}, ) # type: ignore class Metadata(tp.TypedDict): number_of_uses: int created_at: datetime cache_key: str class BaseSerializer: def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]: raise NotImplementedError() def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]: raise NotImplementedError() @property def is_binary(self) -> bool: raise NotImplementedError() class PickleSerializer(BaseSerializer): """ A simple pickle-based serializer. """ def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]: """ Dumps the HTTP response and its HTTP request. :param response: An HTTP response :type response: Response :param request: An HTTP request :type request: Request :param metadata: Additional information about the stored response :type metadata: Metadata :return: Serialized response :rtype: tp.Union[str, bytes] """ clone_response = clone_model(response) clone_request = clone_model(request) return pickle.dumps((clone_response, clone_request, metadata)) def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]: """ Loads the HTTP response and its HTTP request from serialized data. :param data: Serialized data :type data: tp.Union[str, bytes] :return: HTTP response and its HTTP request :rtype: tp.Tuple[Response, Request, Metadata] """ assert isinstance(data, bytes) return tp.cast(tp.Tuple[Response, Request, Metadata], pickle.loads(data)) @property def is_binary(self) -> bool: # pragma: no cover return True class JSONSerializer(BaseSerializer): """A simple json-based serializer.""" def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]: """ Dumps the HTTP response and its HTTP request. :param response: An HTTP response :type response: Response :param request: An HTTP request :type request: Request :param metadata: Additional information about the stored response :type metadata: Metadata :return: Serialized response :rtype: tp.Union[str, bytes] """ response_dict = { "status": response.status, "headers": [ (key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in response.headers ], "content": base64.b64encode(response.content).decode("ascii"), "extensions": { key: value.decode("ascii") for key, value in response.extensions.items() if key in KNOWN_RESPONSE_EXTENSIONS }, } request_dict = { "method": request.method.decode("ascii"), "url": normalized_url(request.url), "headers": [ (key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in request.headers ], "extensions": {key: value for key, value in request.extensions.items() if key in KNOWN_REQUEST_EXTENSIONS}, } metadata_dict = { "cache_key": metadata["cache_key"], "number_of_uses": metadata["number_of_uses"], "created_at": metadata["created_at"].strftime("%a, %d %b %Y %H:%M:%S GMT"), } full_json = { "response": response_dict, "request": request_dict, "metadata": metadata_dict, } return json.dumps(full_json, indent=4) def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]: """ Loads the HTTP response and its HTTP request from serialized data. :param data: Serialized data :type data: tp.Union[str, bytes] :return: HTTP response and its HTTP request :rtype: tp.Tuple[Response, Request, Metadata] """ full_json = json.loads(data) response_dict = full_json["response"] request_dict = full_json["request"] metadata_dict = full_json["metadata"] metadata_dict["created_at"] = datetime.strptime( metadata_dict["created_at"], "%a, %d %b %Y %H:%M:%S GMT", ) response = Response( status=response_dict["status"], headers=[ (key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING)) for key, value in response_dict["headers"] ], content=base64.b64decode(response_dict["content"].encode("ascii")), extensions={ key: value.encode("ascii") for key, value in response_dict["extensions"].items() if key in KNOWN_RESPONSE_EXTENSIONS }, ) request = Request( method=request_dict["method"], url=request_dict["url"], headers=[ (key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING)) for key, value in request_dict["headers"] ], extensions={ key: value for key, value in request_dict["extensions"].items() if key in KNOWN_REQUEST_EXTENSIONS }, ) metadata = Metadata( cache_key=metadata_dict["cache_key"], created_at=metadata_dict["created_at"], number_of_uses=metadata_dict["number_of_uses"], ) return response, request, metadata @property def is_binary(self) -> bool: return False class YAMLSerializer(BaseSerializer): """A simple yaml-based serializer.""" def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]: """ Dumps the HTTP response and its HTTP request. :param response: An HTTP response :type response: Response :param request: An HTTP request :type request: Request :param metadata: Additional information about the stored response :type metadata: Metadata :return: Serialized response :rtype: tp.Union[str, bytes] """ if yaml is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `yaml` extension as shown.\n" "```pip install hishel[yaml]```" ) response_dict = { "status": response.status, "headers": [ (key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in response.headers ], "content": base64.b64encode(response.content).decode("ascii"), "extensions": { key: value.decode("ascii") for key, value in response.extensions.items() if key in KNOWN_RESPONSE_EXTENSIONS }, } request_dict = { "method": request.method.decode("ascii"), "url": normalized_url(request.url), "headers": [ (key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in request.headers ], "extensions": {key: value for key, value in request.extensions.items() if key in KNOWN_REQUEST_EXTENSIONS}, } metadata_dict = { "cache_key": metadata["cache_key"], "number_of_uses": metadata["number_of_uses"], "created_at": metadata["created_at"].strftime("%a, %d %b %Y %H:%M:%S GMT"), } full_json = { "response": response_dict, "request": request_dict, "metadata": metadata_dict, } return yaml.safe_dump(full_json, sort_keys=False) def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]: """ Loads the HTTP response and its HTTP request from serialized data. :param data: Serialized data :type data: tp.Union[str, bytes] :raises RuntimeError: When used without the `yaml` extension installed :return: HTTP response and its HTTP request :rtype: tp.Tuple[Response, Request, Metadata] """ if yaml is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `yaml` extension as shown.\n" "```pip install hishel[yaml]```" ) full_json = yaml.safe_load(data) response_dict = full_json["response"] request_dict = full_json["request"] metadata_dict = full_json["metadata"] metadata_dict["created_at"] = datetime.strptime( metadata_dict["created_at"], "%a, %d %b %Y %H:%M:%S GMT", ) response = Response( status=response_dict["status"], headers=[ (key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING)) for key, value in response_dict["headers"] ], content=base64.b64decode(response_dict["content"].encode("ascii")), extensions={ key: value.encode("ascii") for key, value in response_dict["extensions"].items() if key in KNOWN_RESPONSE_EXTENSIONS }, ) request = Request( method=request_dict["method"], url=request_dict["url"], headers=[ (key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING)) for key, value in request_dict["headers"] ], extensions={ key: value for key, value in request_dict["extensions"].items() if key in KNOWN_REQUEST_EXTENSIONS }, ) metadata = Metadata( cache_key=metadata_dict["cache_key"], created_at=metadata_dict["created_at"], number_of_uses=metadata_dict["number_of_uses"], ) return response, request, metadata @property def is_binary(self) -> bool: # pragma: no cover return False hishel-0.1.2/hishel/_sync/000077500000000000000000000000001477404575600154005ustar00rootroot00000000000000hishel-0.1.2/hishel/_sync/__init__.py000066400000000000000000000002731477404575600175130ustar00rootroot00000000000000from ._client import * # noqa: F403 from ._mock import * # noqa: F403 from ._pool import * # noqa: F403 from ._storages import * # noqa: F403 from ._transports import * # noqa: F403 hishel-0.1.2/hishel/_sync/_client.py000066400000000000000000000020441477404575600173670ustar00rootroot00000000000000import typing as tp import httpx from hishel._sync._transports import CacheTransport __all__ = ("CacheClient",) class CacheClient(httpx.Client): def __init__(self, *args: tp.Any, **kwargs: tp.Any): self._storage = kwargs.pop("storage") if "storage" in kwargs else None self._controller = kwargs.pop("controller") if "controller" in kwargs else None super().__init__(*args, **kwargs) def _init_transport(self, *args, **kwargs) -> CacheTransport: # type: ignore _transport = super()._init_transport(*args, **kwargs) return CacheTransport( transport=_transport, storage=self._storage, controller=self._controller, ) def _init_proxy_transport(self, *args, **kwargs) -> CacheTransport: # type: ignore _transport = super()._init_proxy_transport(*args, **kwargs) # pragma: no cover return CacheTransport( # pragma: no cover transport=_transport, storage=self._storage, controller=self._controller, ) hishel-0.1.2/hishel/_sync/_mock.py000066400000000000000000000026261477404575600170500ustar00rootroot00000000000000import typing as tp from types import TracebackType import httpcore import httpx from httpcore._sync.interfaces import RequestInterface if tp.TYPE_CHECKING: # pragma: no cover from typing_extensions import Self __all__ = ("MockConnectionPool", "MockTransport") class MockConnectionPool(RequestInterface): def handle_request(self, request: httpcore.Request) -> httpcore.Response: assert isinstance(request.stream, tp.Iterable) data = b"".join([chunk for chunk in request.stream]) # noqa: F841 return self.mocked_responses.pop(0) def add_responses(self, responses: tp.List[httpcore.Response]) -> None: if not hasattr(self, "mocked_responses"): self.mocked_responses = [] self.mocked_responses.extend(responses) def __enter__(self) -> "Self": return self def __exit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[TracebackType] = None, ) -> None: ... class MockTransport(httpx.BaseTransport): def handle_request(self, request: httpx.Request) -> httpx.Response: return self.mocked_responses.pop(0) def add_responses(self, responses: tp.List[httpx.Response]) -> None: if not hasattr(self, "mocked_responses"): self.mocked_responses = [] self.mocked_responses.extend(responses) hishel-0.1.2/hishel/_sync/_pool.py000066400000000000000000000174301477404575600170670ustar00rootroot00000000000000from __future__ import annotations import types import typing as tp from httpcore._sync.interfaces import RequestInterface from httpcore._exceptions import ConnectError from httpcore._models import Request, Response from .._controller import Controller, allowed_stale from .._headers import parse_cache_control from .._serializers import JSONSerializer, Metadata from .._utils import extract_header_values_decoded from ._storages import BaseStorage, FileStorage T = tp.TypeVar("T") __all__ = ("CacheConnectionPool",) def fake_stream(content: bytes) -> tp.Iterable[bytes]: yield content def generate_504() -> Response: return Response(status=504) class CacheConnectionPool(RequestInterface): """An HTTP Core Connection Pool that supports HTTP caching. :param pool: `Connection Pool` that our class wraps in order to add an HTTP Cache layer on top of :type pool: RequestInterface :param storage: Storage that handles how the responses should be saved., defaults to None :type storage: tp.Optional[BaseStorage], optional :param controller: Controller that manages the cache behavior at the specification level, defaults to None :type controller: tp.Optional[Controller], optional """ def __init__( self, pool: RequestInterface, storage: tp.Optional[BaseStorage] = None, controller: tp.Optional[Controller] = None, ) -> None: self._pool = pool self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer()) if not isinstance(self._storage, BaseStorage): # pragma: no cover raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`") self._controller = controller if controller is not None else Controller() def handle_request(self, request: Request) -> Response: """ Handles HTTP requests while also implementing HTTP caching. :param request: An HTTP request :type request: httpcore.Request :return: An HTTP response :rtype: httpcore.Response """ if request.extensions.get("cache_disabled", False): request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")]) if request.method.upper() not in [b"GET", b"HEAD"]: # If the HTTP method is, for example, POST, # we must also use the request data to generate the hash. assert isinstance(request.stream, tp.Iterable) body_for_key = b"".join([chunk for chunk in request.stream]) request.stream = fake_stream(body_for_key) else: body_for_key = b"" key = self._controller._key_generator(request, body_for_key) stored_data = self._storage.retrieve(key) request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control")) if request_cache_control.only_if_cached and not stored_data: return generate_504() if stored_data: # Try using the stored response if it was discovered. stored_response, stored_request, metadata = stored_data # Immediately read the stored response to avoid issues when trying to access the response body. stored_response.read() res = self._controller.construct_response_from_cache( request=request, response=stored_response, original_request=stored_request, ) if isinstance(res, Response): # Simply use the response if the controller determines it is ready for use. return self._create_hishel_response( key=key, response=stored_response, request=request, metadata=metadata, cached=True, revalidated=False, ) if request_cache_control.only_if_cached: return generate_504() if isinstance(res, Request): # Controller has determined that the response needs to be re-validated. try: revalidation_response = self._pool.handle_request(res) except ConnectError: # If there is a connection error, we can use the stale response if allowed. if self._controller._allow_stale and allowed_stale(response=stored_response): return self._create_hishel_response( key=key, response=stored_response, request=request, metadata=metadata, cached=True, revalidated=False, ) raise # pragma: no cover # Merge headers with the stale response. final_response = self._controller.handle_validation_response( old_response=stored_response, new_response=revalidation_response ) final_response.read() # RFC 9111: 4.3.3. Handling a Validation Response # A 304 (Not Modified) response status code indicates that the stored response can be updated and # reused. A full response (i.e., one containing content) indicates that none of the stored responses # nominated in the conditional request are suitable. Instead, the cache MUST use the full response to # satisfy the request. The cache MAY store such a full response, subject to its constraints. if revalidation_response.status != 304 and self._controller.is_cachable( request=request, response=final_response ): self._storage.store(key, response=final_response, request=request) return self._create_hishel_response( key=key, response=final_response, request=request, cached=revalidation_response.status == 304, revalidated=True, metadata=metadata, ) regular_response = self._pool.handle_request(request) regular_response.read() if self._controller.is_cachable(request=request, response=regular_response): self._storage.store(key, response=regular_response, request=request) return self._create_hishel_response( key=key, response=regular_response, request=request, cached=False, revalidated=False ) def _create_hishel_response( self, key: str, response: Response, request: Request, cached: bool, revalidated: bool, metadata: Metadata | None = None, ) -> Response: if cached: assert metadata metadata["number_of_uses"] += 1 self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata) response.extensions["from_cache"] = True # type: ignore[index] response.extensions["cache_metadata"] = metadata # type: ignore[index] else: response.extensions["from_cache"] = False # type: ignore[index] response.extensions["revalidated"] = revalidated # type: ignore[index] return response def close(self) -> None: self._storage.close() if hasattr(self._pool, "close"): # pragma: no cover self._pool.close() def __enter__(self: T) -> T: return self def __exit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[types.TracebackType] = None, ) -> None: self.close() hishel-0.1.2/hishel/_sync/_storages.py000066400000000000000000000664621477404575600177560ustar00rootroot00000000000000from __future__ import annotations import datetime import logging import os import time import typing as t import typing as tp import warnings from copy import deepcopy from pathlib import Path try: import boto3 from .._s3 import S3Manager except ImportError: # pragma: no cover boto3 = None # type: ignore try: import sqlite3 except ImportError: # pragma: no cover sqlite3 = None # type: ignore from httpcore import Request, Response if t.TYPE_CHECKING: # pragma: no cover from typing_extensions import TypeAlias from hishel._serializers import BaseSerializer, clone_model from .._files import FileManager from .._serializers import JSONSerializer, Metadata from .._synchronization import Lock from .._utils import float_seconds_to_int_milliseconds logger = logging.getLogger("hishel.storages") __all__ = ( "BaseStorage", "FileStorage", "RedisStorage", "SQLiteStorage", "InMemoryStorage", "S3Storage", ) StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata] RemoveTypes = tp.Union[str, Response] try: import redis except ImportError: # pragma: no cover redis = None # type: ignore class BaseStorage: def __init__( self, serializer: tp.Optional[BaseSerializer] = None, ttl: tp.Optional[tp.Union[int, float]] = None, ) -> None: self._serializer = serializer or JSONSerializer() self._ttl = ttl def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: raise NotImplementedError() def remove(self, key: RemoveTypes) -> None: raise NotImplementedError() def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: raise NotImplementedError() def retrieve(self, key: str) -> tp.Optional[StoredResponse]: raise NotImplementedError() def close(self) -> None: raise NotImplementedError() class FileStorage(BaseStorage): """ A simple file storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param base_path: A storage base path where the responses should be saved, defaults to None :type base_path: tp.Optional[Path], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional :param check_ttl_every: How often in seconds to check staleness of **all** cache files. Makes sense only with set `ttl`, defaults to 60 :type check_ttl_every: tp.Union[int, float] """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, base_path: tp.Optional[Path] = None, ttl: tp.Optional[tp.Union[int, float]] = None, check_ttl_every: tp.Union[int, float] = 60, ) -> None: super().__init__(serializer, ttl) self._base_path = Path(base_path) if base_path is not None else Path(".cache/hishel") self._gitignore_file = self._base_path / ".gitignore" if not self._base_path.is_dir(): self._base_path.mkdir(parents=True) if not self._gitignore_file.is_file(): with open(self._gitignore_file, "w", encoding="utf-8") as f: f.write("# Automatically created by Hishel\n*") self._file_manager = FileManager(is_binary=self._serializer.is_binary) self._lock = Lock() self._check_ttl_every = check_ttl_every self._last_cleaned = time.monotonic() def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Optional[Metadata] """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) response_path = self._base_path / key with self._lock: self._file_manager.write_to( str(response_path), self._serializer.dumps(response=response, request=request, metadata=metadata), ) self._remove_expired_caches(response_path) def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) response_path = self._base_path / key with self._lock: if response_path.exists(): response_path.unlink() def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ response_path = self._base_path / key with self._lock: if response_path.exists(): atime = response_path.stat().st_atime old_mtime = response_path.stat().st_mtime self._file_manager.write_to( str(response_path), self._serializer.dumps(response=response, request=request, metadata=metadata), ) # Restore the old atime and mtime (we use mtime to check the cache expiration time) os.utime(response_path, (atime, old_mtime)) return return self.store(key, response, request, metadata) # pragma: no cover def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and his HTTP request. :rtype: tp.Optional[StoredResponse] """ response_path = self._base_path / key self._remove_expired_caches(response_path) with self._lock: if response_path.exists(): read_data = self._file_manager.read_from(str(response_path)) if len(read_data) != 0: return self._serializer.loads(read_data) return None def close(self) -> None: # pragma: no cover return def _remove_expired_caches(self, response_path: Path) -> None: if self._ttl is None: return if time.monotonic() - self._last_cleaned < self._check_ttl_every: if response_path.is_file(): age = time.time() - response_path.stat().st_mtime if age > self._ttl: response_path.unlink() return self._last_cleaned = time.monotonic() with self._lock: with os.scandir(self._base_path) as entries: for entry in entries: try: if entry.is_file(): age = time.time() - entry.stat().st_mtime if age > self._ttl: os.unlink(entry.path) except FileNotFoundError: # pragma: no cover pass class SQLiteStorage(BaseStorage): """ A simple sqlite3 storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param connection: A connection for sqlite, defaults to None :type connection: tp.Optional[sqlite3.Connection], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, connection: tp.Optional[sqlite3.Connection] = None, ttl: tp.Optional[tp.Union[int, float]] = None, ) -> None: if sqlite3 is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `sqlite` extension as shown.\n" "```pip install hishel[sqlite]```" ) super().__init__(serializer, ttl) self._connection: tp.Optional[sqlite3.Connection] = connection or None self._setup_lock = Lock() self._setup_completed: bool = False self._lock = Lock() def _setup(self) -> None: with self._setup_lock: if not self._setup_completed: if not self._connection: # pragma: no cover self._connection = sqlite3.connect(".hishel.sqlite", check_same_thread=False) self._connection.execute( "CREATE TABLE IF NOT EXISTS cache(key TEXT, data BLOB, date_created REAL)" ) self._connection.commit() self._setup_completed = True def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata] """ self._setup() assert self._connection metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) with self._lock: self._connection.execute("DELETE FROM cache WHERE key = ?", [key]) serialized_response = self._serializer.dumps(response=response, request=request, metadata=metadata) self._connection.execute( "INSERT INTO cache(key, data, date_created) VALUES(?, ?, ?)", [key, serialized_response, time.time()] ) self._connection.commit() self._remove_expired_caches() def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ self._setup() assert self._connection if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) with self._lock: self._connection.execute("DELETE FROM cache WHERE key = ?", [key]) self._connection.commit() def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ self._setup() assert self._connection with self._lock: cursor = self._connection.execute("SELECT data FROM cache WHERE key = ?", [key]) row = cursor.fetchone() if row is not None: serialized_response = self._serializer.dumps(response=response, request=request, metadata=metadata) self._connection.execute("UPDATE cache SET data = ? WHERE key = ?", [serialized_response, key]) self._connection.commit() return return self.store(key, response, request, metadata) # pragma: no cover def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ self._setup() assert self._connection self._remove_expired_caches() with self._lock: cursor = self._connection.execute("SELECT data FROM cache WHERE key = ?", [key]) row = cursor.fetchone() if row is None: return None cached_response = row[0] return self._serializer.loads(cached_response) def close(self) -> None: # pragma: no cover if self._connection is not None: self._connection.close() def _remove_expired_caches(self) -> None: assert self._connection if self._ttl is None: return with self._lock: self._connection.execute("DELETE FROM cache WHERE date_created + ? < ?", [self._ttl, time.time()]) self._connection.commit() class RedisStorage(BaseStorage): """ A simple redis storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param client: A client for redis, defaults to None :type client: tp.Optional["redis.Redis"], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, client: tp.Optional[redis.Redis] = None, # type: ignore ttl: tp.Optional[tp.Union[int, float]] = None, ) -> None: if redis is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `redis` extension as shown.\n" "```pip install hishel[redis]```" ) super().__init__(serializer, ttl) if client is None: self._client = redis.Redis() # type: ignore else: # pragma: no cover self._client = client def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata] """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) if self._ttl is not None: px = float_seconds_to_int_milliseconds(self._ttl) else: px = None self._client.set( key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px ) def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) self._client.delete(key) def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ ttl_in_milliseconds = self._client.pttl(key) # -2: if the key does not exist in Redis # -1: if the key exists in Redis but has no expiration if ttl_in_milliseconds == -2 or ttl_in_milliseconds == -1: # pragma: no cover self.store(key, response, request, metadata) else: self._client.set( key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=ttl_in_milliseconds, ) def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ cached_response = self._client.get(key) if cached_response is None: return None return self._serializer.loads(cached_response) def close(self) -> None: # pragma: no cover self._client.close() class InMemoryStorage(BaseStorage): """ A simple in-memory storage. :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional :param capacity: The maximum number of responses that can be cached, defaults to 128 :type capacity: int, optional """ def __init__( self, serializer: tp.Optional[BaseSerializer] = None, ttl: tp.Optional[tp.Union[int, float]] = None, capacity: int = 128, ) -> None: super().__init__(serializer, ttl) if serializer is not None: # pragma: no cover warnings.warn("The serializer is not used in the in-memory storage.", RuntimeWarning) from hishel import LFUCache self._cache: LFUCache[str, tp.Tuple[StoredResponse, float]] = LFUCache(capacity=capacity) self._lock = Lock() def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata] """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) with self._lock: response_clone = clone_model(response) request_clone = clone_model(request) stored_response: StoredResponse = (deepcopy(response_clone), deepcopy(request_clone), metadata) self._cache.put(key, (stored_response, time.monotonic())) self._remove_expired_caches() def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) with self._lock: self._cache.remove_key(key) def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ with self._lock: try: stored_response, created_at = self._cache.get(key) stored_response = (stored_response[0], stored_response[1], metadata) self._cache.put(key, (stored_response, created_at)) return except KeyError: # pragma: no cover pass self.store(key, response, request, metadata) # pragma: no cover def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ self._remove_expired_caches() with self._lock: try: stored_response, _ = self._cache.get(key) except KeyError: return None return stored_response def close(self) -> None: # pragma: no cover return def _remove_expired_caches(self) -> None: if self._ttl is None: return with self._lock: keys_to_remove = set() for key in self._cache: created_at = self._cache.get(key)[1] if time.monotonic() - created_at > self._ttl: keys_to_remove.add(key) for key in keys_to_remove: self._cache.remove_key(key) class S3Storage(BaseStorage): # pragma: no cover """ AWS S3 storage. :param bucket_name: The name of the bucket to store the responses in :type bucket_name: str :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None :type serializer: tp.Optional[BaseSerializer], optional :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None :type ttl: tp.Optional[tp.Union[int, float]], optional :param check_ttl_every: How often in seconds to check staleness of **all** cache files. Makes sense only with set `ttl`, defaults to 60 :type check_ttl_every: tp.Union[int, float] :param client: A client for S3, defaults to None :type client: tp.Optional[tp.Any], optional """ def __init__( self, bucket_name: str, serializer: tp.Optional[BaseSerializer] = None, ttl: tp.Optional[tp.Union[int, float]] = None, check_ttl_every: tp.Union[int, float] = 60, client: tp.Optional[tp.Any] = None, ) -> None: super().__init__(serializer, ttl) if boto3 is None: # pragma: no cover raise RuntimeError( f"The `{type(self).__name__}` was used, but the required packages were not found. " "Check that you have `Hishel` installed with the `s3` extension as shown.\n" "```pip install hishel[s3]```" ) self._bucket_name = bucket_name client = client or boto3.client("s3") self._s3_manager = S3Manager( client=client, bucket_name=bucket_name, is_binary=self._serializer.is_binary, check_ttl_every=check_ttl_every, ) self._lock = Lock() def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None: """ Stores the response in the cache. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additioal information about the stored response :type metadata: Optional[Metadata]` """ metadata = metadata or Metadata( cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0 ) with self._lock: serialized = self._serializer.dumps(response=response, request=request, metadata=metadata) self._s3_manager.write_to(path=key, data=serialized) self._remove_expired_caches(key) def remove(self, key: RemoveTypes) -> None: """ Removes the response from the cache. :param key: Hashed value of concatenated HTTP method and URI or an HTTP response :type key: Union[str, Response] """ if isinstance(key, Response): # pragma: no cover key = t.cast(str, key.extensions["cache_metadata"]["cache_key"]) with self._lock: self._s3_manager.remove_entry(key) def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None: """ Updates the metadata of the stored response. :param key: Hashed value of concatenated HTTP method and URI :type key: str :param response: An HTTP response :type response: httpcore.Response :param request: An HTTP request :type request: httpcore.Request :param metadata: Additional information about the stored response :type metadata: Metadata """ with self._lock: serialized = self._serializer.dumps(response=response, request=request, metadata=metadata) self._s3_manager.write_to(path=key, data=serialized, only_metadata=True) def retrieve(self, key: str) -> tp.Optional[StoredResponse]: """ Retreives the response from the cache using his key. :param key: Hashed value of concatenated HTTP method and URI :type key: str :return: An HTTP response and its HTTP request. :rtype: tp.Optional[StoredResponse] """ self._remove_expired_caches(key) with self._lock: try: return self._serializer.loads(self._s3_manager.read_from(path=key)) except Exception: return None def close(self) -> None: # pragma: no cover return def _remove_expired_caches(self, key: str) -> None: if self._ttl is None: return with self._lock: converted_ttl = float_seconds_to_int_milliseconds(self._ttl) self._s3_manager.remove_expired(ttl=converted_ttl, key=key) hishel-0.1.2/hishel/_sync/_transports.py000066400000000000000000000251731477404575600203400ustar00rootroot00000000000000from __future__ import annotations import types import typing as tp import httpcore import httpx from httpx import SyncByteStream, Request, Response from httpx._exceptions import ConnectError from hishel._utils import extract_header_values_decoded, normalized_url from .._controller import Controller, allowed_stale from .._headers import parse_cache_control from .._serializers import JSONSerializer, Metadata from ._storages import BaseStorage, FileStorage if tp.TYPE_CHECKING: # pragma: no cover from typing_extensions import Self __all__ = ("CacheTransport",) def fake_stream(content: bytes) -> tp.Iterable[bytes]: yield content def generate_504() -> Response: return Response(status_code=504) class CacheStream(SyncByteStream): def __init__(self, httpcore_stream: tp.Iterable[bytes]): self._httpcore_stream = httpcore_stream def __iter__(self) -> tp.Iterator[bytes]: for part in self._httpcore_stream: yield part def close(self) -> None: if hasattr(self._httpcore_stream, "close"): self._httpcore_stream.close() class CacheTransport(httpx.BaseTransport): """ An HTTPX Transport that supports HTTP caching. :param transport: `Transport` that our class wraps in order to add an HTTP Cache layer on top of :type transport: httpx.BaseTransport :param storage: Storage that handles how the responses should be saved., defaults to None :type storage: tp.Optional[BaseStorage], optional :param controller: Controller that manages the cache behavior at the specification level, defaults to None :type controller: tp.Optional[Controller], optional """ def __init__( self, transport: httpx.BaseTransport, storage: tp.Optional[BaseStorage] = None, controller: tp.Optional[Controller] = None, ) -> None: self._transport = transport self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer()) if not isinstance(self._storage, BaseStorage): # pragma: no cover raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`") self._controller = controller if controller is not None else Controller() def handle_request(self, request: Request) -> Response: """ Handles HTTP requests while also implementing HTTP caching. :param request: An HTTP request :type request: httpx.Request :return: An HTTP response :rtype: httpx.Response """ if request.extensions.get("cache_disabled", False): request.headers.update( [ ("Cache-Control", "no-store"), ("Cache-Control", "no-cache"), *[("cache-control", value) for value in request.headers.get_list("cache-control")], ] ) if request.method not in ["GET", "HEAD"]: # If the HTTP method is, for example, POST, # we must also use the request data to generate the hash. body_for_key = request.read() request.stream = CacheStream(fake_stream(body_for_key)) else: body_for_key = b"" # Construct the HTTPCore request because Controllers and Storages work with HTTPCore requests. httpcore_request = httpcore.Request( method=request.method, url=httpcore.URL( scheme=request.url.raw_scheme, host=request.url.raw_host, port=request.url.port, target=request.url.raw_path, ), headers=request.headers.raw, content=request.stream, extensions=request.extensions, ) key = self._controller._key_generator(httpcore_request, body_for_key) stored_data = self._storage.retrieve(key) request_cache_control = parse_cache_control( extract_header_values_decoded(request.headers.raw, b"Cache-Control") ) if request_cache_control.only_if_cached and not stored_data: return generate_504() if stored_data: # Try using the stored response if it was discovered. stored_response, stored_request, metadata = stored_data # Immediately read the stored response to avoid issues when trying to access the response body. stored_response.read() res = self._controller.construct_response_from_cache( request=httpcore_request, response=stored_response, original_request=stored_request, ) if isinstance(res, httpcore.Response): # Simply use the response if the controller determines it is ready for use. return self._create_hishel_response( key=key, response=res, request=httpcore_request, cached=True, revalidated=False, metadata=metadata, ) if request_cache_control.only_if_cached: return generate_504() if isinstance(res, httpcore.Request): # Controller has determined that the response needs to be re-validated. assert isinstance(res.stream, tp.Iterable) revalidation_request = Request( method=res.method.decode(), url=normalized_url(res.url), headers=res.headers, stream=CacheStream(res.stream), extensions=res.extensions, ) try: revalidation_response = self._transport.handle_request(revalidation_request) except ConnectError: # If there is a connection error, we can use the stale response if allowed. if self._controller._allow_stale and allowed_stale(response=stored_response): return self._create_hishel_response( key=key, response=stored_response, request=httpcore_request, cached=True, revalidated=False, metadata=metadata, ) raise # pragma: no cover assert isinstance(revalidation_response.stream, tp.Iterable) httpcore_revalidation_response = httpcore.Response( status=revalidation_response.status_code, headers=revalidation_response.headers.raw, content=CacheStream(revalidation_response.stream), extensions=revalidation_response.extensions, ) # Merge headers with the stale response. final_httpcore_response = self._controller.handle_validation_response( old_response=stored_response, new_response=httpcore_revalidation_response, ) final_httpcore_response.read() revalidation_response.close() assert isinstance(final_httpcore_response.stream, tp.Iterable) # RFC 9111: 4.3.3. Handling a Validation Response # A 304 (Not Modified) response status code indicates that the stored response can be updated and # reused. A full response (i.e., one containing content) indicates that none of the stored responses # nominated in the conditional request are suitable. Instead, the cache MUST use the full response to # satisfy the request. The cache MAY store such a full response, subject to its constraints. if revalidation_response.status_code != 304 and self._controller.is_cachable( request=httpcore_request, response=final_httpcore_response ): self._storage.store(key, response=final_httpcore_response, request=httpcore_request) return self._create_hishel_response( key=key, response=final_httpcore_response, request=httpcore_request, cached=revalidation_response.status_code == 304, revalidated=True, metadata=metadata, ) regular_response = self._transport.handle_request(request) assert isinstance(regular_response.stream, tp.Iterable) httpcore_regular_response = httpcore.Response( status=regular_response.status_code, headers=regular_response.headers.raw, content=CacheStream(regular_response.stream), extensions=regular_response.extensions, ) httpcore_regular_response.read() httpcore_regular_response.close() if self._controller.is_cachable(request=httpcore_request, response=httpcore_regular_response): self._storage.store( key, response=httpcore_regular_response, request=httpcore_request, ) return self._create_hishel_response( key=key, response=httpcore_regular_response, request=httpcore_request, cached=False, revalidated=False, ) def _create_hishel_response( self, key: str, response: httpcore.Response, request: httpcore.Request, cached: bool, revalidated: bool, metadata: Metadata | None = None, ) -> Response: if cached: assert metadata metadata["number_of_uses"] += 1 self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata) response.extensions["from_cache"] = True # type: ignore[index] response.extensions["cache_metadata"] = metadata # type: ignore[index] else: response.extensions["from_cache"] = False # type: ignore[index] response.extensions["revalidated"] = revalidated # type: ignore[index] return Response( status_code=response.status, headers=response.headers, stream=CacheStream(fake_stream(response.content)), extensions=response.extensions, ) def close(self) -> None: self._storage.close() self._transport.close() def __enter__(self) -> Self: return self def __exit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[types.TracebackType] = None, ) -> None: self.close() hishel-0.1.2/hishel/_synchronization.py000066400000000000000000000016011477404575600202350ustar00rootroot00000000000000import types import typing as tp from threading import Lock as T_LOCK import anyio class AsyncLock: def __init__(self) -> None: self._lock = anyio.Lock() async def __aenter__(self) -> None: await self._lock.acquire() async def __aexit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[types.TracebackType] = None, ) -> None: self._lock.release() class Lock: def __init__(self) -> None: self._lock = T_LOCK() def __enter__(self) -> None: self._lock.acquire() def __exit__( self, exc_type: tp.Optional[tp.Type[BaseException]] = None, exc_value: tp.Optional[BaseException] = None, traceback: tp.Optional[types.TracebackType] = None, ) -> None: self._lock.release() hishel-0.1.2/hishel/_utils.py000066400000000000000000000063151477404575600161430ustar00rootroot00000000000000import calendar import hashlib import time import typing as tp from email.utils import parsedate_tz import anyio import httpcore import httpx HEADERS_ENCODING = "iso-8859-1" class BaseClock: def now(self) -> int: raise NotImplementedError() class Clock(BaseClock): def now(self) -> int: return int(time.time()) def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str: if isinstance(url, str): # pragma: no cover return url if isinstance(url, bytes): # pragma: no cover return url.decode("ascii") if isinstance(url, httpcore.URL): port = f":{url.port}" if url.port is not None else "" return f"{url.scheme.decode('ascii')}://{url.host.decode('ascii')}{port}{url.target.decode('ascii')}" assert False, "Invalid type for `normalized_url`" # pragma: no cover def get_safe_url(url: httpcore.URL) -> str: httpx_url = httpx.URL(bytes(url).decode("ascii")) schema = httpx_url.scheme host = httpx_url.host path = httpx_url.path return f"{schema}://{host}{path}" def generate_key(request: httpcore.Request, body: bytes = b"") -> str: encoded_url = normalized_url(request.url).encode("ascii") key_parts = [request.method, encoded_url, body] # FIPs mode disables blake2 algorithm, use sha256 instead when not found. blake2b_hasher = None sha256_hasher = hashlib.sha256(usedforsecurity=False) try: blake2b_hasher = hashlib.blake2b(digest_size=16, usedforsecurity=False) except (ValueError, TypeError, AttributeError): pass hexdigest: str if blake2b_hasher: for part in key_parts: blake2b_hasher.update(part) hexdigest = blake2b_hasher.hexdigest() else: for part in key_parts: sha256_hasher.update(part) hexdigest = sha256_hasher.hexdigest() return hexdigest def extract_header_values( headers: tp.List[tp.Tuple[bytes, bytes]], header_key: tp.Union[bytes, str], single: bool = False, ) -> tp.List[bytes]: if isinstance(header_key, str): header_key = header_key.encode(HEADERS_ENCODING) extracted_headers = [] for key, value in headers: if key.lower() == header_key.lower(): extracted_headers.append(value) if single: break return extracted_headers def extract_header_values_decoded( headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes, single: bool = False ) -> tp.List[str]: values = extract_header_values(headers=headers, header_key=header_key, single=single) return [value.decode(HEADERS_ENCODING) for value in values] def header_presents(headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes) -> bool: return bool(extract_header_values(headers, header_key, single=True)) def parse_date(date: str) -> tp.Optional[int]: expires = parsedate_tz(date) if expires is None: return None timestamp = calendar.timegm(expires[:6]) return timestamp async def asleep(seconds: tp.Union[int, float]) -> None: await anyio.sleep(seconds) def sleep(seconds: tp.Union[int, float]) -> None: time.sleep(seconds) def float_seconds_to_int_milliseconds(seconds: float) -> int: return int(seconds * 1000) hishel-0.1.2/hishel/py.typed000066400000000000000000000000001477404575600157520ustar00rootroot00000000000000hishel-0.1.2/mkdocs.yml000066400000000000000000000027341477404575600150220ustar00rootroot00000000000000site_name: Hishel repo_url: https://github.com/karpetrosyan/hishel theme: name: material custom_dir: overrides features: - content.code.copy - toc.integrate - toc.follow - navigation.expand palette: - scheme: default primary: "amber" toggle: icon: material/lightbulb name: Switch to dark mode - scheme: slate primary: "amber" toggle: icon: material/lightbulb-outline name: Switch to light mode markdown_extensions: - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - attr_list - pymdownx.tabbed: alternate_style: true - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.inlinehilite - pymdownx.snippets - admonition - pymdownx.details - pymdownx.highlight: anchor_linenums: true - pymdownx.superfences - tables nav: - Introduction: index.md - User Guide: userguide.md - Advanced Usage: - "Storages": advanced/storages.md - Serializers: advanced/serializers.md - Controllers: advanced/controllers.md - HTTP Headers: advanced/http_headers.md - Extensions: advanced/extensions.md - Logging: advanced/logging.md - Examples: - GitHub: examples/github.md - FastAPI: examples/fastapi.md - Flask: examples/flask.md - Contributing: contributing.md hishel-0.1.2/overrides/000077500000000000000000000000001477404575600150135ustar00rootroot00000000000000hishel-0.1.2/overrides/partials/000077500000000000000000000000001477404575600166325ustar00rootroot00000000000000hishel-0.1.2/overrides/partials/footer.html000066400000000000000000000005541477404575600210220ustar00rootroot00000000000000 hishel-0.1.2/pyproject.toml000066400000000000000000000054661477404575600157400ustar00rootroot00000000000000[build-system] requires = ["hatchling", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] name = "hishel" dynamic = ["readme", "version"] description = "Persistent cache implementation for httpx and httpcore" license = "BSD-3-Clause" requires-python = ">=3.9" authors = [ { name = "Kar Petrosyan", email = "kar.petrosyanpy@gmail.com" }, ] classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: Trio", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", ] dependencies = [ "httpx>=0.28.0", ] [project.optional-dependencies] yaml = [ "pyyaml==6.0.1", ] redis = [ "redis==5.0.4" ] sqlite = [ "anysqlite>=0.0.5" ] s3 = [ "boto3>=1.15.0,<=1.15.3; python_version < '3.12'", "boto3>=1.15.3; python_version >= '3.12'" ] [project.urls] Homepage = "https://hishel.com" Source = "https://github.com/karpetrosyan/hishel" [tool.hatch.version] path = "hishel/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/hishel", "/CHANGELOG.md", "/README.md", ] [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] path = "CHANGELOG.md" [tool.mypy] strict = true show_error_codes = true warn_unused_ignores = false exclude = ['venv', '.venv'] [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false check_untyped_defs = true [tool.pytest.ini_options] addopts = ["-rxXs", "--strict-config", "--strict-markers"] filterwarnings = [] [tool.coverage.run] omit = [ "venv/*", "hishel/_sync/*", "hishel/_s3.py" ] include = ["hishel/*", "tests/*"] [tool.coverage.report] exclude_also = [ '__repr__', 'raise NotImplementedError()' ] [tool.ruff] exclude = [ "hishel/_sync", "hishel/__init__.py", "tests/_sync", ] line-length = 120 [tool.ruff.lint] select = [ "E", "F", "W", "I" ] [tool.ruff.lint.isort] combine-as-imports = true [dependency-groups] dev = [ "anyio==4.7.0", "coverage==7.6.10", "hatch==1.9.3", "mkdocs==1.6.1", "mkdocs-material==9.5.1", "mypy==1.14.1", "pytest==8.3.4", "ruff==0.11.0", "trio==0.28.0", "types-boto3==1.0.2", "types-pyyaml==6.0.12.20240311", "types-redis==4.6.0.20240425", "zipp>=3.19.1", ] hishel-0.1.2/scripts/000077500000000000000000000000001477404575600145005ustar00rootroot00000000000000hishel-0.1.2/scripts/check000077500000000000000000000003061477404575600155020ustar00rootroot00000000000000#! /bin/bash -ex export SOURCE_FILES="hishel tests" uv run ruff format $SOURCE_FILES --diff uv run ruff check $SOURCE_FILES uv run --all-extras mypy $SOURCE_FILES uv run python unasync.py --check hishel-0.1.2/scripts/lint000077500000000000000000000001621477404575600153730ustar00rootroot00000000000000#! /bin/bash -ex uv run ruff check --fix $SOURCE_FILES uv run ruff format $SOURCE_FILES uv run python unasync.py hishel-0.1.2/scripts/publish000077500000000000000000000000551477404575600160740ustar00rootroot00000000000000#! /bin/bash -ex uv publish -t $HISHEL_PYPI hishel-0.1.2/scripts/publish-docs000077500000000000000000000000621477404575600170200ustar00rootroot00000000000000#! /bin/bash -ex uv run mkdocs gh-deploy --force hishel-0.1.2/scripts/test000077500000000000000000000002141477404575600154020ustar00rootroot00000000000000#! /bin/bash -ex ./scripts/check uv run coverage run -m pytest tests uv run coverage report --show-missing --skip-covered --fail-under=100hishel-0.1.2/tests/000077500000000000000000000000001477404575600141535ustar00rootroot00000000000000hishel-0.1.2/tests/__init__.py000066400000000000000000000001441477404575600162630ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023-present U.N. Owen # # SPDX-License-Identifier: MIT hishel-0.1.2/tests/_async/000077500000000000000000000000001477404575600154275ustar00rootroot00000000000000hishel-0.1.2/tests/_async/__init__.py000066400000000000000000000000001477404575600175260ustar00rootroot00000000000000hishel-0.1.2/tests/_async/test_client.py000066400000000000000000000217071477404575600203250ustar00rootroot00000000000000import os from datetime import datetime, timedelta from pathlib import Path from time import mktime from wsgiref.handlers import format_date_time import httpx import pytest from httpcore import Request import hishel from hishel._utils import generate_key date_header = format_date_time(mktime((datetime.now() - timedelta(hours=2)).timetuple())) @pytest.mark.anyio async def test_client_301(): async with hishel.MockAsyncTransport() as transport: transport.add_responses([httpx.Response(301, headers=[(b"Location", b"https://example.com")])]) async with hishel.AsyncCacheClient(transport=transport, storage=hishel.AsyncInMemoryStorage()) as client: await client.request( "GET", "https://www.example.com", ) response = await client.request( "GET", "https://www.example.com", ) assert response.extensions["from_cache"] @pytest.mark.anyio async def test_empty_cachefile_handling(use_temp_dir): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], text="test", ) for _ in range(2) ] ) async with hishel.AsyncCacheClient(storage=hishel.AsyncFileStorage(), transport=transport) as client: request = Request(b"GET", "https://example.com/") key = generate_key(request) filedir = Path(os.getcwd() + "/.cache/hishel/" + key) await client.get("https://example.com/") response = await client.get("https://example.com/") assert response.status_code == 200 assert response.text == "test" assert response.extensions["from_cache"] with open(filedir, "w+", encoding="utf-8") as file: file.truncate(0) assert os.path.getsize(filedir) == 0 response = await client.get("https://example.com/") assert response.status_code == 200 assert response.text == "test" assert response.extensions["from_cache"] is False response = await client.get("https://example.com/") assert response.extensions["from_cache"] @pytest.mark.anyio async def test_post_caching(): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], text=f"test-{idx}", ) for idx in range(2) ] ) async with hishel.AsyncCacheClient( storage=hishel.AsyncInMemoryStorage(), transport=transport, controller=hishel.Controller(cacheable_methods=["POST"]), ) as client: # Create cache file. response = await client.post("https://example.com", json={"test": 1}) assert response.status_code == 200 assert not response.extensions["from_cache"] assert response.text == "test-0" # Get from cache file. response = await client.post("https://example.com", json={"test": 1}) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test-0" # Create a new cache file response = await client.post("https://example.com", json={"test": 2}) assert response.status_code == 200 assert not response.extensions["from_cache"] assert response.text == "test-1" # Take second response from cache response = await client.post("https://example.com", json={"test": 2}) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test-1" # Check on first response response = await client.post("https://example.com", json={"test": 1}) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test-0" @pytest.mark.anyio async def test_client_get(): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], text="test text", ) ] ) async with hishel.AsyncCacheClient(storage=hishel.AsyncInMemoryStorage(), transport=transport) as client: response = await client.get("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] assert response.text == "test text" response = await client.get("https://example.com") assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test text" @pytest.mark.anyio async def test_client_head(): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], ) for _ in range(2) ] ) async with hishel.AsyncCacheClient(storage=hishel.AsyncInMemoryStorage(), transport=transport) as client: response = await client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] response = await client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] @pytest.mark.anyio async def test_force_cache(): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "no-store"), ("Date", date_header), ], ) for _ in range(3) ] ) async with hishel.AsyncCacheClient( storage=hishel.AsyncInMemoryStorage(), controller=hishel.Controller(cacheable_methods=["HEAD"]), transport=transport, ) as client: response = await client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] # Check that "no-store" is respected response = await client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] response = await client.head("https://example.com", extensions={"force_cache": True}) assert response.status_code == 200 assert not response.extensions["from_cache"] response = await client.head("https://example.com", extensions={"force_cache": True}) assert response.status_code == 200 assert response.extensions["from_cache"] @pytest.mark.anyio async def test_cache_disabled(): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], ) for _ in range(2) ] ) async with hishel.AsyncCacheClient(storage=hishel.AsyncInMemoryStorage(), transport=transport) as client: response = await client.get( "https://www.example.com/cacheable-endpoint", extensions={"cache_disabled": True} ) assert response.status_code == 200 assert not response.extensions["from_cache"] response = await client.get( "https://www.example.com/cacheable-endpoint", extensions={"cache_disabled": True} ) assert response.status_code == 200 assert not response.extensions["from_cache"] hishel-0.1.2/tests/_async/test_pool.py000066400000000000000000000372771477404575600200310ustar00rootroot00000000000000import httpcore import pytest from httpcore._models import Request, Response import hishel from hishel._utils import BaseClock, extract_header_values, extract_header_values_decoded, header_presents @pytest.mark.anyio async def test_pool_301(): async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses([httpcore.Response(301, headers=[(b"Location", b"https://example.com")])]) async with hishel.AsyncCacheConnectionPool(pool=pool, storage=hishel.AsyncInMemoryStorage()) as cache_pool: await cache_pool.request("GET", "https://www.example.com") response = await cache_pool.request("GET", "https://www.example.com") assert response.extensions["from_cache"] @pytest.mark.anyio async def test_pool_response_validation(): async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"test", ), httpcore.Response( 304, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Content-Type", b"application/json"), ], ), ] ) async with hishel.AsyncCacheConnectionPool(pool=pool, storage=hishel.AsyncInMemoryStorage()) as cache_pool: request = httpcore.Request("GET", "https://www.example.com") await cache_pool.handle_async_request(request) response = await cache_pool.handle_async_request(request) assert response.status == 200 assert response.extensions["from_cache"] assert response.extensions["revalidated"] assert header_presents(response.headers, b"Content-Type") assert extract_header_values(response.headers, b"Content-Type", single=True)[0] == b"application/json" assert await response.aread() == b"test" @pytest.mark.anyio async def test_pool_stale_response(): controller = hishel.Controller(allow_stale=True) async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_pool: await cache_pool.request("GET", "https://www.example.com") response = await cache_pool.request("GET", "https://www.example.com") assert not response.extensions["from_cache"] @pytest.mark.anyio async def test_pool_stale_response_with_connecterror(): controller = hishel.Controller(allow_stale=True) class ConnectErrorPool(hishel.MockAsyncConnectionPool): async def handle_async_request(self, request: Request) -> Response: if not hasattr(self, "not_first_request"): setattr(self, "not_first_request", object()) return await super().handle_async_request(request) raise httpcore._exceptions.ConnectError() async with ConnectErrorPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_pool: await cache_pool.request("GET", "https://www.example.com") response = await cache_pool.request("GET", "https://www.example.com") assert response.extensions["from_cache"] @pytest.mark.anyio async def test_pool_with_only_if_cached_directive_without_stored_response(): controller = hishel.Controller() async with hishel.MockAsyncConnectionPool() as pool: async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_pool: response = await cache_pool.request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) assert response.status == 504 @pytest.mark.anyio async def test_pool_with_only_if_cached_directive_with_stored_response(): controller = hishel.Controller() async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"test", ), ] ) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_pool: await cache_pool.request("GET", "https://www.example.com") response = await cache_pool.request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) assert response.status == 504 @pytest.mark.anyio async def test_pool_with_cache_disabled_extension(): class MockedClock(BaseClock): def now(self) -> int: return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT cachable_response = httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock ], ) async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses([cachable_response, httpcore.Response(201)]) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=hishel.Controller(clock=MockedClock()), storage=hishel.AsyncInMemoryStorage() ) as cache_transport: request = httpcore.Request("GET", "https://www.example.com") # This should create a cache entry await cache_transport.handle_async_request(request) # This should return from cache response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] # This should ignore the cache caching_disabled_request = httpcore.Request( "GET", "https://www.example.com", extensions={"cache_disabled": True} ) response = await cache_transport.handle_async_request(caching_disabled_request) assert not response.extensions["from_cache"] assert response.status == 201 @pytest.mark.anyio async def test_pool_with_custom_key_generator(): controller = hishel.Controller(key_generator=lambda request, body: request.url.host.decode()) async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses([httpcore.Response(301)]) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: request = httpcore.Request("GET", "https://www.example.com") # This should create a cache entry await cache_transport.handle_async_request(request) # This should return from cache response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["cache_key"] == "www.example.com" @pytest.mark.anyio async def test_pool_caching_post_method(): controller = hishel.Controller(cacheable_methods=["POST"]) async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses([httpcore.Response(301), httpcore.Response(200)]) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage(), ) as cache_pool: # This should create a cache entry await cache_pool.request("POST", "https://www.example.com", content=b"request-1") # This should return from cache response = await cache_pool.request("POST", "https://www.example.com", content=b"request-1") assert response.extensions["from_cache"] # This should create a new cache entry instead of using the previous one response = await cache_pool.request("POST", "https://www.example.com", content=b"request-2") assert response.status == 200 assert not response.extensions["from_cache"] @pytest.mark.anyio async def test_revalidation_with_new_content(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current clock = MockedClock() controller = hishel.Controller(clock=clock) storage = hishel.AsyncInMemoryStorage() async with hishel.MockAsyncConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"Hello, World.", ), httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], content=b"Eat at Joe's.", ), httpcore.Response( 304, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:11 GMT"), ], ), ] ) async with hishel.AsyncCacheConnectionPool(pool=pool, controller=controller, storage=storage) as cache_pool: # Miss, 200, store response = await cache_pool.handle_async_request(httpcore.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit response = await cache_pool.handle_async_request(httpcore.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache contains the first response content stored = await storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:00 GMT"] assert stored_response.content == b"Hello, World." # tic, tac... one second passed clock.current += 1 # one second passed # Miss (expired), send revalidation, 200, store response = await cache_pool.handle_async_request(httpcore.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit (cf issue #239) response = await cache_pool.handle_async_request(httpcore.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache was updated and contains the second response content stored = await storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:01 GMT"] assert stored_response.content == b"Eat at Joe's." # tic, tac, tic, tac... ten more seconds passed, let's check the 304 behavious is not broken clock.current += 10 # Miss (expired), send revalidation, 304, update metadata but keep previous content response = await cache_pool.handle_async_request(httpcore.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 2 stored = await storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:11 GMT"] assert stored_response.content == b"Eat at Joe's." @pytest.mark.anyio async def test_poool_revalidation_forward_extensions(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current class MockedConnectionPoolWithExtensionsMemory(hishel.MockAsyncConnectionPool): async def handle_async_request(self, request: httpcore.Request) -> httpcore.Response: self.last_request_extensions = request.extensions return await super().handle_async_request(request) clock = MockedClock() controller = hishel.Controller(clock=clock) async with MockedConnectionPoolWithExtensionsMemory() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpcore.Response( 304, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], ), ] ) async with hishel.AsyncCacheConnectionPool( pool=pool, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_pool: # first request with extensions await cache_pool.handle_async_request( httpcore.Request("GET", "https://www.example.com", extensions={"foo": "bar"}) ) assert pool.last_request_extensions["foo"] == "bar" # cache expires clock.current += 1 # second request with extensions that should be passed to revalidation request response = await cache_pool.handle_async_request( httpcore.Request("GET", "https://www.example.com", extensions={"foo": "baz"}) ) assert response.extensions["revalidated"] is True assert pool.last_request_extensions["foo"] == "baz" hishel-0.1.2/tests/_async/test_storages.py000066400000000000000000000356531477404575600207030ustar00rootroot00000000000000import datetime import os from pathlib import Path import anysqlite import pytest from httpcore import Request, Response from hishel import AsyncFileStorage, AsyncInMemoryStorage, AsyncRedisStorage, AsyncSQLiteStorage from hishel._serializers import Metadata from hishel._utils import asleep, generate_key dummy_metadata = Metadata(cache_key="test", number_of_uses=0, created_at=datetime.datetime.now(datetime.timezone.utc)) async def is_redis_down() -> bool: import redis.asyncio as redis connection = redis.Redis() try: return not await connection.ping() except BaseException: # pragma: no cover return True @pytest.mark.anyio async def test_filestorage(use_temp_dir): storage = AsyncFileStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = await storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_redisstorage(anyio_backend): if await is_redis_down(): # pragma: no cover pytest.fail("Redis server was not found") storage = AsyncRedisStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = await storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" @pytest.mark.anyio async def test_sqlitestorage(): storage = AsyncSQLiteStorage(connection=await anysqlite.connect(":memory:")) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = await storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" @pytest.mark.anyio async def test_inmemorystorage(): storage = AsyncInMemoryStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = await storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_filestorage_expired(use_temp_dir, anyio_backend): storage = AsyncFileStorage(ttl=0.2, check_ttl_every=0.1) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is not None await asleep(0.3) await storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_filestorage_timer(use_temp_dir, anyio_backend): storage = AsyncFileStorage(ttl=0.2, check_ttl_every=0.2) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is not None await asleep(0.1) assert await storage.retrieve(first_key) is not None await storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert await storage.retrieve(second_key) is not None await asleep(0.1) assert await storage.retrieve(first_key) is None assert await storage.retrieve(second_key) is not None await asleep(0.1) assert await storage.retrieve(second_key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_filestorage_ttl_after_hits(use_temp_dir, anyio_backend): storage = AsyncFileStorage(ttl=0.2, check_ttl_every=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() # Storing await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.08 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.16 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.24 second await asleep(0.08) assert await storage.retrieve(key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_redisstorage_expired(anyio_backend): if await is_redis_down(): # pragma: no cover pytest.fail("Redis server was not found") storage = AsyncRedisStorage(ttl=0.1) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is not None await asleep(0.3) await storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_redis_ttl_after_hits(use_temp_dir, anyio_backend): storage = AsyncRedisStorage(ttl=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() # Storing await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.08 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.16 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.24 second await asleep(0.08) assert await storage.retrieve(key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_sqlite_expired(anyio_backend): storage = AsyncSQLiteStorage(ttl=0.1, connection=await anysqlite.connect(":memory:")) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is not None await asleep(0.3) await storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is None @pytest.mark.xfail @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_sqlite_ttl_after_hits(use_temp_dir, anyio_backend): storage = AsyncSQLiteStorage(ttl=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() # Storing await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.08 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.16 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.24 second await asleep(0.08) assert await storage.retrieve(key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_inmemory_expired(anyio_backend): storage = AsyncInMemoryStorage(ttl=0.1) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is not None await asleep(0.3) await storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert await storage.retrieve(first_key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_inmemory_ttl_after_hits(use_temp_dir, anyio_backend): storage = AsyncInMemoryStorage(ttl=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() # Storing await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.08 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.16 second await asleep(0.08) await storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None # Retrieving after 0.24 second await asleep(0.08) assert await storage.retrieve(key) is None @pytest.mark.anyio async def test_filestorage_empty_file_exception(use_temp_dir): """When working with concurrency sometimes Hishel may leave a cache file empty. In this case this should not cause a `JSONDecodeError`, but treat this situation as no cache file was created. Issue #180""" storage = AsyncFileStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = await storage.retrieve(key) assert stored_data is not None filedir = Path(os.getcwd() + "/.cache/hishel/" + key) with open(filedir, "w+", encoding="utf-8") as file: file.truncate(0) assert os.path.getsize(filedir) == 0 assert await storage.retrieve(key) is None @pytest.mark.anyio async def test_filestorage_remove(use_temp_dir): storage = AsyncFileStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None await storage.remove(key) assert await storage.retrieve(key) is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_redisstorage_remove(anyio_backend): if await is_redis_down(): # pragma: no cover pytest.fail("Redis server was not found") storage = AsyncRedisStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None await storage.remove(key) assert await storage.retrieve(key) is None @pytest.mark.anyio async def test_sqlitestorage_remove(): storage = AsyncSQLiteStorage(connection=await anysqlite.connect(":memory:")) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None await storage.remove(key) assert await storage.retrieve(key) is None @pytest.mark.anyio async def test_inmemorystorage_remove(): storage = AsyncInMemoryStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") await response.aread() await storage.store(key, response=response, request=request, metadata=dummy_metadata) assert await storage.retrieve(key) is not None await storage.remove(key) assert await storage.retrieve(key) is None hishel-0.1.2/tests/_async/test_transport.py000066400000000000000000000411351477404575600211000ustar00rootroot00000000000000import httpx import pytest import hishel from hishel._utils import BaseClock, extract_header_values_decoded @pytest.mark.anyio async def test_transport_301(): async with hishel.MockAsyncTransport() as transport: transport.add_responses([httpx.Response(301, headers=[(b"Location", b"https://example.com")])]) async with hishel.AsyncCacheTransport( transport=transport, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") await cache_transport.handle_async_request(request) response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] @pytest.mark.anyio async def test_transport_response_validation(): async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content="test", ), httpx.Response( 304, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Content-Type", b"application/json"), ], ), ] ) async with hishel.AsyncCacheTransport( transport=transport, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") await cache_transport.handle_async_request(request) response = await cache_transport.handle_async_request(request) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.extensions["revalidated"] assert "Content-Type" in response.headers assert response.headers["Content-Type"] == "application/json" assert await response.aread() == b"test" @pytest.mark.anyio async def test_transport_stale_response(): controller = hishel.Controller(allow_stale=True) async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") await cache_transport.handle_async_request(request) response = await cache_transport.handle_async_request(request) assert not response.extensions["from_cache"] @pytest.mark.anyio async def test_transport_stale_response_with_connecterror(): controller = hishel.Controller(allow_stale=True) class ConnectErrorTransport(hishel.MockAsyncTransport): async def handle_async_request(self, request: httpx.Request) -> httpx.Response: if not hasattr(self, "not_first_request"): setattr(self, "not_first_request", object()) return await super().handle_async_request(request) raise httpx._exceptions.ConnectError("test") async with ConnectErrorTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") await cache_transport.handle_async_request(request) response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] @pytest.mark.anyio async def test_transport_with_only_if_cached_directive_without_stored_response(): controller = hishel.Controller() async with hishel.MockAsyncTransport() as transport: async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: response = await cache_transport.handle_async_request( httpx.Request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) ) assert response.status_code == 504 @pytest.mark.anyio async def test_transport_with_only_if_cached_directive_with_stored_response(): controller = hishel.Controller() async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"test", ), ] ) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: await cache_transport.handle_async_request(httpx.Request("GET", "https://www.example.com")) response = await cache_transport.handle_async_request( httpx.Request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) ) assert response.status_code == 504 @pytest.mark.anyio async def test_transport_with_cache_disabled_extension(): class MockedClock(BaseClock): def now(self) -> int: return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT cachable_response = httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock ], ) async with hishel.MockAsyncTransport() as transport: transport.add_responses([cachable_response, httpx.Response(201)]) async with hishel.AsyncCacheTransport( transport=transport, controller=hishel.Controller(clock=MockedClock()), storage=hishel.AsyncInMemoryStorage(), ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") # This should create a cache entry await cache_transport.handle_async_request(request) # This should return from cache response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] # This should ignore the cache caching_disabled_request = httpx.Request( "GET", "https://www.example.com", extensions={"cache_disabled": True} ) response = await cache_transport.handle_async_request(caching_disabled_request) assert not response.extensions["from_cache"] assert response.status_code == 201 @pytest.mark.anyio async def test_transport_with_custom_key_generator(): controller = hishel.Controller(key_generator=lambda request, body: request.url.host.decode()) async with hishel.MockAsyncTransport() as transport: transport.add_responses([httpx.Response(301)]) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage(), ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") # This should create a cache entry await cache_transport.handle_async_request(request) # This should return from cache response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["cache_key"] == "www.example.com" @pytest.mark.anyio async def test_transport_caching_post_method(): controller = hishel.Controller(cacheable_methods=["POST"]) async with hishel.MockAsyncTransport() as transport: transport.add_responses([httpx.Response(301), httpx.Response(200)]) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage(), ) as cache_transport: request = httpx.Request("POST", "https://www.example.com", json={"request": 1}) # This should create a cache entry await cache_transport.handle_async_request(request) # This should return from cache response = await cache_transport.handle_async_request(request) assert response.extensions["from_cache"] # Method and URL are the same but the body is different request = httpx.Request("POST", "https://www.example.com", json={"request": 2}) # This should create a new cache entry instead of using the previous one response = await cache_transport.handle_async_request(request) assert response.status_code == 200 assert not response.extensions["from_cache"] @pytest.mark.anyio async def test_revalidation_with_new_content(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current clock = MockedClock() controller = hishel.Controller(clock=clock) storage = hishel.AsyncInMemoryStorage() async with hishel.MockAsyncTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"Hello, World.", ), httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], content=b"Eat at Joe's.", ), httpx.Response( 304, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:11 GMT"), ], ), ] ) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=storage ) as cache_transport: # Miss, 200, store response = await cache_transport.handle_async_request(httpx.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit response = await cache_transport.handle_async_request(httpx.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache contains the first response content stored = await storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:00 GMT"] assert stored_response.content == b"Hello, World." # tic, tac... one second passed clock.current += 1 # Miss (expired), send revalidation, 200, store response = await cache_transport.handle_async_request(httpx.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit (cf issue #239) response = await cache_transport.handle_async_request(httpx.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache was updated and contains the second response content stored = await storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:01 GMT"] assert stored_response.content == b"Eat at Joe's." # tic, tac, tic, tac... ten more seconds passed, let's check the 304 behavious is not broken clock.current += 10 # Miss (expired), send revalidation, 304, update metadata but keep previous content response = await cache_transport.handle_async_request(httpx.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 2 stored = await storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:11 GMT"] assert stored_response.content == b"Eat at Joe's." @pytest.mark.anyio async def test_transport_revalidation_forward_extensions(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current class MockedTransportWithExtensionsMemory(hishel.MockAsyncTransport): async def handle_async_request(self, request: httpx.Request) -> httpx.Response: self.last_request_extensions = request.extensions return await super().handle_async_request(request) clock = MockedClock() controller = hishel.Controller(clock=clock) async with MockedTransportWithExtensionsMemory() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpx.Response( 304, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], ), ] ) async with hishel.AsyncCacheTransport( transport=transport, controller=controller, storage=hishel.AsyncInMemoryStorage() ) as cache_transport: # first request with extensions await cache_transport.handle_async_request( httpx.Request("GET", "https://www.example.com", extensions={"foo": "bar"}) ) assert transport.last_request_extensions["foo"] == "bar" # cache expires clock.current += 1 # second request with extensions that should be passed to revalidation request response = await cache_transport.handle_async_request( httpx.Request("GET", "https://www.example.com", extensions={"foo": "baz"}) ) assert response.extensions["revalidated"] is True assert transport.last_request_extensions["foo"] == "baz" hishel-0.1.2/tests/_sync/000077500000000000000000000000001477404575600152665ustar00rootroot00000000000000hishel-0.1.2/tests/_sync/__init__.py000066400000000000000000000000001477404575600173650ustar00rootroot00000000000000hishel-0.1.2/tests/_sync/test_client.py000066400000000000000000000207441477404575600201640ustar00rootroot00000000000000import os from datetime import datetime, timedelta from pathlib import Path from time import mktime from wsgiref.handlers import format_date_time import httpx import pytest from httpcore import Request import hishel from hishel._utils import generate_key date_header = format_date_time(mktime((datetime.now() - timedelta(hours=2)).timetuple())) def test_client_301(): with hishel.MockTransport() as transport: transport.add_responses([httpx.Response(301, headers=[(b"Location", b"https://example.com")])]) with hishel.CacheClient(transport=transport, storage=hishel.InMemoryStorage()) as client: client.request( "GET", "https://www.example.com", ) response = client.request( "GET", "https://www.example.com", ) assert response.extensions["from_cache"] def test_empty_cachefile_handling(use_temp_dir): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], text="test", ) for _ in range(2) ] ) with hishel.CacheClient(storage=hishel.FileStorage(), transport=transport) as client: request = Request(b"GET", "https://example.com/") key = generate_key(request) filedir = Path(os.getcwd() + "/.cache/hishel/" + key) client.get("https://example.com/") response = client.get("https://example.com/") assert response.status_code == 200 assert response.text == "test" assert response.extensions["from_cache"] with open(filedir, "w+", encoding="utf-8") as file: file.truncate(0) assert os.path.getsize(filedir) == 0 response = client.get("https://example.com/") assert response.status_code == 200 assert response.text == "test" assert response.extensions["from_cache"] is False response = client.get("https://example.com/") assert response.extensions["from_cache"] def test_post_caching(): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], text=f"test-{idx}", ) for idx in range(2) ] ) with hishel.CacheClient( storage=hishel.InMemoryStorage(), transport=transport, controller=hishel.Controller(cacheable_methods=["POST"]), ) as client: # Create cache file. response = client.post("https://example.com", json={"test": 1}) assert response.status_code == 200 assert not response.extensions["from_cache"] assert response.text == "test-0" # Get from cache file. response = client.post("https://example.com", json={"test": 1}) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test-0" # Create a new cache file response = client.post("https://example.com", json={"test": 2}) assert response.status_code == 200 assert not response.extensions["from_cache"] assert response.text == "test-1" # Take second response from cache response = client.post("https://example.com", json={"test": 2}) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test-1" # Check on first response response = client.post("https://example.com", json={"test": 1}) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test-0" def test_client_get(): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], text="test text", ) ] ) with hishel.CacheClient(storage=hishel.InMemoryStorage(), transport=transport) as client: response = client.get("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] assert response.text == "test text" response = client.get("https://example.com") assert response.status_code == 200 assert response.extensions["from_cache"] assert response.text == "test text" def test_client_head(): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], ) for _ in range(2) ] ) with hishel.CacheClient(storage=hishel.InMemoryStorage(), transport=transport) as client: response = client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] response = client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] def test_force_cache(): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "no-store"), ("Date", date_header), ], ) for _ in range(3) ] ) with hishel.CacheClient( storage=hishel.InMemoryStorage(), controller=hishel.Controller(cacheable_methods=["HEAD"]), transport=transport, ) as client: response = client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] # Check that "no-store" is respected response = client.head("https://example.com") assert response.status_code == 200 assert not response.extensions["from_cache"] response = client.head("https://example.com", extensions={"force_cache": True}) assert response.status_code == 200 assert not response.extensions["from_cache"] response = client.head("https://example.com", extensions={"force_cache": True}) assert response.status_code == 200 assert response.extensions["from_cache"] def test_cache_disabled(): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( status_code=200, headers=[ ("Cache-Control", "public, max-age=86400, s-maxage=86400"), ("Date", date_header), ], ) for _ in range(2) ] ) with hishel.CacheClient(storage=hishel.InMemoryStorage(), transport=transport) as client: response = client.get( "https://www.example.com/cacheable-endpoint", extensions={"cache_disabled": True} ) assert response.status_code == 200 assert not response.extensions["from_cache"] response = client.get( "https://www.example.com/cacheable-endpoint", extensions={"cache_disabled": True} ) assert response.status_code == 200 assert not response.extensions["from_cache"] hishel-0.1.2/tests/_sync/test_pool.py000066400000000000000000000355251477404575600176620ustar00rootroot00000000000000import httpcore import pytest from httpcore._models import Request, Response import hishel from hishel._utils import BaseClock, extract_header_values, extract_header_values_decoded, header_presents def test_pool_301(): with hishel.MockConnectionPool() as pool: pool.add_responses([httpcore.Response(301, headers=[(b"Location", b"https://example.com")])]) with hishel.CacheConnectionPool(pool=pool, storage=hishel.InMemoryStorage()) as cache_pool: cache_pool.request("GET", "https://www.example.com") response = cache_pool.request("GET", "https://www.example.com") assert response.extensions["from_cache"] def test_pool_response_validation(): with hishel.MockConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"test", ), httpcore.Response( 304, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Content-Type", b"application/json"), ], ), ] ) with hishel.CacheConnectionPool(pool=pool, storage=hishel.InMemoryStorage()) as cache_pool: request = httpcore.Request("GET", "https://www.example.com") cache_pool.handle_request(request) response = cache_pool.handle_request(request) assert response.status == 200 assert response.extensions["from_cache"] assert response.extensions["revalidated"] assert header_presents(response.headers, b"Content-Type") assert extract_header_values(response.headers, b"Content-Type", single=True)[0] == b"application/json" assert response.read() == b"test" def test_pool_stale_response(): controller = hishel.Controller(allow_stale=True) with hishel.MockConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage() ) as cache_pool: cache_pool.request("GET", "https://www.example.com") response = cache_pool.request("GET", "https://www.example.com") assert not response.extensions["from_cache"] def test_pool_stale_response_with_connecterror(): controller = hishel.Controller(allow_stale=True) class ConnectErrorPool(hishel.MockConnectionPool): def handle_request(self, request: Request) -> Response: if not hasattr(self, "not_first_request"): setattr(self, "not_first_request", object()) return super().handle_request(request) raise httpcore._exceptions.ConnectError() with ConnectErrorPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage() ) as cache_pool: cache_pool.request("GET", "https://www.example.com") response = cache_pool.request("GET", "https://www.example.com") assert response.extensions["from_cache"] def test_pool_with_only_if_cached_directive_without_stored_response(): controller = hishel.Controller() with hishel.MockConnectionPool() as pool: with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage() ) as cache_pool: response = cache_pool.request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) assert response.status == 504 def test_pool_with_only_if_cached_directive_with_stored_response(): controller = hishel.Controller() with hishel.MockConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"test", ), ] ) with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage() ) as cache_pool: cache_pool.request("GET", "https://www.example.com") response = cache_pool.request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) assert response.status == 504 def test_pool_with_cache_disabled_extension(): class MockedClock(BaseClock): def now(self) -> int: return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT cachable_response = httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock ], ) with hishel.MockConnectionPool() as pool: pool.add_responses([cachable_response, httpcore.Response(201)]) with hishel.CacheConnectionPool( pool=pool, controller=hishel.Controller(clock=MockedClock()), storage=hishel.InMemoryStorage() ) as cache_transport: request = httpcore.Request("GET", "https://www.example.com") # This should create a cache entry cache_transport.handle_request(request) # This should return from cache response = cache_transport.handle_request(request) assert response.extensions["from_cache"] # This should ignore the cache caching_disabled_request = httpcore.Request( "GET", "https://www.example.com", extensions={"cache_disabled": True} ) response = cache_transport.handle_request(caching_disabled_request) assert not response.extensions["from_cache"] assert response.status == 201 def test_pool_with_custom_key_generator(): controller = hishel.Controller(key_generator=lambda request, body: request.url.host.decode()) with hishel.MockConnectionPool() as pool: pool.add_responses([httpcore.Response(301)]) with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage() ) as cache_transport: request = httpcore.Request("GET", "https://www.example.com") # This should create a cache entry cache_transport.handle_request(request) # This should return from cache response = cache_transport.handle_request(request) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["cache_key"] == "www.example.com" def test_pool_caching_post_method(): controller = hishel.Controller(cacheable_methods=["POST"]) with hishel.MockConnectionPool() as pool: pool.add_responses([httpcore.Response(301), httpcore.Response(200)]) with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage(), ) as cache_pool: # This should create a cache entry cache_pool.request("POST", "https://www.example.com", content=b"request-1") # This should return from cache response = cache_pool.request("POST", "https://www.example.com", content=b"request-1") assert response.extensions["from_cache"] # This should create a new cache entry instead of using the previous one response = cache_pool.request("POST", "https://www.example.com", content=b"request-2") assert response.status == 200 assert not response.extensions["from_cache"] def test_revalidation_with_new_content(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current clock = MockedClock() controller = hishel.Controller(clock=clock) storage = hishel.InMemoryStorage() with hishel.MockConnectionPool() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"Hello, World.", ), httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], content=b"Eat at Joe's.", ), httpcore.Response( 304, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:11 GMT"), ], ), ] ) with hishel.CacheConnectionPool(pool=pool, controller=controller, storage=storage) as cache_pool: # Miss, 200, store response = cache_pool.handle_request(httpcore.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit response = cache_pool.handle_request(httpcore.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache contains the first response content stored = storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:00 GMT"] assert stored_response.content == b"Hello, World." # tic, tac... one second passed clock.current += 1 # one second passed # Miss (expired), send revalidation, 200, store response = cache_pool.handle_request(httpcore.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit (cf issue #239) response = cache_pool.handle_request(httpcore.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache was updated and contains the second response content stored = storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:01 GMT"] assert stored_response.content == b"Eat at Joe's." # tic, tac, tic, tac... ten more seconds passed, let's check the 304 behavious is not broken clock.current += 10 # Miss (expired), send revalidation, 304, update metadata but keep previous content response = cache_pool.handle_request(httpcore.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 2 stored = storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:11 GMT"] assert stored_response.content == b"Eat at Joe's." def test_poool_revalidation_forward_extensions(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current class MockedConnectionPoolWithExtensionsMemory(hishel.MockConnectionPool): def handle_request(self, request: httpcore.Request) -> httpcore.Response: self.last_request_extensions = request.extensions return super().handle_request(request) clock = MockedClock() controller = hishel.Controller(clock=clock) with MockedConnectionPoolWithExtensionsMemory() as pool: pool.add_responses( [ httpcore.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpcore.Response( 304, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], ), ] ) with hishel.CacheConnectionPool( pool=pool, controller=controller, storage=hishel.InMemoryStorage() ) as cache_pool: # first request with extensions cache_pool.handle_request( httpcore.Request("GET", "https://www.example.com", extensions={"foo": "bar"}) ) assert pool.last_request_extensions["foo"] == "bar" # cache expires clock.current += 1 # second request with extensions that should be passed to revalidation request response = cache_pool.handle_request( httpcore.Request("GET", "https://www.example.com", extensions={"foo": "baz"}) ) assert response.extensions["revalidated"] is True assert pool.last_request_extensions["foo"] == "baz" hishel-0.1.2/tests/_sync/test_storages.py000066400000000000000000000323321477404575600205310ustar00rootroot00000000000000import datetime import os from pathlib import Path import sqlite3 import pytest from httpcore import Request, Response from hishel import FileStorage, InMemoryStorage, RedisStorage, SQLiteStorage from hishel._serializers import Metadata from hishel._utils import sleep, generate_key dummy_metadata = Metadata(cache_key="test", number_of_uses=0, created_at=datetime.datetime.now(datetime.timezone.utc)) def is_redis_down() -> bool: import redis connection = redis.Redis() try: return not connection.ping() except BaseException: # pragma: no cover return True def test_filestorage(use_temp_dir): storage = FileStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" def test_redisstorage(anyio_backend): if is_redis_down(): # pragma: no cover pytest.fail("Redis server was not found") storage = RedisStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" def test_sqlitestorage(): storage = SQLiteStorage(connection=sqlite3.connect(":memory:")) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" def test_inmemorystorage(): storage = InMemoryStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = storage.retrieve(key) assert stored_data is not None stored_response, stored_request, metadata = stored_data stored_response.read() assert isinstance(stored_response, Response) assert stored_response.status == 200 assert stored_response.headers == [] assert stored_response.content == b"test" def test_filestorage_expired(use_temp_dir, anyio_backend): storage = FileStorage(ttl=0.2, check_ttl_every=0.1) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") response.read() storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is not None sleep(0.3) storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is None def test_filestorage_timer(use_temp_dir, anyio_backend): storage = FileStorage(ttl=0.2, check_ttl_every=0.2) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") response.read() storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is not None sleep(0.1) assert storage.retrieve(first_key) is not None storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert storage.retrieve(second_key) is not None sleep(0.1) assert storage.retrieve(first_key) is None assert storage.retrieve(second_key) is not None sleep(0.1) assert storage.retrieve(second_key) is None def test_filestorage_ttl_after_hits(use_temp_dir, anyio_backend): storage = FileStorage(ttl=0.2, check_ttl_every=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() # Storing storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.08 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.16 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.24 second sleep(0.08) assert storage.retrieve(key) is None def test_redisstorage_expired(anyio_backend): if is_redis_down(): # pragma: no cover pytest.fail("Redis server was not found") storage = RedisStorage(ttl=0.1) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") response.read() storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is not None sleep(0.3) storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is None def test_redis_ttl_after_hits(use_temp_dir, anyio_backend): storage = RedisStorage(ttl=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() # Storing storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.08 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.16 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.24 second sleep(0.08) assert storage.retrieve(key) is None def test_sqlite_expired(anyio_backend): storage = SQLiteStorage(ttl=0.1, connection=sqlite3.connect(":memory:")) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") response.read() storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is not None sleep(0.3) storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is None @pytest.mark.xfail def test_sqlite_ttl_after_hits(use_temp_dir, anyio_backend): storage = SQLiteStorage(ttl=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() # Storing storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.08 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.16 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.24 second sleep(0.08) assert storage.retrieve(key) is None def test_inmemory_expired(anyio_backend): storage = InMemoryStorage(ttl=0.1) first_request = Request(b"GET", "https://example.com") second_request = Request(b"GET", "https://anotherexample.com") first_key = generate_key(first_request) second_key = generate_key(second_request) response = Response(200, headers=[], content=b"test") response.read() storage.store(first_key, response=response, request=first_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is not None sleep(0.3) storage.store(second_key, response=response, request=second_request, metadata=dummy_metadata) assert storage.retrieve(first_key) is None def test_inmemory_ttl_after_hits(use_temp_dir, anyio_backend): storage = InMemoryStorage(ttl=0.2) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() # Storing storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.08 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.16 second sleep(0.08) storage.update_metadata(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None # Retrieving after 0.24 second sleep(0.08) assert storage.retrieve(key) is None def test_filestorage_empty_file_exception(use_temp_dir): """When working with concurrency sometimes Hishel may leave a cache file empty. In this case this should not cause a `JSONDecodeError`, but treat this situation as no cache file was created. Issue #180""" storage = FileStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) stored_data = storage.retrieve(key) assert stored_data is not None filedir = Path(os.getcwd() + "/.cache/hishel/" + key) with open(filedir, "w+", encoding="utf-8") as file: file.truncate(0) assert os.path.getsize(filedir) == 0 assert storage.retrieve(key) is None def test_filestorage_remove(use_temp_dir): storage = FileStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None storage.remove(key) assert storage.retrieve(key) is None def test_redisstorage_remove(anyio_backend): if is_redis_down(): # pragma: no cover pytest.fail("Redis server was not found") storage = RedisStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None storage.remove(key) assert storage.retrieve(key) is None def test_sqlitestorage_remove(): storage = SQLiteStorage(connection=sqlite3.connect(":memory:")) request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None storage.remove(key) assert storage.retrieve(key) is None def test_inmemorystorage_remove(): storage = InMemoryStorage() request = Request(b"GET", "https://example.com") key = generate_key(request) response = Response(200, headers=[], content=b"test") response.read() storage.store(key, response=response, request=request, metadata=dummy_metadata) assert storage.retrieve(key) is not None storage.remove(key) assert storage.retrieve(key) is None hishel-0.1.2/tests/_sync/test_transport.py000066400000000000000000000372531477404575600207450ustar00rootroot00000000000000import httpx import pytest import hishel from hishel._utils import BaseClock, extract_header_values_decoded def test_transport_301(): with hishel.MockTransport() as transport: transport.add_responses([httpx.Response(301, headers=[(b"Location", b"https://example.com")])]) with hishel.CacheTransport( transport=transport, storage=hishel.InMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") cache_transport.handle_request(request) response = cache_transport.handle_request(request) assert response.extensions["from_cache"] def test_transport_response_validation(): with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content="test", ), httpx.Response( 304, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Content-Type", b"application/json"), ], ), ] ) with hishel.CacheTransport( transport=transport, storage=hishel.InMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") cache_transport.handle_request(request) response = cache_transport.handle_request(request) assert response.status_code == 200 assert response.extensions["from_cache"] assert response.extensions["revalidated"] assert "Content-Type" in response.headers assert response.headers["Content-Type"] == "application/json" assert response.read() == b"test" def test_transport_stale_response(): controller = hishel.Controller(allow_stale=True) with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") cache_transport.handle_request(request) response = cache_transport.handle_request(request) assert not response.extensions["from_cache"] def test_transport_stale_response_with_connecterror(): controller = hishel.Controller(allow_stale=True) class ConnectErrorTransport(hishel.MockTransport): def handle_request(self, request: httpx.Request) -> httpx.Response: if not hasattr(self, "not_first_request"): setattr(self, "not_first_request", object()) return super().handle_request(request) raise httpx._exceptions.ConnectError("test") with ConnectErrorTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), ] ) with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage() ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") cache_transport.handle_request(request) response = cache_transport.handle_request(request) assert response.extensions["from_cache"] def test_transport_with_only_if_cached_directive_without_stored_response(): controller = hishel.Controller() with hishel.MockTransport() as transport: with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage() ) as cache_transport: response = cache_transport.handle_request( httpx.Request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) ) assert response.status_code == 504 def test_transport_with_only_if_cached_directive_with_stored_response(): controller = hishel.Controller() with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"test", ), ] ) with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage() ) as cache_transport: cache_transport.handle_request(httpx.Request("GET", "https://www.example.com")) response = cache_transport.handle_request( httpx.Request( "GET", "https://www.example.com", headers=[(b"Cache-Control", b"only-if-cached")], ) ) assert response.status_code == 504 def test_transport_with_cache_disabled_extension(): class MockedClock(BaseClock): def now(self) -> int: return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT cachable_response = httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock ], ) with hishel.MockTransport() as transport: transport.add_responses([cachable_response, httpx.Response(201)]) with hishel.CacheTransport( transport=transport, controller=hishel.Controller(clock=MockedClock()), storage=hishel.InMemoryStorage(), ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") # This should create a cache entry cache_transport.handle_request(request) # This should return from cache response = cache_transport.handle_request(request) assert response.extensions["from_cache"] # This should ignore the cache caching_disabled_request = httpx.Request( "GET", "https://www.example.com", extensions={"cache_disabled": True} ) response = cache_transport.handle_request(caching_disabled_request) assert not response.extensions["from_cache"] assert response.status_code == 201 def test_transport_with_custom_key_generator(): controller = hishel.Controller(key_generator=lambda request, body: request.url.host.decode()) with hishel.MockTransport() as transport: transport.add_responses([httpx.Response(301)]) with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage(), ) as cache_transport: request = httpx.Request("GET", "https://www.example.com") # This should create a cache entry cache_transport.handle_request(request) # This should return from cache response = cache_transport.handle_request(request) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["cache_key"] == "www.example.com" def test_transport_caching_post_method(): controller = hishel.Controller(cacheable_methods=["POST"]) with hishel.MockTransport() as transport: transport.add_responses([httpx.Response(301), httpx.Response(200)]) with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage(), ) as cache_transport: request = httpx.Request("POST", "https://www.example.com", json={"request": 1}) # This should create a cache entry cache_transport.handle_request(request) # This should return from cache response = cache_transport.handle_request(request) assert response.extensions["from_cache"] # Method and URL are the same but the body is different request = httpx.Request("POST", "https://www.example.com", json={"request": 2}) # This should create a new cache entry instead of using the previous one response = cache_transport.handle_request(request) assert response.status_code == 200 assert not response.extensions["from_cache"] def test_revalidation_with_new_content(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current clock = MockedClock() controller = hishel.Controller(clock=clock) storage = hishel.InMemoryStorage() with hishel.MockTransport() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], content=b"Hello, World.", ), httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], content=b"Eat at Joe's.", ), httpx.Response( 304, headers=[ (b"Cache-Control", b"max-age=10"), (b"Date", b"Mon, 25 Aug 2015 12:00:11 GMT"), ], ), ] ) with hishel.CacheTransport( transport=transport, controller=controller, storage=storage ) as cache_transport: # Miss, 200, store response = cache_transport.handle_request(httpx.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit response = cache_transport.handle_request(httpx.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache contains the first response content stored = storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:00 GMT"] assert stored_response.content == b"Hello, World." # tic, tac... one second passed clock.current += 1 # Miss (expired), send revalidation, 200, store response = cache_transport.handle_request(httpx.Request("GET", "https://example.com/")) assert not response.extensions["from_cache"] # Hit (cf issue #239) response = cache_transport.handle_request(httpx.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 1 # Cache was updated and contains the second response content stored = storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:01 GMT"] assert stored_response.content == b"Eat at Joe's." # tic, tac, tic, tac... ten more seconds passed, let's check the 304 behavious is not broken clock.current += 10 # Miss (expired), send revalidation, 304, update metadata but keep previous content response = cache_transport.handle_request(httpx.Request("GET", "https://example.com/")) assert response.extensions["from_cache"] assert response.extensions["cache_metadata"]["number_of_uses"] == 2 stored = storage.retrieve(response.extensions["cache_metadata"]["cache_key"]) assert stored stored_response, stored_request, stored_metadata = stored assert extract_header_values_decoded(stored_response.headers, b"Date") == ["Mon, 25 Aug 2015 12:00:11 GMT"] assert stored_response.content == b"Eat at Joe's." def test_transport_revalidation_forward_extensions(): class MockedClock(BaseClock): current = 1440504000 # Mon, 25 Aug 2015 12:00:00 GMT def now(self) -> int: return self.current class MockedTransportWithExtensionsMemory(hishel.MockTransport): def handle_request(self, request: httpx.Request) -> httpx.Response: self.last_request_extensions = request.extensions return super().handle_request(request) clock = MockedClock() controller = hishel.Controller(clock=clock) with MockedTransportWithExtensionsMemory() as transport: transport.add_responses( [ httpx.Response( 200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ), httpx.Response( 304, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:01 GMT"), ], ), ] ) with hishel.CacheTransport( transport=transport, controller=controller, storage=hishel.InMemoryStorage() ) as cache_transport: # first request with extensions cache_transport.handle_request( httpx.Request("GET", "https://www.example.com", extensions={"foo": "bar"}) ) assert transport.last_request_extensions["foo"] == "bar" # cache expires clock.current += 1 # second request with extensions that should be passed to revalidation request response = cache_transport.handle_request( httpx.Request("GET", "https://www.example.com", extensions={"foo": "baz"}) ) assert response.extensions["revalidated"] is True assert transport.last_request_extensions["foo"] == "baz" hishel-0.1.2/tests/conftest.py000066400000000000000000000002261477404575600163520ustar00rootroot00000000000000import os import pytest @pytest.fixture() def use_temp_dir(tmpdir): cur_dir = os.getcwd() os.chdir(tmpdir) yield os.chdir(cur_dir) hishel-0.1.2/tests/test_controller.py000066400000000000000000001131301477404575600177460ustar00rootroot00000000000000import logging import re import pytest from httpcore import Request, Response from hishel._controller import ( Controller, allowed_stale, get_age, get_freshness_lifetime, get_heuristic_freshness, ) from hishel._utils import BaseClock, Clock def test_is_cachable_for_cachables(): controller = Controller() request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[(b"Expires", b"some-date")]) assert controller.is_cachable(request=request, response=response) response = Response(200, headers=[(b"Cache-Control", b"max-age=10000")]) assert controller.is_cachable(request=request, response=response) def test_force_cache_property_for_is_cachable(): controller = Controller(force_cache=True, cacheable_status_codes=[400]) request = Request("GET", "https://example.com", extensions={"force_cache": False}) uncachable_response = Response(status=400) assert controller.is_cachable(request=request, response=uncachable_response) is False request = Request("GET", "https://example.com") assert controller.is_cachable(request=request, response=uncachable_response) is True def test_force_cache_property_for_construct_response_from_cache(): class MockedClock(BaseClock): def now(self) -> int: return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT controller = Controller(clock=MockedClock(), force_cache=True) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com", extensions={"force_cache": False}) cachable_response = Response( 200, headers=[ (b"Cache-Control", b"max-age=0"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock ], ) assert isinstance( controller.construct_response_from_cache( request=request, response=cachable_response, original_request=original_request, ), Request, ) request = Request("Get", "https://example.com") assert isinstance( controller.construct_response_from_cache( request=request, response=cachable_response, original_request=original_request, ), Response, ) def test_is_cachable_for_non_cachables(caplog): controller = Controller() request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[]) with caplog.at_level(logging.DEBUG): assert not controller.is_cachable(request=request, response=response) assert caplog.messages == [ "Considering the resource located at https://example.com/ as not cachable " "since it does not contain any of the required cache directives." ] def test_is_cachable_for_heuristically_cachable(caplog): controller = Controller(allow_heuristics=True) request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[]) with caplog.at_level(logging.DEBUG): assert controller.is_cachable(request=request, response=response) assert caplog.messages == [ "Considering the resource located at https://example.com/ as " "cachable since its status code is heuristically cacheable." ] def test_is_cachable_for_invalid_method(caplog): controller = Controller(cacheable_methods=["GET"]) request = Request(b"POST", b"https://example.com", headers=[]) response = Response(200, headers=[]) with caplog.at_level(logging.DEBUG): assert not controller.is_cachable(request=request, response=response) assert caplog.messages == [ ( "Considering the resource located at https://example.com/ " "as not cachable since the request method (POST) is not in the list of cacheable methods." ) ] def test_is_cachable_for_post(): controller = Controller(cacheable_methods=["POST"]) request = Request(b"POST", b"https://example.com", headers=[]) response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=3600"), ], ) assert controller.is_cachable(request=request, response=response) def test_controller_with_unsupported_method(): with pytest.raises( RuntimeError, match=re.escape( "Hishel does not support the HTTP method `INVALID_METHOD`.\nPlease use the methods " "from this list: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']" ), ): Controller(cacheable_methods=["INVALID_METHOD"]) def test_is_cachable_for_unsupported_status(caplog): controller = Controller(cacheable_status_codes=[301]) request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[(b"Expires", b"some-date")]) with caplog.at_level(logging.DEBUG): assert not controller.is_cachable(request=request, response=response) assert caplog.messages == [ ( "Considering the resource located at https://example.com/ " "as not cachable since its status code (200) is not in the list of cacheable status codes." ) ] def test_is_cachable_for_not_final(caplog): controller = Controller(cacheable_status_codes=[100]) request = Request(b"GET", b"https://example.com", headers=[]) response = Response(100, headers=[(b"Expires", b"some-date")]) with caplog.at_level(logging.DEBUG): assert not controller.is_cachable(request=request, response=response) assert caplog.messages == [ "Considering the resource located at https://example.com/ as " "not cachable since its status code is informational." ] def test_is_cachable_for_no_store(caplog): controller = Controller(allow_heuristics=True) request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[(b"Cache-Control", b"no-store")]) with caplog.at_level(logging.DEBUG): assert not controller.is_cachable(request=request, response=response) assert caplog.messages == [ "Considering the resource located at https://example.com/ as not cachable" " since the response contains the no-store directive." ] def test_is_cachable_for_shared_cache(): controller = Controller(cache_private=False, allow_heuristics=True) request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[(b"Cache-Control", b"public")]) assert controller.is_cachable(request=request, response=response) response = Response(200, headers=[(b"Cache-Control", b"private")]) assert not controller.is_cachable(request=request, response=response) response = Response(200, headers=[(b"Cache-Control", b"private=set-cookie")]) assert not controller.is_cachable(request=request, response=response) def test_is_cachable_for_private_cache(caplog): controller = Controller() request = Request(b"GET", b"https://example.com", headers=[]) response = Response(200, headers=[(b"Cache-Control", b"private")]) with caplog.at_level(logging.DEBUG): assert controller.is_cachable(request=request, response=response) assert caplog.messages == [ "Considering the resource located at https://example.com/ as cachable since it" " meets the criteria for being stored in the cache." ] def test_get_freshness_lifetime(): response = Response(status=200, headers=[(b"Cache-Control", b"max-age=3600")]) freshness_lifetime = get_freshness_lifetime(response=response) assert freshness_lifetime == 3600 def test_get_freshness_omit(): response = Response(status=200, headers=[]) freshness_lifetime = get_freshness_lifetime(response=response) assert freshness_lifetime is None def test_get_freshness_lifetime_with_expires(): response = Response( status=200, headers=[ (b"Expires", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Date", b"Mon, 24 Aug 2015 12:00:00 GMT"), ], ) freshness_lifetime = get_freshness_lifetime(response=response) assert freshness_lifetime == 86400 # one day def test_get_freshness_lifetime_with_invalid_expires(): response = Response( status=200, headers=[ (b"Expires", b"0"), (b"Date", b"Mon, 24 Aug 2015 12:00:00 GMT"), ], ) freshness_lifetime = get_freshness_lifetime(response=response) assert freshness_lifetime is None def test_get_freshness_lifetime_with_invalid_date(): response = Response( status=200, headers=[ (b"Expires", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Date", b"0"), ], ) freshness_lifetime = get_freshness_lifetime(response=response) assert freshness_lifetime is None def test_get_heuristic_freshness(): ONE_WEEK = 604_800 class MockedClock(BaseClock): def now(self) -> int: return 1093435200 # Mon, 25 Aug 2003 12:00:00 GMT response = Response(status=200, headers=[(b"Last-Modified", "Mon, 25 Aug 2003 12:00:00 GMT")]) assert get_heuristic_freshness(response=response, clock=MockedClock()) == ONE_WEEK def test_get_heuristic_freshness_without_last_modified(): ONE_DAY = 86400 response = Response(200) assert get_heuristic_freshness(response=response, clock=Clock()) == ONE_DAY def test_get_heuristic_invalid_last_modified(): ONE_DAY = 86400 response = Response(status=200, headers=[(b"Last-Modified", "0")]) assert get_heuristic_freshness(response=response, clock=Clock()) == ONE_DAY def test_get_age(): class MockedClock(BaseClock): def now(self) -> int: return 1440590400 response = Response(status=200, headers=[(b"Date", b"Tue, 25 Aug 2015 12:00:00 GMT")]) age = get_age(response=response, clock=MockedClock()) assert age == 86400 # One day def test_get_age_return_inf_for_invalid_date(): response = Response(status=200, headers=[(b"Date", b"0")]) age = get_age(response=response, clock=Clock()) assert age == float("inf") def test_get_age_return_inf_for_no_date(): age = get_age(response=Response(status=200), clock=Clock()) assert age == float("inf") def test_allowed_stale_no_cache(): response = Response(status=200, headers=[(b"Cache-Control", b"no-cache")]) assert not allowed_stale(response) def test_allowed_stale_must_revalidate(): response = Response(status=200, headers=[(b"Cache-Control", b"must-revalidate")]) assert not allowed_stale(response) def test_allowed_stale_allowed(): response = Response(status=200, headers=[(b"Cache-Control", b"max-age=3600")]) assert allowed_stale(response) def test_clock(): date_07_19_2023 = 1689764505 assert Clock().now() > date_07_19_2023 def test_permanent_redirect_cache(caplog): controller = Controller() request = Request(b"GET", b"https://example.com") response = Response(status=301) with caplog.at_level(logging.DEBUG): assert controller.is_cachable(request=request, response=response) assert caplog.messages == [ ( "Considering the resource located at https://example.com/ " "as cachable since its status code is a permanent redirect." ) ] response = Response(status=302) assert not controller.is_cachable(request=request, response=response) def test_make_conditional_request_with_etag(caplog): controller = Controller() request = Request( b"GET", b"https://example.com", headers=[ (b"Content-Type", b"application/json"), ], ) response = Response(status=200, headers=[(b"Etag", b"some-etag")]) with caplog.at_level(logging.DEBUG): controller._make_request_conditional(request=request, response=response) assert request.headers == [ (b"Content-Type", b"application/json"), (b"If-None-Match", b"some-etag"), ] assert caplog.messages == [ ( "Adding the 'If-None-Match' header with the value of 'some-etag' " "to the request for the resource located at https://example.com/." ) ] def test_make_conditional_request_with_last_modified(caplog): controller = Controller() request = Request( b"GET", b"https://example.com", headers=[ (b"Content-Type", b"application/json"), ], ) response = Response(status=200, headers=[(b"Last-Modified", b"Wed, 21 Oct 2015 07:28:00 GMT")]) with caplog.at_level(logging.DEBUG): controller._make_request_conditional(request=request, response=response) assert request.headers == [ (b"Content-Type", b"application/json"), (b"If-Modified-Since", b"Wed, 21 Oct 2015 07:28:00 GMT"), ] assert caplog.messages == [ "Adding the 'If-Modified-Since' header with the value of 'Wed, 21 Oct 2015 07:28:00 GMT' " "to the request for the resource located at https://example.com/." ] def test_construct_response_from_cache_redirect(caplog): controller = Controller() response = Response(status=301) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") with caplog.at_level(logging.DEBUG): assert response is controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as valid for cache use since its status code is a permanent redirect." ] def test_construct_response_from_cache_fresh(): class MockedClock(BaseClock): def now(self) -> int: return 1440504000 controller = Controller(clock=MockedClock()) response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") assert response is controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) def test_construct_response_from_cache_stale(): class MockedClock(BaseClock): def now(self) -> int: return 1440504002 controller = Controller(clock=MockedClock()) response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") conditional_request = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert isinstance(conditional_request, Request) def test_construct_response_from_cache_with_always_revalidate(caplog): controller = Controller(always_revalidate=True) response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") with caplog.at_level(logging.DEBUG): conditional_request = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert isinstance(conditional_request, Request) assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as needing revalidation since the cache is set to always revalidate." ] def test_construct_response_from_cache_with_must_revalidate(caplog): controller = Controller() response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=1, must-revalidate"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") with caplog.at_level(logging.DEBUG): conditional_request = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert isinstance(conditional_request, Request) assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as needing revalidation since the response contains the must-revalidate directive." ] def test_construct_response_from_cache_with_request_no_cache(caplog): controller = Controller(allow_stale=True) response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=1"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com", headers=[(b"Cache-Control", b"no-cache")]) with caplog.at_level(logging.DEBUG): conditional_request = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert isinstance(conditional_request, Request) assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as needing revalidation since the request contains the no-cache directive." ] def test_construct_response_from_cache_with_no_cache(caplog): controller = Controller(allow_stale=True) response = Response( status=200, headers=[ (b"Cache-Control", b"max-age=1, no-cache"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") with caplog.at_level(logging.DEBUG): conditional_request = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert isinstance(conditional_request, Request) assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as needing revalidation since the response contains the no-cache directive." ] def test_construct_response_heuristically(caplog): class MockedClock(BaseClock): def now(self) -> int: return 1440590400 # Mon, 26 Aug 2015 12:00:00 GMT controller = Controller(allow_heuristics=True, clock=MockedClock()) # Age less than 7 days response = Response( status=200, headers=[ (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), (b"Last-Modified", b"Mon, 25 Aug 2003 12:00:00 GMT"), ], ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") with caplog.at_level(logging.DEBUG): res = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert caplog.messages == [ "Could not determine the freshness lifetime of the resource located at " "https://example.com/, trying to use heuristics to calculate it.", "Successfully calculated the freshness lifetime of the resource " "located at https://example.com/ using heuristics.", "Considering the resource located at https://example.com/ as valid for cache use since it is fresh.", ] assert isinstance(res, Response) # Age more than 7 days response = Response( status=200, headers=[ (b"Date", b"Mon, 18 Aug 2015 12:00:00 GMT"), (b"Last-Modified", b"Mon, 25 Aug 2003 12:00:00 GMT"), ], ) caplog.clear() with caplog.at_level(logging.DEBUG): res = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert caplog.messages == [ "Could not determine the freshness lifetime of the resource located at " "https://example.com/, trying to use heuristics to calculate it.", "Successfully calculated the freshness lifetime of the resource" " located at https://example.com/ using heuristics.", "Considering the resource located at https://example.com/ as needing revalidation since it is not fresh.", "Adding the 'If-Modified-Since' header with the value of 'Mon, 25 Aug 2003 12:00:00 GMT'" " to the request for the resource located at https://example.com/.", ] assert not isinstance(res, Response) def test_handle_validation_response_changed(): controller = Controller() old_response = Response(status=200, headers=[(b"old-response", b"true")], content=b"old") new_response = Response(status=200, headers=[(b"new-response", b"true")], content=b"new") response = controller.handle_validation_response(old_response=old_response, new_response=new_response) response.read() assert response.headers == [(b"new-response", b"true")] assert response.content == b"new" def test_handle_validation_response_not_changed(): controller = Controller() old_response = Response(status=200, headers=[(b"old-response", b"true")], content=b"old") new_response = Response( status=304, headers=[(b"new-response", b"false"), (b"old-response", b"true")], content=b"new", ) response = controller.handle_validation_response(old_response=old_response, new_response=new_response) response.read() assert response.headers == [(b"old-response", b"true"), (b"new-response", b"false")] assert response.content == b"old" def test_vary_validation(): original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Vary", b"Content-Type, Content-Language"), ], ) controller = Controller() assert controller._validate_vary(request=request, response=response, original_request=original_request) original_request.headers.pop(0) assert not controller._validate_vary(request=request, response=response, original_request=original_request) def test_construct_response_from_cache_with_vary_mismatch(caplog): original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/xml"), (b"Content-Language", b"en-US"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Vary", b"Content-Type, Content-Language"), ], ) controller = Controller() with caplog.at_level(logging.DEBUG): cached_response = controller.construct_response_from_cache( original_request=original_request, request=request, response=response ) assert cached_response is None assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as invalid for cache use since the vary headers do not match." ] def test_vary_validation_value_mismatch(): original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/html"), (b"Content-Language", b"en-US"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Vary", b"Content-Type, Content-Language"), ], ) controller = Controller() assert not controller._validate_vary(request=request, response=response, original_request=original_request) def test_vary_validation_value_wildcard(): original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Vary", b"Content-Type, Content-Language, *"), ], ) controller = Controller() assert not controller._validate_vary(request=request, response=response, original_request=original_request) def test_max_age_request_directive(caplog): class MockedClock(BaseClock): def now(self) -> int: return 1440507600 # Mon, 25 Aug 2015 13:00:00 GMT original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=3599"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=3600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller(clock=MockedClock()) with caplog.at_level(logging.DEBUG): cached_response = controller.construct_response_from_cache( original_request=original_request, request=request, response=response ) assert cached_response is None assert caplog.messages == [ ( "Considering the resource located at https://example.com/ " "as invalid for cache use since the age of the response exceeds the max-age directive." ) ] def test_max_age_request_directive_with_max_stale(caplog): class MockedClock(BaseClock): def now(self) -> int: return 1440507600 # Mon, 25 Aug 2015 13:00:00 GMT original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=3600, max-stale=10000"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller(clock=MockedClock()) with caplog.at_level(logging.DEBUG): cached_response = controller.construct_response_from_cache( original_request=original_request, request=request, response=response ) assert isinstance(cached_response, Response) assert caplog.messages == [ "Considering the resource located at https://example.com/ as valid for " "cache use since the freshness lifetime has been exceeded less than max-stale." ] def test_max_stale_request_directive(caplog): class MockedClock(BaseClock): def now(self) -> int: return 1440507600 # Mon, 25 Aug 2015 13:00:00 GMT original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", b"max-stale=2999"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=600"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller(clock=MockedClock()) with caplog.at_level(logging.DEBUG): cached_response = controller.construct_response_from_cache( original_request=original_request, request=request, response=response ) assert cached_response is None assert caplog.messages == [ "Considering the resource located at https://example.com/ as invalid for" " cache use since the freshness lifetime has been exceeded more than max-stale." ] def test_min_fresh_request_directive(caplog): class MockedClock(BaseClock): def now(self) -> int: return 1440507600 # Mon, 25 Aug 2015 13:00:00 GMT original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", b"min_fresh=10000"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=4000"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller(clock=MockedClock()) with caplog.at_level(logging.DEBUG): cached_response = controller.construct_response_from_cache( original_request=original_request, request=request, response=response ) assert cached_response is None assert caplog.messages == [ "Considering the resource located at https://example.com/ as invalid for cache" " use since the time left for freshness is less than the min-fresh directive." ] def test_no_cache_request_directive(): original_request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", b"no-cache"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=4000"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller() cached_response = controller.construct_response_from_cache( original_request=original_request, request=request, response=response ) assert isinstance(cached_response, Request) def test_no_store_request_directive(): request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", b"no-store"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", "max-age=4000"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller() assert not controller.is_cachable(request=request, response=response) def test_no_store_response_directive(): request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", b"no-store, max-age=4000"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller() assert not controller.is_cachable(request=request, response=response) def test_must_understand_response_directive(caplog): request = Request( method="GET", url="https://example.com", headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), ], ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Content-Language", b"en-US"), (b"Cache-Control", b"no-store, must-understand, max-age=4000"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), ], ) controller = Controller() with caplog.at_level(logging.DEBUG): assert controller.is_cachable(request=request, response=response) assert caplog.messages == [ "Skipping the no-store directive for the resource located at https://example.com/" " since the response contains the must-understand directive.", "Considering the resource located at https://example.com/ as cachable " "since it meets the criteria for being stored in the cache.", ] def test_freshness_lifetime_invalid_information(caplog): controller = Controller() response = Response( status=400, ) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") with caplog.at_level(logging.DEBUG): conditional_request = controller.construct_response_from_cache( request=request, response=response, original_request=original_request ) assert isinstance(conditional_request, Request) assert caplog.messages == [ "Could not determine the freshness lifetime of the resource located at https://example.com/, " "trying to use heuristics to calculate it.", ( "Could not calculate the freshness lifetime of the resource located at https://example.com/. " "Making a conditional request to revalidate the response." ), ] def test_force_cache_extension_for_is_cachable(caplog): controller = Controller(cacheable_status_codes=[400]) request = Request("GET", "https://example.com") uncachable_response = Response(status=400) assert controller.is_cachable(request=request, response=uncachable_response) is False request = Request("GET", "https://example.com", extensions={"force_cache": True}) with caplog.at_level(logging.DEBUG): assert controller.is_cachable(request=request, response=uncachable_response) is True assert caplog.messages == [ "Considering the resource located at https://example.com/ as" " cachable since the request is forced to use the cache." ] def test_force_cache_extension_for_construct_response_from_cache(caplog): class MockedClock(BaseClock): def now(self) -> int: return 1440504001 # Mon, 25 Aug 2015 12:00:01 GMT controller = Controller(clock=MockedClock()) original_request = Request("GET", "https://example.com") request = Request("GET", "https://example.com") cachable_response = Response( 200, headers=[ (b"Cache-Control", b"max-age=0"), (b"Date", b"Mon, 25 Aug 2015 12:00:00 GMT"), # 1 second before the clock ], ) with caplog.at_level(logging.DEBUG): assert isinstance( controller.construct_response_from_cache( request=request, response=cachable_response, original_request=original_request, ), Request, ) assert caplog.messages == [ "Considering the resource located at https://example.com/ as needing revalidation since it is not fresh." ] request = Request("Get", "https://example.com", extensions={"force_cache": True}) caplog.clear() with caplog.at_level(logging.DEBUG): assert isinstance( controller.construct_response_from_cache( request=request, response=cachable_response, original_request=original_request, ), Response, ) assert caplog.messages == [ "Considering the resource located at https://example.com/ " "as valid for cache use since the request is forced to use the cache." ] hishel-0.1.2/tests/test_headers.py000066400000000000000000000106061477404575600172020ustar00rootroot00000000000000import pytest from hishel import ParseError, ValidationError, Vary from hishel._headers import parse_cache_control def test_blank_directive(): header = [","] with pytest.raises(ParseError, match="The directive should not be left blank."): parse_cache_control(header) def test_blank_directive_after_ows_stripping(): header = [" ,"] with pytest.raises(ParseError, match="The directive should not contain only whitespaces."): parse_cache_control(header) def test_invalid_key_symbol(): header = ["\x12 ,"] with pytest.raises( ParseError, match=r"The character ''\\x12'' is not permitted in the directive name.", ): parse_cache_control(header) def test_blank_directive_value(): header = ["max-age= ,"] with pytest.raises(ParseError, match="The directive value cannot be left blank."): parse_cache_control(header) def test_blank_invalid_quotes(): header = ['max-age="123,'] with pytest.raises(ParseError, match="Invalid quotes around the value."): parse_cache_control(header) def test_invalid_symbol_in_unquoted(): header = ["max-age=1\x123,"] with pytest.raises( ParseError, match=r"The character ''\\x12'' is not permitted for the unquoted values.", ): parse_cache_control(header) def test_invalid_symbol_in_quoted(): header = ['max-age="\x123",'] with pytest.raises( ParseError, match=r"The character ''\\x12'' is not permitted for the quoted values.", ): parse_cache_control(header) def test_time_field_without_value(): header = ["max-age"] with pytest.raises(ValidationError, match="The directive 'max_age' necessitates a value."): parse_cache_control(header) def test_time_field_with_quote(): header = ['max-age="123"'] with pytest.raises( ValidationError, match="The argument 'max_age' should be an integer, but a quote was found.", ): parse_cache_control(header) def test_time_field_invalid_int(): header = ["max-age=123t1"] with pytest.raises( ValidationError, match="The argument 'max_age' should be an integer, but got ''123t1''.", ): parse_cache_control(header) def test_boolean_fields_with_value(): header = ["no-store=1"] with pytest.raises( ValidationError, match="The directive 'no_store' should have no value, but it does.", ): parse_cache_control(header) def test_list_value_empty(): header = ['no-cache="," '] with pytest.raises(ValidationError, match="The list value must not be empty."): parse_cache_control(header) def test_single_directive_parsing(): header = ["max-age=3600"] cache_control = parse_cache_control(header) assert cache_control.max_age == 3600 def test_multiple_directives_parsing(): header = ["max-age=3600", "s-maxage=3600"] cache_control = parse_cache_control(header) assert cache_control.max_age == 3600 assert cache_control.s_maxage == 3600 def test_boolean_directives_parsing(): header = ["no-store", "public"] cache_control = parse_cache_control(header) assert cache_control.no_store assert cache_control.public def test_list_directives_parsing(): header = ['no-cache="age, authorization"'] cache_control = parse_cache_control(header) assert cache_control.no_cache == ["age", "authorization"] def test_multiple_list_directives_parsing(): header = ['no-cache="age, authorization"', 'private="age, authorization"'] cache_control = parse_cache_control(header) assert cache_control.no_cache == ["age", "authorization"] assert cache_control.private == ["age", "authorization"] def test_blank_list_directives(): header = ["no-cache, private"] cache_control = parse_cache_control(header) assert cache_control.no_cache == True # noqa: E712 assert cache_control.private == True # noqa: E712 def test_single_vary_header(): header = ["Accept, Location"] vary = Vary.from_value(header) assert vary._values == ["Accept", "Location"] def test_multiple_vary_headers(): header = ["Accept", "Location"] vary = Vary.from_value(header) assert vary._values == ["Accept", "Location"] def test_multiple_vary_headers_with_multiple_values(): header = ["Accept, Location", "Transfer-Encoding"] vary = Vary.from_value(header) assert vary._values == ["Accept", "Location", "Transfer-Encoding"] hishel-0.1.2/tests/test_lfu_cache.py000066400000000000000000000031241477404575600174750ustar00rootroot00000000000000import pytest from hishel import LFUCache def test_lfu_cache(): cache: LFUCache[int, int] = LFUCache(2) cache.put(1, 2) a = cache.get(1) assert a == 2 def test_lfu_cache_delete(): cache: LFUCache[int, int] = LFUCache(2) cache.put(1, 2) cache.put(3, 4) cache.put(5, 6) cache.get(3) cache.get(3) with pytest.raises(KeyError): cache.get(1) def test_lfu_cache_invalid_capacity(): with pytest.raises(ValueError, match="Capacity must be positive"): LFUCache(0) def test_lfu_cache_delete_least_frequently(): cache: LFUCache[int, int] = LFUCache(2) cache.put(1, 10) cache.put(2, 10) cache.get(1) cache.put(3, 10) with pytest.raises(KeyError): cache.get(2) cache: LFUCache[int, int] = LFUCache(2) # type: ignore[no-redef] cache.put(1, 10) cache.put(2, 10) cache.get(2) cache.put(3, 10) with pytest.raises(KeyError): cache.get(1) def test_lfu_cache_remove_key(): cache: LFUCache[int, int] = LFUCache(2) cache.put(1, 10) cache.put(2, 10) cache.remove_key(1) with pytest.raises(KeyError): cache.get(1) cache.put(1, 10) cache.get(1) assert cache.min_freq == 1 cache.remove_key(2) assert cache.min_freq == 2 def test_lfu_cache_put_existing(): cache: LFUCache[int, int] = LFUCache(2) cache.put(1, 10) cache.put(2, 10) cache.put(1, 20) assert cache.get(1) == 20 cache: LFUCache[int, int] = LFUCache(2) # type: ignore[no-redef] cache.put(1, 10) cache.put(1, 20) assert cache.get(1) == 20 hishel-0.1.2/tests/test_serializers.py000066400000000000000000000226211477404575600201230ustar00rootroot00000000000000import datetime from httpcore import Request, Response from hishel._serializers import ( JSONSerializer, Metadata, PickleSerializer, YAMLSerializer, ) from hishel._utils import normalized_url def test_pickle_serializer_dumps_and_loads(): request = Request( method="GET", url="https://example.com", headers=[(b"Accept-Encoding", b"gzip")], extensions={"sni_hostname": "example.com"}, ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Transfer-Encoding", b"chunked"), ], content=b"test", extensions={"reason_phrase": b"OK", "http_version": b"HTTP/1.1"}, ) response.read() metadata = Metadata( cache_key="test", number_of_uses=0, created_at=datetime.datetime(year=2003, month=8, day=25, hour=12), ) raw_response = PickleSerializer().dumps(response=response, request=request, metadata=metadata) response, request, metadata = PickleSerializer().loads(raw_response) response.read() assert response.status == 200 assert response.headers == [ (b"Content-Type", b"application/json"), (b"Transfer-Encoding", b"chunked"), ] assert response.content == b"test" assert response.extensions == {"http_version": b"HTTP/1.1", "reason_phrase": b"OK"} assert request.method == b"GET" assert normalized_url(request.url) == "https://example.com/" assert request.headers == [(b"Accept-Encoding", b"gzip")] assert request.extensions == {"sni_hostname": "example.com"} assert metadata["cache_key"] == "test" assert metadata["number_of_uses"] == 0 assert metadata["created_at"] == datetime.datetime(year=2003, month=8, day=25, hour=12) def test_dict_serializer_dumps(): request = Request( method="GET", url="https://example.com", headers=[(b"Accept-Encoding", b"gzip")], extensions={"sni_hostname": "example.com"}, ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Transfer-Encoding", b"chunked"), ], content=b"test", extensions={"reason_phrase": b"OK", "http_version": b"HTTP/1.1"}, ) response.read() metadata = Metadata( cache_key="test", number_of_uses=0, created_at=datetime.datetime(year=2003, month=8, day=25, hour=12), ) full_json = JSONSerializer().dumps(response=response, request=request, metadata=metadata) assert full_json == "\n".join( [ "{", ' "response": {', ' "status": 200,', ' "headers": [', " [", ' "Content-Type",', ' "application/json"', " ],", " [", ' "Transfer-Encoding",', ' "chunked"', " ]", " ],", ' "content": "dGVzdA==",', ' "extensions": {', ' "reason_phrase": "OK",', ' "http_version": "HTTP/1.1"', " }", " },", ' "request": {', ' "method": "GET",', ' "url": "https://example.com/",', ' "headers": [', " [", ' "Accept-Encoding",', ' "gzip"', " ]", " ],", ' "extensions": {', ' "sni_hostname": "example.com"', " }", " },", ' "metadata": {', ' "cache_key": "test",', ' "number_of_uses": 0,', ' "created_at": "Mon, 25 Aug 2003 12:00:00 GMT"', " }", "}", ] ) def test_dict_serializer_loads(): raw_response = "\n".join( [ "{", ' "response": {', ' "status": 200,', ' "headers": [', " [", ' "Content-Type",', ' "application/json"', " ],", " [", ' "Transfer-Encoding",', ' "chunked"', " ]", " ],", ' "content": "dGVzdA==",', ' "extensions": {', ' "reason_phrase": "OK",', ' "http_version": "HTTP/1.1"', " }", " },", ' "request": {', ' "method": "GET",', ' "url": "https://example.com/",', ' "headers": [', " [", ' "Accept-Encoding",', ' "gzip"', " ]", " ],", ' "extensions": {', ' "sni_hostname": "example.com"', " }", " },", ' "metadata": {', ' "cache_key": "test",', ' "number_of_uses": 0,', ' "created_at": "Mon, 25 Aug 2003 12:00:00 GMT"', " }", "}", ] ) response, request, metadata = JSONSerializer().loads(raw_response) response.read() assert response.status == 200 assert response.headers == [ (b"Content-Type", b"application/json"), (b"Transfer-Encoding", b"chunked"), ] assert response.content == b"test" assert response.extensions == {"http_version": b"HTTP/1.1", "reason_phrase": b"OK"} assert request.method == b"GET" assert normalized_url(request.url) == "https://example.com/" assert request.headers == [(b"Accept-Encoding", b"gzip")] assert request.extensions == {"sni_hostname": "example.com"} assert metadata["cache_key"] == "test" assert metadata["number_of_uses"] == 0 assert metadata["created_at"] == datetime.datetime(year=2003, month=8, day=25, hour=12) def test_yaml_serializer_dumps(): request = Request( method="GET", url="https://example.com", headers=[(b"Accept-Encoding", b"gzip")], extensions={"sni_hostname": "example.com"}, ) response = Response( status=200, headers=[ (b"Content-Type", b"application/json"), (b"Transfer-Encoding", b"chunked"), ], content=b"test", extensions={"reason_phrase": b"OK", "http_version": b"HTTP/1.1"}, ) response.read() metadata = Metadata( cache_key="test", number_of_uses=0, created_at=datetime.datetime(year=2003, month=8, day=25, hour=12), ) full_json = YAMLSerializer().dumps(response=response, request=request, metadata=metadata) assert full_json == "\n".join( [ "response:", " status: 200", " headers:", " - - Content-Type", " - application/json", " - - Transfer-Encoding", " - chunked", " content: dGVzdA==", " extensions:", " reason_phrase: OK", " http_version: HTTP/1.1", "request:", " method: GET", " url: https://example.com/", " headers:", " - - Accept-Encoding", " - gzip", " extensions:", " sni_hostname: example.com", "metadata:", " cache_key: test", " number_of_uses: 0", " created_at: Mon, 25 Aug 2003 12:00:00 GMT", "", ] ) def test_yaml_serializer_loads(): raw_response = "\n".join( [ "response:", " status: 200", " headers:", " - - Content-Type", " - application/json", " - - Transfer-Encoding", " - chunked", " content: dGVzdA==", " extensions:", " reason_phrase: OK", " http_version: HTTP/1.1", "request:", " method: GET", " url: https://example.com/", " headers:", " - - Accept-Encoding", " - gzip", " extensions:", " sni_hostname: example.com", "metadata:", " cache_key: test", " number_of_uses: 0", " created_at: Mon, 25 Aug 2003 12:00:00 GMT", "", ] ) response, request, metadata = YAMLSerializer().loads(raw_response) response.read() assert response.status == 200 assert response.headers == [ (b"Content-Type", b"application/json"), (b"Transfer-Encoding", b"chunked"), ] assert response.content == b"test" assert response.extensions == {"http_version": b"HTTP/1.1", "reason_phrase": b"OK"} assert request.method == b"GET" assert normalized_url(request.url) == "https://example.com/" assert request.headers == [(b"Accept-Encoding", b"gzip")] assert request.extensions == {"sni_hostname": "example.com"} assert metadata["cache_key"] == "test" assert metadata["number_of_uses"] == 0 assert metadata["created_at"] == datetime.datetime(year=2003, month=8, day=25, hour=12) hishel-0.1.2/tests/test_utils.py000066400000000000000000000076221477404575600167330ustar00rootroot00000000000000from unittest import mock import httpcore import pytest from httpcore import Request from hishel._controller import get_updated_headers from hishel._utils import ( extract_header_values, extract_header_values_decoded, float_seconds_to_int_milliseconds, generate_key, get_safe_url, header_presents, parse_date, ) def test_generate_key(): request = Request(b"GET", "https://example.com", headers=[]) key = generate_key(request) assert key == "bd152069787aaad359c85af6f2edbb25" def test_fips_generate_key(): request = Request(b"GET", "https://example.com", headers=[]) # Simulate FIPS mode by using sha256 instead of blake2b with mock.patch("hashlib.blake2b", side_effect=AttributeError("ERROR")): key = generate_key(request) assert key == "ea96dc6995764a0e6cf26bd2550deb01c18f69c0e586aa1fe201683129b8c15a" def test_extract_header_values(): headers = [ (b"Content-Type", b"application/json"), (b"Content-Type", b"application/html"), ] values = extract_header_values(headers, b"Content-Type") assert values == [b"application/json", b"application/html"] def test_extract_header_values_decoded(): headers = [ (b"Content-Type", b"application/json"), (b"Content-Type", b"application/html"), ] values = extract_header_values_decoded(headers, b"Content-Type") assert values == ["application/json", "application/html"] def test_extract_header_single_value(): headers = [ (b"Content-Type", b"application/json"), (b"Content-Type", b"application/html"), ] values = extract_header_values(headers, b"Content-Type", single=True) assert values == [b"application/json"] def test_header_presents(): headers = [ (b"Content-Type", b"application/json"), (b"Content-Type", b"application/html"), (b"Accept", b"application/json"), ] accept_presents = header_presents(headers, b"Accept") assert accept_presents transfer_encoding_presents = header_presents(headers, b"Transfer-Encoding") assert not transfer_encoding_presents def test_get_updated_headers(): old_headers = [(b"Content-Type", b"application/json"), (b"Language", b"en")] new_headers = [ (b"Language", b"am"), (b"Content-Length", b"1024"), (b"Authorization", b"secret-key"), ] update_headers = get_updated_headers(stored_response_headers=old_headers, new_response_headers=new_headers) assert len(update_headers) == 3 assert extract_header_values(update_headers, b"Language")[0] == b"am" assert extract_header_values(update_headers, b"Content-Type")[0] == b"application/json" assert extract_header_values(update_headers, b"Authorization")[0] == b"secret-key" def test_parse_date(): date = "Mon, 25 Aug 2015 12:00:00 GMT" timestamp = parse_date(date) assert timestamp == 1440504000 def test_parse_invalid_date(): date = "0" timestamp = parse_date(date) assert timestamp is None def test_float_seconds_to_milliseconds(): seconds = 1.234 milliseconds = float_seconds_to_int_milliseconds(seconds) assert milliseconds == 1234 @pytest.mark.parametrize( "url, expected", [ pytest.param( "https://example.com/path?query=1", "https://example.com/path", id="url_with_query_is_ignored", ), pytest.param( "https://example.com/path", "https://example.com/path", id="url_without_query", ), pytest.param("https://example.com", "https://example.com/", id="url_without_path"), pytest.param( "https://xn--e1afmkfd.xn--p1ag", "https://пример.ру/", id="url_with_idna", ), ], ) def test_safe_url( url: str, expected: str, ) -> None: httpcore_url = httpcore.URL(url) safe_url = get_safe_url(httpcore_url) assert safe_url == expected hishel-0.1.2/unasync.py000066400000000000000000000076541477404575600150570ustar00rootroot00000000000000#!venv/bin/python import os import re import sys SUBS = [ ("async def", "def"), ("async with", "with"), ("await ", ""), ("async for", "for"), ("__aiter__", "__iter__"), ("AsyncIterator", "Iterator"), ("AsyncFileStorage", "FileStorage"), ("AsyncBaseStorage", "BaseStorage"), ("AsyncFileManager", "FileManager"), ("MockAsyncConnectionPool", "MockConnectionPool"), ("MockAsyncTransport", "MockTransport"), ("AsyncRedisStorage", "RedisStorage"), ("AsyncSQLiteStorage", "SQLiteStorage"), ("AsyncInMemoryStorage", "InMemoryStorage"), ("AsyncS3Storage", "S3Storage"), ("AsyncS3Manager", "S3Manager"), ("import redis.asyncio as redis", "import redis"), ("AsyncCacheTransport", "CacheTransport"), ("AsyncBaseTransport", "BaseTransport"), ("AsyncCacheClient", "CacheClient"), ("AsyncClient", "Client"), ("AsyncIterable", "Iterable"), ("AsyncCacheStream", "CacheStream"), ("AsyncByteStream", "SyncByteStream"), ("AsyncCacheConnectionPool", "CacheConnectionPool"), ("handle_async_request", "handle_request"), ("aread", "read"), ("aclose", "close"), ("asleep", "sleep"), ("AsyncLock", "Lock"), ( "from httpcore._async.interfaces import AsyncRequestInterface", "from httpcore._sync.interfaces import RequestInterface", ), ("from hishel._async._transports", "from hishel._sync._transports"), ("AsyncRequestInterface", "RequestInterface"), ("__aenter__", "__enter__"), ("__aexit__", "__exit__"), ("*@pytest.mark.anyio", ""), (r'*@pytest.mark.parametrize\("anyio_backend", \["asyncio"\]\)', ""), ("anysqlite", "sqlite3"), ] COMPILED_SUBS = [(re.compile(r"(^|\b)" + regex + r"($|\b)"), repl) for regex, repl in SUBS] USED_SUBS = set() def unasync_line(line): for index, (regex, repl) in enumerate(COMPILED_SUBS): old_line = line line = re.sub(regex, repl, line) if index not in USED_SUBS: if line != old_line: USED_SUBS.add(index) return line def unasync_file(in_path, out_path): with open(in_path) as in_file: with open(out_path, "w", newline="") as out_file: for line in in_file.readlines(): line = unasync_line(line) out_file.write(line) def unasync_file_check(in_path, out_path): with open(in_path) as in_file: with open(out_path) as out_file: for in_line, out_line in zip(in_file.readlines(), out_file.readlines()): expected = unasync_line(in_line) if out_line != expected: print(f"unasync mismatch between {in_path!r} and {out_path!r}") print(f"Async code: {in_line!r}") print(f"Expected sync code: {expected!r}") print(f"Actual sync code: {out_line!r}") sys.exit(1) def unasync_dir(in_dir, out_dir, check_only=False): for dirpath, dirnames, filenames in os.walk(in_dir): for filename in filenames: if not filename.endswith(".py"): continue rel_dir = os.path.relpath(dirpath, in_dir) in_path = os.path.normpath(os.path.join(in_dir, rel_dir, filename)) out_path = os.path.normpath(os.path.join(out_dir, rel_dir, filename)) print(in_path, "->", out_path) if check_only: unasync_file_check(in_path, out_path) else: unasync_file(in_path, out_path) def main(): check_only = "--check" in sys.argv unasync_dir("hishel/_async", "hishel/_sync", check_only=check_only) unasync_dir("tests/_async", "tests/_sync", check_only=check_only) if len(USED_SUBS) != len(SUBS): unused_subs = [SUBS[i] for i in range(len(SUBS)) if i not in USED_SUBS] from pprint import pprint print("This SUBS was not used") pprint(unused_subs) exit(1) if __name__ == "__main__": main()